Agent为什么会死循环?
------无限Tool调用、ReAct循环、上下文污染与业界解决方案深度解析
真正优秀的Agent,不是最聪明,而是最稳定。
一、为什么Agent会死循环?
先看几个真实案例
先说 Claude Code。2025年8月,GitHub Issue #6004 记录了一个经典故障:用户让 Claude Code 为 Go 项目补充测试文件。Claude 正确识别了需要读取的5个已有测试文件,但在读取完成后,它进入了一个诡异的循环:
go
Read test_a.go → Read test_b.go → Read test_c.go →
Read test_d.go → Read test_e.go → [Compact conversation] →
Read test_a.go → Read test_b.go → ...
它反复读取同样的文件、触发同样的对话压缩、然后重新开始------但一次都没有真正创建任何测试文件。这是一个过程失败(Process Failure),而非能力不足:所有需要的信息都已在第一轮获取,但控制流卡死了。
OpenHands 社区的典型场景更直观:Agent 被告知修复一个 SyntaxError。它修改了代码、执行、仍然报错------同样的语法错误。然后它再次修改、再次执行、再次报错。循环往复。但它从来不去想:"也许我应该换一种修复方式。"
Cursor 用户也经常碰见:Agent 在修改一个 React 组件后,因为缺少可以验证"修改是否正确"的测试或检查命令,于是它反复调整同一个文件的同一段代码------细微的参数变化、改回原名、再改回新名。"没有验证手段,Agent 就不知道该停手。"
AutoGen 的多 Agent 对话更离谱:如果没有设置 max_turns 或 is_termination_msg,两个 Agent 可以持续对话直到 API 预算耗尽------互相讲笑话、互相夸赞、永远不会主动说"够了"。
LangGraph 开发者碰到的是另一个版本:GRAPH_RECURSION_LIMIT。当 StateGraph 中的两个节点互相连接(A → B,B → A)而没有设置 END 时,图会无限执行直到触碰递归限制。
核心问题
你可能会问:为什么模型越来越强(GPT-4 → Claude 4 → Gemini 2.5),但死循环问题反而越来越突出?
答案看起来矛盾但合理:正是因为模型能力足够强,所以它们被赋予了更多自主权。 当 Agent 能自己决定"要不要调用工具""调用哪个工具""要不要继续思考"时,它也获得了"永远不停下来"的能力。
Agent 的自主性 = Agent 的安全性风险。两者成正比。
二、Agent执行流程到底是什么?
不要先讲死循环------先理解正常循环
Agent 不是"一次提问、一次回答"的对话模型。它的核心是一个循环。这个循环本身不是Bug------它是 Agent 之所以是 Agent 的根本原因。
text
┌────────────┐ ┌──────────────────┐ ┌──────────┐
│ 用户输入 │─────▶│ Planner / System │─────▶│ Reason │
└────────────┘ └──────────────────┘ └────┬─────┘
│
┌──────────────┐ │
│ 需要工具? │◀────────┘
└──────┬───────┘
是 / \ 否
▼ ▼
┌────────┐ ┌────────┐
│ Tool │ │ Finish │
└───┬────┘ └───┬────┘
│ │
┌────┴────┐ │
│Observation│ │
└────┬────┘ │
└────────────┘
这个循环在行业内有不同叫法:Agent Loop 、Tool Use Loop 、Reasoning Loop 、Action-Observation Cycle,但本质相同------Agent 在"思考→行动→观察→再思考"之间循环,直到达成目标。
为什么一定会循环?
想象你要 Agent "帮我找到 Python 项目中所有未使用的导入"。它的执行过程天然就是循环的:
- Think:我需要先列出所有 Python 文件
- Act:执行
find . -name "*.py" - Observe:得到了237个文件路径
- Think:需要逐个分析每个文件的导入和实际使用
- Act:执行
grep "import" file1.py - Observe:file1.py 导入了 os, json, requests
- Think:需要检查哪些被实际使用了
- Act:分析文件中 os, json, requests 的使用
- Observe:requests 没有被使用
- ...继续处理 file2.py, file3.py...
这个循环不是 Bug,它是 Agent 完成复杂任务的必要手段。
什么时候循环变成死循环?
循环变成问题,取决于三个维度的判断:
| 判断维度 | 正常循环 | 死循环 |
|---|---|---|
| 进展 | 每轮产生新信息或状态变化 | 重复相同动作,无实质性推进 |
| 收敛 | 逐步逼近目标 | 偏离目标,或在原地打转 |
| 成本 | 消耗与复杂度匹配 | 消耗远超任务价值 |
用代码化的判断标准:
python
# 这不是死循环
for file in project_files:
analyze(file) # 每次都处理不同文件
# → 有进展,会收敛
# 这是死循环
while True:
read_same_file() # 反复读取同一文件
compact_context() # 触发压缩
# → 无进展,不会收敛
Claude Code 的源码(query.ts)最直白地体现了这一点:它就是一个 while(true) 循环,只有当模型不再输出 tool_use、达到预算上限、或用户中断时才退出。整个 Agent 的生命就是在这个 while(true) 里度过的。
三、Agent为什么会无限调用Tool?
工具调用(Tool Call)是 Agent 与外部世界交互的唯一方式。也是死循环最常见的原因。以下是导致无限 Tool 调用的10大原因:
完整故障分析
text
┌──────────────────┐
│ Agent 调用 Tool │
└────────┬─────────┘
│
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌────────┐ ┌──────────┐
│结果不满足│ │ Prompt │ │ 执行层面 │
│结束条件? │ │ 设计? │ │ 问题? │
└────┬─────┘ └────┬───┘ └────┬─────┘
│ │ │
┌────┴────┐ ┌───┴───┐ ┌───┴───┐
│Tool结果 │ │缺少停止│ │LLM │
│不满足 │ │条件 │ │判断失败│
│Tool返回│ │Tool描述│ │JSON │
│异常 │ │不清晰 │ │解析失败│
│ │ │ │ │MCP异常 │
│ │ │ │ │选错工具│
└────┬────┘ └───┬───┘ └───┬───┘
│ │ │
└────────────┴──────────┘
│
▼
┌──────────────┐
│ 继续调用 Tool │
└──────┬───────┘
│
▼
┌──────────────┐
│ 有进展? │
└──────┬───────┘
无 / \ 有
▼ ▼
┌────────┐ ┌────────┐
│ 死循环 │ │ 继续执行 │
└────────┘ └────────┘
1. Tool结果不满足结束条件
最常见的原因。LLM 期待一个"完成"信号,但 Tool 返回的内容不够明确。
yaml
Agent: 检查端口 3000 是否被占用
Tool: lsof -i :3000 → (空输出,端口未占用)
Agent: 不确定,再检查一次
Tool: lsof -i :3000 → (空输出)
Agent: 还是不确定,使用 netstat 检查
Tool: netstat -an | grep 3000 → (空输出)
...
根因:Tool 返回一个模糊结果(这里是一个空输出),LLM 无法判断"空输出 = 端口未占用 = 成功",于是反复验证。
2. Tool返回异常
OpenHands 的 StuckDetector 专门识别了一种模式:动作-错误循环 ------Agent 连续3次执行相同动作且均返回错误。典型场景是 Python 代码反复抛出相同的 SyntaxError,Agent 修改的不是语法错误,而是无关部分。
3. Prompt设计错误
给 Agent 的指令缺少明确的"完成标准"。比如只说"优化代码性能",但不说什么算"性能达标"(响应时间 < 200ms?内存下降 30%?),Agent 就会不断尝试优化,永远停不下来。
4. Tool描述不清晰
Tool 的 function description 如果过于模糊,LLM 就无法正确判断"这个 Tool 是否真的帮到了忙"。比如一个 Tool 叫 fetch_data,描述是 "获取数据"------获取什么数据?返回什么格式?成功标准是什么?LLM 拿到的就是一个黑盒。
5. LLM判断失败
大模型本身不是可靠的决策引擎。它可能在相同的输入下产生不同的输出,也可能因为 context 太长而"遗忘"已经完成的任务。这就是 Claude Code 无限压缩循环的根因------模型本应意识到"数据已获取 → 开始创建测试",但它判断错误,选择了重新读取。
6. 输出格式错误 / JSON解析失败
Tool 返回格式损坏时:
lua
Tool: {"status": "succes ← 缺少闭合引号,JSON 非法
Agent: (解析失败) 重试...
Tool: {"status": "success"}
Agent: 现在正确了,但前面已经浪费了一轮
7. MCP返回异常
MCP(Model Context Protocol)是一个分布式的 Tool 调用协议。当 MCP Server 超时、返回错误或连接中断时,Client 端的 Agent 可能将"异常"理解为"需要重试",而非"需要换方法"。
8. Tool选择错误
Agent 选用了错误的工具来解决问题。比如应该用 Replace 精确替换字符串,却用了 Write 重写整个文件------结果其他内容丢了,问题更复杂,需要更多 Tool 调用来修复。
9. 工具之间互相调用
在多 Agent 系统中,Agent A 调用 Tool B,Tool B 内部又触发 Agent C,Agent C 又调用 Tool A------形成跨 Agent 的循环。
10. 上下文信息丢失
当 Agent 执行了很多步之后,早期的关键信息可能因为上下文太长而被"遗忘"或"稀释"。Agent 可能回到起点,重新执行已经被完成的操作------不是它不想停,而是它不记得已经做过了。
四、为什么ReAct最容易死循环?
ReAct的经典模式
ReAct(Reasoning + Acting)是当前绝大多数 Agent 的基础范式。它的流程如下:
text
Thought ──▶ Action ──▶ Observation
▲ │
└──────────────────────┘
- Thought:思考下一步做什么
- Action:执行具体操作
- Observation:观察操作结果
- 循环:基于观察再次思考
为什么天然容易无限循环?
ReAct 论文(Yao et al., 2022)本身并没有专门讨论"无限循环"问题,但从其设计可以清晰看出三个内在风险:
风险一:没有内置终止机制
ReAct 的终止完全依赖 LLM 自行判断"任务已完成"。但 LLM 的判断本身是不确定的------同样的状态,不同的采样可能产生"继续"和"停止"两个相反的结论。更糟糕的是,当任务复杂时,LLM 倾向于过度谨慎:"我再检查一遍以确保万无一失。"
风险二:Thinking越来越长
随着循环次数增加,Thought 中包含的历史信息越来越多。每一轮 Thought 都在"回顾"之前的所有 Observation,导致 Thought 长度累加:
scss
第1轮:Thought (50 tokens)
第2轮:Thought (150 tokens) --- 回顾了第1轮的Observation
第3轮:Thought (300 tokens) --- 回顾了第1、2轮
第5轮:Thought (800 tokens) --- LLM花费大量token在"记忆"而非"推进"
这导致两个问题:(1)Token成本指数增长;(2)LLM在长上下文中推理质量下降。
风险三:Observation污染Context
这是最隐蔽也最致命的问题。每次 Tool 调用返回的 Observation 都会进入上下文。如果 Observation 中包含了大量无关信息(比如运行测试输出了300行日志,但只有最后5行是关键错误),这些噪声会:
- 稀释有效信息
- 误导 LLM 关注无关细节
- 推动上下文总量迅速膨胀
Claude Code 的源码验证了这一点:每轮迭代结束时,tool_result 以 user 消息类型拼回对话,该消息会出现在下一轮 API 调用的完整上下文中。如果一个任务需要30轮 Tool 调用,Agent 的上下文会包含30组完整的 Action-Observation 对。
改进方向
Reflexion(Shinn et al., NeurIPS 2023)提出了"语言强化学习":Agent 在任务失败后,不是简单地重试,而是生成一段"反思"------分析为什么失败、下次应该怎么做。这段反思存储在长期记忆中,帮助后续尝试避开同样的陷阱。
Self-Refine(Madaan et al., 2023)则是让 LLM 对自己的输出进行"自我批评和改进",在同一个推理步骤内迭代优化,而非反复调用外部工具。
ReWOO(Reasoning WithOut Observation, Xu et al., 2023)提出了更激进的思路:把"推理"和"工具执行"彻底解耦。Agent 先一次性生成完整的"推理链+工具调用计划",然后批量执行所有工具,最后用工具结果一次性生成最终答案。这从根本上消灭了"每轮 Observation 都回到循环起点"的结构------循环只有一轮。
说白了,这些方法没有改掉 ReAct 的底层循环逻辑。它们只是帮 Agent 更聪明地喊停,而不是让循环本身消失。
五、上下文污染(Context Pollution)
为什么Agent越来越"笨"?
如果你长期使用 Claude Code 或 Cursor Agent,一定有过这样的体验:Agent 一开始表现很聪明,但随着对话推进,它越来越迟钝、越来越容易犯错、甚至在简单任务上也出错。这不是模型变差了,是上下文被污染了。
五种核心污染形式
text
上下文窗口
┌────────────────────────────────────────────┐
│ 历史Observation ──▶ 上下文污染 │
│ 错误信息 ──▶ │
│ 重复Tool输出 ──▶ │
│ Prompt膨胀 ──▶ │
│ Memory碎片 ──▶ │
└────────────────────────────────────────────┘
│
▼
推理质量持续下降
Context Drift(上下文漂移)
2026年1月,一篇题为 "Agent Drift: Quantifying Behavioral Degradation in Multi-Agent LLM Systems" 的论文(arXiv:2601.04170)量化了这个现象。研究发现:
- 任务成功率从 87.3% 下降到 50.6%(-42.0%),仅因为长时间交互
- 人工干预需求从每任务 0.31 次暴增到 0.98 次(+216.1%)
- 漂移在第 73 次交互时开始出现,到第 500 次交互后加速恶化
论文提出了三种漂移类型:
| 漂移类型 | 表现 | 示例 |
|---|---|---|
| 语义漂移 | 输出逐渐偏离原始任务意图 | 金融分析 Agent 从风险导向变成机会导向 |
| 协调漂移 | 多 Agent 间共识崩溃 | 路由 Agent 开始偏袒特定子 Agent |
| 行为漂移 | 发展出初始交互中不存在的策略 | 合规 Agent 开始在对话历史中缓存中间结果 |
Context Pollution(上下文污染)
在 Agent 实际运行中,上下文窗口是一个有限资源。每次 Tool 调用的输出都会挤占这个空间。污染的来源包括:
-
历史 Observation 堆积:每轮循环都在上下文里留下一份 Observation。30轮过后,20%的上下文可能都是历史记录。
-
错误信息越来越多:每次失败的尝试都在上下文里留下错误日志。这些错误日志不仅占空间,还会误导 LLM:"上次 error 是 X,让我看看是不是又发生了 X。"
-
重复 Tool 输出 :当 Agent 多次调用同一个 Tool(比如反复
ls同一个目录),重复的输出不仅浪费 Token,还制造了"回声效应"------LLM 看到的上下文中有大量相似内容。 -
Prompt Injections 累积:Tool 输出可能包含不可信的外部内容(网页抓取、API 响应等),这些内容本质上是对 Agent 的一种"间接注入",可能改变 Agent 的行为方向。
Claude Code 的验证
Claude Code 源码分析(query.ts)证实,它的每次 API 调用都在处理不断增长的上下文。为了应对这个问题,它设计了六层上下文管理机制:
| 层级 | 机制 | 激进程度 |
|---|---|---|
| 1 | Snip | 保守 --- 删除过时 Tool 消息 |
| 2 | Microcompact | 保守 --- 编辑 API cache key |
| 3 | Context Collapse | 中等 --- 折叠旧消息为摘要行 |
| 4 | Auto Compact | 激进 --- Fork Agent 做摘要压缩 |
| 5 | Reactive Compact | 激进 --- API 413 错误后的紧急压缩 |
| 6 | Manual Compact | 激进 --- 用户触发 /compact |
其中最值得关注的是 Auto Compact :当 Token 用量接近上限时,系统启动一个 fork 子 Agent 来总结历史对话。为了防止这项操作本身陷入死循环,它还设计了熔断器:连续失败3次后自动停止。
Memory Pollution(记忆污染)
在多轮交互中,Agent 可能形成"错误记忆":比如第一次调用 API 返回了 rate_limit 错误,Agent 可能在后续所有 API 调用前都加上了"请确保不要触发频率限制"的逻辑,即使这个限制早已被解除。这是 Memory 变成了束缚。
六、目前业界如何解决死循环?(横向对比)
在研究各框架的源码和文档后,我梳理了16种通用的防死循环机制,以及它们在主流框架中的支持情况:
| 机制 | Claude Code | OpenAI Agents | LangGraph | OpenHands | AutoGen | CrewAI | Google ADK | Mastra | LlamaIndex |
|---|---|---|---|---|---|---|---|---|---|
| Max Iteration / Turn | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tool Budget | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Cost Budget | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Timeout | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ |
| Loop Detection | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Reflection | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Human Approval | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ |
| Guardrails | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ |
| Stop Condition | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| State Machine | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
| Checkpoint/Persistence | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Context Compression | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Memory Summary | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ |
| Confidence Score | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Retry Policy | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Sub-agent Isolation | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
注:✅ = 明确支持,❌ = 未在公开文档/源码中找到支持证据。"官方未公开具体实现"的标记为 ❌,不做事实性断言。
各机制的优缺点分析
Max Iteration / Turn --- 最简单也最常用
原理:设置最大循环次数,达到后强制终止。
优点 :零心智负担,一行配置。
缺点:是"暴力截断",任务可能未完成就被终止。
LangGraph 的做法更为优雅:它提供了 RemainingSteps 托管值,允许 Node 在达到限制前 主动感知剩余步数并优雅收尾,而不是等 GraphRecursionError 抛出来再处理。
Loop Detection --- OpenHands 的 StuckDetector
原理:通过模式识别检测重复行为。
OpenHands 的 StuckDetector 可以识别5种卡死模式,这是目前开源框架里唯一的系统化死循环检测方案。
优点 :能发现"表层看起来不同但本质重复"的循环。
缺点:检测滞后,需要循环发生几次后才能发现。
Human Approval --- 人的判断是最终的防线
原理:在关键节点暂停执行,等待人工批准。
Claude Code 的权限模式系统(permission_mode)允许开发者设置 default、acceptEdits、plan、dontAsk、bypassPermissions 等多种审批级别。AutoGen 的 human_input_mode 可以设置为 NEVER、ALWAYS 或 TERMINATE。
优点 :人能识别 LLM 无法识别的"无意义循环"。
缺点:打断执行流,需要人实时在线。
Guardrails --- 预防而非治疗
OpenAI Agents SDK 的 Guardrails 分为 Input 和 Output 两类:Input Guardrails 在输入阶段拦截问题,Output Guardrails 在最终输出阶段验证。但只有第一个 Agent 的 Input Guardrails 会被执行,后续 Handoff 的 Agent 不会再次触发------这是一个容易被忽视的安全边界。
Context Compression --- Claude Code 独有的深度机制
Claude Code 的六层上下文管理是目前市面上最成熟的方案。它的 Auto Compact 不是简单的截断,而是启动一个子 Agent 来"读懂并总结"历史对话。但这个机制本身也是死循环的来源------GitHub Issue #6004 就是压缩循环本身变成了问题。
Cost Budget --- 经济上的防护栏
Claude Code 的 max_budget_usd 是基于花费阈值来限制循环的:一旦 API 调用费用达到预算,循环停止。这是一个非常实用的"商业化思维"------用钱来控制风险,而非用技术手段猜测风险。
第六章展示了各框架的共同防线,但 LangGraph 的防死循环策略有本质不同------它不是靠限制次数,而是靠状态机消除循环本身。
七、Claude Code为什么很少死循环?
很多深度使用过 Claude Code 和 Cursor 的开发者都有一个共同感受:Claude Code 虽然偶尔会出问题,但整体稳定性远超同类工具。 原因不在于某个单独的魔法,而是一套精心设计的工程体系。
1. 多层上下文管理的"瑞士军刀"
如前所述,Claude Code 的六层上下文管理(Snip → Microcompact → Collapse → Auto Compact → Reactive Compact → Manual Compact)确保上下文不会无限膨胀。这解决了循环的最底层诱因------上下文污染导致判断力下降 → 判断力下降导致重复决策 → 重复决策导致更多上下文污染。
2. Sub-agent 隔离 --- 每个子任务都重新开始
这是 Claude Code 防循环效果最明显的机制之一。当 Agent 需要执行子任务时,它会 spawn 一个子 Agent。这个子 Agent 拥有全新的上下文,不继承父 Agent 的对话历史。 它完成任务后只将最终结果返回给父 Agent------父 Agent 看到的只是一条简洁的结果摘要,而不是子 Agent 的完整执行过程。
text
父Agent上下文
┌────────────────────────────────────────────┐
│ 用户查询 ──▶ 分析任务 │
│ │ │
│ ▼ │
│ Spawn Sub-agent 1 ──▶ 收到摘要1 │
│ │ │
│ ▼ │
│ Spawn Sub-agent 2 ──▶ 收到摘要2 │
│ │ │
│ ▼ │
│ 输出最终结果 │
└────────────────────────────────────────────┘
Sub-agent 1 上下文 Sub-agent 2 上下文
┌──────────────────┐ ┌──────────────────┐
│ 新上下文 ──▶ 任务 │ │ 新上下文 ──▶ 任务 │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ 返回摘要 │ │ 返回摘要 │
└──────────────────┘ └──────────────────┘
效果:父 Agent 不会因为子任务的细节而污染上下文。即使子 Agent 内部出问题(比如某个文件反复读取),这个"局部混乱"不会扩散到全局。
3. 为什么大量使用 Markdown?
Claude Code 的核心配置通过 CLAUDE.md 文件进行。这不是巧合------Markdown 是一种结构化但自然的格式,LLM 对它理解得最好。在 CLAUDE.md 中写入的规则和项目上下文,会在每次 API 请求中被重新注入 。这意味着即使 Auto Compact 压缩了历史对话,CLAUDE.md 中的关键指令也不会丢失。
这一点和普通对话完全不同:你在对话中说的"记住 XYZ"可能被压缩掉,但写在 CLAUDE.md 里的指令是永久的。
4. 为什么强调 Project Context?
Claude Code 的 CLAUDE.md 支持多层级配置:全局级别(~/.claude/CLAUDE.md)和项目级别(项目根目录的 CLAUDE.md)。这种设计在启动时就将"你是谁、你要做什么、你的边界在哪里"编码进 System Prompt------在 Agent "自由发挥"之前先设好护栏。
5. 工具限制的精细化
Claude Code 的权限系统提供了5种模式,并且支持对单个工具进行作用域限制:
bash
# 只允许运行 npm 相关的 Bash 命令
"Bash(npm *)"
# 允许特定工具
allowed_tools: ["Read", "Glob", "Grep"]
权限越少,出错空间越小。 一个只有 Read、Glob、Grep 权限的 Agent 几乎不可能死循环------它没有可以重复调用的"写操作"。
6. 任务拆分与原子化
Claude Code 的 Task 系统(TaskCreate、TaskUpdate、TaskList)强制任务分解。当一个大型任务被拆成10个小任务时,每个小任务都有明确的"完成标准"。这天然限制了每个步骤的循环次数------你不可能在"安装 psycopg2"这个任务上循环100次而不自知。
7. Token Budget 和边际效应检测
Claude Code 的 Token Budget 系统(query/tokenBudget.ts)不仅能追踪 Token 消耗,还能检测边际效应递减------当模型在后续轮次中产出越来越少时,它会提前终止而不是继续"磨洋工"。这是一个非常工程化的"智能止损"机制。
关于 Loop Protection 的官方说明
Claude Code 的 Agent SDK 文档明确列出了 max_turns、max_budget_usd 和 stop_reason 作为终止机制。但官方未公开是否存在专门的"循环检测"算法(如 OpenHands 式的模式匹配)。 从 GitHub Issues 来看(#6004、#2283、#2391等),Claude Code 确实存在循环问题,所以不能说它"完全免疫"------它的优势在于让循环发生的概率大幅降低,并在发生时能较快恢复。
八、LangGraph如何避免死循环?
LangGraph 的防死循环策略与其他所有框架有本质区别。大多数框架的做法是"在 while 循环里加限制",LangGraph 的做法是"不用 while 循环,用状态机。"
核心设计哲学
text
传统Agent LangGraph
┌────────────────────┐ ┌────────────────────┐
│ while True │ │ Node A ──▶ Node B │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ Reason │ │ Conditional Edge │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ Tool ──▶ Observe │ │ Node B ──▶ END │
│ │ │ │ │ │
│ ▼ │ │ └────────▶ Node A│
│ Reason │ └────────────────────┘
└────────────────────┘
传统 Agent 的结构是**循环+条件判断**:
```python
while True:
thought = llm.reason(context)
if thought.is_final:
break
tool_result = execute(thought.action)
context.append(tool_result)
LangGraph 的结构是图+状态驱动的条件跳转:
python
builder.add_conditional_edges(
"reasoning_node",
decide_next, # 基于 State 决定下一步
{
"need_tool": "tool_node",
"done": END # ← 明确的终止节点
}
)
核心区别 :在传统循环中,终止条件是一个隐式的 LLM 判断("我觉得我完成了")。在 LangGraph 中,终止是一个显式的路由决策,基于结构化 State,而非 LLM 的自然语言输出。
State驱动的条件跳转
LangGraph 的 State 是 TypedDict,每个字段都有明确的类型和 reducer:
python
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
task_completed: bool # 显式的完成标志
tool_call_count: int # 工具调用计数
remaining_steps: RemainingSteps # 剩余步数(由框架管理)
条件边路由函数基于 State 做决策,而非 LLM 的文本输出:
python
def decide_next(state: AgentState) -> str:
if state["task_completed"]:
return END
if state["tool_call_count"] >= 10:
return "force_summary" # 超过限制 → 强制走收尾节点
if len(state["messages"]) > 50:
return "compress_context" # 上下文过长 → 压缩后继续
return "continue_reasoning"
为什么这比 while 循环好? 因为 while 循环的终止条件是 LLM 的"意图"(我是否想停),而 LangGraph 的终止条件是 State 的"事实"(计数器到了、标志位设了)。前者是不可靠的,后者是确定性的。
Conditional Edge --- 让流程天生有终点
text
START ──▶ reasoning
│
┌─────────┼─────────┬─────────┐
│ │ │ │
▼ ▼ ▼ ▼
need_tool done max_steps (其他)
│ │ │
▼ ▼ ▼
tool ──▶ END force_finish
executor │ │
│ └─────────┘
▼
observation
parser
│
└────────▶ reasoning
传统 while 循环的问题是:每一轮循环的终点都是下一轮循环的起点 。LangGraph 的图的问题是:每个 Node 都可以跳转到 END。 这不是语义差异,而是工程差异------在图结构中,END 是一个"物理存在"的节点,你可以通过多种路径到达它。
Interrupt / Checkpoint / Human-in-the-loop
这三者结合起来,形成了 LangGraph 最有用的一套安全机制------把人嵌入执行流。
python
def potentially_dangerous_action(state):
# 在执行前暂停,等待人工确认
approval = interrupt(f"即将执行: {state['planned_action']},是否继续?")
if approval != "yes":
return Command(goto="abort")
result = execute_action()
return {"result": result}
interrupt() 会在该点暂停整个图的执行,保存一个 Checkpoint。人可以在任意时间后回来,检查当前状态,然后通过 Command(resume=...) 恢复执行。
Checkpoint 的额外价值:如果图在执行到第47步时崩溃了,重启后它不会从第1步重新开始------它从第46步的 Checkpoint 恢复。这意味着即使在异常情况下,Agent 也不会"失去进度"然后从头来过(这是一类常见的变相循环)。
Command / Send --- 动态路由
Command 允许 Node 在一次返回中同时完成"状态更新"和"控制流决策":
python
def tool_executor(state) -> Command[Literal["reasoning", "error_handler"]]:
try:
result = run_tool(state["pending_action"])
return Command(update={"result": result}, goto="reasoning")
except ToolError as e:
return Command(update={"error": str(e)}, goto="error_handler")
Send 实现动态并行------当你不确定需要并行执行多少个任务时:
python
def fan_out(state):
return [Send("sub_worker", {"task": t}) for t in state["tasks"]]
Recursion Limit --- 最后的防线
LangGraph 的 recursion_limit 相当于图中的"天花板"。每个 super-step 计数一次,默认值设计为适合复杂图的合理上限。当图达到限制时,触发 GraphRecursionError。
但与传统的 max_turns 不同,LangGraph 还提供了 RemainingSteps------一个托管值,在图的内部被更新,节点可以主动读取:
python
def my_node(state):
if state["remaining_steps"] < 3:
# 步数不多了,快速收尾
return {"summary": quick_wrap_up(state)}
# 正常执行
return {"result": do_work(state)}
这是"主动优雅降级"而非"被动崩溃"。
为什么LangGraph推荐显式状态机而非自由循环?
- 可预测性:图的拓扑结构在编译时就确定了所有可能的执行路径。
- 可测试性:每个 Node 是纯函数(State in → State out),容易单独测试。
- 可观测性:每次状态转换都经过明确的 Edge,可以打点、记录、追踪。
- 可恢复性:Checkpoint 保存了整个 State,恢复后可以精确续接。
- 安全性 :
interrupt()在任何危险步骤前可以强制暂停。
九、如何设计不会死循环的Agent?
这一章不讲理论,只讲工程实践。
1. 限制最大步骤数 --- 最基础但最有效【必做】
python
# Claude Code Agent SDK
agent.run(prompt, max_turns=25)
# OpenAI Agents SDK
Runner.run(agent, input, max_turns=20)
# LangGraph
graph.invoke(input, config={"recursion_limit": 50})
为什么有效? 因为"无限"是人类直觉中最大的漏洞。我们总觉得"Agent 应该能自己判断何时停止",但 LLM 的"判断"是一个概率采样过程,不是确定性的逻辑。设置硬上限就是承认这一点。
建议值:简单任务 5-10 步,中等任务 15-25 步,复杂任务 30-50 步。超过 50 步的任务应该拆分成多个子任务。
2. 限制工具调用次数 --- 比限制步骤更精确【必做】
限制步骤数是粗略的(一步可能包含多个工具调用),限制工具调用次数更直接:
python
if tool_call_count >= 15:
return "请基于已有信息给出最佳答案,不要再调用工具。"
为什么有效? 很多死循环是"一直在查、一直在搜、一直在读文件,但从不开始真正的输出"。限制工具调用次数就是在强制 Agent 从"收集模式"切换到"产出模式"。
3. 增加 Reflection 节点 --- 但不要让它循环【推荐】
在每个重要阶段后插入一个"反思步骤":
python
def reflection_node(state):
reflection = llm.generate(
f"已完成的操作:{state['completed_actions']}"
f"当前状态:{state['status']}"
f"原始目标:{state['original_goal']}"
"判断:任务是否已完成?如果是,输出 DONE;如果否,输出需要继续的下一步。"
)
if "DONE" in reflection:
return {"status": "completed"}
return {"next_action": parse_next_action(reflection)}
关键设计:Reflection 的输出应该是结构化的(DONE / CONTINUE),而不是让 LLM 自由决定。自由决定 = 可能永远不说 DONE。
4. 增加 Task Success 判断 --- 二进制的完成标准【推荐】
与其让 Agent 判断"我是不是做完了",不如给它一个可以自动验证的完成条件:
python
task = {
"goal": "修复 auth.py 中的类型错误",
"success_criteria": "python -m mypy auth.py -- 零错误",
"max_attempts": 5
}
Cursor 的文档中强调:"Without a test or command it cannot tell it is done." 这是一个非常精准的总结。给 Agent 一个它可以运行的验证命令,让它自己检查自己。
5. 增加 Budget --- 用钱控制行为【必做】
python
# 方式1:Token Budget
if total_tokens > 100_000:
return force_conclusion()
# 方式2:Cost Budget(Claude Code 方式)
agent.run(prompt, max_budget_usd=2.00)
# 方式3:Time Budget
if elapsed_time > 300: # 5分钟
return force_conclusion()
为什么用钱? 因为技术指标(步数、Token 数)LLM 不一定理解其意义。但"这个任务只值 $2"是一个 LLM 能理解的商业约束------它会在预算内做出最优决策。
6. 减少 Observation 污染 --- 只保留有效信息【必做】
python
def compress_observation(raw_output):
"""压缩 Tool 输出,只保留关键信息"""
if len(raw_output) > 1000:
return f"[工具输出过长,已压缩] 关键发现:{extract_key_info(raw_output)}"
return raw_output
Claude Code 的 Snip 和 Collapse 机制就是做这件事。实战建议:
- 测试输出:只保留失败的部分,截断成功的日志
- 文件内容:只保留修改过的区域前后5行
- 网页抓取:只保留和问题相关的段落
7. Memory 压缩 --- 有选择地遗忘【推荐】
python
def compress_memory(messages, max_size=8000):
"""压缩对话历史"""
# 始终保留:
# 1. 原始任务描述
# 2. 最近的3轮交互
# 3. 已确认的关键发现(结构化存储)
# 可以被压缩的:
# 1. 中间的完整 Tool 输出
# 2. 失败的尝试
# 3. 中间的 Thought 过程
8. Context Summary --- 定期"重新对齐"【推荐】
每隔 10-15 步,生成一个当前状态的结构化摘要,把它作为新的起点:
python
summary = {
"original_goal": "...",
"completed": ["步骤1", "步骤2", "步骤3"],
"current_state": "正在处理步骤4,已读取3个文件...",
"key_findings": ["发现X", "确认Y"],
"remaining": ["步骤4", "步骤5", "步骤6"]
}
# 将 summary 替代之前的完整历史,减少上下文负担
9. 状态机设计 --- 消灭自由循环【进阶】
text
开始 ──▶ 分析任务 ──▶ 制定计划 ──▶ 执行步骤
│
▼
┌──────────┐
│ 验证结果 │
└────┬─────┘
通过 / \ 失败
▼ ▼
┌──────────┐ ┌──────────┐
│还有步骤? │ │重试<3次? │
└────┬─────┘ └────┬─────┘
是 / \ 否 是 / \ 否
▼ ▼ ▼ ▼
执行步骤 生成摘要 执行步骤 报告失败
│ │
└────────┬─────────┘
▼
结束
状态机的好处:每个状态都有明确的后继状态,没有"自由发挥"的空间。 Agent 在 "EXECUTE" 状态下就是执行,在 "VERIFY" 状态下就是验证------它不能"在执行的同时又在思考要不要继续执行"。
10. 人工审批节点 --- 关键操作前必须确认【必做】
python
dangerous_actions = [
"删除文件",
"修改数据库 Schema",
"执行 git push --force",
"超过 $1 的 API 调用",
"修改超过 5 个文件"
]
if action in dangerous_actions:
await human_approval(action)
不要让人审批所有操作 (那还不如自己写)。只在不可逆操作 和高频操作的阈值上设置审批。
十、未来Agent为什么一定会走向"可控执行"
从"自主探索"到"确定性执行"的范式转移
2026年的 Agent 领域正在经历一个微妙的转变:开发者不再追求"让 Agent 自己想办法",而是追求"给 Agent 划定清晰的执行边界"。
这种转变体现在几个关键趋势上:
Deterministic Workflow + Agentic Intelligence
Google ADK 2.0 的口号是 "Weave deterministic code with adaptive AI reasoning"。Mastra 强调 "Use models where you need reasoning, plain functions where you don't"。这两者指向同一方向:在确定性的工作流框架中,在需要判断的节点上调用 LLM。
text
纯Workflow 纯Agent Hybrid: 2026趋势
┌────────────┐ ┌────────────┐ ┌──────────────────┐
│ Step 1 │ │ Reason │ │ Step 1: 确定性 │
│ │ │ │ │ │ │ │ │
│ ▼ │ │ ▼ │ │ ▼ │
│ Step 2 │ │ Tool │ │ Step 2: LLM推理 │
│ │ │ │ │ │ │ │ │
│ ▼ │ │ ▼ │ │ ▼ │
│ Step 3 │ │ Reason │ │ Step 3: 确定性 │
└────────────┘ └────────────┘ │ │ │
│ ▼ │
│ Step 4: LLM决策 │
│ │ │
│ ▼ │
│ Step 5: 确定性 │
└──────────────────┘
Planning + Verification
"Source Code Agent" 论文(arXiv:2508.02721, 2025)提出了一个激进的方案:将 Agent 的操作逻辑编码为确定性的源代码蓝图。 在执行前生成一个可审查的 Plan,Plan 通过后再执行。这本质上是在 Agent 的"黑盒推理"和"白盒执行"之间加了一层"可验证的蓝图层"。
Tree Search + Self-Reflection
不只是线性推理,未来的 Agent 会在关键决策点上展开搜索树------评估多个可能路径、选择最优、执行、验证、回溯。这需要比目前的 ReAct 循环更复杂的执行框架。
Model Context Protocol(MCP)
MCP 的核心价值不只是"统一工具调用协议",它还通过工具发现 和延迟加载 的方式减少了无效调用。Agent 不需要提前加载所有工具的定义,而是在需要时才通过 ToolSearch 查找------减少了上下文污染和"选错工具"的概率。
Hybrid Agent --- Rule + LLM
"规则引擎负责下限,LLM 负责上限"正在成为共识。一个生产级的 Agent 系统应该是:
python
if is_simple_and_clear(task):
use_rule_based_workflow(task) # 确定、快速、零出错
else:
use_llm_agent(task, constraints={
"max_turns": 25,
"human_approval_required": True,
"verification_command": "pytest -x"
})
总结
这篇文章的核心论点可以归纳为一句话:
Agent 的自主性是其能力的来源,也是其风险的来源。 你给 Agent 的自由度越大,它帮你完成复杂任务的潜力越大------但它陷入死循环的概率也越大。
所以设计 Agent 不是在"让它更聪明"和"让它更稳定"之间做选择。而是用工程手段来框定聪明的边界。
具体来说:
- 硬限制:Max Turns、Tool Budget、Cost Budget------到了就停,不商量
- 状态机:StateGraph、条件边、显式终止节点------用图消灭循环本身
- 人肉兜底:Human-in-the-loop、审批节点------关键操作必须人点头
- 压缩:Context Compression、Memory Summary------把上下文里的噪声挤出去
- 隔离:Sub-agent 独立上下文、MCP 延迟加载------局部坏了不影响全局
- 检测:循环模式识别、Token 异常监控------发现苗头就掐掉
真正优秀的 Agent,不是最聪明的,而是最稳定的。
在工程中,"每次都做到 80 分"远比"有时候 100 分、有时候 0 分"更有价值。
参考资料:
- Anthropic. Claude Code Agent SDK Documentation --- Agent Loop. code.claude.com/docs, 2026.
- Anthropic. Claude Code GitHub Repository. github.com/anthropics/claude-code, 2025-2026.
- LangChain. LangGraph Documentation --- Graph API, GRAPH_RECURSION_LIMIT. docs.langchain.com, 2026.
- OpenAI. OpenAI Agents SDK Documentation --- Runner, Guardrails. openai.github.io/openai-agents-python, 2026.
- Google. Agent Development Kit (ADK) Documentation. adk.dev, 2026.
- OpenHands. OpenHands GitHub Repository --- StuckDetector. github.com/OpenHands/OpenHands, 2025-2026.
- Microsoft. AutoGen Documentation --- Chat Termination. microsoft.github.io/autogen, 2026.
- Yao, S. et al. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629, 2022.
- Shinn, N. et al. Reflexion: Language Agents with Verbal Reinforcement Learning. NeurIPS 2023.
- Xu, B. et al. ReWOO: Decoupling Reasoning from Observations for Efficient Augmented Language Models. arXiv:2305.18323, 2023.
- Agent Drift: Quantifying Behavioral Degradation in Multi-Agent LLM Systems. arXiv:2601.04170, 2026.
- Mastra. Mastra AI Agent Framework Documentation. mastra.ai, 2026.
- CrewAI. CrewAI Documentation. docs.crewai.com, 2026.
- Source Code Agent: A Framework for Deterministic LLM Agents. arXiv:2508.02721, 2025.
- Yoyo_Lee. Claude Code 架构深度解析. yo666666yo.github.io, 2026.
- Learn Cursor. Cursor Agent Stuck in a Loop Fix. learncursor.dev, 2026.
- Ralphable. The Claude Code 'Infinite Loop' Bug. ralphable.com, 2026.