跳到主要内容

重新规划而非重试:为什么大多数智能体错误并非瞬时性的

· 阅读需 12 分钟
Tian Pan
Software Engineer

一次日历写入返回了 409 Conflict。框架默认的错误处理器开始介入:退避 200ms,重试。同样的冲突。退避 400ms,重试。同样的冲突。退避 800ms,重试。等到智能体放弃并告诉用户“我无法预订会议”时,它已经浪费了三秒钟的延迟预算,去证明第一条响应就已经告诉它的事实:该时段已被占用。世界没有改变。它也不会在 800 毫秒内改变。重试永远不会奏效,因为这个错误中没有任何瞬时性的成分。

这是智能体系统中最为常见的错误处理 bug,而且它就隐藏在当今几乎每一个发布的框架之中。带有指数退避的重试模式是从无状态 HTTP 客户端中照搬过来的——在那里这种模式完全正确——但被引入到有状态的规划循环中时,它就完全错误了。对于智能体中的工具错误,正确的默认处理方式不是重试,而是重新规划。

借来的反射动作

指数退避是分布式系统中最成功的模式之一。不稳定的网络丢包、服务短暂过载、依赖项重启——稍等片刻,再次尝试,大多数情况下问题都会自行消失。数十年的 HTTP 客户端、消息队列和数据库驱动程序已经将这种模式烙印在每位工程师的反射神经中。当 LLM 智能体这一代框架发布时,它们直接引入了这种反射动作,而没有审视其背后的假设是否依然成立。

事实并非如此。在无状态 HTTP 客户端中,故障几乎总是关于传输的:服务器正常运行,请求有效,只是你和服务器之间出现了小故障。在有状态的智能体循环中,故障几乎总是关于现实世界的:日历时段已被占用、支付校验拒绝了银行卡、搜索返回零结果、你想读取的文件不存在。这些不是传输故障。它们是环境在告诉规划器:它所依赖的假设是错误的。

重试传输故障是合理的,因为请求相同,但第二次尝试时的传输环境不同。重试语义故障是不合理的,因为请求相同,而现实世界也相同。你是在针对相同的状态运行相同的确定性计算,却期待得到不同的结果,这正是 bug 的教科书级定义。

二维分类法是破局的关键

团队之所以一直沿用“默认重试”的做法,是因为他们的错误模型只有一个分类——“工具返回了错误,做点什么”——而“等待并重试”是编写成本最低的“点什么”。破局的关键在于沿着两个具有完全不同恢复语义的维度来拆分这个分类。

第一个维度是瞬时性(transient)与持久性(persistent)。瞬时性错误只要等待就会消失:例如 429 速率限制、刚触发报警的服务的 503 错误、负载下的网络超时。持久性错误则不会消失:例如 409 冲突、422 校验拒绝、已删除资源的 404 错误。重试是处理瞬时性错误的正确举措,而对于持久性错误,重试则是范畴错误。

第二个维度是基础设施相关(infrastructural)与语义相关(semantic)。基础设施错误涉及调用本身——线路、认证、速率限制、运行时。语义错误涉及调用的含义——时段被占用、价格变动、行违反唯一约束、客户处于欺诈拦截状态。基础设施错误通常需要重试或回退策略;语义错误几乎总是需要重新规划。

这些维度是相互独立的。503 是瞬时的且属于基础设施层面的——重试是正确的。429 是瞬时的,且在某种程度上是关于速率限制预算的语义错误——应带预算上限地进行退避重试。409 冲突是持久的且属于语义层面的——重试是徒劳的;唯一有用的反应是重新观察并重新规划。401 是持久的且属于基础设施层面的——重试是徒劳的,但重新规划也毫无意义,因为该凭证下的任何计划都不会成功;这种情况下应该升级处理。

如今大多数技术栈中提供的框架默认值将所有四个象限都压缩到了“带退避的重试”这一格中,而这仅对其中一种情况是正确的。

为什么这不仅仅是对代码质量的抱怨

生产环境中的三种失败模式推动了这种升级。

第一种是注定失败的重试带来的延迟税。重试预算为 3 次,退避时间分别为 200ms / 400ms / 800ms,这会给任何返回持久语义错误的调用增加 1.4 秒的固定延迟——而这些延迟最终由用户承担。在出现日历冲突时,用户只能盯着加载动画,而智能体原本可以在 200 毫秒内检测并呈现这一冲突。最近针对 ReAct 风格智能体的基准测试发现,大部分重试预算都花在了永远不会成功的错误上;在一次包含 200 个任务的评估中,超过 90% 的重试是针对根本不存在的工具。

第二种是非幂等重试带来的正确性退化。许多看起来是幂等的工具实际上并非如此。一个超时的支付创建调用可能在服务器端已经成功了;重试会产生重复扣费。一个返回 5xx 的发送邮件调用可能已经将消息排入队列;重试会导致重复发送。框架默认重试,应用开发者默认“工具应该是幂等的”,当这些假设发生冲突时,用户就会被扣费两次。相比之下,“重新规划”强迫智能体在行动前重新观察状态,这使得重复副作用的风险在规划阶段就暴露出来,而不是在执行阶段被掩盖。

