0%

用Go打造多轮工具调用AI Agent:从代码到实战

在大模型时代,单纯的文本生成已无法满足复杂需求,能调用工具的AI Agent才是主流。本文将基于Go语言和go-openai库,带你从零构建一个支持天气查询、数学计算、时区时间查询的多轮工具调用Agent,完整覆盖工具定义、参数校验、递归调用全流程。

核心原理:AI Agent为何能调用工具?

AI Agent实现工具调用的核心逻辑,本质是“模型决策+工具执行+上下文管理”的闭环:

  1. 模型决策:通过向大模型传递工具定义(名称、参数、功能),让模型自主判断是否需要调用工具,以及调用哪个工具。
  2. 工具执行:解析模型返回的工具调用指令,调用对应的本地/第三方工具,获取执行结果。
  3. 上下文管理:将工具执行结果回传给模型,作为下一轮决策的依据,实现多轮调用的连贯性。

本文的Agent正是基于此逻辑,通过递归调用实现多步骤工具串联(如“查天气→算温差平均值→查时区时间”)。

项目结构:模块化设计拆解

整个项目按功能划分为6个核心模块,每个模块职责单一,便于维护和扩展:

模块核心功能关键代码文件
工具参数结构体定义各工具的输入参数格式,用于解析模型返回的调用指令WeatherParams/CalcParams/TimeParams
工具实现逻辑封装具体工具的业务逻辑(模拟第三方API调用)getWeather/calculate/getCurrentTime
工具定义(核心)jsonschema标准化工具参数,传递给大模型getOpenAITools()
工具调用执行器解析模型指令,分发并执行对应工具executeToolCall()
递归多轮调用管理上下文,控制调用流程,处理模型终止逻辑recursiveAgent()
入口函数初始化客户端,构造测试用例,启动Agentmain()

关键实现:从工具定义到多轮调用

1. 工具参数标准化:用jsonschema约束输入

大模型调用工具的前提,是明确“工具需要什么参数”。通过go-openai/jsonschema库,我们可以标准化参数定义,让模型更精准地生成调用指令。

以天气查询工具为例,参数定义需包含3个核心信息:

  • 参数类型:明确是字符串、数字还是对象。
  • 参数描述:告知模型参数的含义和格式(如“城市名称,仅支持国内主流城市”)。
  • 必传字段:标记哪些参数是必须的,避免模型漏传。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 天气工具参数定义(jsonschema)
weatherCityParam := jsonschema.Definition{
    Type:        jsonschema.String,        // 参数类型:字符串
    Description: "城市名称(如北京、上海,仅支持国内主流城市)", // 参数描述
}
// 天气工具整体参数Schema
weatherParamsSchema := jsonschema.Definition{
    Type:       jsonschema.Object,
    Properties: map[string]jsonschema.Definition{"city": weatherCityParam},
    Required:   []string{"city"}, // 必传字段:city
}

同理,数学计算工具需定义expr(表达式)参数,时间查询工具需定义timezone(时区)参数,最终通过getOpenAITools()函数组装成工具列表,传递给大模型。

2. 工具逻辑封装:模拟真实API调用

工具实现需考虑3个关键点:超时控制、业务逻辑、异常处理,确保工具调用稳定可靠。

以天气查询工具getWeather()为例:

  • 超时控制:用context.WithTimeout设置3秒超时,避免工具调用阻塞。
  • 业务逻辑:模拟网络延迟(500ms),用预定义的weatherMap返回天气数据。
  • 异常处理:未查询到城市时,返回明确的支持城市列表,提升用户体验。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func getWeather(ctx context.Context, city string) (string, error) {
    // 3秒超时控制
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

    // 模拟网络延迟
    select {
    case <-ctx.Done():
        return "", fmt.Errorf("查询超时:%v", ctx.Err())
    case <-time.After(500 * time.Millisecond):
    }

    // 模拟天气数据
    weatherMap := map[string]string{
        "北京": "晴,20-28℃,微风",
        "上海": "多云,18-25℃,东北风3级",
        // 更多城市...
    }

    if weather, ok := weatherMap[city]; ok {
        return fmt.Sprintf("[天气工具结果] 城市:%s → %s", city, weather), nil
    }
    return fmt.Sprintf("[天气工具结果] 未查询到「%s」的天气数据(支持城市:北京、上海、广州、深圳、杭州)", city), nil
}

