跳到主要内容

你的重试逻辑正在给 Agent 传达错误的教训

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个工具调用失败了。你的 Agent 框架使用指数退避(exponential backoff)重试了三次。第三次尝试成功了。追踪记录(trace)显示一个绿色的对勾。没人收到报警,错误计数器没有增加,用户得到了他们的答案。根据你所有的仪表盘,系统运行正常。

事实并非如此。工具失败是因为 Agent 传递了一个格式错误的参数,而第三次尝试之所以成功,仅仅是因为 Agent 在每次采样时表现不同,刚好在第三次尝试时正确表述了调用。你并没有从瞬时故障(transient fault)中恢复。你只是在玩老虎机直到它中奖,然后记录下中奖结果,并扔掉了那两次告诉你 Agent 已经坏掉的拉杆记录。

这就是重试逻辑悄悄腐蚀 Agent 系统的方式。重试是为“调用者正确且网络不稳定”的世界设计的。而 Agent 颠覆了这个假设:网络通常是正常的,而调用者才是不可靠的部分。当你把为第一种世界构建的重试策略应用到第二种世界时,它就不再是一种恢复机制,而变成了一种将 Bug “洗”成绿色对勾的手段。

两种看起来一样但本质不同的失败

每个重试策略都基于一个分类:这个失败是瞬时的还是确定性的?找准了,重试就是免费的可靠性。找错了,它们就是助燃剂。

瞬时失败是指完全相同的调用在重复时有真正的成功机会。例如 429 速率限制、连接重置、过载服务的 503 错误、网关超时。输入是有效的;外部世界只是暂时繁忙。重试完全相同的请求是正确的响应,而带有抖动(jitter)的指数退避是正确的处理形式。

确定性失败是指同样的调用每次都会以同样的方式失败。例如因凭据错误导致的 401,因 JSON Schema 验证未通过导致的 400,因记录 ID 不存在导致的 404。或者是语义失败,即工具返回了一个语法正确但并非 Agent 实际需要的结果。重试这些纯粹是浪费——三次相同的调用,三次相同的失败,三倍的延迟和成本。

经典的分布式系统建议清晰地划定了这条界线:在 429 和 5xx 时重试,绝不在 401、403、400 或上下文窗口溢出时重试。这个建议是正确的,但对于 Agent 来说还不够——因为它假设输入是固定的。对于 Agent,输入是每次重新生成的,这改变了一切。

Agent 破坏了重试契约

这是每个传统重试库都遵循但从未明说的假设:你重试的请求与失败的请求在字节层面是完全一致的。这就是瞬时/确定性分类奏效的原因。如果输入是恒定的,那么确定性失败就保持是确定性的,因此你可以安全地拒绝重试它。

Agent 违反了这一点。当编排器(orchestrator)重试一个 Agent 步骤时,它并不是在重放一个固化的请求——它是在重新调用模型,模型会重新采样,从而可能产生一个不同的工具调用。你在第一次尝试时所做的分类在第二次尝试时不再成立,因为第二次尝试并不是同一个调用。

这产生了一种在普通 RPC 中没有对应物的失败模式:间歇性成功的确定性失败。Agent 有一个真正的 Bug——它总是误解某个参数、选错工具或幻觉出一个字段名。但由于采样引入了噪声,也许四次生成中会有一次意外碰巧做出了正确的调用。三次重试策略将 75% 损坏的行为转换成了大部分时间都能“通过”的行为。

现在问问 Agent 学到了什么。在一次成功的运行中,Agent 看到的是:我调用了工具,它起作用了,我的方法是正确的。它从未看到那两次失败的尝试——那些尝试被底层的重试封装器吞掉了。模型的上下文中只包含成功的记录。所以下次遇到同样的情况时,它还会做同样错误的事情,因为没有任何信息告诉它那是错的。你的重试逻辑不仅仅是对仪表盘隐藏了 Bug,它还对唯一能够修复该 Bug 的组件隐藏了它。

这是错误的经验,而你正是那个教给它错误经验的人。

为什么这比不稳定的读取更糟糕

你可能会想:重试一向是用来掩盖不稳定性的,这只不过是老调重弹。并非如此,有两个专门针对 Agent 的原因。

第一点是副作用。根据定义,重试不稳定的读取是安全的——它没有后果。但 Agent 工具调用越来越多地涉及写入操作:发送电子邮件、信用卡扣款、提交工单、更新记录。当编排器因为步骤二超时而重试一个多步骤的 Agent 运行时,已经发送过邮件的步骤一会再次运行。现在客户收到了两封邮件。重试并没有恢复运行,而是重复了现实世界中的操作。模型是不确定的,但它触发的副作用却不允许是不确定的,而幼稚的重试恰恰破坏了这一规则。

