Agent 该什么时候停——终止策略的三种流派

0 阅读8分钟

Agent 该什么时候停——终止策略的三种流派

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,000∗∗,产出为零(vectara/awesome−agent−failures)。另一个案例:847步推理,每分钟47,000**,产出为零(vectara/awesome-agent-failures)。另一个案例:847 步推理,每分钟 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 步高效重构花 3不停,20步死循环花3 不停,20 步死循环花 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"),且依赖模型的诚实性。

流派二的局限性

预算计量对齐了用户真正关心的维度(钱),但有两个盲区:

  1. 价格依赖:不同模型 token 价格差 100 倍。同一个 $5 预算在 GPT-4o-mini 上能跑几千步,在 claude-4.6-opus 上只能跑几十步。预算线的含义随模型变化
  2. 死循环仍在空转:5预算的死循环只是在5 预算的死循环只是在 5 后停,那 5全是浪费。预算限制了∗∗损失上限∗∗,但没有∗∗提前检测∗∗到循环。行为分析能在第10步就发现死循环,预算计量在5 全是浪费。预算限制了**损失上限**,但没有**提前检测**到循环。行为分析能在第 10 步就发现死循环,预算计量在 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 签名检测的基础上多走了两步——工具分类和伪进展独立检测。

三个检测维度同时运行:

  1. 完全相同检测:工具名 + 参数 + 结果完全一样——连续 5-7 次就确定是死循环。和 Hermes StallDetector 类似,但阈值按工具类型不同
  2. 模式连续性检测:同类工具连续调用(连续 50 次 read 不做任何 write)——可能迷失方向
  3. 伪进展检测(Hermes 没有的):独立的 ErrorStreak 安全线——连续 N 次工具调用全部返回错误。操作在变化(签名不同),但结果始终是失败。只要有一次成功就重置计数

工具四级分类——不同工具用不同阈值:

分类工具示例模式阈值理由
探索read、grep、search100读 100 个文件是正常搜索
动作edit、write、exec5050 次编辑通常说明在反复修改
规划plan30反复修改计划几乎可确定在循环
自定义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 检测能抓住"完全卡死",抓不住"忙碌但无效"。


三种流派对比

三种流派 × 9 个产品的终止策略对比

维度硬上限预算计量行为分析
代表LangGraph / CrewAI / OpenAI SDK / AutoGenClaude Code / Codex CLIHermes 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 才知道。



引用来源

本文所有数据均可通过以下链接验证。