1. 工具调用的失败模式
AI 助手调用工具时,失败是常态而非例外:外部 API 抖动、数据库超时、工具本身出错、模型重试导致副作用重复。MCP 工具的可靠性,核心是让"调用可能失败、可能重复"在模型与用户面前可感知、可恢复、可兜底。
1.1 四类典型失败
| 失败类型 | 例子 | 可重试 |
|---|---|---|
| 瞬时网络/资源 | 外部 API 超时、连接重置 | 通常可重试 |
| 工具业务失败 | 数据校验、权限不足 | 看场景 |
| 幂等破坏 | 重发导致重复下单/重复写 | 不可盲目重试 |
| 长任务超时 | 生成报告耗时超过阈值 | 异步化 |
1.2 失败的双重表达
# 工具失败的两层
# 协议层(error): 传输/协议故障(JSON-RPC error)
# 业务层(isError): 工具执行失败但协议成功
# 客户端处理
# 协议错误 → 传输层面处理(重连/重发)
# 业务失败 → 把失败信息交回模型,模型决定重试/降级
# 关键: 别把业务失败伪装成协议成功,也别反过来
2. 超时策略
2.1 为什么必须有超时
# 无超时的后果
# 1) 工具调用挂起 → 客户端卡死,模型等不到结果
# 2) 资源泄漏 → 连接/进程被长任务占住
# 3) 模型幻觉 → 等太久后"猜一个结果"
# 原则: 每个工具调用都要有明确超时
2.2 超时设置
# 超时按工具类型分
# 快速工具(查询/简单计算): 5-15 秒
# 中速工具(外部 API): 30-60 秒
# 慢工具(生成/大数据): 异步化(见长任务)
# 设置方法
# 客户端: 请求级超时(传输层)
# 服务器: 工具执行限时(超时返回 isError + 原因)
# 服务端比客户端更了解工具耗时 → 服务端限时 + 客户端兜底
3. 重试与退避
3.1 什么值得重试
# 可重试: 瞬时故障(超时、5xx、连接重置)
# 不可重试: 参数错误(重试还是错)、业务被拒(权限)、幂等风险操作
# 判断: 失败信息里带"是否可重试"提示(服务器可给 retryable 标记)
# 铁律: 写操作重试前确认幂等
3.2 退避策略
# 指数退避 + 抖动(避免重试风暴)
# 第 1 次重试: 1 秒
# 第 2 次: 2 秒
# 第 3 次: 4 秒
# 上限: 3-5 次,超过交给模型/用户决策
# 注意: 多个客户端同时重试同一工具 → 抖动分散
3.3 重试与模型的协作
# 重试该由谁做?
# 1) 客户端自动重试: 适合瞬时故障(不打断模型)
# 2) 模型决定重试: 工具返回 isError 后,模型判断"换参数重试/放弃"
# 分层: 客户端做"立即重试 1-2 次",模型做"策略性重试"
# 避免: 客户端无限重试(打断模型判断),模型盲目重发(副作用)
4. 幂等工具设计
4.1 幂等的必要性
# 场景: 客户端超时重发 → 工具执行两次
# 下单、转账、发通知等写工具 → 必须幂等
# 幂等定义: 同一请求重复执行,结果一致(只生效一次)
4.2 幂等键模式
# 工具参数带幂等键(idempotency_key)
# 服务器
# 1) 收到调用,按 key 查是否已处理
# 2) 已处理 → 返回原结果(不重复执行)
# 3) 未处理 → 执行,记录 key → 结果
# 客户端
# 1) 发起写操作时生成幂等键(UUID)
# 2) 重发同一操作复用同一键
# 服务器用 DB/Redis 存幂等键(TTL 覆盖重试窗口)
4.3 只读与读改写
# 只读工具天然幂等(查询重复无害)
# 读改写(先读后写): 并发下可能覆盖 → 加版本号/条件更新
# 无副作用优先: 工具能只读则只读,写操作显式 + 幂等
5. 长任务处理
5.1 长任务的三种模式
| 模式 | 机制 | 适用 |
|---|---|---|
| 同步阻塞 | 长超时等待 | 短长任务(<1 分钟) |
| 进度通知 | 任务中发 progress | 可视化进度 |
| 异步完成 | 立即返回 jobId,轮询/回调取结果 | 分钟级任务 |
5.2 异步完成模式
# 模式: 提交 → 立即返回 job 状态 → 后续查询/回调
# 1) 工具 submitReport(params) → { jobId, status: "running" }
# 2) 工具 getJob(jobId) → { status: "done", result }
# 3) 或服务器发 notifications/progress
# 优点: 不占模型等待、可恢复(跨会话)
# 注意: 模型要能"记得 jobId 再查"——工具返回清晰状态机
5.3 进度通知
# 服务器发 notifications/progress:
# { requestId, progress: 45, total: 100, message: "处理中" }
# 客户端展示给用户("正在生成报告 45%")
# 价值: 长任务不黑盒,用户与模型都知道进展
# 结合取消: 进度 + 可取消(notifications/cancelled)
6. 并发与背压
6.1 并发控制
# 问题: 模型一次发起多个工具调用,或客户端多请求并发
# 1) 客户端限制并发数(工具是外部资源)
# 2) 服务器限流(拒绝超限,返回可重试)
# 3) 慢工具与快工具分开队列
# 4) 避免: 无节制并发打爆外部 API
6.2 背压信号
# 服务器过载的信号
# 1) 429(限流)→ 客户端退避
# 2) 错误码: rate_limit_exceeded → 模型稍后重试
# 3) 排队: 服务器可返回"排队中"(异步化)
# 客户端/模型识别背压信号,退避而非硬撞
7. 失败降级与兜底
7.1 模型侧的降级路径
# 工具失败后模型应有一条降级链
# 1) 重试(参数不变/换参数)
# 2) 替代工具(另一实现)
# 3) 部分结果(返回可用的部分)
# 4) 告知用户(坦诚"此操作不可用")
# 工程: 工具返回结构化失败(code/原因/可替代建议),模型才好决策
7.2 用户可感知的失败
# 失败不是"静默",用户要明白发生了什么
# 1) 工具失败 → 明确告知(不是"我忘了")
# 2) 可操作 → 给出下一步(重试/换方式/联系人工)
# 3) 不可恢复 → 诚实说明,不编造成功
8. 可靠性的观测
# 工具调用监控指标
# 1) 调用量(按工具)
# 2) 失败率(协议失败 vs isError 业务失败分开)
# 3) 延迟分布(p50/p95/p99,按工具)
# 4) 重试率与幂等冲突(重发被去重次数)
# 5) 长任务完成率与超时率
# 日志: 每次调用 trace(工具、参数摘要、结果、耗时)
# 告警: 某工具失败率骤升 → 依赖故障预警
9. 可靠性测试
# 1) 超时: 注入慢响应 → 断言客户端超时 + 模型降级
# 2) 重试: 注入瞬时失败 → 断言退避 + 最终成功
# 3) 幂等: 同幂等键重发 → 断言只执行一次
# 4) 长任务: 异步模式全流程(提交/进度/取结果)
# 5) 并发: 多调用并发 → 断言限流与排队
# 6) 混沌: 随机断网/抖动 → 验证不崩、可恢复
# 工具: InMemoryTransport + 可控延迟模拟
10. 常见陷阱
- 写操作不幂等:超时重发重复下单——必须幂等键。
- 重试无退避:重试风暴打爆外部依赖。
- 长任务同步阻塞:模型等到超时,用户等不到结果——异步化。
- 业务失败伪装成成功:isError 语义被忽略,模型误以为成功。
- 失败不告知:工具失败了还继续编造结果——诚实失败优于虚假成功。
11. 总结
MCP 工具调用的可靠性,是"失败可感知、重试有策略、写操作幂等、长任务异步"的系统设计:超时防挂起、退避防风暴、幂等键防重复、异步化防等待、结构化失败帮模型决策、监控让问题可见。把可靠性做成工具协议的一部分,AI 助手调用外部能力时才能既大胆又稳妥——能接受失败,但不能制造重复与等待。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。