Agent 系统中的重试风暴问题:为什么每次失败的工具调用都在烧掉你的 Token 预算
每个后端工程师都知道重试是必不可少的。每个分布式系统工程师都知道重试是危险的。当你让 LLM agent 负责重试工具调用时,你会同时遇到这两个问题,而且还有一个新问题:每次重试都会消耗 token。一个不稳定的 API 端点可能会在不到一分钟的时间内,将一个 0.01 美元的 agent 任务变成一场 2 美元的灾难。
重试风暴问题并不新鲜。分布式系统几十年来一直在处理惊群效应(thundering herds)和级联故障。但 agent 系统放大了这个问题,而微服务模式无法完全解决它,因为重试逻辑存在于一个不理解背压(backpressure)的概率推理引擎中。
为什么 Agent 重试从根本上有所不同
在传统的微服务中,重试成本很低。你重新发送一个 HTTP 请求——可能只有几 KB 的负载和几毫秒的计算。十次重试的成本大约是单次请求成本的十倍。
在 agent 系统中,每次重试都会将整个对话上下文发送回 LLM。第一次尝试失败的工具调用不只是重试该工具——它会重新处理完整的 prompt、所有先前的消息以及工具调用历史。如果你的 agent 已经积累了 8,000 个 token 的上下文,而一个工具调用失败了,那么每次重试都会消耗 8,000 个输入 token,再加上模型生成的任何内容。十次重试意味着为了零生产力工作而进行了 80,000 个 token 的输入处理。
这产生了一个传统重试计算无法衡量的成本放大因子。生产报告显示,不受控制的 agent 重试循环产生的 token 成本可能是单次成功执行的 200 倍。失败不仅在于 agent 重试次数过多,还在于每次重试的成本都以 HTTP 重试所不具备的方式变得昂贵。
还有一个更微妙的问题。LLM 不只是机械地重新调用失败的工具。它会推理失败的原因,可能会更改参数、尝试替代方法,或生成冗长的关于出错原因的思维链(chain-of-thought)。这种推理本身会消耗 token 并增加延迟,还可能使 agent 进一步偏离正确的恢复路径。
三种级联故障模式
Agent 系统中的重试风暴遵循三种不同的级联模式,每种模式都需要不同的防御措施。
模式 1:工具失败 → 重试循环 → token 大出血。 下游 API 返回 500 错误。Agent 重试工具调用,但每次重试都包含完整的对话上下文。如果没有重试预算,agent 会一直尝试,直到达到 token 限制或超时——以先到者为准。与成功执行花费 0.01 美元相比,一个调用不稳定端点的支付处理 agent 可能会在毫无进展的情况下烧掉 2 美元的 token。
模式 2:速率限制 → 背压崩溃 → 超时级联。 LLM 提供商本身返回 429(速率限制)。Agent 的编排层使用指数退避重试 LLM 调用。但在退避期间,上游请求开始堆积。当速率限制解除时,所有排队的请求同时触发——典型的惊群效应。提供商再次进行速率限制,循环往复。具有 N 个 agent 的系统有 N(N-1)/2 个潜在的并发交互,因此协调复杂度呈二次方增长。
模式 3:用户重试 → Agent 放大 → 系统过载。 由于陷入重试循环,agent 花费的时间太长。用户在没看到响应的情况下刷新页面或重新提交。这会产生第二个 agent 实例,使本已压力巨大的基础设施负载翻倍。第二个 agent 也会陷入困境,用户再次尝试,现在你运行着三个 agent 实例,都在猛攻同一个发生故障的服务。生产监控观察到,仅此一种模式就能在几秒钟内产生 10 倍的负载放大。
为什么微服务熔断器无法直接套用
熔断器模式(circuit breaker pattern)——跟踪失败率、达到阈值后熔断、定期以半开状态探测——是微服务中防御重试风暴的标准手段。但将其直接应用于 agent 系统会发现三个不匹配之处。
不匹配 1:Agent 不受控的重试。 在微服务中,你用熔断器包装 HTTP 客户端。熔断器拦截调用并在开启时快速失败。在 agent 系统中,LLM 本身决定是否重试。你可以指示它不要重试,但模型仍可能尝试访问相同底层服务的替代工具调用,或者通过推理得出功能上等同的重试。重试逻辑是涌现的,而非程序化的。
不匹配 2:故障分类模糊。 熔断器需要区分瞬时故障(值得重试)和永久故障(不要重试)。对于传统 API,这种映射是明确定义的:503 是瞬时的,404 是永久的,429 意味着等待。但 agent 工具调用失败的方式可能没有清晰的 HTTP 状态码。一个返回零结果的搜索工具——这是失败还是仅仅是空结果?一个以意外格式返回数据的 API——agent 应该尝试使用不同的参数重试还是放弃?LLM 以概率方式做出这些判断,并且经常出错。
不匹配 3:重试之间的状态变化。 在微服务重试中,请求通常是幂等的。在 agent 循环中,世界可能在第一次尝试和重试之间发生了变化。Agent 可能已经执行了副作用——发送了电子邮件、创建了数据库记录、扣了信用卡。重试多步工作流与重试单个 HTTP 调用不同。Agent 需要了解哪些操作已完成,哪些未完成,而这是传统熔断器无法解决的问题。
为 Agent 构建重试韧性
在 Agent 系统中,有效的重试处理需要一种多层级的方法,运行在技术栈的不同层面。
第 1 层:工具级重试预算。 每个工具都应该有明确的重试预算 —— 最大尝试次数、最大总时长和最大 Token 消耗。这些限制存在于工具的执行包装器中,而不是提示词(Prompt)中。当预算耗尽时,工具会返回一个 结构化的错误,告知 Agent 停止尝试。一个合理的默认设置是三次尝试,采用指数退避(1s, 2s, 4s)以及 30 秒的总超时时间。
第 2 层:Agent 级失败预算。 除了单个工具的重试外, Agent 自身对于整个任务也需要一个失败预算。如果一个 Agent 在单个任务中遇到了跨不同工具的五次失败,那么很可能是系统性问题。此时, Agent 应该升级给人工处理或优雅降级,而不是继续探测已经损坏的基础设施。跟踪失败操作上的累计 Token 消耗,并设置一个以美元计价的断路器 —— 如果 Agent 在没有进展的情况下重试花费超过 0.50 美元,请停止。
第 3 层:编排级背压 (Backpressure)。 管理 Agent 实例的编排层需要自身的保护机制。为每个服务、每个用户和每种 Agent 类型实施并发限制。当下游服务开始返回错误时,减少依赖该服务的新的 Agent 任务速率。这就是防止用户重试放大的背压机制:如果系统已经在为该用户处理一个任务,请拒绝或排队重复请求,而不是产生一个并行的 Agent。
第 4 层:工具边界的错误分类。 工具应该返回结构化的错误响应,帮助 Agent 做出更好的重试决策。不要只返回原始异常消息,而是返回错误分类:
- Retryable-transient (可重试-瞬时):服务暂时不可用,延迟后重试
- Retryable-modified (可重试-修改后):请求格式错误,使用不同参数重试
- Terminal-permanent (终端-永久):资源不存在、权限被拒绝、非法操作
- Terminal-budget (终端-预算):重试预算耗尽,升级或降级
这种分类将重试决策从 LLM 的概率判断转移到了工具包装器中的确定性逻辑,这才是它该待的地方。
让一切可见的仪表化监测
你无法修复你看不到的重试风暴。大多数 Agent 框架会记录工具调用,但不会显现出预示故障的重试模式。生产环境中的 Agent 系统需要三个特定的观测信号。
每个工具的重试率。 跟踪每个工具的重试尝试次数与成功完成次数的比例。一个健康的工具重试率应低于 0.1(每 10 次调用少于 1 次重试)。比例超过 0.5 意味着工具失败多于成功,并且正在疯狂消耗你的 Token 预算。当持续比例超过 0.3 时发出告警。
失败的 Token 成本归因。 将你的 Token 支出分为生产性 Token(用于成功完成)和浪费性 Token(用于失败尝试和重试)。在一个调优良好的系统中,浪费应低于总支出的 5%。在重试风暴期间,浪费可能会飙升至 80% 以上。这个指标是发现问题的最快信号。
级联深度。 当一个 Agent 的失败触发另一个 Agent 的重试时,跟踪级联的深度。深度为 1(Agent 重试自己的工具)是正常的。深度达到 3 以上(Agent A 的失败导致 Agent B 重试,进而导致 Agent C 重试)表明存在系统性问题,单个 Agent 的断路器无法捕获。你需要可以暂停整个 Agent 工作流而不仅仅是单个工具的系统级健康检查。
生产团队报告称,与基于日志的调试相比,具备这些信号的分布式链路追踪在处理多 Agent 故障时,能将平均修复时间 (MTTR) 降低 70%。