引言:从“问答机器人”到“行动型AI助手”

传统的聊天机器人大多停留在“理解-回答”的单向交互模式。而随着大模型能力的演进,我们不再满足于“知道答案”,更希望 AI 能“采取行动”——比如查询天气、发送邮件、执行数据库操作等。

这正是 Model Context Protocol (MCP)OpenAI 函数调用(Function Calling) 技术的用武之地。本文将带你从零开始,用 Go 语言实现一个基于 MCP 协议与 OpenAI SDK 深度整合的 Demo,构建一个能动态发现并调用远程工具的智能对话系统。

我们将深入剖析核心架构、关键设计模式,并最终实现一个可扩展、高可用的 AI 助手原型。

核心目标:

  • 多轮递归调用:支持 LLM 决策后多次调用工具。
  • 上下文累积:确保每一轮工具调用的结果都能被 LLM 看到。
  • 高效并发处理:利用 Go 的并发特性提升响应速度。
  • 精准流程控制:正确解析 FinishReason,避免冗余请求。

一、技术选型与核心概念

1.1 OpenAI Function Calling(函数调用)

OpenAI 的 gpt-3.5-turbo 及以上模型支持“函数调用”功能。你可以向模型描述一组函数(工具)的能力,模型会根据用户输入决定是否调用、调用哪个函数,并生成符合函数签名的参数。

1.2 Model Context Protocol (MCP)

MCP 是一种新兴的标准化协议,用于描述和暴露 LLM 可调用的“工具”(Tools)。它允许 LLM 客户端动态发现远程服务提供的功能,并安全地执行调用。

  • 核心价值
    • 动态发现:无需硬编码工具列表,运行时自动获取。
    • 解耦架构:LLM 核心与工具提供方完全分离,便于微服务部署。
    • 标准化接口:统一的 ListToolsCallTool 接口,降低集成成本。
  • 参考实现modelcontextprotocol/go-sdk

1.3 为什么选择 Go?

  • 高性能:适合高并发的 API 网关场景。
  • 强类型与并发支持sync, errgroup 等包让并发控制更安全。
  • 丰富的生态go-openai, gin, grpc 等库成熟稳定。

二、系统架构设计

我们的系统由三部分组成:

Git LFS 完全指南:轻松管理 Git 仓库中的大文件

在日常开发中,你是否遇到过这样的问题:Git 仓库因为几张设计图、一个安装包而体积暴增,克隆代码需要几十分钟,git status 命令卡顿严重?如果你正在被大文件拖累 Git 仓库性能,那么 Git LFS 就是你的救星。

什么是 Git LFS?

Git LFS(Large File Storage,大文件存储)是 Git 的一个扩展工具,专门为解决 Git 对大文件支持不足的问题而设计。它的核心思路是:用轻量的「指针文件」替代实际大文件存储在 Git 仓库中,而真正的大文件则单独存储在 LFS 服务器,从而避免 Git 仓库体积膨胀,提升操作效率。

为什么需要 Git LFS?

Git 本身的设计更适合管理文本文件(如代码、配置文件),因为文本文件体积小,且 Git 能通过「差异对比」(只记录修改部分)高效存储版本历史。但面对图片、视频、安装包等大文件时,Git 会暴露明显缺陷:

  1. 仓库体积爆炸:Git 会完整保存大文件的每一个版本,几次修改后仓库可能从 MB 级膨胀到 GB 级,克隆/拉取速度极慢。
  2. 操作卡顿git statusgit commit 等命令需要扫描文件,大文件会显著拖慢这些操作。
  3. 存储浪费:重复存储大文件的多个版本,占用大量本地和远程存储空间。

