跳到主要内容

重试放大:2% 的工具错误率如何演变成 20% 的智能体故障

· 阅读需 15 分钟
Tian Pan
Software Engineer

在值班文档的表格上,搜索工具的错误率为 2%。事故审查报告称,在三个小时的时间窗口内,Agent 平台的故障率为 20%。没人对这两个数字有异议。搜索团队没有过错。平台团队也没有发布 Bug。这两个数字之间的差距就是故事的全部,而这是一个关于算术的故事,而不是工程能力的平庸。

重试逻辑是 Agent 系统中最常被借用且最少针对性调整的模式之一。团队从他们的 REST 客户端复制 tenacity 装饰器,将它们堆叠在 SDK、网关和 Agent 循环中,然后直接上线。每一层单独看都是合理的。但这种组合就像是一件指向集群中最不稳定依赖项的攻城武器,而且它恰恰在那个依赖项最需要降低负载的时刻开火最猛烈。

本篇文章将探讨这种数学逻辑是如何运作的,为什么 Agent 循环比请求-响应系统更剧烈地放大错误,以及如何通过重试规范防止瞬时波动演变成印着你自己公司 Logo 的关联性宕机事故。

重试放大的算术逻辑

先看一个无状态的事实。如果 N 个连续工具调用中的每一个都有 p 的失败概率,且失败是相互独立的,那么一次运行在没有任何失败的情况下完成的概率是 (1 - p)^N。在 p = 0.02N = 10 时,这个概率是 81.7%。反过来:单次会话 18.3% 的失败率建立在 2% 的单次工具错误率之上,这纯粹是步骤累加的结果。这还没算重试。当 p 很小时,单次运行的失败率大约是 1 - (1 - p)^N ≈ N·p —— 10 个具有 2% 错误率的工具调用,在发生任何其他错误之前,就会给你带来 20% 的 Agent 失败率。

现在加入重试。简单的封装通常是这样的:SDK 重试 3 次,网关重试 3 次,Agent 循环重试 3 次。运维人员称之为“深度防御”。系统则称之为每个逻辑请求 27 次尝试,因为各层是几何级数倍增的。当下游服务降级时,每个调用者突然在依赖项最无法承受的时刻贡献了基准负载的 27 倍。这就是典型的重试风暴,而 Agent 循环恰恰是其中最狂热的参与者,因为每个用户操作的扇出(fan-out)更大,且堆栈比同等的 Web 应用更高。

2025 年 12 月的一项关于微服务重试行为的实证研究为这种模式提供了一个清晰的基准。没有抖动(jitter)的指数退避产生了 2,600 毫秒的 p99 延迟和 17% 的错误率,这主要归因于重试放大。加入抖动后,p99 降至 1,400 毫秒,错误率降至 6%。在熔断器保护下的有限重试产生了 1,100 毫秒的 p99 和 3% 的错误率。这些数字具有普适性:无限制的重试并不能恢复服务,它们只会延迟恢复过程,并将爆炸半径隐藏在你的账单里。

为什么 Agent 循环比请求-响应放大得更严重

三个结构性差异使 Agent 的重试行为从令人恼火变成了极其危险。

首先,每个用户操作的扇出更高。一个 REST 端点平均每个请求触发一次下游调用。而一个 Agent 计划通常会触发 10 次或 20 次,并且在每一步中,语言模型都可以决定是否再次尝试同一个工具,理由是“也许结果不对”。这在经典意义上并不是重试 —— 它是模型因为工具输出模糊而发起的相同请求。你的可观测性层不会将其标记为重试。你的限流器不会对其进行去重。你的成本仪表板会将其显示为“正常的规划活动”。

其次,每次尝试的成本都高于一次网络往返。Agent 循环中的每次重试都会消耗输入 Token(在下一轮中,整个对话历史都会通过 LLM 重新处理)和输出 Token(模型在看到错误后生成的计划)。一个 ReAct 风格的 Agent 在认定工具损坏之前,如果重试三次失败的工具,很容易额外消耗 10,000 个输入 Token —— 完整的对话被计费了三次。在一次分析中,针对 200 个任务的基准测试里,90.8% 的重试浪费在了不可重试的错误上,例如调用不存在的工具。Agent 却一直在尝试。账单一直在攀升。下游服务一直在恶化。

第三,上下文累积与重试的交互效果很糟糕。当工具失败时,错误信息进入对话历史并保留在那里。下一轮不仅要为重现失败买单,还要为模型更长的计划、现在变得带有防御性的提示词,以及它倾向于过度解释为什么下一次尝试会不同的倾向买单。一个运行轮数多出 2 倍的会话,其成本往往会高出 3-4 倍,因为后面的轮次携带了更重的上下文。Agent 中的重试放大不仅是一个请求量问题;它是一个对你产生双重打击的 Token 数量问题。

