Function Calling 教程:用 Go 实现大模型工具调用(Tool Use 原理 + openai-go 实战)
引言:给模型一双手
系列第 1 篇 《大模型 API 基础》 里,我们让模型只会「说话」。但真实业务中你需要它「做事」:查数据库、调内部 API、读文件、执行计算。大模型本身没有这些能力——它的全部本领是根据上下文预测下一个 token。
Function Calling(工具调用,也叫 Tool Use)就是给模型一双手的协议:模型负责「决定调用哪个函数、传什么参数」,你的 Go 代码负责真正执行,再把结果喂回去让模型总结。本文讲清原理,并给出一段可直接 go run 的完整代码。
一、原理:模型不执行函数,只发「调用单」
整个协议只有三步循环:
用户提问 ──▶ 模型判断需要查天气
│ 输出一条 function_call 指令(不是最终答案):
│ {"name": "get_weather", "arguments": "{\"location\": \"纽约\"}"}
▼
你的 Go 代码解析指令 → 真正执行 get_weather("纽约") → 得到 "晴,22℃"
│
▼
把结果通过 function_call_output 回传 ──▶ 模型基于真实数据生成最终回答
三个关键认知:
- 模型不产生数据,只产生调用意图。
arguments是模型「猜」出来的 JSON,可能字段缺失、格式错误,生产代码必须校验(后文有处理)。 - 模型不知道函数真的被执行了。你回传什么,它就信什么——所以模拟数据也能跑通整个流程(本文示例正是如此,没有 API Key 时可以用 mock 客户端学习结构)。
- 一轮可以并发返回多个 function_call。
resp.Output是一个数组,多个工具调用都要逐个回传,再进入下一轮。
为什么不用「Prompt 工程让模型输出 JSON」?对比一下:
| 维度 | Prompt 约定 JSON | Function Calling |
|---|---|---|
| 参数校验 | 无,靠解析失败重试 | JSON Schema 声明式校验 |
| 模型参与度 | 模型可能闲聊/不遵守格式 | 协议级支持,输出结构化指令 |
| 生态兼容 | 各家 prompt 各写各的 | OpenAI / Anthropic / 国内模型均支持 |
| 代码复杂度 | 自己写解析和重试 | SDK 提供类型化 API |
二、动手:用 openai-go 实现天气查询
2.1 环境与安装
mkdir weather-agent && cd weather-agent
go mod init weather-agent
go get github.com/openai/openai-go/v3@v3.90.0
export OPENAI_API_KEY=你的key
2.2 完整代码
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/responses"
)
// 工具的真正实现:模型只"下指令",干活的是这段 Go 代码
func getWeather(location string) string {
return location + ":晴,22℃"
}
func main() {
client := openai.NewClient(
option.WithAPIKey(os.Getenv("OPENAI_API_KEY")),
)
ctx := context.Background()
// 1. 发起请求,并用 JSON Schema 向模型"注册"可用工具
params := responses.ResponseNewParams{
Model: openai.ChatModelGPT5_2,
Input: responses.ResponseNewParamsInputUnion{
OfString: openai.String("纽约天气怎么样?适合出门吗?"),
},
Tools: []responses.ToolUnionParam{{
OfFunction: &responses.FunctionToolParam{
Name: "get_weather",
Description: openai.String("查询指定城市的天气"),
Parameters: map[string]any{ // JSON Schema 描述参数
"type": "object",
"properties": map[string]any{
"location": map[string]string{
"type": "string",
"description": "城市名,如 New York",
},
},
"required": []string{"location"},
},
},
}},
}
// 2. 工具调用循环:模型可能要调用多轮工具才给出最终答案
for {
resp, err := client.Responses.New(ctx, params)
if err != nil {
log.Fatal(err)
}
called := false
for _, item := range resp.Output {
if item.Type != "function_call" {
continue
}
called = true
call := item.AsFunctionCall()
fmt.Println("▶ 模型请求调用:", call.Name, call.Arguments)
// 3. 解析参数并执行本地函数(参数必须校验!)
var result string
switch call.Name {
case "get_weather":
var args struct {
Location string `json:"location"`
}
if err := json.Unmarshal([]byte(call.Arguments), &args); err != nil || args.Location == "" {
result = `{"error": "invalid arguments"}`
} else {
result = getWeather(args.Location)
}
default:
result = `{"error": "unknown tool"}`
}
// 4. 通过 PreviousResponseID 续接对话,回传执行结果
params = responses.ResponseNewParams{
Model: openai.ChatModelGPT5_2,
PreviousResponseID: openai.String(resp.ID),
Input: responses.ResponseNewParamsInputUnion{
OfInputItemList: []responses.ResponseInputItemUnionParam{{
OfFunctionCallOutput: &responses.ResponseInputItemFunctionCallOutputParam{
CallID: call.CallID, // 必须与模型发的 call id 对应
Output: responses.ResponseInputItemFunctionCallOutputOutputUnionParam{
OfString: openai.String(result),
},
},
}},
},
}
}
// 5. 输出里没有 function_call 了,就是最终自然语言回答
if !called {
fmt.Println("✔ 最终回答:", resp.OutputText())
break
}
}
}
运行后你会看到类似这样的输出:
▶ 模型请求调用: get_weather {"location": "New York"}
✔ 最终回答: 纽约今天晴,22℃,天气舒适,适合出门。
2.3 代码里的五个关键点
Tools字段是「注册表」:Name+Description+Parameters(JSON Schema) 三件事决定模型会不会、以及怎么调用你的函数。Description 写清楚,参数校验就省一半事。- 循环而不是一次调用:模型可能连续调用多个工具(先查天气再查路线),循环直到输出里没有
function_call为止。 PreviousResponseID续接:第二次请求不必重发全部历史,带上上次响应的 ID 即可,SDK 会自动串起上下文,也省 token。CallID必须对应:每个function_call有唯一CallID,回传结果时填错,模型会把结果张冠李戴。item.Type判断输出类型:resp.Output里混着消息、工具调用等多种 item,只有Type == "function_call"的才需要执行。
三、生产环境的四个注意
- 参数校验不能省:模型是概率机器,
arguments可能缺字段、类型不对。示例里对解析失败回传了{"error": ...},让模型自己纠错重试,比直接 panic 优雅。 - 给循环加上限:正常情况下模型几轮就能收敛,但异常时可能无限调用。生产代码建议在
for上加maxRounds计数。 - 工具执行也可能是慢操作:查数据库、调外部 API 都会阻塞,记得用
context.WithTimeout控制总时长,避免一次请求拖死一个 goroutine。 - 危险操作加人工确认:如果工具是「发邮件」「删记录」这类写操作,不要模型说调就调。常见做法是让模型先输出确认文案,由用户点击确认后再执行真正操作(人机协同,系列后续 Agent 篇会展开)。
四、速查清单
| 概念 | 一句话解释 |
|---|---|
| Tool / Function | 你提供给模型的能力清单,用 JSON Schema 描述 |
| function_call | 模型输出的调用指令(函数名 + JSON 参数),不是答案 |
| function_call_output | 你回传给模型的执行结果 |
| PreviousResponseID | Responses API 的会话续接 ID,免去重发历史 |
| CallID | 每次调用的唯一编号,结果回传必须对应 |
版本说明
- 本文基于 openai-go v3.90.0(2026-10-30 核对,GitHub releases 最新 tag v3.90.0,发布于 2026-10-28);安装命令
go get github.com/openai/openai-go/v3@v3.90.0。 - 该版本要求 Go 1.26 或更高版本(官方 README 与 go.mod 核实;若项目停留在 Go 1.25,需 pin SDK v3.74.0)。
- 文中所有 API(
responses.ResponseNewParams、responses.ToolUnionParam、FunctionToolParam、AsFunctionCall()、PreviousResponseID等)均逐字对照官方 README「Tool calling」示例与 v3.90.0 源码核实。 - 官方文档:pkg.go.dev/github.com/openai/openai-go/v3 · github.com/openai/openai-go。
本文是「Go AI 应用开发」系列第 2 篇(共 18 篇规划)。上一篇:《大模型 API 基础:用 Go 发起你的第一次 LLM 调用》;下一篇预告:《Embedding 与语义检索原理(Go 实现)》——让程序拥有「语义理解」的钥匙。Go 并发基础薄弱的读者可以同步复习站内 《Go Channel 底层实现全解析》 与 《Context 详解》。
🛒 站长推荐(京东联盟)
- 《Go 程序设计语言》(Go 语言圣经,机械工业出版社) —— Go 官方团队出品,公认入门第一书
- 《Go 语言实战》(异步图书,人民邮电出版社) —— 实战导向,面试前巩固工程手感
- 《Go 语言编程之旅:一起用 Go 做项目》(博文视点,电子工业出版社) —— 项目驱动入门,从零写出完整应用
- Keychron K1Max 矮轴机械键盘(Mac/Win 双系统,QMK/VIA 开源改键) —— 轻薄矮轴 + 开源固件,程序员桌面常客
- 罗技 MX Master 3S 无线鼠标(Flow 跨屏,静音电磁滚轮) —— 人体工学设计,写代码一整天手不酸
以上链接含京东联盟推广代码;通过链接购买本站可能获得少量佣金,不影响你的购买价格。