而 Git LFS 正是为解决这些问题而生——它让 Git 既能管理大文件的版本,又不用承受大文件带来的性能负担。

整改 Dashboard.json ConfigMap 过大导致 kubectl apply 失败问题

在实际部署 Grafana Dashboard 过程中,因 kubectl apply 会将完整配置写入 kubectl.kubernetes.io/last-applied-configuration 注释,导致 ConfigMap 的 metadata.annotations 长度超限制,出现部署报错。本文围绕「拆分 ConfigMap + 挂载到指定目录」的核心思路,提供 subPath 和 Projected 两种落地方案,同时结合 Kubernetes 官方源码解析限制本质。

一、问题核心回顾

1. 报错现象

执行 kubectl apply 创建 Grafana 相关 ConfigMap(如 grafana-dashboards-general)时,终端提示如下错误,配置创建失败:

1
ConfigMap "grafana-dashboards-general" is invalid: metadata.annotations: Too long: must have at most 262144 characters

2. 根源分析

  • kubectl apply** 的注释机制**:kubectl apply 作为 Kubernetes 声明式部署的核心命令,会自动在 ConfigMap 的 metadata.annotations 中添加 kubectl.kubernetes.io/last-applied-configuration 注释 —— 该注释会完整记录上一次 apply 时的配置内容,用于后续对比配置差异、实现增量更新。当 dashboard.json 包含大量可视化配置(如多指标面板、复杂筛选规则)时,注释携带的完整配置会让 metadata.annotations 总长度骤增。

Golang 实现支持过期功能的 Map:从设计到实践

在日常的 Golang 开发中,我们经常会遇到需要缓存临时数据的场景,比如存储用户会话信息、接口请求结果等。这些数据通常不需要长期保留,若手动管理过期删除,不仅代码繁琐,还容易出现内存泄漏问题。此时,一个支持自动过期的 Map 就成了刚需。本文将带大家从零开始,设计并实现一个高性能、线程安全的过期 Map,并对核心逻辑进行深度解析。

一、需求分析:为什么需要过期 Map?

在正式编码前,我们先明确一个合格的过期 Map 应具备哪些核心能力,避免后续开发偏离需求:

  1. 自动过期:支持为键值对设置过期时间,过期后自动删除,无需手动干预;

  2. 线程安全:在高并发场景下(如多 Goroutine 读写),不会出现数据竞争问题;

  3. 高性能:读写操作耗时低,即使存储大量数据,也不会因锁竞争导致性能瓶颈;

  4. 可配置化:默认参数(如默认过期时间、清理间隔)可自定义,适应不同业务场景;

  5. 基础工具方法:提供获取活跃元素数量、手动删除键等功能,方便业务监控与调试。

二、设计思路:如何兼顾性能与安全性?

针对上述需求,我们采用以下设计方案,平衡性能、安全性与易用性:

设计要点实现方案优势
线程安全基于 sync.Map 实现sync.Map 是 Golang 标准库提供的线程安全 Map,内置原子操作,避免手动加锁的繁琐与风险
减少锁竞争分段存储(Sharding)将全局 Map 拆分为多个 sync.Map 分片,键通过哈希计算分配到指定分片,降低单个分片的竞争频率
过期清理定时清理 + 惰性删除- 定时清理:启动独立 Goroutine,按固定间隔扫描所有分片,删除过期键;- 惰性删除:获取键时先检查是否过期,若过期则立即删除,避免 “过期键残留” 问题
活跃计数原子操作(atomic.Int64新增 / 删除键时通过原子操作更新计数,确保高并发下计数准确,且性能开销极低
可配置化选项模式(Option Pattern)通过自定义函数动态设置过期时间、清理间隔,不破坏默认参数的易用性

三、完整实现:代码与核心逻辑解析

1. 定义核心结构体与默认参数

首先定义存储过期值的结构体和过期 Map 的主体结构,同时设置默认参数(默认清理间隔 1 分钟,默认过期时间 5 分钟):