MCP 工具超时、重试与幂等:把不确定的网络调用变成可恢复流程
MCP 工具会面对模型重试、网络抖动和下游长任务。本文给出超时预算、重试条件、幂等键、取消语义和观测指标的完整设计,避免重复写入与无限等待。
本文要点
- 主题聚焦:AI Agent、MCP、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
模型调用工具时,失败不是例外。网络会抖动,下游会限流,用户会重新提问,客户端也可能在结果回来前断开。若 Server 只做一层 try/catch,很容易出现两种问题:读操作永远卡住,写操作被重复执行。解决它的关键不是增加重试次数,而是先定义每类操作的恢复语义。
给一次调用分配时间预算
不要让工具只接受一个全局 60 秒超时。一次调用通常包括排队、鉴权、下游请求、结果转换和写审计日志。先为整个请求设置上限,再为关键下游设置更短上限,例如总预算 20 秒,其中数据库查询 8 秒、第三方 API 6 秒、日志提交 2 秒。这样某个依赖变慢时,系统仍有时间返回结构化错误。
超时不等于操作已经停止。对于写操作,超时后最危险的状态是“客户端没有收到结果,但下游可能已经成功”。因此,Server 不能把超时直接等同于失败后重试,而要进入查询或补偿路径。
按操作类型决定是否重试
可以把工具分为三类:
- 纯读取:例如查询工单。遇到连接重置、短暂 503 可做有限重试。
- 幂等写入:例如按确定资源 ID 更新状态。使用相同幂等键后可以重放。
- 非幂等写入:例如创建付款、发送外部通知。默认不能自动重试,必须先查询结果或等待人工确认。
重试策略也要有边界。使用指数退避加随机抖动,限制总尝试次数和总时间预算。不要在模型层、MCP 客户端、Server 和下游 SDK 同时开启三层自动重试,否则一次用户请求会放大成几十次真实写入。
幂等键必须和业务操作绑定
每次可能写入的工具调用都生成一个幂等键,并将它保存到下游业务记录。一个实用格式可以包含会话、工具名、用户确认轮次和请求内容 hash:
mcp:session-8f2:change-priority:7:8a2d...
同一个键再次到达时,服务返回第一次的结果,而不是再执行一次。关键点是键的生命周期和存储位置:只放在内存缓存无法覆盖进程重启;只按用户 ID 去重又会误吞真实的新请求。把键与业务资源、过期时间和结果快照一起持久化,才能正确处理“请求成功但响应丢失”。
支持取消,但不要承诺无法取消的事情
客户端断开或用户点击取消后,Server 应停止尚未开始的工作,并把取消信号传给支持取消的下游请求。对于已提交的异步任务,返回任务 ID 和当前状态,而不是假装已经回滚。工具说明里要写明取消语义:是取消排队、取消运行,还是只停止等待结果。
长任务更适合拆成 start_export、get_export_status、download_export 这类可轮询流程。这样模型可以在每一步检查用户是否仍希望继续,也能在失败后从任务 ID 恢复,而不是重新发起整条昂贵流程。
给模型的错误也要可恢复
工具向模型返回错误时,最差的做法是只说“request failed”。模型会换一种措辞重试,或者误以为自己应该调用另一个写工具。错误响应需要同时说明失败阶段、是否已知业务结果、是否安全重试以及下一步动作。例如写入超时后,应返回 outcome: unknown、幂等键和查询结果的工具名;而不是返回 retryable: true。
对读取工具,响应可以建议降低查询范围、改用分页或稍后重试。对写工具,响应应优先建议 get_operation_status,只有确认没有产生副作用时才允许重放。把这些策略写进工具 contract 和回归测试,能避免模型在压力场景下把一个偶发错误放大成连续写入。
还要给每种工具设置并发上限。一个模型会话可能并发发起多个相似查询,多个会话也会同时命中同一个下游。按租户、工具和依赖服务限制并发,并把排队时间计入总预算。这样系统即使被大量重试,也会以受控的拒绝或排队响应保护关键依赖。
观测要回答恢复是否正确
至少记录工具名、下游依赖、耗时、重试次数、超时阶段、幂等键状态和最终业务结果。监控不要只看错误率,还要看“超时后查询到成功”的比例、“重复键命中”的比例和每种工具的 P95/P99 耗时。
上线前用故障注入验证:在下游完成写入后故意丢弃响应;让第一次请求返回 503;在异步任务执行中断开客户端。若系统能稳定返回同一个结果、不会双写、也不会无限占用连接,才说明 MCP 工具具备生产级恢复能力。
延伸阅读
Related