跳到主要内容

Agent 系统中的重试风暴问题:为什么每次失败的工具调用都在烧掉你的 Token 预算

· 阅读需 12 分钟
Tian Pan
Software Engineer

每个后端工程师都知道重试是必不可少的。每个分布式系统工程师都知道重试是危险的。当你让 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%。

区分可恢复失败与终端失败

Agent 重试设计中最难的部分是分类问题:Agent 什么时候应该再试一次,什么时候应该停止?在任何一个方向上出错都是昂贵的。过度重试会浪费 Token 并耽误用户。重试不足则会放弃那些本来可以在下次尝试中成功的任务。

一个实用的框架从两个维度对失败进行分类:原因(基础设施 vs. 逻辑)和范围(局部 vs. 系统性)。

局部范围的基础设施失败 —— 单个 API 超时、瞬时 DNS 错误 —— 是典型的重试候选项。使用带抖动 (Jitter) 的指数退避和较小的重试预算。

系统性范围的基础设施失败 —— 供应商范围的停机、所有端点的速率限制 —— 需要断路器并回退到备用供应商或降级功能。此时重试只会让情况变得更糟。

局部范围的逻辑失败 —— 参数格式错误、选错了工具 —— 应该尝试使用修改后的参数进行重试。给 Agent 一次纠正其方法的机会,但上限设为两次尝试。如果 Agent 在两次尝试内无法搞定参数,那么它尝试十次也搞不定。

系统性范围的逻辑失败 —— 对任务的根本性误解、该工作所需的工具库存错误 —— 是终端性的。Agent 应该报告它尝试了什么、什么失败了,并移交给人工处理。对于使用了错误工具来执行任务的 Agent,再多的重试也无济于事。

将重试预算作为一项设计约束

最有效的模式是将重试预算视为一等公民的设计约束,而不是在错误处理之后才附加的补丁。当你设计 Agent 工作流时,应明确分配故障预算:总共重试多少次、用于恢复的 Token 数量、以及整个任务超时前的时长。

这种预算机制会迫使你在早期做出架构决策。一个重试预算为 0.10 美元的工作流可以负担得起三次简单工具调用的重试,但却无法负担哪怕一次复杂的多工具序列重试。这一约束会促使你转向更小、更具可组合性的工具操作——而这些操作本身就更加可靠。

那些将重试视为免费的团队不可避免地会发现,事实并非如此。无休止重试一切的 Agent 最终在失败尝试上花费的成本,会超过它在成功自动化中节省的成本。真正达到生产级标准的 Agent,是那个知道何时该停止尝试并寻求帮助的 Agent。

References:Let's stay in touch and Follow me for more thoughts and updates