用分层错误治理阻断 Agent 重试死循环
文章将工具调用死循环的排查重点放在执行层错误语义,并给出类型化错误、带抖动的指数退避、熔断器和原子状态检查点四层方案。实现基于 Python 3.11+,并称通过 20 次对照测试比较调用量、耗时和恢复表现,但摘要未展示具体结果。
AI 深度解读
Agent 连续调用失败的工具,第一反应往往是换模型、改提示词,或者砍掉最大执行步数。这篇文章提出另一种诊断:很多循环来自工具执行层,系统只返回一句错误信息,却没告诉 Agent 能不能重试、该等多久、何时必须停。
作者用错误分类、退避、熔断和检查点组成四层治理方案,并给出部分测试结果。它对做 AI 编程工具和多步骤工作流的团队有参考价值,但实验披露不足,错误分类和恢复机制也有需要补齐的前提。
工具报错后,谁来决定下一步
作者描述了一次生产事故:search_docs 连续调用 31 次,每次返回 429 Too Many Requests,Agent 仍然重试,持续 40 分钟才碰到步数上限。作者还称,在两个月内监控的 6 个并发任务中,有 4 个出现类似循环。这个样本提示了问题,但不足以代表所有 Agent 系统。
常见执行流程是:模型选动作,系统调用工具,再把结果追加进状态。失败也走同一条路。如果返回的只是文本,模型就得自己猜:网络抖了一下,还是权限根本不够?
作者认为,把 MAX_STEPS 从 40 降到 15 只能让任务更早失败。这个判断成立,但步数限制仍然有用:它是最后一道成本边界,不能替代错误处理。
第一层改动,是把工具异常统一成三个类别。这段代码定义了后续处理所依赖的错误契约:
class AgentToolError(Exception):
retryable: bool = False
error_type: str = "unknown"
class TransientError(AgentToolError):
retryable = True
error_type = "transient"
class PermanentError(AgentToolError):
retryable = False
error_type = "permanent"
class FatalError(AgentToolError):
retryable = False
error_type = "fatal"
要看的是 retryable:TransientError 允许重试;PermanentError 表示原样重试不会改善;FatalError 则要求终止整个任务。
真正费心的是 normalize_error。它需要把各工具的原生异常、HTTP 状态码和错误消息映射到上述分类。作者称,完整映射通常花几小时;代表性测试中,仅加分类就让同一任务、同一工具的冗余调用从 27 次降到 3 次。
错误分类要成为执行规则,不能只成为模型读到的另一段文字。
作者后面的建议也是如此:中间重试由程序处理,模型只接收最终结果。这比让模型每次读完错误再决定等待多久,更容易控制。
重试要有间隔,还要有整项任务的预算
能重试,不代表应该立即重试。5 个并发任务同时失败、同时再发请求,很容易形成同步的请求波峰,让下游更难恢复。
第二层使用带随机抖动的截断指数退避:失败次数增加,等待时间逐步增长,同时随机错开请求。原文列出的参数是 max_attempts = 3、base_delay = 0.8 秒、cap = 8 秒,以及 jitter_factor = 0.5。
下面两行决定每次等待多久:
delay = min(cap, base_delay * (2 ** attempt))
delay = delay * (0.5 + random() * jitter_factor)
要注意第二行:按给定参数,随机系数落在 0.5 到 1.0 之间,实际等待会缩短到前一行结果的一半至全部,并非在基准值上下对称波动。原文也没交代 attempt 从哪里开始计数。
还有一处实现前必须澄清:作者把 max_attempts = 3 解释成每次逻辑工具调用最多重试 3 次,但参数名又容易被理解为包含首次调用在内共尝试 3 次。两种口径会产生不同的请求总量。
单次调用有限额还不够。作者举例,17 个工具各有 3 次额度,可能累积到 51 次调用;因此又设置任务级计数器,把整项任务的重试总预算限制为 6 次。不过这里的「调用」和「重试」口径同样不够清楚,不能直接把 51 当成完整请求量。
作者称,并发测试中下游峰值负载大约下降 50%。它支持「错峰有帮助」的方向,但没有披露并发规模、负载曲线和统计波动,不能直接拿来估算自己系统的收益。
下游持续故障时,怎样停下来并保住进度
第三层是熔断器(Circuit Breaker)。退避适合短暂故障;服务持续不可用时,还需要暂时停止真实请求。
原文采用 Hystrix 推广的三状态模型:Closed 正常放行并累计连续失败;达到阈值后进入 Open,直接返回 TransientError;冷却结束进入 Half-open,允许一次探测,成功则恢复,失败则重新熔断。
默认阈值是连续失败 5 次,冷却时间 60 秒,每次成功都会清零失败计数。在持续半小时的限流场景中,它可以在第 5 次失败后拦住后续下游请求。
但熔断返回的仍是可重试错误,所以它必须和预算、退避配合。否则 Agent 仍可能反复调用已熔断的工具,只是请求没有到达下游。模型能否切换替代工具、调整计划,还取决于系统有没有提供这些路径。
第四层是检查点(Checkpoint):每完成一个子任务,就序列化并保存任务状态。启动时有检查点就恢复,没有才从头执行。
作者用 30 步任务举例:第 18 步失败,应保留前 17 步的结果。永久错误也要记入状态,否则恢复后又会撞上同一条失败路径。作者称,长批任务的总耗时因此可能减少一半以上,但没有提供这一结果的详细测量条件。
文件写入使用 os.replace 做原子替换。工程上应先写临时文件,再替换正式文件;原子替换能避免读到半份文件,却不能自动解决外部操作重复执行。例如工具已经提交修改、进程却在保存状态前退出,恢复后仍可能重复提交,这需要幂等设计配合。
我的判断
我赞成这篇文章的优先级:先把错误变成系统能执行的规则,再讨论模型选择。对于工具调用,重试次数、等待时间和终止条件都适合由程序管理。提示词可以帮助模型选择替代方案,却不适合承担完整的流量控制。
但三类错误不能机械套用。作者把凭证过期、额度耗尽和完整下游故障归入 FatalError,这只是示例策略。凭证能否刷新、是否存在备用服务、失败工具是否阻塞整个任务,都会改变处理方式。分类的对象应是具体业务操作,不能只看状态码。
实验部分也需要克制理解。作者声称做了 20 次相同任务、相同提示词的对照实验,但没有列出模型、工具集合、完整结果表和成功率分布。因此,「大多数循环并非模型能力导致」以及「循环基本消除」仍是作者基于自身样本的结论,尚不能当作普遍比例。
方案声明基于 Python 3.11+,兼容 LangChain、CrewAI,但原文没有展示框架接入代码。末尾推荐的 4sapi 网关可被理解为集中限流、全局熔断和错误遥测的补充方向;文章没有提供产品对照测试,我不会据此判断它优于其他网关或现有基础设施。
这篇最值得借鉴的是治理顺序,具体收益和默认参数需要用自己的任务验证。
能拿来做什么
- 给现有 AI 编程 Agent 的高频工具补上错误映射,先区分临时故障、原样重试无效和必须终止任务三种情况,并让执行器落实重试规则。
- 在 Node 工具封装层统一处理退避、随机等待和任务级重试预算,明确首次调用是否计入次数,避免不同工具各自重试后放大请求量。
- 对持续限流或故障的服务增加熔断,同时把最终不可用状态交给 Agent,使它能够选择已有替代工具或返回已完成的结果。
- 为长批任务保存检查点,记录成功结果和已知永久失败;涉及写文件、提交修改等操作时,同时检查恢复后的重复执行问题。