第三种是掩盖真实问题的评估(eval)分数。成功率评估无法区分“智能体在一次调用后成功”与“智能体在八次重试和一次恢复后成功”。优化评估通过率的团队往往在不知不觉中优化了“重试容忍度”——模型学会了依赖框架免费提供的重试——结果策略在测试集中看起来很好,但在生产环境中感觉迟缓且昂贵。PALADIN 系列工作专门针对工具失败恢复轨迹进行训练,报告称通过在训练期间而非事故处理期间让模型接触失败,恢复率从 32% 提高到接近 90%。

真正有效的默认策略

起点是对每个工具采用类型化的错误契约——不是自由格式的字符串,而是一个智能体可以推理的结构化对象:一个类别(瞬态基础设施型、持久基础设施型、瞬态语义型、持久语义型)、一个代码、一个人类可读的原因,以及产生该错误的观测状态。有了这份契约,恢复策略就变得机械化,而非即兴发挥。

持久语义型错误会触发重新规划(replanning)步骤,而非重试。智能体被告知错误类别、失败的步骤、观测到的状态以及原始目标,并被要求生成一个新的计划,以应对刚刚发生的变化。日历冲突会产生一个新的计划,建议一个不同的时间,要求用户做出选择,或者转向不同的参会者群体——而不是在同一个时间槽上进行更密集的循环重试。

瞬态基础设施错误会触发带退避机制的有界重试,就像在 HTTP 客户端世界中一样。这种限制是针对每次调用的,而不是针对每个轮次(turn)的,因此单个不稳定的工具不会耗尽整个延迟预算。

持久基础设施错误——无效的凭证、缺失的工具、格式错误的 Schema——会立即上报(escalate)。围绕 401 错误进行重试或重新规划是浪费计算资源;智能体应该将失败呈现给更高级别的权限(用户、操作员或具有更广泛权限的备用智能体),而不是消耗更多的轮次。

随后,每个轮次的重新规划预算也被设定了上限,这与每次调用的重试预算是分开的。一个典型的配置是:每次调用重试 3 次,每个轮次重新规划 2 次,然后上报。这就是最近关于结构化异常处理的学术研究(去年的 SHIELDA 和基于调度理论的图表工具论文)所趋向的有界恢复形态,因为它防止了两种病态循环:对确定性失败的无限重试,以及对结构上不可能实现的目标进行无限的重新规划。

仪器化:错误后重新规划应该是你的主要平滑路径

一个内化了这种设计的团队会在其遥测数据中看到一些有趣的现象:“错误后重新规划”(replan-after-error)成为了最常见的平滑恢复路径,其比例远超“重试并成功”(retry-and-succeed)。这才是正确的比例。大多数生产环境中的失败都是世界在向智能体传达某些信息。你应该担心的指标恰恰相反——高“重试后成功”率通常意味着要么是应该在平台层修复的不稳定基础设施,要么是持久性错误被误分类为瞬态错误,并且仅仅因为他人的幂等性救了你而偶然成功。

这种视角下产生的具体仪表板包括:按错误类别细分的每轮次重新规划率、语义失败后的重新规划时间、每个工具的重试预算消耗,以及触及用户的上报次数。每一个指标都能告诉你一些通用成功率评估(eval)无法提供的信息。日历工具重新规划率的激增意味着外部世界的竞争比你的计划预想的更频繁——也许是某个发布破坏了你的可用性启发式算法。支付工具重试预算消耗的激增意味着供应商正在降级,你应该在更多用户调用碰壁之前进行熔断。

这所要求的评估纪律

只有当你的评估套件对其进行测试时,“默认重新规划”才有效。当今大多数智能体基准测试都构建了理想路径追踪(happy-path traces)——世界是配合的,每个工具都返回成功,智能体的任务是将它们串联起来。而对恢复而言,真正重要的追踪是对抗性的,即世界在计划和执行之间发生了变化:智能体刚刚确认的时间槽被另一个调用者预订了,智能体报价的价格刚刚变动了,智能体计划阅读的文档被删除了。

一个好的重新规划评估会在执行中途注入这些状态变化,并断言一个属性——不是“智能体完成了任务”,因为有时正确的答案是“告诉用户时间槽已消失”,而是“智能体重新观察并生成了连贯的下一步计划,没有针对相同的状态重试相同的调用,也没有在无效往复中耗尽预算”。这是 SHIELDA 和 PALADIN 系列研究正在构建评估工具的属性,也是你的团队必须编写的那类测试,因为目前还没有现成的基准测试会在你的工具上测试这些。

架构框架

一旦你完成了这种转换,通常会随之产生一个更深层的认识:智能体不是 API 调用链,而是一个控制循环。面对来自环境的大多数信号,正确的响应是重新观察并根据新状态决定下一步做什么。重试是该循环的一个特例,即新状态恰好与旧状态相同,且调用恰好是幂等的——这是一个值得针对瞬态基础设施故障进行优化的狭窄特例,但它终究只是个特例。

主导情况——也就是应该支配你默认设置的情况——是世界已经发生了变化,而智能体的计划现在出现了轻微的偏差。最廉价、最平滑的响应是察觉并调整。重试是你假装世界没有变化时的做法,而重新规划是你接受了世界几乎总是在变化这一事实后的做法。

框架最终会赶上来。在此之前,每小时工程时间能换取最高可靠性的举动是:遍历注册表中的每个工具,根据上述两个维度对其可能的错误模式进行分类,并将“持久-语义”象限中所有内容的默认设置从“带退避的重试”更改为“抛出异常并重新规划”。你的延迟 P95 将会改善,重复副作用 bug 的数量将会下降,你的团队也将不再混淆“世界拒绝了我们”与“网络抖动”。

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