Function Calling 原理:让 AI 调用你的函数
一只用 AI Agent 搭副业产线的程序员上篇我们写了 Agent 的骨架——感知-决策-执行循环。但循环里最关键的一步没有展开AI 怎么知道该调用哪个函数很多人以为 Function Calling 是 API 自带的功能点了开关就行。其实不是。Function Calling 本质上就是「你告诉 AI 有哪些函数可以用AI 在合适的时机告诉你该调哪个」。这篇我们把这个过程拆到骨头。手写一遍你就彻底懂了。Function Calling 不是魔法是一份说明书整个过程分 4 步① 你定义 Tool Schema函数的说明书 ② 你把 Schema 塞进 API 请求 ③ AI 返回时告诉你「我要调用 tool_xxx参数是...」 ④ 你执行函数把结果再发回给 AI没有哪一步是 API 自动帮你做的。API 只负责第 3 步——告诉你该调什么。第 1、2、4 步全是你自己写的代码。完整实现packagemainimport(bytesencoding/jsonfmtionet/httposstringstime)// ───────── 1. 定义 Tool Schema ─────────typeToolstruct{Namestringjson:nameDescriptionstringjson:descriptionParametersmap[string]interface{}json:parameters}typeToolResultstruct{ToolNamestringSuccessboolDatastringErrorstring}// 定义一个「获取天气」的工具varweatherToolTool{Name:get_weather,Description:获取指定城市的当前天气信息,Parameters:map[string]interface{}{type:object,properties:map[string]interface{}{city:map[string]interface{}{type:string,description:城市名称例如 北京 或 Shanghai,},unit:map[string]interface{}{type:string,enum:[]string{celsius,fahrenheit},description:温度单位默认 celsius,},},required:[]string{city},},}// 定义一个「发送邮件」的工具varemailToolTool{Name:send_email,Description:发送一封邮件到指定地址,Parameters:map[string]interface{}{type:object,properties:map[string]interface{}{to:map[string]interface{}{type:string,description:收件人邮箱地址,},subject:map[string]interface{}{type:string,description:邮件主题,},body:map[string]interface{}{type:string,description:邮件正文,},},required:[]string{to,subject,body},},}// ───────── 2. 构建 API 请求 ─────────typeMessagestruct{Rolestringjson:roleContentstringjson:content}typeChatRequeststruct{Modelstringjson:modelMessages[]Messagejson:messagesTools[]Tooljson:tools}typeToolCallRequeststruct{IDstringjson:idTypestringjson:typeFuncstruct{Namestringjson:nameArgumentsstringjson:arguments// JSON 字符串}json:function}typeChatResponsestruct{Choices[]struct{Messagestruct{Rolestringjson:roleContentstringjson:contentToolCalls[]ToolCallRequestjson:tool_calls}json:message}json:choices}funccallLLMWithTools(messages[]Message,tools[]Tool)(ChatResponse,error){reqBody:ChatRequest{Model:deepseek-v4-pro,Messages:messages,Tools:tools,}data,_:json.Marshal(reqBody)req,_:http.NewRequest(POST,https://api.deepseek.com/anthropic/v1/chat/completions,bytes.NewReader(data))req.Header.Set(Authorization,Bearer os.Getenv(DEEPSEEK_API_KEY))req.Header.Set(Content-Type,application/json)client:http.Client{Timeout:30*time.Second}resp,err:client.Do(req)iferr!nil{returnChatResponse{},fmt.Errorf(API 调用失败: %w,err)}deferresp.Body.Close()body,_:io.ReadAll(resp.Body)varchatResp ChatResponseiferr:json.Unmarshal(body,chatResp);err!nil{returnChatResponse{},fmt.Errorf(解析响应失败: %w,err)}returnchatResp,nil}// ───────── 3. 执行本地函数 ─────────funcexecuteWeatherTool(argsmap[string]interface{})ToolResult{city,_:args[city].(string)unit:celsiusifu,ok:args[unit].(string);ok{unitu}// 模拟天气查询实际项目在这里调真实的天气 APIweatherData:fmt.Sprintf({city: %s, temperature: 22, condition: 晴, humidity: 55, unit: %s},city,unit,)returnToolResult{ToolName:get_weather,Success:true,Data:weatherData}}funcexecuteEmailTool(argsmap[string]interface{})ToolResult{to,_:args[to].(string)subject,_:args[subject].(string)body,_:args[body].(string)fmt.Printf( 模拟发送邮件:\n 收件人: %s\n 主题: %s\n 正文: %s\n,to,subject,body)returnToolResult{ToolName:send_email,Success:true,Data:fmt.Sprintf({status: sent, to: %s},to),}}funcexecuteTool(toolNamestring,argsJSONstring)ToolResult{varargsmap[string]interface{}iferr:json.Unmarshal([]byte(argsJSON),args);err!nil{returnToolResult{ToolName:toolName,Success:false,Error:fmt.Sprintf(参数解析失败: %v,err)}}switchtoolName{caseget_weather:returnexecuteWeatherTool(args)casesend_email:returnexecuteEmailTool(args)default:returnToolResult{ToolName:toolName,Success:false,Error:未知工具}}}// ───────── 4. 完整的 Agent 循环 ─────────funcmain(){messages:[]Message{{Role:system,Content:你是一个生活助手。当用户询问天气时调用 get_weather 工具。当需要发送邮件时调用 send_email 工具。},{Role:user,Content:北京今天天气怎么样如果气温超过 20 度发一封邮件给 bosscompany.com主题今日天气提醒正文北京今天很暖和。},}tools:[]Tool{weatherTool,emailTool}maxSteps:10forstep:0;stepmaxSteps;step{resp,err:callLLMWithTools(messages,tools)iferr!nil{fmt.Printf(❌ 第 %d 步出错: %v\n,step1,err)break}iflen(resp.Choices)0{fmt.Println(❌ 空响应)break}msg:resp.Choices[0].Message// 有工具调用 → 执行iflen(msg.ToolCalls)0{for_,tc:rangemsg.ToolCalls{fmt.Printf( AI 请求调用: %s(%s)\n,tc.Func.Name,tc.Func.Arguments)// 记录 AI 的工具调用请求messagesappend(messages,Message{Role:assistant,Content:fmt.Sprintf(调用工具 %s,tc.Func.Name),})// 执行本地函数result:executeTool(tc.Func.Name,tc.Func.Arguments)// 把结果发回给 AIresultMsg:fmt.Sprintf(工具 %s 返回: %s,result.ToolName,result.Data)if!result.Success{resultMsgfmt.Sprintf(工具 %s 执行失败: %s,result.ToolName,result.Error)}messagesappend(messages,Message{Role:user,Content:resultMsg})}continue}// 没有工具调用 → 最终答案fmt.Println(\n✅ AI 最终回复:)fmt.Println(msg.Content)break}}关键细节拆解1. Tool Schema 就是函数的说明书varweatherToolTool{Name:get_weather,Description:获取指定城市的当前天气信息,// 这句话最重要Parameters:map[string]interface{}{...},// JSON Schema 格式}AI 不知道你的代码。它只会根据你的Description和Parameters判断该不该用这个工具。Description 写得好不好决定了 AI 能不能正确地选对工具。2. AI 只做「决策」不做「执行」AI 返回的是{tool_calls:[{function:{name:get_weather,arguments:{\city\: \北京\}}}]}它说「我建议调 get_weather参数是北京」。真正执行get_weather的是你的代码。AI 没有权限访问你的文件系统、网络或数据库——除非你给它工具。3. 结果反馈是关键messagesappend(messages,Message{Role:user,Content:fmt.Sprintf(工具 %s 返回: %s,result.ToolName,result.Data),})执行完函数后把结果格式化成文本塞回消息历史。AI 读到结果后才能决定下一步。一个真实的坑参数幻觉有一次我定义了一个工具search_database参数query的描述我偷懒写成搜索条件。AI 传回来的参数是{query: 最近一周销量最高的产品}。这是个自然语言句子不是 SQL。教训Tool 的 Description 和参数定义要精确到「机器可执行的级别」。写法则是——如果参数是 SQL就在描述里写明「SQL 查询语句例如 SELECT * FROM orders WHERE…」别给 AI 自由发挥的空间。一眼看懂的流程你的代码 AI API │ │ ├─ 定义 Tool Schema │ ├─ 发送 [messages, tools] ───────→│ │ ├─ 分析用户意图 │ ├─ 决定用哪个 tool │ ←──── [tool_calls] ──────────┤ ├─ 解析 tool call │ ├─ 执行本地函数 │ ├─ 发送 [result] ────────────────→│ │ ├─ 理解结果 │ ├─ 决定是否继续 │ ←──── [final_answer] ────────┤ ├─ 输出给用户 │总结Function Calling 不是 API 替你执行的。它只是 AI 说「我建议调这个函数」——剩下的都是你的代码在做。理解了这一层你就能写出完全受控的 Agent。下一篇我们解决一个工程问题当你有 10 个 Tool 时怎么设计统一的接口怎么处理超时怎么让错误不炸掉整个 Agent关注我别错过。 一只用 AI Agent 搭副业产线的程序员全平台同名虾哥不加班需要定制 AI 工具来聊聊 → lob_ai源码GitHub - lobster-bujiaban