某团队Agent循环8小时产生12.7万次API调用、约4.7万美元账单;文章给出服务端限流层设计、429响应格式和级联重试死锁的解决思路。
我上季度接触的一个团队,出了一个持续调用同一个工具 8 小时的卡死 Agent。等有人发现时,已经累积了一张五位数的云账单。这不是假设场景——有报道过一起失控自动化循环在约 8 小时内发起 127,000 次 API 调用,最终费用约 47,000 美元。没有限速器、没有熔断器,Agent 和 API 计量表之间什么保护都没有。
本文涵盖如何为 MCP 服务器设计限速与重试/退避逻辑,让卡死的 Agent 被廉价地阻止而非付出高昂代价。不涉及 OAuth token 刷新(见前一篇博文),也不涉及完整的可观测性搭建——那是另一个话题。范围限定在:在哪一层做限速、如何返回一个 Agent 真正能处理的 429 响应,以及那个导致大多数 MCP 生产环境故障的重试错误。
大多数团队只在客户端加上重试逻辑就收工了。这只是及格线,不是解决方案——它能防止短暂的抖动,但对结构性过载毫无作用,而且如果放置位置不对,反而会让情况更糟。
真正让 MCP 服务器在生产环境挂掉的故障模式不是"没有退避"。而是单个 Agent 调用回合内的级联重试:模型调用一个工具,收到 429,MCP 服务器内部用退避重试,而在这个重试还未完成时,同一个 Agent 回合又调用了第二个工具,撞上了同一个上游 bucket。现在你有两个重试循环在同一个配额上赛跑,bucket 恢复时间变成了两个退避schedule中较长的那个,而不是其中任何一个。
解决方案不是更复杂的退避算法。完全不应该在服务器端对 Agent 可见的调用做重试——直接把 429 冒泡返回给模型,并带上清晰的结构化错误和等待时间提示,让 Agent 决定是等待、切换工具还是告知用户。在服务器内部静默重试会把真实信号隐藏起来,而唯一能实际阻止同一个工具在接下来两秒内再被调用五次的东西(Agent 的推理循环)反而收不到信号。
"给服务器限速"这个说法太模糊了。至少有三种层次,把它们混为一谈的团队往往只监控了其中一层,而被另外两层偷袭:
Tenant 层比看起来更重要。如果一个 OAuth 凭证在多个客户间共享,第一个被限速的 Agent 会触发级联效应,同一个凭证背后的所有其他会话都会继承这个拦截——有时在第一次调用后二十秒内就会发生。按 tenant 隔离的 token bucket 能限制爆炸半径;底层的凭证模型决定了半径本来有多大。如果你给一个服务 40 个 tenant 的后端只发了一个共享 API key,你的限速器需要感知 tenant,而不是只看请求。
Token bucket 是大多数 MCP 工具调用限速的正确原语,因为它允许突发流量(合法的 Agent 批量处理 10 个条目),同时又能限制持续速率。
import time
import asyncio
class TenantTokenBucket:
def __init__(self, capacity: int, refill_per_sec: float):
self.capacity = capacity
self.tokens = capacity
self.refill_per_sec = refill_per_sec
self.last_refill = time.monotonic()
self.lock = asyncio.Lock()
async def try_consume(self, cost: int = 1) -> bool:
async with self.lock:
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(
self.capacity,
self.tokens + elapsed * self.refill_per_sec
)
self.last_refill = now
if self.tokens >= cost:
self.tokens -= cost
return True
return False
两个容易跳过但很重要的细节:
它是 async 并带锁保护的。MCP 的流式架构意味着一个慢速工具调用如果限速器不具备 async 感知能力,就会阻塞事件循环——最终死锁那些只是等待响应的客户端,看起来像是挂起了,而不是限速。
cost 是参数但不一定是 1。一个工具如果 fan out 成五个上游调用,应该消耗五个 token,而不是一个。扁平化的按请求限速会让廉价调用浪费预算,而让昂贵调用定价过低。
限速的价值取决于它发回的信号。常见失败是一个裸 429 没有任何机器可读的指导——客户端不知道该等 500ms 还是 5 分钟,所以做了些随意的事,通常是立即重试,让情况更糟。
把你的限速错误结构化,让 Agent(不是看日志的人)能解析:
{
"error": {
"code": "rate_limited",
"message": "Tool call rate limit exceeded for this session",
"retry_after_ms": 4200,
"scope": "tenant:acme-corp:tool:search_invoices"
}
}
retry_after_ms 字段在做真正的工作——它告诉 Agent 的重试逻辑确切需要退避多久,而不是靠猜。而且如果上游 API 给你发了 Retry-After 头,要把它当作强制性的下限,而不是建议。有些 API 强制硬性冷却时间(Airtable 的 30 秒窗口是众所周知的例子),在这个窗口关闭前重试只会重置时钟。
对于那些确实应该写在代码里的重试(Agent 到工具的重试,不是上面那种级联的服务器端重试),生产环境指导中一致出现的策略:
每次重试翻倍,上限 30 秒
加上 ±20% 的抖动,这样多个会话从同一故障中恢复时不会全在同一时刻重试(雷鸣般的羊群效应)
永远不要比 500ms 更紧——任何更快的速度在那个你正试图尊重的限速器看来都像是一阵突发流量
限速器保护的是你的服务器,但无法修复那个卡在循环里的 Agent——只是让循环更便宜。值得单独加一个机制:如果一个会话连续多次用完全相同的参数调用同一个工具,那就是卡死 Agent 的强烈信号,不是合法流量。真正的 Agent 每次调用之间会改变参数(不同的 ID、不同的查询);卡死的 Agent 则重复完全相同的调用。标记这个模式并返回硬停(不只是更慢的重试),比等着 token bucket 一个一个拦截要便宜。
限速只是更长列表中的一个环节——会话处理、超时预算、错误分类,以及在上线前能发现这些问题的前置检查。其余内容在包里有:AI Agent Incident Postmortem & Permission-Scoping Template Pack 包含了限速被绕过、某东西最终还是失控时的Incident响应模板——专门为 Agent 工具调用事故(而非通用基础设施故障)构建的权限范围工作表和事后分析模板。
如果你本月要交付一个 MCP 服务器,经受住负载的组合是:按 tenant 隔离的 token bucket、带 retry_after_ms 的结构化 429、Agent 可见调用上无服务器端重试,以及一个不依赖 bucket 耗尽来检测的独立卡死循环检测器。其余都是调优。
Written with AI assistance and reviewed for accuracy.