🦞 一只用 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("bytes""encoding/json""fmt""io""net/http""os""strings""time")// ───────── 1. 定义 Tool Schema ─────────typeToolstruct{Namestring`json:"name"`Descriptionstring`json:"description"`Parametersmap[string]interface{}`json:"parameters"`}typeToolResultstruct{ToolNamestringSuccessboolDatastringErrorstring}// 定义一个「获取天气」的工具varweatherTool=Tool{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"},},}// 定义一个「发送邮件」的工具varemailTool=Tool{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{Rolestring`json:"role"`Contentstring`json:"content"`}typeChatRequeststruct{Modelstring`json:"model"`Messages[]Message`json:"messages"`Tools[]Tool`json:"tools"`}typeToolCallRequeststruct{IDstring`json:"id"`Typestring`json:"type"`Funcstruct{Namestring`json:"name"`Argumentsstring`json:"arguments"`// JSON 字符串}`json:"function"`}typeChatResponsestruct{Choices[]struct{Messagestruct{Rolestring`json:"role"`Contentstring`json:"content"`ToolCalls[]ToolCallRequest`json:"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:="celsius"ifu,ok:=args["unit"].(string);ok{unit=u}// 模拟天气查询(实际项目在这里调真实的天气 API)weatherData:=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{case"get_weather":returnexecuteWeatherTool(args)case"send_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 度,发一封邮件给 boss@company.com,主题'今日天气提醒',正文'北京今天很暖和'。"},}tools:=[]Tool{weatherTool,emailTool}maxSteps:=10forstep:=0;step<maxSteps;step++{resp,err:=callLLMWithTools(messages,tools)iferr!=nil{fmt.Printf("❌ 第 %d 步出错: %v\n",step+1,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 的工具调用请求messages=append(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{resultMsg=fmt.Sprintf("工具 %s 执行失败: %s",result.ToolName,result.Error)}messages=append(messages,Message{Role:"user",Content:resultMsg})}continue}// 没有工具调用 → 最终答案fmt.Println("\n✅ AI 最终回复:")fmt.Println(msg.Content)break}}关键细节拆解
1. Tool Schema 就是函数的说明书
varweatherTool=Tool{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. 结果反馈是关键
messages=append(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