给 Agent 加工具:先确定失败边界
文章目录
摘要:给 Agent 接搜索、数据库、脚本执行一类工具时,大家容易先写「调用成功返回什么」。线上真正耗时间的,是超时、权限失败、参数不合法、部分成功时 Agent 会怎么说、会不会死循环重试。本文把失败契约写成可落地的检查清单,并区分工具层、适配层、策略层和用户预期层。适合正在给 Agent / MCP / function calling 加工具的开发者。
说明:不同产品的工具协议细节会变,本文讲工程约定,不绑定某一家 SDK 版本。
1. 结论先行
- 先定义失败,再定义成功。 成功路径好写,失败路径决定系统稳不稳。
- 失败要可分类:可重试、不可重试、需人介入,别都当成再试一次。
- 把错误变成结构化结果:错误码、是否可重试、给模型看的短说明、给日志看的细节。
- 重试必须有上限和退避;否则工具抖动会被放大成费用和延迟雪崩。
- 工具能调通 ≠ Agent 会用对;还要约束何时允许调用。
2. 为什么成功 demo 扛不住真实对话
本地演示通常是:
- 工具总是在线
- 参数总是合法
- 返回总是完整 JSON
用户场景里更常见:
- 第三方 API 偶发 429/5xx
- 模型捏造了不存在的字段
- 工具执行成功了一半(写了库但通知失败)
- 用户以为已经帮我提交了,实际只是查了状态
如果只实现 happy path,Agent 会在失败时胡编、空转或重复调用。

图1. 这些不是边角料,而是工具接口的一部分。
一个真实感很强的例子:客服 Agent 调用「创建退款单」。上游超时后,若返回值只有一句自然语言“好像失败了”,模型可能再调一次,造成重复退款;若返回 error_code=TIMEOUT, retryable=false, side_effect=unknown,策略层就可以要求人工确认,而不是自动重试。
3. 一份最小失败契约
每个工具建议至少写清:
| 字段 | 作用 |
|---|---|
| error_code | 稳定枚举,便于策略分支 |
| retryable | true/false,禁止模型自由发挥 |
| user_message | 可直接对用户说的短句 |
| log_detail | 仅日志,含请求 id,不含密钥 |
| timeout_ms | 超时阈值 |
| max_retries | 适配层重试上限 |
| side_effect | none / possible / done,标明是否可能已产生副作用 |
示例(示意):
{
"ok": false,
"error_code": "UPSTREAM_TIMEOUT",
"retryable": true,
"side_effect": "unknown",
"user_message": "上游服务超时,请稍后重试或转人工",
"request_id": "req_xxx"
}
模型侧提示词里应写明:遇到 retryable=false 不要盲着重试;side_effect 不是 none 时,禁止在未确认前再次执行写操作。
4. 失败通常落在哪一层

图2. 先定位层,再改提示词或重装框架。
| 层 | 典型问题 | 优先动作 |
|---|---|---|
| 工具本身 | 服务挂、权限、配额 | 修凭证、熔断、降级 |
| 适配/Schema | 参数类型、必填、枚举 | 收紧 schema,加校验 |
| Agent 策略 | 该不该调、何时调 | 改策略与权限,不先怪工具 |
| 用户预期 | 以为已提交 | 产品文案与确认步骤 |
很多 Agent 很蠢的问题,其实是适配层允许了过宽的参数,或策略层允许在高风险操作上自动重试。
4.1 参数校验为什么要放在适配层
不要指望模型永远生成合法参数。适配层应:
- 校验必填与范围
- 拒绝未知字段(或明确忽略策略)
- 把校验失败映射成
VALIDATION_ERROR, retryable=false
这样模型会被引导修正参数,而不是把脏请求打到上游。
5. 部分成功:最容易被忽略的坑
例如:工单已创建,但发邮件失败。若只返回一个布尔值,Agent 无法解释状态。
更稳妥:
- 返回明确状态机:
created/notified/failed_notify - 提供补偿接口或人工工单链接
- 禁止在不确定时假装全部完成
对用户可以说:退款单已创建(单号 xxx),通知短信发送失败,已转人工补发。这句话比含糊的失败有用得多。
6. 重试策略的底线
| 错误类型 | 建议 |
|---|---|
| 网络抖动、429、502 | 有限次退避重试 |
| 参数错误、权限拒绝 | 不重试,改输入或停 |
| 超时且可能有副作用 | 不自动重试写操作 |
| 未知错误 | 记日志,降级或转人工 |
退避要带抖动,避免多会话同时打爆上游。总重试预算应写入契约,而不是留给模型临场发挥。
7. 适合马上做的,和不适合一上来做的
适合马上做:
- 为每个工具补齐错误码与
retryable - 给超时和重试写死上限
- 高风险操作(转账、删数据、发公告)强制确认
- 给每个写工具标明
side_effect
不适合一上来做:
- 没失败契约就接一堆工具
- 用再问一遍模型代替结构化错误
- 让 Agent 在不可重试错误上循环调用
- 把生产写权限和只读查询混在同一工具里且不分层级
7.1 快速能做的改造清单
若你已有三个以上工具,不必重写框架,按这个顺序补:
- 给每个工具返回值统一包一层:
ok+error_code+retryable+user_message - 写操作补
side_effect;读操作固定为none - 适配层加超时;超时默认
retryable=true且side_effect=unknown(除非你能证明请求未达上游) - 策略提示词加三条硬规则:不可重试不重试;未知副作用不重做写操作;连续失败转人工
- 准备四个故障用例:超时、401、参数缺失、上游 500,全部要跑过
做完这五步,成功率未必立刻上升,但失败时系统会变得可解释,排障时间通常会明显下降。
和 MCP / function calling 的关系
协议解决的是怎么暴露工具;失败契约解决的是工具坏了以后系统怎么表现。两者叠在一起才接近可运营。
8. 常见误区
| 误区 | 更好的做法 |
|---|---|
| 失败就返回一段自然语言 | 同时给机器可读字段 |
| 所有错误都重试三次 | 按 retryable 分支 |
| 工具越多 Agent 越强 | 工具越多,失败面越大 |
| 只测成功样例 | 用故障注入测超时与权限 |
| 信任模型自己总结错误 | 契约字段为准 |
9. 术语速查
| 术语 | 含义 |
|---|---|
| 失败契约 | 对失败形态、字段、重试策略的事先约定 |
| 可重试错误 | 临时故障,允许有限次重试 |
| 适配层 | 把外部 API 转成 Agent 可用的工具接口 |
| 熔断 | 连续失败后短时间停止调用上游 |
| 副作用 | 调用可能已改变外部状态 |
9.1 评审或上线前的验收问题
可以拿去问自己或问供应商:
- 工具超时后,会不会自动再打一枪写接口?
- 权限失败时,用户看到的是什么,日志里能不能定位到请求 id?
- 部分成功有没有状态机,还是一个布尔值糊弄过去?
- 连续失败是否会熔断,熔断恢复条件是什么?
- 高风险操作有没有强制确认,确认超时怎么处理?
五问答不清,就还没到「可以放心把工具交给 Agent」的程度。连通性 demo 只能证明网络通,不能证明失败可运营。
10. 小结
给 Agent 加工具,工程上半场是连通,下半场是失败怎么被理解、被限制、被恢复。先写失败契约,成功路径反而好写,也更接近可上线。
更多推荐

所有评论(0)