AI 智能体的工具契约
工具本质上是一个以函数形式呈现的权限边界。JSON Schema 校验只能证明参数符合格式,却无法证明操作是否经过授权、意图是否正确、重试是否安全,或者执行是否真正完成。
模型提出请求之后,谁执行工具?
模型请求调用函数,真正执行函数的是应用代码。函数调用指南把这一过程描述为包含工具定义、调用、结果以及后续回答的多轮对话。以只读工具 search_notes 为例:
- 应用向模型提供名称、用途和参数 schema:参数是对象,只有必填字符串
query,长度为 1–200,不接受其他字段。实现只允许搜索已获授权的公开笔记索引。 - 模型返回工具调用消息,其中包含调用标识、工具名称和参数。应用要等消息完整,不能拿流式输出中的参数片段提前执行。
- 应用解析参数、检查 schema 和权限,再分派给允许列表中的函数。格式错误或权限不足时,返回错误,不运行搜索。
- 应用把搜索结果与原调用标识对应起来,连同对话状态回送。模型据此回答,或再请求一次允许的操作。
下面用 Responses 风格的字段展示简化消息,不是完整的 API 请求:
{"type":"function_call","call_id":"call_1","name":"search_notes","arguments":"{\"query\":\"KV cache\"}"}
{"type":"function_call_output","call_id":"call_1","output":"{\"status\":\"ok\",\"matches\":[]}"}
换成 Anthropic 的接口,字段名不同、流程相同:工具调用是 tool_use 块,结果用 tool_result 块回送(工具使用概览)。
空结果表示这次搜索没有命中,不代表这个主题不存在。普通文本即使长得像这段 JSON,也不会因此获得调用权限。供应商适配器或本地解析器须先识别协议层的请求,Harness 再决定是否执行。结果仍是不可信数据,不是新的指令;后续行动和停止条件由有界 Agent 循环管理。
契约的关键维度
契约实例
name: replace_text
purpose: replace one exact block in one repository file
input:
path: repository-relative path, no traversal
old_text: non-empty and must match exactly once
new_text: string
preconditions:
- path is inside approved write scope
- working tree version equals observed version
effects: reversible local write
idempotency: not retryable after success without re-reading
result:
status: succeeded | conflict | no-match | multiple-matches | denied
changed_file: path or null
approval: required when path is outside task scope
这份契约让 Agent 循环能够区分具体的失败原因:no-match 说明要先重新读文件,multiple-matches 说明 old_text 要再长一点,denied 说明这条路径根本不该碰。通用的 error: edit failed 错误提示分不出这三种情况,只会诱导盲目重试。
结果封装
一份稳定的结果要把两件事分开写:请求有没有送到、服务有没有回话(传输层),以及要做的事做成了多少(业务层)。下面这个批量操作的结果里,transport 是 ok,说明请求送到了;status 是 partial,说明 10 项里只做成 8 项:
{
"request_id": "req-123",
"transport": "ok",
"status": "partial",
"completed": 8,
"failed": 2,
"retry_safe": false,
"artifact": "results/batch-123.json",
"errors": [{"code": "permission-denied", "item": "..."}]
}
不要将单个项目的成功等同于整个批次的成功。
“结果未知”也要和“失败”分开。设想一个转账工具:请求发出后连接超时,你不知道对方扣没扣钱。如果把这次当成失败再发一次,就可能扣两次款。所以结果未知时,Harness 要先查询这笔转账的状态,而不是直接重试;带同一个幂等键重试的做法见长任务 Agent。
让模型稳定地用对 Schema
契约写清楚以后,模型能不能按它调用,还取决于几处设置。OpenAI 的函数调用指南给了几条具体做法:
- 打开严格模式。 指南写的是“我们建议始终启用严格模式”。设了
strict: true,每个对象都要写additionalProperties: false,properties里的每个字段都要列进required;可选字段用null类型表示。 - 开始时少给工具。 指南建议一轮开始时可用的函数“少于 20 个”,并说明这只是一条软建议。不常用的工具用
tool_search推迟加载;只想限制某几轮能调哪些工具,用allowed_tools,工具列表本身不变,提示缓存也就保得住。 - 把描述写成说明书。
description写清用途、什么时候不该用、参数的格式和单位,必要时给一个例子。
MCP 的 2026-07-28 版也放宽了 Schema:inputSchema 和 outputSchema 可以使用任何 JSON Schema 2020-12 关键字(变更记录)。
设计原则
- 避免 Shell 插值。 尽可能直接传递程序名和参数数组。这能消除 Shell 解析环节,但参数本身仍可能危险:
git、云 CLI 和包管理器中都有具备破坏性或代码执行能力的标志位。 - 验证解析后的路径。 在规范化并解析符号链接后检查仓库边界;仅拒绝字面量
../是不够的。应将敏感路径排除在工具权限之外,而非依赖黑名单。 - 停止无效重试。 同一调用连续失败到一定次数就停下,这个断路器怎样设阈值,见有界 Agent 循环。
- 返回结构化失败信息。 区分输入格式错误、权限拒绝、临时传输故障、业务逻辑拒绝、部分完成和结果未知。后续动作取决于这些差异。
注入与不可信结果
类型化响应能让每个字段的含义清楚,却管不了字段里写的是什么:一条格式完全合法的 "title" 里,照样可以写着“忽略之前的指令”。工具结果只能当数据,模型据此提出的下一步仍要重新过一遍策略;整套威胁模型见 Agent 安全。
小型模型的权衡
较小的模型通常能更稳定地生成浅层 Schema,但移除 Schema 细节可能会丢弃关键的安全信息。要知道一个小模型够不够用,就用你实际部署的提示词模板和量化版本(权重被压到更低精度、体积更小的那个模型文件)去测,统计两件事:模型发出的工具调用有几成能通过校验,以及任务最终有几成做成。不要为了适应较弱模型而静默降低权限;应优先选择简单的单一用途工具、受限适配器或更强的模型。