先定义失败:Agent 工具调用的最小工程契约
从输入校验、超时到安全重试,给工具调用一个可检查的边界。
本文由 AI 辅助撰写。示例用于说明设计,未提供实测结论;来源见文末。
一个 Agent 能选出正确的工具,并不等于它能可靠地完成任务。工具可能收到缺失的参数,外部操作可能超时,而一条“再试一次”的指令也可能把同一件事做两遍。
我更愿意先写出工具的工程契约:它接受什么,成功如何判断,失败后允许做什么。下面是一份设计建议,没有附带性能实验,也不意味着任何模型已经通过这些检查。
输入:把自然语言要求变成可检查的字段
工具描述用于帮助模型选择工具,输入校验则由程序执行。两者都需要,但不能互相替代。
JSON Schema 官方入门文档介绍了如何声明对象字段、类型和必填项。用于 Agent 工具时,可以从很小的输入结构开始:
{
"type": "object",
"properties": {
"document_id": { "type": "string", "minLength": 1 },
"format": { "type": "string", "enum": ["markdown", "text"] }
},
"required": ["document_id", "format"],
"additionalProperties": false
}
这是一个读取文档工具的示意定义。真正执行前,仍需要校验器检查输入,并由业务层检查文档是否存在、调用者是否有权读取。符合结构,只说明输入满足声明的规则。
建议把“字段不合法”“资源不存在”和“没有权限”分成不同的错误类别。Agent 才能决定是修改参数、换一个资源,还是停止操作。
超时:知道没有等到结果,不代表知道没有执行
超时是一种观察结果:调用方在给定时间内没有拿到完成确认。对于远端写入,这时操作究竟有没有发生,可能仍然未知。
Python 的 subprocess 文档说明,run(..., timeout=...) 超时后会终止并等待子进程,再抛出异常;某些平台上的初始进程创建无法立即中断。这是本地子进程的行为,不能直接推导成远端服务会撤销已执行的操作。
在工具契约里,我建议分别记录:
- 调用预算:最多等待多久,允许执行几次。
- 结果状态:成功、明确失败,或状态尚未确认。
- 后续动作:是否有只读查询可以核实结果。
例如,发布文档超时后,可以先查询目标文档的版本。查询也没有成功时,就保留“未确认”,不要为了给用户一个确定答案而猜测。
重试:先说明重复执行会发生什么
RFC 9110 的幂等方法定义关注多次相同请求对服务端的预期效果。它也提醒客户端,不应在缺少依据时自动重试非幂等请求。
对 Agent 工具而言,幂等性需要来自实际接口设计,不能来自工具名字。一个叫 save_note 的工具,如果每次都会新建文档,重复调用就会产生两份笔记。
可以把工具分成三种情况来设计:
- 只读查询:为暂时性失败设置有限次数的重试;读取仍需遵守访问权限。
- 可确认的幂等写入:使用服务端确实支持的幂等标识,并记录它对应的操作结果。
- 可能重复产生副作用的写入:先核实已有结果,无法核实时停下来交给人判断。
退避等待能减轻重复请求的频率,却不能让一个非幂等操作自动变得安全。
返回值:让下一步决定有依据
不要把所有失败都压成一句“工具调用失败”。一份小而明确的返回值,可以包含操作标识、结果状态、错误类别,以及是否允许重试。
{
"operation_id": "example-operation",
"status": "unconfirmed",
"error_kind": "timeout",
"retry_allowed": false,
"next_step": "check_operation_status"
}
这仍然只是设计示例。retry_allowed 应由可信的执行层根据接口语义计算,而不是让模型自行承诺;next_step 也只能指向已有且获准的工具。
日志里保留足够的状态信息,便于事后解释发生了什么。输入全文、文档内容和访问令牌则不应该默认全部写进日志。
从一张检查表开始
给每个准备投入使用的工具问五个问题:输入有没有程序校验?权限由谁检查?超时后状态是否能确认?重复执行会有什么副作用?失败后是否有明确的停止条件?
这些问题不会解决 Agent 的全部可靠性问题,却能让一部分错误更早暴露,也让人有机会在副作用发生前接管。我的建议是先让一个工具的边界清楚,再增加工具数量。
参考资料
资料核对日期:2026-10-12。本文由 AI 辅助撰写,属于基础工程设计笔记;示例未连接外部服务,也未提供实测结论。