自我 DoS 模式:当你的 Agent 攻击你自己的基础设施

教科书式的级联故障模式是这样的:一个下游工具 —— 比如搜索索引 —— 开始返回超时。Agent 将超时解释为瞬时故障并进行重试。重试与 Agent 的每步截止时间竞速,而截止时间足够宽裕,足以容纳三次尝试。当下游服务处于最脆弱的时刻,它现在正承受着 3 倍于平时的负载。延迟增加。Agent 的截止时间开始触发。用户点击 UI 中的重试按钮,这会产生带有全新重试预算的新 Agent 运行。在三分钟内,搜索层达到饱和;在七分钟内,甚至共享工作池的无关工作流也开始降级。初始诱因在第四分钟消失了,但到第九分钟,系统仍在攀升,因为重试流量现在正在自我维持。

这是一种亚稳态故障(metastable failure)。导致故障的输入已经消失,但系统无法自行恢复,因为其自身负载使其保持在降级状态。唯一的出路是停止重试,而这恰恰是你的重试逻辑设计初衷要防止的行为。

第二种变体是 Agent 系统特有的:Agent 正在 DoS 它自己的配额。单个用户请求扇出为数十个并发工具调用,这些调用都从同一个每用户 Token 桶中抽取资源。前几个调用触碰了限制。网关返回 429。Agent 重试。重试也触碰了限制。Agent 现在将会话的剩余时间花在与自己争夺配额上,没有任何工作能完成,并且用户的整个 Token 配额都消耗在了自找的恢复尝试中。根本不需要对手攻击。

重试放大故障模式和自我 DoS 故障模式是应用在两个方向上的同一种数学逻辑 —— 一个针对共享的下游容量,另一个针对每个租户的配额 —— 且两者都需要相同的解决方案。

四层重试预算

借鉴 Google SRE 的生产重试策略,一套真正有效的规范包含四个相互配合而非叠加的层级。

第一层:单次请求上限。在整个调用栈中,单次操作的重试次数总计绝不能超过 2-3 次。这是一条硬性规则,而不是每层重试 2-3 次。如果 SDK 重试 3 次,网关重试 0 次,Agent 循环重试 0 次——这就是系统的预算。最糟糕的模式是三个层级都认为自己拥有重试合约。明确写下 2-3 这个数字,选择执行它的层级,并将其他层级设为零。请求元数据应携带尝试计数器,以便每一层都能看到预算是否已耗尽。

第二层:客户端维度的重试预算。在客户端跟踪重试与成功请求的比例。Google 的生产规则是,只有当该比例低于 10% 时,才会进行重试——这意味着重试在健康运行期间最多只能贡献 10% 的额外负载。如果下游发生故障,比例会在几秒内冲破 10%,客户端将彻底停止重试。这是一个反馈循环:依赖服务越脆弱,击中它的重试就越少,这与朴素的重试行为正好相反。

第三层:带抖动的指数退避。封顶的指数退避加上全抖动(0 到计算出的指数值之间的随机延迟)或去相关抖动(下次延迟在基础值和 3 × 上次延迟 之间随机取值)。AWS 的数据显示,与确定性退避相比,这能减少 60-80% 的重试风暴。关键词是“抖动 (jitter)”:如果没有它,同步发生的失败会产生同步的重试,进而在所有客户端的同一个时钟时刻产生第二次失败。

第四层:熔断器。当下游的错误率超过阈值时,熔断器开启,后续调用会立即失败,完全不触碰网络。经过一段冷却期后,熔断器进入半开状态,允许探测流量通过,并根据成功与否决定关闭还是重新开启。这是四层中唯一能真正减轻下游压力的一层;其他三层只是限制了你“增加”的压力。一个拥有前三层但没有熔断器的系统仍然会发送流量,只是速度较慢且受限。而熔断器在故障期间发送的流量为零,这正是下游真正需要的。

每一层级的存在都是为了处理其他层级无法处理的失败模式。单次请求上限防止单个调用陷入循环。客户端预算防止单个客户端成为 DoS 攻击源。抖动退避停止同步风暴。熔断器在故障期间完全切断流量。一个只部署了其中一层的重试策略,注定会让其运维人员感到“惊喜”。

针对 Agent 的重试预算:Token,而不只是请求

上述四层源自请求-响应系统。Agent 需要第五层:以 Token 为单位的重试预算。在 Agent 会话中,限制用户体验的稀缺资源不是请求,而是 Token。如果一个重试策略在“尝试 3 次”后停止,但允许每次尝试消耗 15,000 个输入 Token,那么它为了从一个损坏的工具调用中恢复,就耗尽了一个中端用户一天的预算。