第二点是成本和反馈形式。重试一次数据库读取只需花费几毫秒。重试一个 Agent 步骤则需要重新运行模型推理,通常还要重新运行该步骤中的每个工具调用,并再次消耗完整的 Token 费用。对一个昂贵的步骤进行三次重试,并不是对廉价操作造成的 3 倍延迟打击——而是花费了系统中最高昂操作的 4 倍成本,专门花在了你已经决定忽略的尝试上。更糟糕的是,当你将工具错误反馈给 Agent 并让重试时,失败的自我修正不仅仅消耗 Token——它还会让 Agent 在错误的道路上更加自信。通过检测发现,工具错误并没有变成有用的反馈,反而变成了燃料,让 Agent 挖出更深的坑,并为工具为什么“出错”编写更详尽的辩解。

因此,Agent 重试在两个方向上都行不通。在编排器层面盲目重试,你会导致副作用重复。将错误交给 Agent 让其重试自己的推理,你可能会放大错误而非修复它。这两种默认做法都不安全。你必须设计中间路径。

重试真正应该做的事

修复方法不是“减少重试”,而是让重试变得“诚实”——明确失败的原因、告知对象以及重复执行的内容。

在重试之前先分类,并默认将智能体(Agent)的调用视为可疑。 网络层的瞬态故障——429、503、重置、超时——重试“完全相同”的调用,这正是退避(backoff)机制的作用。确定性失败——4xx、模式验证(schema-validation)错误、未知工具、错误参数——不要重试完全相同的调用,因为它会以同样的方式失败。有趣的情况是智能体生成的输入导致了确定性失败:不要为了碰运气而静默地重新采样。要么停止,要么“将错误反馈到智能体的上下文中”进行重试,这样下一次尝试就是一次基于信息的纠错,而不是盲目的重新博弈。

将决策与执行分离。 在生产级智能体系统中,最稳健的模式是将大语言模型(LLM)的“决策”(使用哪个工具、哪些参数)与副作用的“执行”分开。重试时,你回放缓存的决策而不是重新询问模型,并且由执行层强制执行实际操作的幂等性。模型的不确定性与重试变得无关,因为你永远不会在重试时重新采样。这也是幂等键(idempotency keys)发挥作用的地方:在每个改变状态的工具调用中传递一个稳定的键,这样重放的“发送邮件”在第二次执行时就是空操作(no-op)。

让失败对智能体可见,而不仅仅是对操作员可见。 当工具返回错误时,该错误应作为明确的上下文进入智能体的下一步推理:工具失败了,这是错误信息,这是你发送的内容。如果智能体能看到“你的 date 参数被拒绝,预期格式为 ISO-8601”,它就能修复它。如果智能体的失败尝试被重试包装器(retry wrapper)吞没,它将学不到任何东西并重复同样的错误。反馈胜过吞没——但要配合对自我纠错尝试的硬上限,因为一个可以无限重试其推理过程的智能体,一定会这样做,而且还充满自信。

限制一切并路由残留任务。 限制重试次数。限制自我纠错循环。为整个运行过程设置超时预算,而不仅仅是针对单次调用。当预算耗尽时,该运行过程不会再获得重试机会——而是进入死信队列(dead-letter queue)供人工检查。无法自动解决的失败应该变成一个工单,而不是陷入更紧密的循环。

重试率是一个信号——开始解读它

最深层的修复在于文化。大多数团队将重试计数器视为底层架构——是框架管理的东西,而不是人类会去看的东西。对于智能体来说,重试率是你最真实的质量指标之一,而你可能正在忽视它。

第三次尝试成功的运行与第一次就成功的运行“不同”,如果你的遥测系统(telemetry)将它们合并为一个“成功”类别,你就是在自欺欺人。白盒监控的存在正是为了捕捉那些本会被重试掩盖的问题:服务显示为“在线”,交易显示为“已完成”,而实际上,成功率完全是靠重试撑起来的。一旦背景噪声发生变化——模型更新、输入分布稍微变难——重试就不再起作用,原本看起来健康的 99% 成功率就会崩溃,因为它从未真正达到过 99%。

所以,为你一直隐藏的东西安装监控工具。在每个智能体步骤中输出尝试次数。即使最终成功率看起来很平稳,也要针对“上升”的首轮尝试失败率发出警报——那个差距就是正在形成的 Bug。追踪哪些工具被重试的次数最多;一个重试率高的工具通常是因为它的模式(schema)令人困惑,导致智能体不断出错,修复方法应该是改进模式,而不是修改重试策略。当重试率攀升时,要像 SRE 对待延迟上升那样对待它:将其视为预警,而非噪声。

核心观点

为不稳定的网络设计的重试机制假设调用者是正确的。但智能体使调用者成为了不可靠的部分,如果不了解这种区别,重试策略就会悄悄地将你智能体的 Bug 转化为绿色复选框——通过采样噪声将确定性失败“洗白”成看起来像是瞬态的故障。

通过三个问题来审计你的智能体重试路径:重试是重复一个“字节级别相同”的调用,还是重新对模型采样并寄予希望?失败的尝试是否作为反馈进入智能体的上下文,还是在模型看到之前就被吞没了?你的遥测系统是否能区分首轮尝试成功和三次尝试后的成功——还是它已经决定视而不见了?

好的重试能从噪声中恢复并告诉你它发生了。坏的重试则隐藏了缺陷,并教导智能体继续犯错。两者的界限在于是否允许失败被看见。

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