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 回传 ──▶ 模型基于真实数据生成最终回答

三个关键认知:

  1. 模型不产生数据,只产生调用意图。arguments 是模型「猜」出来的 JSON,可能字段缺失、格式错误,生产代码必须校验(后文有处理)。
  2. 模型不知道函数真的被执行了。你回传什么,它就信什么——所以模拟数据也能跑通整个流程(本文示例正是如此,没有 API Key 时可以用 mock 客户端学习结构)。
  3. 一轮可以并发返回多个 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 代码里的五个关键点

  1. Tools 字段是「注册表」:Name + Description + Parameters(JSON Schema) 三件事决定模型会不会、以及怎么调用你的函数。Description 写清楚,参数校验就省一半事。
  2. 循环而不是一次调用:模型可能连续调用多个工具(先查天气再查路线),循环直到输出里没有 function_call 为止。
  3. PreviousResponseID 续接:第二次请求不必重发全部历史,带上上次响应的 ID 即可,SDK 会自动串起上下文,也省 token。
  4. CallID 必须对应:每个 function_call 有唯一 CallID,回传结果时填错,模型会把结果张冠李戴。
  5. item.Type 判断输出类型:resp.Output 里混着消息、工具调用等多种 item,只有 Type == "function_call" 的才需要执行。

三、生产环境的四个注意

  1. 参数校验不能省:模型是概率机器,arguments 可能缺字段、类型不对。示例里对解析失败回传了 {"error": ...},让模型自己纠错重试,比直接 panic 优雅。
  2. 给循环加上限:正常情况下模型几轮就能收敛,但异常时可能无限调用。生产代码建议在 for 上加 maxRounds 计数。
  3. 工具执行也可能是慢操作:查数据库、调外部 API 都会阻塞,记得用 context.WithTimeout 控制总时长,避免一次请求拖死一个 goroutine。
  4. 危险操作加人工确认:如果工具是「发邮件」「删记录」这类写操作,不要模型说调就调。常见做法是让模型先输出确认文案,由用户点击确认后再执行真正操作(人机协同,系列后续 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 详解》。

🛒 站长推荐(京东联盟)

以上链接含京东联盟推广代码;通过链接购买本站可能获得少量佣金,不影响你的购买价格。

wx

关注公众号

©2017-2026 鲁ICP备17023316号-1 Powered by Hugo · 隐私政策