如果 Agent 陷入了死循环怎么办?
Agent 死循环不是极端情况,是给模型自由度必须付的代价。
- 直接原因:工具调用失败后盲重试——不换参数,反复撞同一堵墙。
- 深层原因:循环缺少收敛信号,Thought 和 Action 之间没有"该停了"的显式判断。
- 第一层防御:max_steps 硬限制,两行代码兜住 80% 的翻车。
- 第二层防御:把 Finish 做成一个 Action,循环有了天然终止条件。
- 第三层防御:system prompt 里写死"重试失败就换策略",主动避障。
- 工程底线:step_id + 全局超时 + 连续重复 Action 检测,别信 Agent 能自己收敛。
- 三层防御一起上,单靠任何一层都有盲区。
Agent 为什么会死循环
ReAct 循环的代价是模型在"想"和"做"之间可以无限切——它每轮都在评估"还能不能再调个工具多搞点信息",而不是"够不够了"。模型没有内置的"停"。
实际跑下来,死循环分三种:
| 类型 | 症状 | 根因 |
|---|---|---|
| 重试死循环 | 工具返回 error → 相同参数再调 → 继续 error | 模型分不清"暂时失败"和"逻辑错误" |
| 摇摆死循环 | A 工具 → B 工具 → A 工具 → B 工具 | 两个工具互相依赖,谁都没给够决策信息 |
| 探索死循环 | 上下文越来越长,模型忘了原始任务 | 没有目标锚定,开始"看看还有啥能玩的" |
三种里最致命的是第一种——短、快、密集,几秒内烧掉大量 token。摇摆型至少还在换工具,重试型是纯浪费。
三层防御,缺一不可
只加 max_steps 我踩过坑:模型在 step 49 返回了一句"抱歉我还在分析中"——用户体验比直接报错还差。单靠一层防御总有盲区。
第一层:硬限制
MAX_STEPS = 20
for step in range(MAX_STEPS):
response = agent.step()
if response.is_final:
return response.content
return "任务超时,已终止"
max_steps 是保险丝,不是终止策略。它的作用是防止无限烧 token,但不保证截止时任务已完成。
设多少?代码生成 30-50 步,信息检索 10-15 步。通用原则:宁可在生产环境让 Agent 提前终止,也不要让它无限烧 token。
第二层:让模型主动喊停
在 system prompt 中加一个 Finish Action:
当你确认任务已完成或已收集足够信息时,
调用 finish 工具并给出最终答案。不要继续调用其他工具。
关键点:Finish 必须是显式的工具调用,不是让模型自己决定"下面我开始回答"。Thought 没有约束力——模型可以在 Thought 中说"我觉得够了"然后继续调下一个工具。
两种方式我对比过:
- 只靠 prompt 说"完成任务后直接回答" → 模型大约 30% 概率继续调工具
- 把 Finish 做成一个 Action → 终止率接近 100%
区别在于:前者是"建议",后者是"接口契约"。模型对 API 契约的遵守远比自然语言指令严格。
第三层:重试自动避障
这是性价比最高的优化——system prompt 里加一条:
如果同一个工具调用连续失败两次,必须换一个方法。
不要用相同的参数重试失败的调用。
模型默认行为是"再试一次",很多场景重试确实有效——网络抖动、工具偶发异常。但它区分不了"暂时失败"和"用错参数"——后者重试一万次也没用。
主动避障 + max_steps 配合,死循环率能压到很低的水平。
工程侧的兜底
Agent 侧的约束不够,工程侧也得有防线——说白了,不该完全信任一个 LLM 驱动的循环能自己收敛。
- step_id:每次 Action 带递增 id,前端渲染时检测连续 N 次相同 Action 就截断
- 全局超时:不管跑到第几步,60 秒没完成就熔断
- 重复 Action 检测:连续 3 次调同一工具且参数完全相同,直接终止
这三个是"不相信 Agent 的防御"。工程上把 trust-but-verify 反过来:verify first, trust later。
说穿了
死循环不是 bug,是交互式架构的必然产物。你把决策权交给模型,就得同时给它边界——max_steps 是墙,Finish Action 是门,retry 规则是导航。三层 缺一层都能跑,但总会在某个场景下翻车。
具体数字取决于你的任务,但一条原则不变:宁可让 Agent 提前终止并告知用户,也不要让它默默烧 token。