第5讲:Function Calling 实战——让 AI 真的能帮你干活

发布时间:2026/10/30 4:53:59
第5讲:Function Calling 实战——让 AI 真的能帮你干活 一、为什么需要 Function Calling没有 Function Calling 的 AI ┌──────────────────────────────────────────────┐ │ 用户帮我查一下订单 OD20251027001 的物流状态 │ │ LLM抱歉我无法查询实时物流信息。 │ │ 用户那帮我发一封邮件给张三 │ │ LLM我没有发送邮件的权限。 │ └──────────────────────────────────────────────┘ 有了 Function Calling 的 AI ┌──────────────────────────────────────────────┐ │ 用户帮我查一下订单 OD20251027001 的物流状态 │ │ │ │ LLM 内部用户想查物流 → 匹配到 queryLogistics │ │ 函数 → 生成参数 {orderId: ...} │ │ → 调用你的代码 → 拿到结果 → 组织回答 │ │ │ │ LLM订单 OD20251027001 目前状态已发货 │ │ 预计明天下午到达快递员王师傅 138xxxx │ └──────────────────────────────────────────────┘Function CallingLLM 根据用户意图自主决定调用你预先注册的函数并把结果融入回答。二、整体架构用户输入 │ ▼ ┌──────────────────────────────────────────────┐ │ LLM 网关 │ │ 1. 发送用户消息 已注册的 Tool 列表给 LLM │ │ 2. LLM 判断是否需要调用函数 │ │ 3. 如果需要 → 返回函数名 参数 │ │ 4. 不需要 → 直接返回文本回答 │ └──────────────────────┬───────────────────────┘ │ ┌───────▼───────┐ │ 需要调函数 │ └───────┬───────┘ ╱ ╲ ╱ ╲ 是 否 │ │ ▼ ▼ ┌─────────────────┐ ┌──────────┐ │ Tool 执行引擎 │ │ 直接返回 │ │ ┌─────────────┐│ │ 文本回答 │ │ │ 参数校验 ││ └──────────┘ │ │ 函数路由 ││ │ │ 执行并捕获 ││ │ │ 结果格式化 ││ │ └─────────────┘│ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ 结果送回 LLM │ │ LLM 基于结果 │ │ 生成自然语言回答 │ └─────────────────┘三、Tool 定义规范// fcall/tool.go package fcall import ( context encoding/json fmt reflect strings ) // Tool 工具定义 type Tool struct { Name string json:name // 函数名全局唯一 Description string json:description // 描述LLM 据此判断何时调用 Parameters interface{} json:parameters // JSON Schema 格式的参数定义 Handler ToolHandler json:- // 实际的 Go 函数 } // ToolHandler 工具处理函数签名 type ToolHandler func(ctx context.Context, args json.RawMessage) (interface{}, error) // ToolCall LLM 返回的工具调用指令 type ToolCall struct { ID string json:id Type string json:type // function Name string json:name Args json.RawMessage json:args // JSON 格式的参数 } // ToolResult 工具执行结果 type ToolResult struct { ToolCallID string json:tool_call_id Name string json:name Result interface{} json:result Error string json:error,omitempty } // ─── 参数 Schema 构建器 ─── // ParamBuilder 参数 Schema 构建器 type ParamBuilder struct { properties map[string]interface{} required []string } func NewParamBuilder() *ParamBuilder { return ParamBuilder{ properties: make(map[string]interface{}), required: make([]string, 0), } } func (b *ParamBuilder) StringProperty(name, description string, required bool) *ParamBuilder { prop : map[string]interface{}{ type: string, description: description, } b.properties[name] prop if required { b.required append(b.required, name) } return b } func (b *ParamBuilder) NumberProperty(name, description string, required bool) *ParamBuilder { prop : map[string]interface{}{ type: number, description: description, } b.properties[name] prop if required { b.required append(b.required, name) } return b } func (b *ParamBuilder) IntegerProperty(name, description string, required bool) *ParamBuilder { prop : map[string]interface{}{ type: integer, description: description, } b.properties[name] prop if required { b.required append(b.required, name) } return b } func (b *ParamBuilder) BooleanProperty(name, description string, required bool) *ParamBuilder { prop : map[string]interface{}{ type: boolean, description: description, } b.properties[name] prop if required { b.required append(b.required, name) } return b } func (b *ParamBuilder) ArrayProperty(name, description string, itemsType string, required bool) *ParamBuilder { prop : map[string]interface{}{ type: array, description: description, items: map[string]string{ type: itemsType, }, } b.properties[name] prop if required { b.required append(b.required, name) } return b } func (b *ParamBuilder) EnumProperty(name, description string, enum []string, required bool) *ParamBuilder { prop : map[string]interface{}{ type: string, description: description, enum: enum, } b.properties[name] prop if required { b.required append(b.required, name) } return b } func (b *ParamBuilder) Build() map[string]interface{} { schema : map[string]interface{}{ type: object, properties: b.properties, } if len(b.required) 0 { schema[required] b.required } return schema } // ─── 常用工具示例 ─── // SendEmailTool 发送邮件工具 func SendEmailTool() *Tool { params : NewParamBuilder(). StringProperty(to, 收件人邮箱地址, true). StringProperty(subject, 邮件主题, true). StringProperty(body, 邮件正文, true). StringProperty(cc, 抄送邮箱地址多个用逗号分隔, false). Build() return Tool{ Name: send_email, Description: 发送电子邮件给指定收件人, Parameters: params, Handler: func(ctx context.Context, args json.RawMessage) (interface{}, error) { var params struct { To string json:to Subject string json:subject Body string json:body CC string json:cc,omitempty } if err : json.Unmarshal(args, params); err ! nil { return nil, fmt.Errorf(invalid args: %w, err) } // 实际调用邮件服务 emailID : fmt.Sprintf(EMAIL_%s_%d, params.To, len(params.Subject)) return map[string]interface{}{ email_id: emailID, status: sent, to: params.To, subject: params.Subject, }, nil }, } } // QueryWeatherTool 查询天气工具 func QueryWeatherTool() *Tool { params : NewParamBuilder(). StringProperty(city, 城市名称如北京、上海, true). StringProperty(date, 日期格式 YYYY-MM-DD默认为今天, false). Build() return Tool{ Name: query_weather, Description: 查询指定城市的天气信息, Parameters: params, Handler: func(ctx context.Context, args json.RawMessage) (interface{}, error) { var params struct { City string json:city Date string json:date,omitempty } if err : json.Unmarshal(args, params); err ! nil { return nil, fmt.Errorf(invalid args: %w, err) } // 模拟天气数据 weatherData : map[string]interface{}{ city: params.City, date: params.Date, weather: 晴, temp: 22.5, humidity: 45, wind: 3级, } return weatherData, nil }, } } // QueryOrderTool 查询订单工具 func QueryOrderTool() *Tool { params : NewParamBuilder(). StringProperty(order_id, 订单编号, true). Build() return Tool{ Name: query_order, Description: 根据订单编号查询订单详情和物流状态, Parameters: params, Handler: func(ctx context.Context, args json.RawMessage) (interface{}, error) { var params struct { OrderID string json:order_id } if err : json.Unmarshal(args, params); err ! nil { return nil, fmt.Errorf(invalid args: %w, err) } // 模拟订单查询 return map[string]interface{}{ order_id: params.OrderID, status: 已发货, express: 顺丰快递, tracking_no: SF1234567890, estimated: 2026-10-29, items: []map[string]interface{}{ {name: 无线蓝牙耳机, quantity: 1, price: 299.00}, }, }, nil }, } }四、Tool 注册与执行引擎// fcall/engine.go package fcall import ( context encoding/json fmt log sync time ) // Engine Function Calling 执行引擎 type Engine struct { tools map[string]*Tool maxRetries int timeout time.Duration mu sync.RWMutex } // EngineOption 引擎配置 type EngineOption struct { MaxRetries int // 工具调用最大重试次数 Timeout time.Duration // 工具调用超时 } // NewEngine 创建执行引擎 func NewEngine(opt EngineOption) *Engine { if opt.MaxRetries 0 { opt.MaxRetries 2 } if opt.Timeout 0 { opt.Timeout 30 * time.Second } return Engine{ tools: make(map[string]*Tool), maxRetries: opt.MaxRetries, timeout: opt.Timeout, } } // RegisterTool 注册工具 func (e *Engine) RegisterTool(tool *Tool) { e.mu.Lock() defer e.mu.Unlock() e.tools[tool.Name] tool log.Printf([FCall] Registered tool: %s, tool.Name) } // RegisterTools 批量注册工具 func (e *Engine) RegisterTools(tools []*Tool) { for _, tool : range tools { e.RegisterTool(tool) } } // GetToolSchemas 获取所有工具的 Schema发给 LLM func (e *Engine) GetToolSchemas() []map[string]interface{} { e.mu.RLock() defer e.mu.RUnlock() schemas : make([]map[string]interface{}, 0, len(e.tools)) for _, tool : range e.tools { schema : map[string]interface{}{ type: function, function: map[string]interface{}{ name: tool.Name, description: tool.Description, parameters: tool.Parameters, }, } schemas append(schemas, schema) } return schemas } // ExecuteToolCall 执行单个工具调用 func (e *Engine) ExecuteToolCall(ctx context.Context, call *ToolCall) *ToolResult { result : ToolResult{ ToolCallID: call.ID, Name: call.Name, } // 1. 查找工具 e.mu.RLock() tool, ok : e.tools[call.Name] e.mu.RUnlock() if !ok { result.Error fmt.Sprintf(tool %s not found, call.Name) return result } // 2. 参数校验JSON Schema 校验 if err : validateArgs(call.Args, tool.Parameters); err ! nil { result.Error fmt.Sprintf(parameter validation failed: %v, err) return result } // 3. 带重试的执行 var execErr error for attempt : 0; attempt e.maxRetries; attempt { if attempt 0 { log.Printf([FCall] Retrying tool %s (attempt %d/%d), call.Name, attempt, e.maxRetries) } // 带超时的上下文 execCtx, cancel : context.WithTimeout(ctx, e.timeout) res, err : tool.Handler(execCtx, call.Args) cancel() if err nil { result.Result res return result } execErr err time.Sleep(time.Duration(attempt*100) * time.Millisecond) } result.Error fmt.Sprintf(execution failed after %d retries: %v, e.maxRetries, execErr) return result } // ExecuteToolCalls 批量执行工具调用 func (e *Engine) ExecuteToolCalls(ctx context.Context, calls []*ToolCall) []*ToolResult { results : make([]*ToolResult, len(calls)) var wg sync.WaitGroup for i, call : range calls { wg.Add(1) go func(idx int, c *ToolCall) { defer wg.Done() results[idx] e.ExecuteToolCall(ctx, c) }(i, call) } wg.Wait() return results } // validateArgs 参数校验简化版 JSON Schema 校验 func validateArgs(args json.RawMessage, schema interface{}) error { // 1. 确保 args 是合法 JSON var parsed interface{} if err : json.Unmarshal(args, parsed); err ! nil { return fmt.Errorf(invalid JSON: %w, err) } // 2. 检查是否为空对象 argsMap, ok : parsed.(map[string]interface{}) if !ok { return fmt.Errorf(args must be a JSON object) } // 3. 检查必填字段 schemaMap, ok : schema.(map[string]interface{}) if !ok { return nil } if required, ok : schemaMap[required]; ok { reqFields, ok : required.([]interface{}) if !ok { return nil } for _, field : range reqFields { fieldStr, ok : field.(string) if !ok { continue } if _, exists : argsMap[fieldStr]; !exists { return fmt.Errorf(missing required field: %s, fieldStr) } } } return nil }五、与 LLM 网关集成// fcall/integration.go package fcall import ( context encoding/json fmt log ) // FCallGateway Function Calling 集成网关 type FCallGateway struct { llmGateway *gateway.LLMGateway // 复用第2讲的网关 fcEngine *Engine systemPrompt string } // NewFCallGateway 创建集成网关 func NewFCallGateway(llmGW *gateway.LLMGateway, fcEngine *Engine) *FCallGateway { return FCallGateway{ llmGateway: llmGW, fcEngine: fcEngine, systemPrompt: 你是一个智能助手可以通过调用工具来帮助用户完成任务。 当你需要获取实时信息或执行操作时请使用提供的工具。 如果工具返回的结果足以回答问题请基于结果组织回答。 如果工具执行出错请告知用户并提供替代方案。, } } // ProcessMessage 处理用户消息支持 Function Calling func (g *FCallGateway) ProcessMessage(ctx context.Context, userMsg string, history []gateway.ChatMessage) (string, error) { // 1. 构建请求 messages : []gateway.ChatMessage{ {Role: system, Content: g.systemPrompt}, } messages append(messages, history...) messages append(messages, gateway.ChatMessage{Role: user, Content: userMsg}) // 2. 获取工具 Schema tools : g.fcEngine.GetToolSchemas() toolDefs : make([]gateway.Tool, len(tools)) for i, t : range tools { data, _ : json.Marshal(t) var tool gateway.Tool json.Unmarshal(data, tool) toolDefs[i] tool } // 3. 第一次调用 LLM req : gateway.ChatRequest{ Model: gpt-4, Messages: messages, Tools: toolDefs, } resp, err : g.llmGateway.Chat(ctx, req) if err ! nil { return , fmt.Errorf(first LLM call: %w, err) } // 4. 检查是否有工具调用 if len(resp.Choices) 0 { return , fmt.Errorf(no response from LLM) } assistantMsg : resp.Choices[0].Message // 5. 解析工具调用 // 注意实际 OpenAI 响应中 tool_calls 在 assistant message 的额外字段中 // 这里简化为从 content 中检测 JSON 格式的工具调用 toolCalls : extractToolCalls(assistantMsg.Content) if len(toolCalls) 0 { // 没有工具调用直接返回 return assistantMsg.Content, nil } log.Printf([FCall] Detected %d tool calls, len(toolCalls)) // 6. 执行工具调用 toolResults : g.fcEngine.ExecuteToolCalls(ctx, toolCalls) // 7. 将工具结果加入消息历史 messages append(messages, assistantMsg) for _, tr : range toolResults { resultJSON, _ : json.Marshal(tr.Result) messages append(messages, gateway.ChatMessage{ Role: tool, Content: string(resultJSON), }) } // 8. 第二次调用 LLM基于工具结果生成回答 req2 : gateway.ChatRequest{ Model: gpt-4, Messages: messages, } resp2, err : g.llmGateway.Chat(ctx, req2) if err ! nil { return , fmt.Errorf(second LLM call: %w, err) } if len(resp2.Choices) 0 { return , fmt.Errorf(no final response from LLM) } return resp2.Choices[0].Message.Content, nil } // extractToolCalls 从 LLM 回复中提取工具调用简化版 // 生产环境中应使用 OpenAI 返回的 tool_calls 结构化字段 func extractToolCalls(content string) []*ToolCall { // 这里假设 LLM 返回格式化的 JSON 表示工具调用 // 实际项目中OpenAI SDK 会在响应中直接返回 tool_calls 字段 return nil }六、完整使用示例// fcall/example.go package fcall import ( context encoding/json fmt log ) func Example() { ctx : context.Background() // 1. 创建 LLM 网关复用第2讲 llmGW : gateway.NewLLMGateway(gateway.GatewayOption{ QPSLimit: 100, BurstLimit: 20, }) llmGW.RegisterAdapter(gateway.NewOpenAIAdapter(sk-your-key)) // 2. 创建 Function Calling 引擎 engine : NewEngine(EngineOption{ MaxRetries: 2, Timeout: 30 * time.Second, }) // 3. 注册工具 engine.RegisterTools([]*Tool{ SendEmailTool(), QueryWeatherTool(), QueryOrderTool(), }) // 4. 打印已注册的工具 schemas : engine.GetToolSchemas() fmt.Println(已注册工具:) for _, s : range schemas { data, _ : json.MarshalIndent(s, , ) fmt.Printf(%s\n\n, string(data)) } // 5. 创建集成网关 fcallGW : NewFCallGateway(llmGW, engine) // 6. 模拟对话 scenarios : []struct { userMsg string desc string }{ {帮我查一下订单 OD20251027001 的状态, 查询订单}, {北京今天天气怎么样, 查询天气}, {给 zhangsanexample.com 发一封邮件主题是会议邀请内容是明天下午3点开会, 发送邮件}, } for _, s : range scenarios { fmt.Printf(\n %s \n, s.desc) fmt.Printf(用户: %s\n, s.userMsg) // 手动模拟工具调用无需真实 LLM simulateToolCall(ctx, engine, s.userMsg) } // 7. 查看工具执行统计 fmt.Println(\n 执行统计 ) fmt.Println(工具注册数:, len(engine.tools)) } // simulateToolCall 模拟工具调用演示用 func simulateToolCall(ctx context.Context, engine *Engine, msg string) { // 根据消息内容模拟 LLM 返回的 ToolCall var toolCall *ToolCall switch { case contains(msg, 订单): toolCall ToolCall{ ID: call_001, Name: query_order, Args: json.RawMessage({order_id: OD20251027001}), } case contains(msg, 天气): toolCall ToolCall{ ID: call_002, Name: query_weather, Args: json.RawMessage({city: 北京}), } case contains(msg, 邮件): toolCall ToolCall{ ID: call_003, Name: send_email, Args: json.RawMessage({ to: zhangsanexample.com, subject: 会议邀请, body: 明天下午3点开会 }), } default: fmt.Println(AI: 我不确定你需要什么帮助) return } // 执行工具 result : engine.ExecuteToolCall(ctx, toolCall) // 输出结果 if result.Error ! { fmt.Printf(执行出错: %s\n, result.Error) return } resultJSON, _ : json.MarshalIndent(result.Result, , ) fmt.Printf(工具返回:\n%s\n, string(resultJSON)) // 模拟 LLM 基于结果生成回答 fmt.Print(AI: ) switch toolCall.Name { case query_order: fmt.Println(已查到您的订单信息目前状态为已发货顺丰快递单号 SF1234567890预计后天送达。) case query_weather: fmt.Println(北京今天天气晴朗温度22.5°C湿度45%风力3级非常适合外出活动。) case send_email: fmt.Println(邮件已成功发送给 zhangsanexample.com主题为会议邀请。) } } func contains(s, substr string) bool { return len(s) len(substr) containsSubstring(s, substr) } func containsSubstring(s, substr string) bool { for i : 0; i len(s)-len(substr); i { if s[i:ilen(substr)] substr { return true } } return false }七、生产坑位#坑位现象解决方案1参数幻觉​LLM 生成不存在的参数值严格 JSON Schema 校验 枚举约束2循环调用​LLM 反复调用同一个工具设置最大调用轮次如 5 轮3工具超时​第三方 API 响应慢导致 LLM 等待独立超时控制 异步执行4并发安全​多个请求同时修改共享状态工具 Handler 必须是线程安全的5结果过长​工具返回大量数据撑爆上下文结果截断 摘要6权限失控​用户诱导 LLM 调用危险工具敏感操作二次确认 操作审计日志八、本讲小结┌─────────────────────────────────────────────────────────────┐ │ 第5讲 核心收获 │ │ │ │ ✅ 理解了 Function Calling 的工作原理 │ │ ✅ 实现了完整的 Tool 定义规范JSON Schema 参数构建 │ │ ✅ 搭建了工具注册与执行引擎带重试和超时 │ │ ✅ 实现了与 LLM 网关的集成两次调用模式 │ │ ✅ 编写了三个实用工具示例邮件/天气/订单查询 │ │ ✅ 了解了 6 个常见坑位及其解决方案 │ │ │ │ 核心理念Function Calling 让 LLM 从嘴炮变成实干家 │ └─────────────────────────────────────────────────────────────┘九、配套工具推荐 开发之余的小工具推荐在调试 Function Calling 时经常需要验证 JSON Schema 格式、解析 LLM 返回的参数 JSON、生成测试数据JSON 格式化与校验zz365.top 的 JSON 工具可以验证 LLM 生成的参数是否符合 Schema 定义正则表达式测试验证参数中的邮箱、手机号等格式Base64 编解码处理某些工具返回的二进制数据UUID 生成器为每个 ToolCall 分配唯一的 call_id同类可备站点tool.lu综合在线工具箱360站长工具zz360.cc偏 SEO/建站方向提醒所有涉及 API Key 和业务数据的操作务必先在本地脱敏再使用在线工具。下一讲预告《Streaming 优化——告别转圈圈的用户体验》你会学到SSE 协议实现、流式 Token 分发、客户端逐字展示、中断恢复、流式场景下的工具调用处理。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询