2026-10-15·终止与自愈·终止与自愈
行业已从 max_turns=25 分化成三种终止流派——硬上限、预算计量、行为分析。大多数框架仍在第一种,少数产品走向第二种和第三种。真正的前沿不是 N 设多大,而是能不能在第 10 步就区分'正常工作'和'死循环'。
每个 Agent 开发者都遇过的场景
你搭了一个 Agent,设了 max_iterations=25。大多数时候 5-10 步完成。然后有一天它接到一个大型代码重构任务——读了 15 个文件、改了 8 个、跑了 3 轮测试——在第 25 步被框架砍掉了。改了一半的代码留在那里,测试没跑完。
你提高到 50。然后某天 Agent 陷入死循环——反复读同一个文件,读了 50 次才被砍掉,烧了 $4。
N 应该设多少? 这个问题本身就说明 max_turns 不是正确的抽象。
Agent 行业迭代极快。2024 年"几乎所有框架 max_turns=25"是事实。到了 2026 年 Q4,行业已经分化——不是线性演进,是三种流派并存。搞清楚每种流派在解决什么问题、在什么维度上失效,比讨论 N 应该是 25 还是 1000 有意义得多。
流派一:硬上限——数到 N 就停
原理:数步数、数消息、数 token、数时间。到了就停。
这是截至本文写作时最普遍的做法——不是因为它好,是因为它简单。一行计数器解决"防止无限循环"的最基本需求。
LangGraph:从 25 调到 1000,问题没变
LangGraph 的 recursion_limit 在 2024 年默认 25——大量用户抱怨"Agent 没完成就被砍了"。v1.0.6(2026 年)把默认值调到了 1000(LangGraph 官方文档 graph-api.md)。
1000 解决了"正常任务被截断"的问题。但如果 Agent 在第 5 步就进入死循环,它得空转 995 步才停——比 25 还糟。调高 N 减少假阳性(误杀正常任务),同时增加假阴性(放过死循环更久)。两者不可兼得。
LangGraph 的真正进步是新增了 RemainingSteps 和 IsLastStep——Node 可以在运行时检查"还剩多少步"并优雅降级(LangGraph graph-api.md, "Proactive recursion handling")。但这仍然是计数器语义:正在做 200 步重构的 Agent 和第 5 步就死循环的 Agent,RemainingSteps 看到的数字一样。
CrewAI:max_iter=20,且有 Bug 停不住
CrewAI 把 max_iter 从 25 调到了 20(CrewAI docs, agents page)。但更有意思的是 GitHub issue #3847(2026 年仍 open):handle_max_iterations_exceeded 被调用后 break 语句在某次提交中被意外删掉了——Agent 到达上限后继续跑。一位用户在 issue 中写道:"任务描述稍有歧义时,LLM 会忽略框架的硬停信号,因为它觉得自己'就差一步了'"。
这揭示了硬上限的一个隐蔽风险:框架实现本身也可能出 Bug——在一个复杂的异步执行流程中,一个计数器和一个 break 之间可能隔着几十行代码,而这几十行代码的任何一次重构都可能让 break 丢失。
OpenAI Agents SDK:max_turns + error_handlers
OpenAI 的 Agents SDK 提供了 max_turns——到达后抛 MaxTurnsExceeded,可通过 error_handlers={"max_turns": handler} 优雅降级(OpenAI Agents SDK docs, running_agents)。还提供了 reset_tool_choice 自动重置为 auto 以防工具调用死循环。
但没有内置行为检测。社区 issue #3353(BudgetGuard 提案)的开题者写得很直接:"#2848 有用户报告 Agent 循环触发了数千次 API 调用。官方建议'run 结束后看 result.usage'或'设平台级月度预算'——前者太晚(runaway 已经发生),后者太粗(一个坏 run 能烧完整月预算)。"
AutoGen:10 种终止条件,全是阈值
AutoGen 在可组合性上做了行业最丰富的设计——约 10 种 TerminationCondition,可以用 &(AND)和 |(OR)自由组合(AutoGen 官方文档, termination tutorial):MaxMessageTermination / TokenUsageTermination / TimeoutTermination / TextMentionTermination / FunctionCallTermination / ExternalTermination / FunctionalTermination 等。
可组合性本身是明显进步——max_msg | text_termination 比单一 max_turns=25 好得多。但每一个组件仍是阈值:数消息、数 token、数时间、看文本。没有一个条件能回答"Agent 是不是在兜圈子"。
流派一的局限性
一个跑了 30 步的 Agent 可能是:
| 情况 | 该停吗 |
|---|---|
| 读了 15 个文件 + 改了 8 个 + 跑了 3 轮测试 | ❌ 正常工作 |
| 读了同一个文件 30 次,每次参数一样结果一样 | ✅ 死循环 |
| 试了 30 种修复方案,每次测试都失败 | ⚠️ 伪进展 |
计数器对这三种情况一视同仁。 不管 N 是 25 还是 1000——它区分的只有"N 步"vs"N+1 步"。
真实案例:一个 LangChain 四 Agent 市场调研管线进入反馈循环——分析器生成报告,验证器要求"进一步分析"——循环了 11 天,烧了 **47,无最终答案(Particula, 2025)。即使设了 recursion_limit=1000,这些案例只会在 1000 步之后才停。
流派二:预算计量——数钱不数步
原理:用户不在乎 Agent 跑了几步,在乎花了多少钱。从"数步数"转向"数花费"。
这是 2026 年出现的新方向,代表产品是 Claude Code 和 Codex CLI。
Claude Code:--max-budget-usd
Claude Code 的交互模式依然没有 max_turns——模型自己决定什么时候停(返回 end_turn)。但在 SDK/headless 场景,v2.1.217 新增了 --max-budget-usd——按美元设预算(Claude Code 官方文档, cli-reference)。
claude -p --max-budget-usd 5.00 "重构这个模块"
子 Agent 的花费计入总预算。到达上限后,返回 error_max_budget_usd 结果,停止一切后台子 Agent。
这是一个范式转变——200 步高效重构花 10 就停。因为死循环的 token 消耗率通常高于正常工作(反复重读长上下文),预算限制在统计意义上比步数限制对死循环更敏感。
Claude Code 同时保留 --max-turns 作为可选兜底,以及 Stop Hook 机制(Agent 完成后 hook 可以 block 停止让它继续,最多 8 次连续 block)。
Codex CLI:max_budget_tokens + Goal 状态机
Codex CLI(OpenAI 的 Rust SDK)支持 max_turns + max_budget_tokens 双限制(docs.rs/codex-cli-sdk)。但更有意思的是它的 Goal 系统(openai/codex #23094):Agent 可以通过 update_goal 工具把状态设为 blocked("我卡住了")或 usageLimited("API 配额不够了")。两种状态都会暂停 goal continuation 循环,等待用户 /goal resume。
这是一种模型主动声明停滞的机制——不是外部数到 N 才停,是 Agent 自己说"我做不下去了"。但它需要 Prompt 引导("至少尝试三次后才能声明 blocked"),且依赖模型的诚实性。
流派二的局限性
预算计量对齐了用户真正关心的维度(钱),但有两个盲区:
- 价格依赖:不同模型 token 价格差 100 倍。同一个 $5 预算在 GPT-4o-mini 上能跑几千步,在 claude-4.6-opus 上只能跑几十步。预算线的含义随模型变化
- 死循环仍在空转:5 后停,那 5 花完才知道
流派三:行为分析——看模式不看数字
原理:不数步数也不数钱——检测行为有没有异常。
这是截至本文写作时最前沿、也最复杂的方向。核心思想:同一个工具被调了 20 次,重要的不是"20"这个数字,是"20 次调用之间有没有变化"。
Hermes:签名级检测(开源社区最接近的方案)
Hermes Agent(Nous Research, 开源)在 2026 年叠加了三层行为检测:
StallDetector(~200 行代码,PR #41477):计算每批工具调用的"签名"——工具名 + 参数 MD5。连续 3 批签名相同,或窗口 7 批中 5 批相同 → 第一次注入系统消息"你在重复",第二次硬停。
ToolLoopDetector(PR #6784,三策略并行):generic_repeat(同工具同参数 N 次)、poll_no_progress(同结果哈希 N 次)、ping_pong(A-B-A-B 交替模式)。还能从模型 reasoning 中提取意图工具名,检测"推理说对了,调用写错了"的不一致。
Session Caps(PR #66600,灵感来自 Claude Code v2.1.212):会话内最多 200 次 web_search + 200 次 delegate_task——防无人值守场景的单类工具 runaway。
Hermes 的做法是开源社区里最接近行为分析的方案。但签名级检测有一个盲区:两次 edit 的参数 MD5 不同(编辑内容确实不同),但如果每次编辑后测试都失败——这种"换着花样犯同一类错"的伪进展模式,签名检测捕捉不到。
我们的实践:多维度 CycleDetector + ErrorStreak
我们的做法在 Hermes 签名检测的基础上多走了两步——工具分类和伪进展独立检测。
三个检测维度同时运行:
- 完全相同检测:工具名 + 参数 + 结果完全一样——连续 5-7 次就确定是死循环。和 Hermes StallDetector 类似,但阈值按工具类型不同
- 模式连续性检测:同类工具连续调用(连续 50 次 read 不做任何 write)——可能迷失方向
- 伪进展检测(Hermes 没有的):独立的 ErrorStreak 安全线——连续 N 次工具调用全部返回错误。操作在变化(签名不同),但结果始终是失败。只要有一次成功就重置计数
工具四级分类——不同工具用不同阈值:
| 分类 | 工具示例 | 模式阈值 | 理由 |
|---|---|---|---|
| 探索 | read、grep、search | 100 | 读 100 个文件是正常搜索 |
| 动作 | edit、write、exec | 50 | 50 次编辑通常说明在反复修改 |
| 规划 | plan | 30 | 反复修改计划几乎可确定在循环 |
| 自定义 | exec:readonly | 自定义 | 按场景覆盖默认阈值 |
进度感知的减半计数(Halving):如果工具参数在变化(每次 edit 内容不同),疑似循环的计数减半。为什么是减半不是归零?因为"参数变化但全部失败"仍然可疑——减半意味着需要指数级更多的变化调用才能掩盖一次真重复。正常工作中很难触及,伪进展中会被累积捕获。
两阶段响应:先自愈再问人
检测到异常后不直接杀——分两步:
第一次触发:注入一条系统消息"你似乎在重复操作,考虑换个方向"。重置计数器。大部分"循环"只是模型的临时困惑,一条提示就能打破。
第二次触发:升级到 HITL——问用户"Agent 似乎困住了,继续 / 停止 / 给指导?"。用户有 60 秒回复,超时 = 拒绝(fail-closed)。
教训:为什么每个检测维度需要独立的自愈机会
早期设计中,三个检测维度共享一个全局的"已自愈一次"标记。结果:Identical Call 触发自愈,模型改变了方法(Identical 重置),但随后陷入新的 Tool Streak 模式——因为全局标记已经用过了,Tool Streak 直接跳到 HITL。
修复:每个维度有独立的自愈计数器。Identical Call 的自愈不消耗 Tool Streak 的机会。这样一个 Agent 可以在 Identical Call 维度被纠正一次、Tool Streak 维度再被纠正一次——总共两次自愈机会,覆盖两种不同的"临时困惑"。
实际效果
在我们的引擎中,这套机制(多维度行为分析 + 两阶段响应)替代了 max_turns:
- 正常任务不被截断——200 步的大型重构能跑完
- 死循环在 10-20 步内被检测到——而非等到硬上限
- 兜底硬上限设在 5000——那不是
max_turns,是"防引擎 Bug 的安全网"
不在同一问题空间的两个特例
Aider:没有工具循环,不存在死循环问题
Aider 经常被拿来和 Claude Code、Cursor 比较,但它的架构完全不同(Aider 底层分析, dev.to/masihmoafi)。Aider 不是 while-tool_call 循环——它的唯一循环是"反射":编辑后发现 parse 失败 / 文件不匹配 / lint 报错 / 测试失败,把问题发回模型重试。这个反射最多 3 次(max_reflections=3),四种触发条件共享预算。
Aider 没有 max_turns 不是因为信任模型,是因为架构里不存在无限循环的可能。它回避了终止问题而非解决了它。
Cursor:有 stall 检测,但循环仍是活跃痛点
Cursor 是闭源产品。从用户论坛和官方回复可以确认(Cursor 论坛, #161234, #158619):有 stall 检测(流式响应无进展 ~30s 后 abort + retry)、有 terminal timeout(系统级,不可用户配置)。
但死循环仍是 2026 年的活跃用户痛点。论坛持续有报告"Agent 在 Plan Mode 下花 2-3 小时执行 echo、监控后台任务、搜索探索,没有实际进展"。官方承认是 Opus 4.7 Plan Mode 的已知行为,暂无 ETA,建议换模型或换模式。
这说明即使最商业化的产品,也还没有完全解决"Agent 看起来在忙但没在干正事"的问题。stall 检测能抓住"完全卡死",抓不住"忙碌但无效"。
三种流派对比
| 维度 | 硬上限 | 预算计量 | 行为分析 |
|---|---|---|---|
| 代表 | LangGraph / CrewAI / OpenAI SDK / AutoGen | Claude Code / Codex CLI | Hermes StallDetector / 我们的 CycleDetector |
| 检测什么 | 步数/消息/token/时间 | 美元或 token 花费 | 操作签名 / 工具模式 / 连续失败 |
| 能区分正常工作和死循环吗 | ❌ | 部分(死循环消耗更高) | ✅ |
| 假阳性 | 高(正常长任务被截断) | 低(用完预算才停) | 低(有自愈缓冲) |
| 假阴性 | 高(死循环在 N-5 步后才停) | 中(花完才知道在浪费) | 低(10-20 步内检测到) |
| 实现复杂度 | 一行计数器 | 中(需嵌入价格表) | 高(行为窗口 + 工具分类 + 阈值) |
| 适用场景 | Demo / 短任务 / 原型 | SDK / headless / SaaS | 长任务 / 生产级 / 无人值守 |
实际搭配:截至本文写作时最成熟的方案都是组合使用:
| 层 | 机制 | 角色 |
|---|---|---|
| 最外层 | 硬上限(5000 步) | 防引擎 Bug 的安全网 |
| 中层 | 行为分析(CycleDetector + ErrorStreak) | 检测真循环和伪进展 |
| 内层 | 预算计量 + 应用层决策 | 引擎报数,应用层按商业逻辑停 |
那些看似需要 max_turns 的场景
"防止失控"——行为分析在十几步内就能检测到真循环。兜底硬上限(5000 步)是"安全网",不是 max_turns——正常任务不可能触及。
"控制成本"——"花多少算多"是商业决策。正确的做法是引擎计量消耗并提供信号,应用层决定停不停。Claude Code 的 --max-budget-usd 走在了这个方向上——用美元而非步数做预算。
"给用户进度感"——进度不该用"已消耗 / 最大步数"表达。更好的信号:Plan 步骤完成率(5/8)、token 消耗占预算比(65%)、模型自己的判断。
一句话总结
终止的正确抽象不是"跑了多少步",是"行为有没有异常"。 行业已从 max_turns=25 分化为三种流派——硬上限(大多数框架还在这里)、预算计量(Claude Code / Codex 在探索)、行为分析(Hermes 和我们的实践)。真正的前沿在于:能不能在第 10 步就区分正常工作和死循环,而不是等到 N 步或 $N 才知道。
引用来源
本文所有数据均可通过以下链接验证。
- LangGraph recursion_limit 默认值(1000)与 RemainingSteps — docs.langchain.com/oss/python/…
- LangGraph Running Agents max_iterations — Running agents
- CrewAI max_iter 与 Bug #3847 — Agents - CrewAI / github.com/crewAIInc/c…
- OpenAI Agents SDK max_turns 与 MaxTurnsExceeded — Running agents - OpenAI Agents SDK
- OpenAI Agents SDK BudgetGuard 提案 #3353 — github.com/openai/open…
- AutoGen TerminationCondition 组合器 — Termination — AutoGen
- Hermes StallDetector #41477 — github.com/NousResearc…
- Hermes ToolLoopDetector #6784 — github.com/NousResearc…
- Hermes Session Caps #66600 — github.com/NousResearc…
- Claude Code --max-budget-usd 与 Agent Loop — code.claude.com/docs/en/age…
- Claude Code CLI reference — code.claude.com/docs/en/cli…
- Codex CLI SDK max_turns / max_budget_tokens — ThreadOptions in codex_cli_sdk::config - Rust
- Codex Goal blocked/usageLimited #23094 — github.com/openai/code…
- Aider 底层分析(max_reflections=3) — How Aider works under the hood - DEV Community
- Cursor stall 检测 — Agent should auto-abort and retry when stalled (~30s), not hang until manual Stop - Feature Requests - Cursor - Community Forum
- Cursor Plan Mode 死循环 — Persistent "Extension Host Timeout" & Agent Execution Hang - Bug Reports - Cursor - Community Forum
- LangChain $47K 死循环 11 天 — vectara/awesome-agent-failures (GitHub)
- Particula (Jul 2025) — 847 步推理 $47/分钟无最终答案
- Agent 循环终止评分方法论 — Stop an agent before a repeated tool call becomes a loop | QABattle