具体而言,这意味着:

  • 在重试之前先对错误进行分类。ReAct 基准测试中 90% 的重试浪费几乎全是不可重试的错误——缺失工具、身份验证失败、参数格式错误——却被当作可重试错误处理。将分类硬编码到工具结果的 Schema 中(例如在错误包中设置 retryable: false),并让 Agent 循环在不咨询模型的情况下遵守它。对于不可重试的错误,无论计划看起来多么自信,重试次数都应为零。
  • 以 Token 而非尝试次数来计算预算。累加会话内失败尝试所花费的输入和输出 Token。当总和超过会话上限(一个现实的数字是会话目标 Token 预算的 10%)时,停止重试并将失败呈现给你。该上限防止了单个不稳定的工具烧掉整个会话的额度。
  • 让规划器感知重试决策。大多数 Agent 在不告知模型的情况下进行重试。这意味着模型会不断制定相同的计划并得到相同的错误。相反,应将重试耗尽作为一个结构化信号呈现在对话中(“搜索工具失败 3 次,预算耗尽,请选择替代方案”),以便规划器能够绕过故障,而不是盲目地撞墙。允许模型进行规划;但不允许它自主决定重试。

幂等性:本不该重试的重试

最后一块硬骨头是副作用。经典的重复操作 Bug 如下:Agent 调用 send_email,工具成功执行,响应在返回路径上超时,Agent 重试,发出了两封邮件。在 Web 应用中,浏览器会看到重复提交,用户也会察觉。但在 Agent 系统中,模型读取到第二次成功,并自信地叙述邮件已发送。这种重复是无声的。

三种防御手段至关重要:

  • 为每一个有副作用的工具提供幂等键。将 (workflow_id, tool_name, normalized_args) 哈希成一个键。工具服务器维护一个账本。带有相同键的重复调用将返回存储的结果,而不是重新执行。Stripe 的 API 文档是权威参考——这种模式虽然枯燥但经过实战检验,Agent 工具服务器没有任何理由不直接采用它。
  • 将超时视为歧义,而非失败。超时的操作可能已完成,也可能未完成。在写路径上重试非幂等操作是导致重复副作用的根源。要么让工具具备幂等性,要么将超时标记为写路径上的不可重试错误。
  • 将破坏性操作置于确认层之后。对于真正不可逆的操作——金融交易、生产部署、面向客户的邮件——在重试路径上引入人工确认可以防止无声的重复。这是当所有其他防御失效时,唯一能生还的防线。

衡量指标

如果重试放大没有包含在你的监控栈中,那么它正在发生且你无法察觉。最起码的监控指标包括:

  • 每个下游的重试率。重试次数除以总请求数,以一分钟为窗口。如果该比例持续超过 10%,则说明下游服务或你的重试策略存在问题。
  • 单次会话的重试 Token 消耗。在产生错误结果的尝试中所消耗的 Token。以占会话总 Token 的百分比表示。任何超过 15% 的指标都是依赖项性能下降的先行指标。
  • 熔断器开启率。每个下游熔断器处于开启状态的时间比例。这是“该功能对用户实际不可用的频率”的真实反映。一个从未开启过的熔断器很可能没有被正确调优。
  • 自我 DoS 事件。单个用户的重试消耗了超过其单租户 Token 预算 50% 的会话计数。发生一次是 Bug;多次发生则是重试循环中的系统性问题。

这些都不需要新的基础设施。如果你根据工具调用结果进行聚合,这四个指标都可以从你现有的追踪和计费遥测数据中计算出来。较难的部分是选择触发报警的阈值,而唯一诚实的回答是,你需要根据你自己的 SLO 来进行调优。

核心观点

重试放大并不是任何单一层级的 Bug;它是互不感知的层级之间产生的突现行为。解决方法不是移除重试——瞬时故障是真实存在的,而且一个经过正确重试的调用,其用户体验要比原始下游成功率所显示的要好得多。解决方法是停止将重试视为“免费”,并像对待任何其他有限资源一样对其进行核算:设立预算、执行层、指标,以及当预算本身成为攻击面时的熔断器。

Agent 循环会将核算中的每一个错误变成倍率。当你将步骤相乘时,2% 的工具错误会变成 20% 的会话失败。当重试相乘时,20% 的会话失败会变成自我 DoS。当重试支撑了导致重试的负载时,自我 DoS 就会变成亚稳态故障。数学对于那些仅凭直觉部署重试逻辑的系统是无情的。战胜数学的纪律是枯燥的:限制尝试次数、预算 Token、对退避增加抖动、执行熔断,并让规划器看到基础设施所看到的情况。构建这五样东西,你的电子表格数据和事故数量就会达成一致。

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