在 langchaingo 中组合使用 OpenAI Function Calling 与流式响应:以 GPT-4 Turbo 天气查询示例为例
【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo
导读
本篇技术指南以 langchaingo 官方示例 openai-function-call-streaming-example 为主线,讲解如何在一个 Go 程序中同时启用 OpenAI 的函数调用(Function Calling)与流式输出(Streaming):既能通过llms.WithTools向模型注册天气查询等外部工具,又能借助llms.WithStreamingFunc逐块实时打印模型生成内容,并在响应到达后从ContentResponse.Choices[0].FuncCall检测模型发起的函数调用。读完本文,你将掌握 langchaingo 中「工具声明 → 请求组装 → 流式回调 → 函数调用检测」的完整调用链,并理解其背后的类型定义与底层实现原理。
示例概览:这个程序做了什么
示例文件 openai_function_call_example.go 的核心流程可拆解为五个步骤:
- 模型初始化:通过
openai.New(openai.WithModel("gpt-4-turbo"))连接 OpenAI 的 GPT-4 Turbo 模型; - 工具定义:声明三个可供模型调用的函数工具——
getCurrentWeather(获取某地当前天气)、getTomorrowWeather(获取某地预报天气)、getSuggestedPrompts(根据用户输入生成相关建议提示词); - 用户查询:向模型发送问题 "What is the weather like in Boston?";
- 流式响应:注册流式回调函数,把模型每次生成的文本块实时打印到控制台;
- 函数调用检测:请求完成后检查
resp.Choices[0].FuncCall,若模型决定调用函数则打印该调用信息。
整体设计体现了 langchaingo 的一个典型用法:把 LLM 当作"决策者",让它通过工具声明决定是否调用外部能力,同时以流式方式观察其生成过程。
环境准备与依赖
示例位于examples/openai-function-call-streaming-example目录,其 go.mod 声明:
module github.com/tmc/langchaingo/examples/openai-function-call-streaming-example go 1.24.3 require github.com/tmc/langchaingo v0.1.14-pre.4运行前需设置 OpenAI API Key。langchaingo 的 OpenAI 实现会从环境变量读取凭证,缺少时返回错误"missing the OpenAI API key, set it in the OPENAI_API_KEY environment variable"(见 llms/openai/llm.go 中的tokenEnvVarName逻辑)。启动方式:
export OPENAI_API_KEY=your_api_key_here go run openai_function_call_example.go模型初始化与请求组装
创建模型实例
llm, err := openai.New(openai.WithModel("gpt-4-turbo")) if err != nil { log.Fatal(err) }openai.WithModel用于指定模型名称。从 llms/openai/openaillm.go 的实现可以看到,每次请求时opts.Model会优先覆盖实例默认模型(effectiveModel := opts.Model; if effectiveModel == "" { effectiveModel = o.model }),也就是说模型既可以在创建实例时指定,也可以在单次请求中通过 CallOption 覆盖。
构造用户消息
ctx := context.Background() resp, err := llm.GenerateContent(ctx, []llms.MessageContent{ llms.TextParts(llms.ChatMessageTypeHuman, "What is the weather like in Boston?"), }, llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error { fmt.Printf("Received chunk: %s\n", chunk) return nil }), llms.WithTools(tools))这里用到了三个关键 API:
llms.MessageContent:一条发给模型的聊天消息,包含Role(消息角色)与Parts(内容片段序列),定义于 llms/generatecontent.go;llms.TextParts:便捷构造函数,将若干字符串打包成指定角色的MessageContent(llms/generatecontent.go)。其中llms.ChatMessageTypeHuman表示人类消息,其值为"human"(见 llms/chat_messages.go,同一枚举还包含ai、system、tool等角色);llms.GenerateContent:langchaingo 各模型统一实现的内容生成入口,签名兼容多模态(文本、图片 URL、二进制内容等均可作为 Part)。
流式响应:WithStreamingFunc 的原理
回调注册
llms.WithStreamingFunc定义于 llms/options.go:
// WithStreamingFunc specifies the streaming function to use. func WithStreamingFunc(streamingFunc func(ctx context.Context, chunk []byte) error) CallOption {它接收一个回调函数,每次模型产出新的文本块(chunk)时被调用一次。示例中的实现仅做打印:
llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error { fmt.Printf("Received chunk: %s\n", chunk) return nil }),底层流转
从源码结构看,流式响应的底层链路为:
llms.GenerateContent解析 CallOption 后,将opts.StreamingFunc传递给 OpenAI 客户端(llms/openai/openaillm.go 处的StreamingFunc: opts.StreamingFunc);- OpenAI 客户端在 llms/openai/internal/openaiclient/chat.go 检测到
payload.StreamingFunc != nil时切换到流式请求模式,并在 chat.go 对每个到达的 SSE 数据块执行payload.StreamingFunc(ctx, chunk); - 最终每个 chunk 回到示例的回调函数中,被实时打印。
因此,启用流式输出无需改动业务代码结构,只需通过 CallOption 注入回调——这是 langchaingo 设计上的一致性体现:同一套GenerateContent接口,同步与流式仅是选项差异。
流式与函数调用同时生效
值得注意:WithStreamingFunc与WithTools可以同时传入同一个GenerateContent调用。在流式模式下,函数调用的参数通常也是通过流式块逐步返回的;WithStreamingFunc回调中的chunk内容是原始文本块,如需在流式过程中解析函数调用参数,可以在回调内自行累积拼接 JSON。而示例选择了更简洁的路径:流式只用于观察生成过程,函数调用结果则等请求完成后统一读取。
工具声明:WithTools 与函数定义
工具集合
示例通过llms.WithTools(tools)注册三个工具,tools是[]llms.Tool切片(llms/options.go 中WithTools的签名即为func WithTools(tools []Tool) CallOption)。其中Tool结构定义于 llms/options.go:
// Tool is a tool that can be used by the model. type Tool struct { // Type is the type of the tool. Type string `json:"type"` // Function is the function to call. Function *FunctionDefinition `json:"function,omitempty"` }Type固定为"function",Function则指向具体的函数定义。FunctionDefinition(llms/options.go)包含四个字段:
type FunctionDefinition struct { Name string `json:"name"` // 函数名,如 getCurrentWeather Description string `json:"description"` // 对模型描述该函数何时被调用 Parameters any `json:"parameters,omitempty"` // JSON Schema 参数定义 Strict bool `json:"strict,omitempty"` // 严格模式(结构化输出保证,需提供商支持) }三个示例函数
getCurrentWeather:获取指定地点的当前天气。
{ Type: "function", Function: &llms.FunctionDefinition{ Name: "getCurrentWeather", Description: "Get the current weather in a given location", Parameters: jsonschema.Definition{ Type: jsonschema.Object, Properties: map[string]jsonschema.Definition{ "rationale": { Type: jsonschema.String, Description: "The rationale for choosing this function call with these parameters", }, "location": { Type: jsonschema.String, Description: "The city and state, e.g. San Francisco, CA", }, "unit": { Type: jsonschema.String, Enum: []string{"celsius", "fahrenheit"}, }, }, Required: []string{"rationale", "location"}, }, }, },getTomorrowWeather:获取指定地点的明日预报,参数结构与getCurrentWeather一致(rationale、location、unit)。
getSuggestedPrompts:根据用户输入生成相关建议提示词,参数中包含数组类型suggestions:
"suggestions": { Type: jsonschema.Array, Items: &jsonschema.Definition{ Type: jsonschema.String, Description: "A suggested prompt", }, },三个函数共同展示了 langchaingo 参数定义的几个要点:
- 参数使用JSON Schema风格声明,类型常量(
jsonschema.Object、jsonschema.String、jsonschema.Array)来自仓库的 jsonschema 包; - 每个参数可附带
Description,用于指导模型正确填充参数值; - 可通过
Enum限定取值范围(如温度单位仅允许celsius或fahrenheit); - 可通过
Required指定必填字段; - 数组类型用
Items描述元素结构。
示例代码中还保留了一段被注释掉的原始 JSON Schema 写法(json.RawMessage(...)),说明参数声明支持从任意 JSON Schema 形态转换为jsonschema.Definition结构体,两种方式均可工作。
本地函数实现
虽然模型只会"声明"要调用哪个函数,但真正执行逻辑仍由本地代码完成。示例的getCurrentWeather是模拟实现,返回 JSON 格式的天气数据:
func getCurrentWeather(location string, unit string) (string, error) { weatherInfo := map[string]interface{}{ "location": location, "temperature": "72", "unit": unit, "forecast": []string{"sunny", "windy"}, } b, err := json.Marshal(weatherInfo) if err != nil { return "", err } return string(b), nil }函数调用检测:读取 FuncCall
请求完成后,示例通过以下代码检测模型是否发起了函数调用:
choice1 := resp.Choices[0] if choice1.FuncCall != nil { fmt.Printf("Function call: %v\n", choice1.FuncCall) }resp是*llms.ContentResponse(llms/generatecontent.go),其Choices是[]*ContentChoice切片。ContentChoice(llms/generatecontent.go)包含以下关键字段:
type ContentChoice struct { Content string // 模型的文本回复 StopReason string // 停止生成的原因 GenerationInfo map[string]any // 模型附加的任意信息 FuncCall *FunctionCall // 非 nil 表示模型请求调用某个函数/工具 ToolCalls []ToolCall // 模型请求调用的工具调用列表 ReasoningContent string // 推理模型的思考内容(如 deepseek-reasoner) }而FunctionCall(llms/generatecontent.go)只包含两个字段——函数名与参数 JSON 字符串:
type FunctionCall struct { Name string `json:"name"` // 要调用的函数名 Arguments string `json:"arguments"` // 参数,JSON 字符串 }由于示例只传入一个用户问题且未提供多轮对话,正常情况下模型会生成一个 Choice;取出Choices[0]后判断FuncCall是否为 nil 即可知道模型是否决定调用函数。若模型返回的是ToolCalls(多工具调用场景),可遍历该切片逐个解析;每个ToolCall携带ID、Type(通常为"function")以及内嵌的FunctionCall(llms/generatecontent.go)。
说明:
ContentChoice的注释明确指出,当模型一次发起多个工具调用时,FuncCall字段只包含第一个;完整列表应通过ToolCalls获取(见 llms/generatecontent.go)。
完整调用链小结
将上述内容串联,示例的完整执行路径为:
openai.New(WithModel("gpt-4-turbo")) // ① 初始化模型 │ llm.GenerateContent(ctx, messages, // ② 发起请求 WithStreamingFunc(print chunk), // 注入流式回调 WithTools(tools)) // 注入工具声明 │ ├── 流式路径:opts.StreamingFunc → OpenAI 客户端 │ 逐个 chunk 回调 → fmt.Printf 实时打印 │ └── 返回路径:*llms.ContentResponse ├── resp.Choices[0].Content → 文本回复 └── resp.Choices[0].FuncCall → 非 nil 则打印函数调用常见问题与延伸
- 为什么流式回调打印的 chunk 不是完整句子?这是流式(SSE)的本质——模型按 token 逐步返回内容,回调每收到一块就触发一次。这正是示例所强调的"实时观察 AI 生成过程"(见 README)。
- 如何真正执行模型请求的函数?检测到
FuncCall后,需要自己写一个switch fn.Name分发到本地实现(如示例中的getCurrentWeather),再将执行结果以ToolCallResponse形式作为下一轮消息回传给模型。langchaingo 的 agents 包(agents)提供了更完整的 Agent 循环封装,agents目录下的 openai_functions_agent.go 即展示了自动化的工具执行流程。 - 函数调用相关选项:除了
WithTools,langchaingo 还提供WithToolChoice(强制/指定使用某个工具,见 llms/options.go)与已废弃的WithFunctions(建议改用WithTools,见 llms/options.go)。 - 多工具同时调用:当模型返回
ToolCalls列表时,应遍历处理而非只读FuncCall;这两个字段的差异详见前文ContentChoice结构说明。
结语
这个示例虽然代码量不大,却浓缩了 langchaingo 与 OpenAI 交互中最实用的两个能力:函数调用让 LLM 具备与外部系统协作的能力,流式响应让生成过程对用户实时可见。通过WithTools声明 JSON Schema 化的函数签名、用WithStreamingFunc挂载逐块回调、最后从ContentResponse中解析FuncCall,即可在一套简洁的 API 之上组合出具备实时反馈和工具协作能力的 Go LLM 应用。以此为起点,可进一步研究仓库中的 agents 模块与 openai_functions_agent_test.go 中的完整 Agent 循环实现,将"检测函数调用"升级为"自动执行并继续对话"的闭环。
【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考