你的重试逻辑正在给 Agent 传达错误的教训
一个工具调用失败了。你的 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 让其重试自己的推理,你可能会放大错误而非修复它。这两种默认做法都不安全。你必须设计中间路径。
