AI 辅助:Function Calling 落地:工具调用链路的参数校验与回滚设计
一、模型会调用工具,但系统必须负责后果
Function Calling 让大模型从“会说”变成“能做”。它可以查订单、改配置、发通知、写数据。问题也随之出现:模型生成的参数不一定可信,工具执行不一定成功,执行成功后也可能需要回滚。如果只把函数列表丢给模型,就让模型直接触发生产动作,风险非常高。
工程上应该把 Function Calling 看成一层意图解析,而不是最终执行器。模型负责把自然语言转成结构化意图,系统负责校验权限、参数、幂等和副作用。这个分工非常重要。模型输出是概率结果,生产系统需要确定性约束。
常见事故包括:模型把用户的“帮我看看配置”理解成“更新配置”;日期格式解析错误导致查询范围扩大;重复调用接口造成多次发送通知;工具返回失败后模型继续生成“已完成”的回复。这些问题不是提示词能彻底解决的,必须靠执行链路治理。
二、工具调用链路:从意图到执行的安全闸门
sequenceDiagram participant User as 用户 participant LLM as 模型 participant Guard as 执行网关 participant Tool as 业务工具 participant Log as 审计日志 User->>LLM: 自然语言需求 LLM->>Guard: 函数名与参数 Guard->>Guard: Schema 校验 Guard->>Guard: 权限与幂等检查 Guard->>Tool: 执行工具 Tool-->>Guard: 结果或错误 Guard->>Log: 记录输入、输出、操作者、trace Guard-->>LLM: 结构化执行结果 LLM-->>User: 基于结果生成回复执行网关是关键。模型不能直接访问业务工具。所有函数调用都应该经过网关,网关做五件事:校验函数是否存在,校验参数是否符合 Schema,校验操作者是否有权限,校验请求是否幂等,记录审计日志。
对于有副作用的工具,还要增加确认机制。只读工具可以自动执行,写操作必须分级。低风险写操作可以自动执行但保留回滚,高风险写操作必须人工确认。这个分级不应该写在提示词里,而应该写在工具元数据里。
三、Go 侧执行网关:不要信任模型参数
下面是一个执行网关的简化实现。重点是先校验,再执行,并且所有工具都返回结构化结果。
type ToolCall struct { Name string `json:"name"` Args json.RawMessage `json:"args"` RequestID string `json:"request_id"` UserID string `json:"user_id"` } type ToolResult struct { OK bool `json:"ok"` Data map[string]any `json:"data,omitempty"` Message string `json:"message,omitempty"` } type Tool interface { Name() string ReadOnly() bool Validate(args json.RawMessage) error Run(ctx context.Context, args json.RawMessage) (ToolResult, error) } func ExecuteTool(ctx context.Context, call ToolCall, registry map[string]Tool, audit AuditStore) (ToolResult, error) { tool, ok := registry[call.Name] if !ok { return ToolResult{OK: false, Message: "unknown tool"}, fmt.Errorf("unknown tool: %s", call.Name) } if err := tool.Validate(call.Args); err != nil { return ToolResult{OK: false, Message: "invalid args"}, err } if err := checkPermission(call.UserID, tool.Name(), tool.ReadOnly()); err != nil { return ToolResult{OK: false, Message: "permission denied"}, err } if !tool.ReadOnly() && seenRequest(call.RequestID) { return ToolResult{OK: false, Message: "duplicate request"}, nil } result, err := tool.Run(ctx, call.Args) audit.Write(call, result, err) return result, err }这段代码明确了模型输出不能直接进入业务层。即使模型给出了合法 JSON,也必须做业务校验。比如金额不能为负,日期范围不能超过限制,资源 ID 必须属于当前用户。Schema 只能保证形状,不能保证语义。
工具返回结果也要结构化。模型需要基于真实执行结果回复用户,而不是凭空补一句“操作成功”。如果工具失败,模型应该解释失败原因和下一步建议,不能伪造成已完成。
四、边界与权衡:自动化越强,审计越不能省
Function Calling 的收益是降低操作摩擦,代价是引入新的执行风险。只读工具风险较低,适合快速接入。写操作、外部通知、资金相关、权限相关工具都必须谨慎。不要因为模型能生成参数,就放弃传统后端的安全边界。
幂等设计也很重要。模型可能因为网络重试、用户重复点击或上下文误判,发起多次相同调用。每个有副作用的调用都应有request_id。业务工具要能识别重复请求,并返回之前的结果,而不是再次执行。
回滚不是万能的。有些操作无法回滚,比如已经发送给外部用户的通知。对这类工具,应该采用“生成草稿 + 人工确认”的两阶段模式。模型负责草稿,系统负责确认和发送。
五、总结
Function Calling 的工程核心是执行治理。模型负责意图解析,系统负责校验、权限、幂等、审计和回滚。不要让模型直接触碰业务工具,也不要把安全策略写成提示词。
落地路线建议从只读工具开始,建立统一网关和审计日志。随后接入低风险写操作,并强制使用幂等键。高风险工具必须保留人工确认。这样才能在提升效率的同时,把风险控制在工程系统能承受的范围内。