)
Agent 保姆级万字详解·第三章Function Call让 AI 真正动手干活上一章咱们聊清楚了 LLM 的三大硬伤——幻觉、知识滞后、只能输出文字动不了手。也聊了 Agent 的核心公式Agent LLM 规划 记忆 工具。工具就是给大脑装上的手脚没有工具的 Agent 就像一个被困在玻璃瓶里的巨人看着外面精彩的世界干着急。那问题来了怎么让 LLM 能够看见并调用这些工具呢答案就是今天这章的主题——Function Call中文叫函数调用。这是 Agent 落地的第一块基石搞不懂这个后面讲 MCP、讲 Skills 都是空中楼阁。Function Call 到底是什么咱先来一个场景。想象你是个公司的老板请了一个超级顾问。这个顾问智商爆表学富五车什么问题都能给你分析得头头是道。唯一的毛病是——他只会说一口流利的学术英语而你公司里的员工只听得懂中文。你们俩之间完全没法直接沟通。这时候你怎么办请个翻译啊。翻译坐在你们俩中间把你的中文问题翻译成顾问能理解的英文问题把顾问的英文回答再翻译成你能听懂的中文。来来回回翻译几次问题就解决了。Function Call 在 LLM 和外部工具之间扮演的就是这个翻译官的角色。LLM 很聪明它知道查天气是什么意思知道调用 weather API是一个合理的行动。但它不知道怎么写 HTTP 请求不知道 API 的地址是什么不知道参数该怎么组织。Function Call 的作用就是让 LLM 输出一个结构化的指令我要调用哪个函数、参数是什么然后开发者的代码负责真正去执行这个函数把结果返回给 LLM让 LLM 把结果组织成人类能听懂的话。LLM 不需要自己会游泳它只需要知道这个人会游泳可以委托给他。Function Call 就是那个告诉 LLM 谁会游泳的机制。Function Call 的完整闭环光知道比喻还不够咱得把 Function Call 的技术流程掰开了讲。这个流程是整个 Agent 系统的核心骨架你得把它刻进脑子里。第一步用户发起请求用户跟 AI 说“明天北京天气怎么样”这是整个链条的起点。用户用自然语言提出需求可能是问天气、查订单、发邮件、做计算——什么都有可能。第二步LLM 判断要不要调用工具这一步是 Function Call 的精髓。用户说明天北京天气怎么样LLM 拿到这个请求之后它会做几件事看工具列表你的系统给 LLM 提供了一份工具清单清单上写着weather_tool查天气需要 city 参数。LLM 先把这个清单看一遍。判断意图LLM 理解用户想要查天气这正好匹配weather_tool。提取参数LLM 从用户的问句里提取出参数值——北京就是 city 参数的值。输出结构化指令LLM 不是直接回答用户而是输出一个结构化的 JSON大致长这样{name:get_weather,arguments:{city:北京}}这个 JSON 就是 Function Call 的核心产物。它告诉开发者“我要调用 get_weather参数 city 是北京。”LLM 本身不知道 get_weather 具体怎么实现但它准确地识别出了用户需要这个工具并且正确地提取了参数。这就是 Function Call 的神奇之处——它把理解要做什么LLM 的强项和真正去做代码的强项解耦开了。第三步开发者执行函数LLM 输出 Function Call 指令之后控制权交回给开发者的代码。你的代码拿到这个 JSON 之后要做几件事解析函数名看看 LLM 想调哪个工具提取参数把参数从 JSON 里拿出来真正执行调用天气 API获取真实数据格式化结果把 API 返回的数据整理成文本准备反馈给 LLM这一步完全由开发者控制。你可以让代码真的去调天气 API也可以做 mock模拟返回数据用于测试一切都由你说了算。LLM 只负责下指令不负责执行。第四步把结果返回给 LLM执行完函数之后开发者把结果塞回 LLM 的上下文中告诉它“get_weather 返回了结果北京明天晴气温15到22度。”这里有个关键的交互模式需要注意LLM 在收到 Function Call 结果之后会再次思考然后决定怎么回复用户。它可能会直接说北京明天晴天15到22度也可能加一些温馨提示建议穿薄外套——这些说人话的部分都由 LLM 来完成。第五步LLM 组织最终回答最后一步LLM 把结果翻译成用户能听懂的话。这就是 Function Call 的完整闭环用户提问 → LLM 判断是否调用工具 输出函数名和参数 → 开发者代码执行函数 → 结果返回给 LLM → LLM 组织自然语言回答用户整个过程中LLM 和代码各司其职LLM 做理解、做推理、做决策代码做执行、做 IO、做真实世界的数据交互。两者配合天衣无缝。实战用 Go 实现一个天气查询 Function Call说完了理论咱来点实际的。下面这段代码基于 Go 1.22调用兼容 OpenAI 格式的 API。为了避免手写 SSE 流式解析的坑我们直接使用目前最流行的开源库——github.com/sashabaranov/go-openai。这个库在 GitHub 上有将近两万颗星维护活跃基本 cover 了所有 OpenAI 兼容接口的细节。你只需要替换 API Key 和地址就能直接跑起来。代码分三块讲结构定义 → 核心逻辑 → main 入口。第一部分安装依赖go mod init weather-agent go get github.com/sashabaranov/go-openailatest第二部分完整可运行代码packagemainimport(contextfmtosgithub.com/sashabaranov/go-openai)// executeFunction 根据函数名执行对应的真实逻辑// 这里演示了如何处理天气查询的函数调用funcexecuteFunction(functionNamestring,argumentsstring)(string,error){switchfunctionName{caseget_weather:// 解析参数提取城市名// go-openai 会把参数解析好传过来这里直接用字符串解析即可// 真实场景里可以用 json.Unmarshal 反序列化returnfetchWeather(arguments)default:return,fmt.Errorf(未知函数: %s,functionName)}}// fetchWeather 模拟调用天气 API// 真实项目中这里应该调用和风天气、OpenWeatherMap 等真实接口funcfetchWeather(citystring)(string,error){// 简化处理从参数字符串中提取城市名// 实际应用中建议用 json 反序列化得到干净的 city 字符串weatherDB:map[string]string{北京:北京晴气温15~22°C西北风2-3级空气质量优,上海:上海多云气温18~25°C东风1-2级空气质量良,广州:广州雷阵雨气温24~30°C南风3-4级空气质量中,深圳:深圳阴天气温22~28°C东南风2级空气质量良,}ifweather,ok:weatherDB[city];ok{returnweather,nil}returnfmt.Sprintf(%s暂无天气数据,city),nil}// buildToolDefinitions 构建工具列表// 这里统一定义所有可以被 LLM 调用的工具// go-openai 的 Tool 结构已经很清晰了直接照着写就行funcbuildToolDefinitions()[]openai.Tool{return[]openai.Tool{{Type:openai.ToolTypeFunction,Function:openai.FunctionDefinition{Name:get_weather,Description:查询指定城市的天气信息包括温度、天气状况、风力、空气质量等,Parameters:openai.FunctionJSONParameters{Type:object,Properties:map[string]openai.FunctionJSONProperty{city:{Type:string,Description:城市名称用中文如北京、上海、广州、深圳,},},Required:[]string{city},},},},}}funcmain(){// ---------- 1. 读取配置 ----------apiKey:os.Getenv(OPENAI_API_KEY)apiURL:os.Getenv(OPENAI_API_URL)ifapiKey{fmt.Println(【错误】请设置环境变量 OPENAI_API_KEY)return}// 如果没有设置代理地址默认使用 OpenAI 官方地址// 国产模型请替换为对应的兼容端点地址如硅基流动等ifapiURL{apiURLhttps://api.openai.com/v1}// 创建 OpenAI 客户端// go-openai 支持自定义 base URL可以兼容任何 OpenAI 格式的 APIcfg:openai.DefaultConfig(apiKey)cfg.BaseURLapiURL client:openai.NewClientWithConfig(cfg)fmt.Println(【用户提问】明天北京天气怎么样需要穿外套吗)fmt.Println(【等待 LLM 响应...】\n)// ---------- 2. 构造对话上下文 ----------// 初始化对话历史第一条是用户的提问messages:[]openai.ChatCompletionMessage{{Role:openai.ChatMessageRoleUser,Content:明天北京天气怎么样需要穿外套吗,},}// 工具列表告诉 LLM 它可以调用哪些函数tools:buildToolDefinitions()// ---------- 3. 调用 LLM ----------ctx:context.Background()// 第一次请求resp,err:client.Chat(ctx,openai.ChatCompletionRequest{Model:gpt-4,Messages:messages,Tools:tools,})iferr!nil{fmt.Printf(【错误】LLM 请求失败: %v\n,err)return}// 获取 LLM 的回复assistantMsg:resp.Choices[0].Message fmt.Printf(【LLM 回复内容】%v\n,assistantMsg)// 把助手的消息加到对话历史里messagesappend(messages,assistantMsg)// ---------- 4. 检查是否触发了 Function Call ----------// 如果 LLM 返回了 tool_calls说明它想调用工具ifassistantMsg.ToolCalls!nillen(assistantMsg.ToolCalls)0{// 遍历所有要调用的工具通常一次只调一个for_,call:rangeassistantMsg.ToolCalls{funcName:call.Function.Name funcArgs:call.Function.Arguments fmt.Printf(\n【Function Call 触发】调用函数: %s参数: %s\n,funcName,funcArgs)// 执行真正的函数逻辑result,err:executeFunction(funcName,funcArgs)iferr!nil{fmt.Printf(【错误】执行函数失败: %v\n,err)continue}fmt.Printf(【函数执行结果】%s\n,result)// 把工具执行结果追加到对话上下文// 注意role 必须是 toolcontent 是执行结果tool_call_id 要对应上messagesappend(messages,openai.ChatCompletionMessage{Role:openai.ChatMessageRoleTool,Content:result,ToolCallID:call.ID,})}// ---------- 5. 把工具结果再次发给 LLM让它组织最终回答 ----------fmt.Println(\n【再次请求 LLM组织最终回答】)resp2,err:client.Chat(ctx,openai.ChatCompletionRequest{Model:gpt-4,Messages:messages,Tools:tools,})iferr!nil{fmt.Printf(【错误】LLM 二次请求失败: %v\n,err)return}finalAnswer:resp2.Choices[0].Message.Content fmt.Printf(\n【LLM 最终回答】%s\n,finalAnswer)}else{// 如果没有触发 Function Call直接输出 LLM 的回复fmt.Printf(\n【LLM 直接回答】%s\n,assistantMsg.Content)}}代码运行说明这段代码要跑起来一共就三步安装依赖需要 Go 1.21go mod init weather-agent go get github.com/sashabaranov/go-openailatest设置环境变量# Linux / macOSexportOPENAI_API_KEY你的真实密钥exportOPENAI_API_URLhttps://api.openai.com/v1# 官方地址国产模型填对应的兼容端点# Windows PowerShell$env:OPENAI_API_KEY你的真实密钥$env:OPENAI_API_URLhttps://api.openai.com/v1运行go run main.go如果一切正常你会看到完整的输出【用户提问】明天北京天气怎么样需要穿外套吗 【等待 LLM 响应...】 【LLM 回复内容】{Role:assistant Content: Tools:0xc000... ToolCalls:[0xc000...]} 【Function Call 触发】调用函数: get_weather参数: {city:北京} 【函数执行结果】北京晴气温15~22°C西北风2-3级空气质量优 【再次请求 LLM组织最终回答】 【LLM 最终回答】根据查询结果明天北京是晴天气温在15到22度之间早晚温差较大建议带一件薄外套出门。整个流程就是这样LLM 判断要查天气 → 代码执行查天气的真实逻辑 → 把结果反馈给 LLM → LLM 组织了一段贴心的回答。代码里用到的关键点我逐一解释一下openai.Tool这是 go-openai 封装的工具定义结构体对应 OpenAI 的tools参数。把工具的名称、描述、参数 schema 填好LLM 就知道这个工具是干什么的、怎么调用。ToolCalls当 LLM 决定调用工具时它会返回一个tool_calls数组里面有函数名function.name和参数function.arguments是 JSON 字符串。注意这里是 JSON 字符串不是 map你需要二次解析。openai.ChatMessageRoleTool工具执行完之后的返回消息role 必须填tool并且要带上tool_call_id告诉 LLM 这个结果是哪个调用的返回值。两轮请求这是 Function Call 的标准模式。第一轮 LLM 分析用户意图并决定调用工具第二轮带着工具的执行结果再发一次请求LLM 才能看到结果并组织最终回答。Function Call 的局限性Function Call 很好用但它不是银弹。在实际项目中你会发现几个让人头疼的问题。第一个问题每个模型厂商的 Function Call 实现不一样。OpenAI 有 OpenAI 的格式Claude 有 Claude 的格式国产模型又是另一套。你在这个模型上写的工具定义换到那个模型上可能就跑不通了。同一个 Function Call你得写多套适配代码维护成本直接翻倍。第二个问题工具定义没法复用。你给模型 A 定义了一套天气查询工具现在想给模型 B 也加上同样的能力对不起你得重新写一遍。工具的定义和模型是紧耦合的换模型就得重来。第三个问题缺乏统一的标准协议。现在的 Agent 开发有点像互联网早期——每家网站自己定义自己的协议互相不兼容。你做一个工具只能给自己的 Agent 用想给别人用不好意思重写一遍吧。这三个问题听起来很眼熟对不对没错当年的 HTTP 协议就是为了解决各家网站协议不兼容的问题而诞生的。那么AI 时代的HTTP 协议是什么答案就是下一章我们要讲的——MCP 协议Model Context Protocol。这是由 Anthropic 主推的一个开放标准目标是让 AI 模型和工具之间有一个统一的接口规范彻底解决互操作性的问题。MCP 到底是怎么回事它跟 Function Call 是什么关系企业落地应该怎么用下一章全部给你讲透。三连催更咱们不见不散作者利威尔xu一个正在死磕 AI 应用落地、热爱分享的普通后端开发。如果这篇文章对你有帮助欢迎点赞、收藏、关注更多硬核内容持续更新中。