跳到主要内容

重试放大: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 这个数字,选择执行它的层级,并将其他层级设为零。请求元数据应携带尝试计数器,以便每一层都能看到预算是否已耗尽。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates