防止 Agent 无限循环的终止策略
Agent 的核心能力是“观察结果,再决定下一步”,但同一种循环结构也可能失控:工具持续返回相同错误,模型换一种说法后再次调用;搜索结果已经足够,Agent 仍不断补资料;页面按钮没有生效,自动化程序重复点击;两个子任务互相等待,谁也不结束。
只设置 max_steps = 20 能挡住最坏成本,却不能区分“第 20 步刚好完成”和“第 4 步已经进入死循环”。生产级终止策略要同时回答三个问题:何时必须停、如何判断没有进展、停止后怎样保住已经完成的工作。
一、先定义什么叫“完成”和“无法继续”
很多无限循环不是模型异常,而是任务没有明确终态。“调研这个主题”可以永远继续找资料,“把代码优化好”也没有天然结束点。创建任务时应把目标改写为可验证的完成条件,例如:收集至少三个独立来源、生成包含固定字段的报告、测试全部通过、目标记录已写入且回读一致。
终态至少分为四类:
| 终态 | 含义 | 对用户的输出 |
|---|---|---|
completed | 验收条件全部满足 | 最终成果与证据 |
partial | 有可用结果,但预算已尽 | 已完成部分与缺口 |
waiting_human | 缺少授权或关键选择 | 明确的一项待确认内容 |
failed | 无安全路径可继续 | 原因、已尝试动作、恢复建议 |
不要把所有非成功情况都压成 failed。如果 Agent 已完成数据收集,只差一个非关键格式转换,返回部分结果比把整个任务丢弃更有价值。明确终态也能避免模型把“等待用户批准”误当成“继续尝试说服用户”。
二、硬预算是不可绕过的最后防线
任何 Agent loop 都应有独立于模型的硬预算,至少覆盖步数、总时长、模型 token、工具调用次数和外部费用。预算由编排器计数,模型不能通过重新规划或创建子任务重置它。
def should_stop(state, budget):
return any([
state.steps >= budget.max_steps,
state.elapsed_seconds >= budget.max_seconds,
state.total_tokens >= budget.max_tokens,
state.tool_calls >= budget.max_tool_calls,
state.estimated_cost >= budget.max_cost,
])
不同预算防不同事故:单工具超时防止一次调用卡死,总时长防止许多慢调用叠加,步数防止快速空转,费用上限防止昂贵模型或工具失控。它们不能互相替代。具体时间预算可以参考 Agent 工具调用的超时预算设计,本文更关注“调用虽然都成功,整体却没有前进”的情况。
预算还应预留收尾空间。如果把所有 token 都用于循环,触发上限时就没有余量生成部分结果。常见做法是把总预算的一小部分锁定给 finalizer,主循环只能使用其余配额。
三、用状态指纹识别完全重复
最容易检测的是完全重复:同一个工具、相同参数、相同结果连续出现。可以为每一步生成规范化指纹,忽略时间戳、请求 ID 等无关字段,再维护短期窗口。
fingerprint = hash(
normalized_goal,
tool_name,
canonical_json(tool_args),
normalized_result_class,
relevant_state_version
)
若同一指纹连续出现,第一次可以把错误反馈给模型并要求换路径;第二次禁止原样重试;再次出现则终止或转人工。阈值要区分工具:幂等读操作可以有限重试,有副作用的写操作则应先查询上次是否已经生效,不能因响应丢失就重复提交。
仅比较文本相似度不够。模型可能每次生成不同解释,但工具参数和状态完全相同;也可能参数只改变无意义的排序。规范化后的动作与环境状态才是判断循环的核心。
四、用“进展指标”识别换皮循环
更隐蔽的循环不会重复同一步。Agent 可能轮流搜索 A、总结 A、搜索 A 的同义词,再回到总结;每个动作都不同,却没有增加任务完成度。对此需要为任务定义单调进展指标。
例如调研任务可以跟踪已满足的证据槽位,而不是网页数量;修复任务跟踪失败测试是否减少,而不是执行了多少命令;表单任务跟踪必填字段和提交状态,而不是点击次数。每轮动作后都比较:
- 是否新增了完成条件所需的事实或产物;
- 是否消除了一个已知阻塞;
- 是否改变了外部系统中可验证的目标状态。
若连续若干轮没有任何指标改善,就进入 stalled。此时编排器可以允许一次“重新规划”,但新计划必须说明与旧路径的实质差异;如果只是换一种措辞重复原动作,应直接收尾。
五、限制重规划和子 Agent 扩散
重新规划是有用的恢复手段,也最容易被滥用。若每次失败都重新生成完整计划,Agent 可能反复回到起点,已经完成的步骤也被重新执行。应给 replan 单独计数,并把已完成事实、禁止重复动作和剩余预算明确传入。
多 Agent 系统还要防止任务树无限扩张。父任务创建子任务时,应继承而不是复制预算;限制最大深度、总子任务数和同时运行数;同一目标指纹不能重复派发。子任务结束后必须返回结构化状态,父任务不能因一句“需要更多研究”就自动再生成相同子任务。
一个实用规则是:只有出现新证据或新约束,才允许重新规划。单纯的工具失败应该进入有上限的重试或降级路径,而不是把问题重新包装成新任务。
六、循环检测要接入状态机
终止不应是散落在 while 循环里的 break。建议把 running、stalled、waiting_human、finalizing 和各类终态纳入正式状态机,并记录触发事件:
running --no_progress--> stalled
stalled --novel_plan--> running
stalled --replan_exhausted--> finalizing
running --approval_required--> waiting_human
running --hard_budget_hit--> finalizing
finalizing --partial_saved--> partial
这样监控系统能区分主动完成、预算耗尽、重复动作和人工等待。人工批准也不能偷偷重置任务;恢复时应沿用原任务 ID、剩余预算和已有副作用记录。关于状态持久化与合法转移,可参考 AI Agent 状态机设计与落地。
七、安全终止比“立即杀进程”多一步
检测到循环后,编排器应先阻止新的副作用动作,再进入受限 finalizer。finalizer 只允许读取已有状态和生成摘要,不允许继续搜索、写入或创建子任务。它需要保存:已经完成的成果、最后一个一致检查点、未确认的副作用、停止原因和继续任务所需条件。
对于可能已经发生的写操作,要先做幂等查询。例如付款请求超时,不能简单标记失败并重试;应查询交易状态,若无法确认则进入人工处理。需要撤销时,走预先设计的补偿动作,而不是让模型自由发挥。相关方法可参考 AI Agent 写操作回滚策略。
用户看到的结束消息应具体:“连续三轮未增加有效来源,已停止;当前报告包含两项已验证结论,还缺供应商文档”,而不是笼统地说“任务失败”。终止策略的价值不仅是省成本,也是让不完整结果仍然可理解、可继续。
八、上线前构造循环测试集
正常用例无法证明终止策略有效。至少要模拟这些情形:工具永远返回相同 500、工具成功但状态不变、搜索结果不断重复、写操作成功但响应超时、两个子任务互相依赖、模型持续创建同义子目标,以及人工迟迟不批准。
评估指标应包括平均检测步数、错误终止率、终止后的成果保留率、重复副作用次数和恢复成功率。先在 shadow mode 中只记录“本应终止”的判断,与真实执行轨迹对比;阈值稳定后,再让检测器实际切断循环。
九、相关阅读
如果你的 Agent 需要通过统一入口调用多种模型,YoTradeApi 可以减少协议适配工作;循环预算、进展检测和副作用保护仍应放在应用编排层强制执行。