Tool Calling 的本质:让模型“提案”,系统“执行”
在工程上,你应该把工具调用理解为:
- 模型负责:提出调用意图与参数(proposal)
- 系统负责:校验、授权、执行、记录(execution)
如果你让模型直接“执行”,那不是 tool calling,那是风险放大器。
设计目标:可控、可审计、可回滚
一个好的工具调用设计要做到:
- 可控:模型只能调用你允许的工具,不能越权
- 可审计:每次调用都能追踪输入、输出、耗时、错误
- 可回滚:对外部系统的写操作要可撤销或至少可补偿
Tool Spec 模板(最小可用)
下面是一个“只读 GitHub PR”的工具规范示例(伪 spec,重点看结构):
{
"name": "github.get_pull_request",
"description": "Fetch pull request metadata and diff summary (read-only).",
"input_schema": {
"type": "object",
"properties": {
"repo": { "type": "string" },
"pull_number": { "type": "number" }
},
"required": ["repo", "pull_number"],
"additionalProperties": false
},
"permissions": {
"scope": "read",
"resources": ["repo"]
}
}
关键点:
additionalProperties: false:防止“幻觉参数”悄悄混进来required:明确必填permissions:把权限当作工具的一部分
参数校验:不要相信模型
即使你写了 schema,也要在执行层做强校验:
- 类型校验:string/number/enum
- 范围校验:pull number > 0,max_words 限制
- 白名单校验:repo 必须在允许列表或当前工作空间
- 大小校验:字符串长度、列表长度、payload 总大小
校验失败要返回结构化错误(并映射到你的失败枚举)。
权限最小化:读写分离 + 人类确认点
建议做两条硬规则:
- 工具按“读/写”拆分
例如:
github.get_pull_request(read)github.create_comment(write)
- 写操作默认需要“人类确认”
让模型先生成“将要写入的内容”,你再决定是否提交。
幂等性与去重:避免重复写入
写操作常见灾难是“重试导致重复”。
建议在写工具里支持:
idempotency_key:由上层传入(task_id + tool_call_id)- 或者对目标资源做去重(例如同一个 PR 同一段评论不重复发)
动手做:把工具调用接入 Skill 的三段式流程
以 PR 摘要 Skill 为例,一个稳定流程是:
- 计划:模型输出“需要哪些工具调用”(只读)
- 执行:系统按 schema 校验后调用工具,拿到真实数据
- 生成:模型基于真实数据生成输出(结构化输出契约)
如果你把“计划”和“执行”混在一起,排障会非常痛苦。
工程化清单
- 工具 spec 有 schema 且
additionalProperties=false - 执行层强校验,返回结构化错误
- 读写工具分离,写工具默认人类确认
- 每次工具调用可审计(输入、输出、耗时、错误、调用链)
常见坑
- “工具参数靠模型猜”:会发生幻觉参数/错字段
- “写工具无护栏”:越权、误操作、不可回滚
- “没有幂等策略”:重试写爆外部系统
下一篇:MCP 入门:把你的服务变成可插拔工具。