OpenClaw会话死循环案例分析

今天在使用 AI Agent 的过程中发现了一个典型问题:一个无人值守的 Agent 会话陷入了死循环,短时间内消耗了 65 万 tokens 却几乎没有产出。记录整个发现、分析和复盘过程。

现象

通过监控发现某个 Agent 会话(模型 deepseek-v4-flash)反复执行相同的命令,每次调用都没有产出有效结果,但上下文在持续膨胀。

发送 /stop 中断后,让该会话分析自身死循环的原因——结果它又陷入了同样的循环模式。这说明 stop 只能中断当前 turn,无法修复根因

死循环数据

指标 数值 说明
exec 总调用次数 166 次 大部分是重复命令
上下文大小 654K tokens(输入) 正常会话约 30-50K
有效输出 212 tokens 几乎为零
turn failed 次数 9 次 模型无法产出内容
prompt-error 次数 4 次 请求被拒绝
最终状态 killed(手动 /stop) 用户通过 /stop 强制终止

循环模式分析

从 166 次 exec 调用中提取重复模式:

命令 重复次数 说明
ls -la .../blog-publisher-wordpress/ 33 次 反复列出同一目录
python3 ...读取 session JSONL 20 次 反复解析同一文件
find .../blog-publisher-wordpress -type f 19 次 反复搜索同一路径
python3 -c "...读取 session" 8 次 换种写法做同样的事
tail -100 ...log 5 次 反复读同一段日志

同一个 ls 命令调用了 33 次,find 调用了 19 次。模型完全没有意识到自己在重复。

根因分析

1. 模型缺乏自我觉察

deepseek-v4-flash 在处理”找不到上下文”的情况时,没有能力判断”我已经搜过 30 次了,再搜也不会有结果”。每次搜索的结果被追加到上下文,模型看到更多数据后反而更困惑,于是继续搜索。

2. 没有 Circuit Breaker

当前 Agent 框架没有检测”同一命令重复 N 次”的机制。无论模型调用同一命令多少次,系统都会忠实地执行并返回结果。166 次 exec 调用全部被执行了。

3. 上下文窗口失控

每次 exec 的输入和输出都被追加到上下文。166 次调用的结果累积到 654K tokens,远超模型的有效处理能力。当上下文过大时,模型开始报错(assistant turn failed),但报错本身又被追加到上下文,形成恶性循环。

恶性循环链

找不到上下文 → 搜索 session 文件 → 结果加入上下文
→ 上下文膨胀 → 模型处理不了 → 重新搜索
→ 更多上下文 → 更多错误 → 最终被手动 /stop

浪费的代价

这次死循环消耗了约 654K 输入 tokens,几乎没有有效输出。按主流 API 定价估算:

  • 以 deepseek-v4-flash 的价格计算,单次死循环的成本虽然不高
  • 但如果 无人值守,一个晚上可能触发多次这样的循环
  • 多个 Agent 同时失控,累积消耗会非常可观

真正的风险不是单次成本,而是 无人监控下的累积浪费。如果没有人工 /stop 干预,session 会一直跑到 context window 上限才可能停止——甚至可能不会停止。

一个残酷的事实

OpenClaw 目前 没有自动防护机制 能阻止这类死循环。已有的 globalCircuitBreakerThreshold 是按工具类型计数的,agent 通过轮换不同工具(ls → find → python3 → tail)就能绕过。GitHub 上已有相关 issue(#79252),修复方案(#97577)正在推进中。

防御建议

1. 模型选择

复杂上下文追踪任务避免使用 deepseek-v4-flash。实测 mimo-v2.5 在同一场景下不会陷入重复调用循环,具备更好的自我觉察能力。

2. 建立监控机制

定期检查活跃 session 的 token 消耗和工具调用频率,超阈值自动告警:

# 检查活跃 session 的 token 消耗
openclaw sessions list --active

3. 考虑 Session Token 预算

为单个 session 设置 token 上限,超过后自动中断。这是最直接的防护,但目前 Agent 框架尚未内置此功能。

4. 工具调用频率限制

检测同一命令的连续重复调用,达到阈值后自动中断并通知用户。这需要在框架层面实现。

结论

AI Agent 的死循环不是理论风险,而是真实会发生的问题。关键认知:

  1. /stop 只能中断当前 turn,不能修复根因
  2. 模型重复调用同一命令时,系统不会自动干预
  3. 无人值守的 Agent 可能白白消耗大量 tokens
  4. 选择更稳定的模型 + 建立监控是最现实的防线

在 Agent 真正可靠之前,有人值守仍然是必要的安全网。


二、深入源码分析(2026-07-01 更新)

在初次发布本文后,我们对 OpenClaw 源码进行了深入分析,发现死循环的根因比表面现象更复杂。以下是完整分析过程。

1. 死循环类型:大循环 vs 调度器自循环

最初我们怀疑这是 OpenClaw 调度器层面的”自激循环”——调度器自己反复调用同一个工具,不经过大模型。但经过源码分析确认:这是标准的 OpenClaw → 大模型 → OpenClaw 大循环

调度器层面没有 auto-retry/auto-continue 机制。每次 exec 工具调用都必须由大模型发起:

  1. 模型发出 tool call(exec)
  2. OpenClaw 执行命令,结果返回给模型
  3. 模型收到结果后,决定再次调用 exec
  4. 循环往复

之所以感觉”速度快”,是因为 exec 输出很短,模型处理快,每轮只需几百毫秒。

2. Loop Detection 机制分析

OpenClaw 内置了 tool-loop-detection 模块,包含 4 种检测器:

检测器 触发条件 Warning 阈值 Critical 阈值 适用场景
genericRepeat 相同 toolName + 相同 argsHash + 相同 resultHash 10 20 工具调用完全相同且结果不变
knownPollNoProgress process poll/log 工具,相同参数,结果不变 10 20 轮询死循环
pingPong 两个工具交替调用,参数签名来回切换,结果不变 10 20 A→B→A→B 交替死循环
unknownToolRepeat 连续调用不存在的工具 10 模型反复尝试已删除的工具

响应级别:

  • Warning(≥10次):给模型发警告消息,让模型自己停止
  • Critical(≥20次):直接 block,返回 status: "blocked"
  • Global Circuit Breaker(≥30次):全局熔断,阻止整个 session 继续执行

3. 为什么 Loop Detection 没有阻止死循环?

我们的 openclaw.json 中已正确配置了 loopDetection:

"tools": {
  "loopDetection": {
    "enabled": true,
    "warningThreshold": 10,
    "criticalThreshold": 20,
    "globalCircuitBreakerThreshold": 30,
    "detectors": {
      "genericRepeat": true,
      "knownPollNoProgress": true,
      "pingPong": true
    }
  }
}

但死循环仍然发生了。根因在于 hashExecToolOutcome() 函数的实现:

对于 exec 工具的 status: "completed"status: "failed" 结果,哈希计算包含:

digestStable({
  status,
  exitCode,
  timedOut,
  output: normalizeNullableString(details.aggregated) ?? text
})

关键问题output 字段(完整的命令输出文本)被纳入了 resultHash 计算。即使命令相同、exit code 相同,只要输出文本有任何细微差异(比如时间戳、路径顺序、错误信息的微小变化),resultHash 就会不同。

这导致 noProgressStreak(要求连续相同的 resultHash)始终为 1,永远不会达到 criticalThreshold(20)或 globalCircuitBreakerThreshold(30)。

检测路径 使用的计数器 是否触发 原因
Warning recentCount(相同 args 计数,不看 result) ✅ 触发 参数相同即可
Critical noProgressStreak(要求 resultHash 相同) ❌ 不触发 exec 输出略有变化,resultHash 每次不同
Global Circuit Breaker noProgressStreak ❌ 不触发 同上

Warning 虽然触发了,但模型(GLM-5.2)忽略了警告文本,继续循环。

4. 复现验证

在分析过程中,我们多次复现了死循环。最典型的场景:模型反复执行 grep -n "loopDetection" /root/.openclaw/openclaw.json,每次输出为空(exit code 1),模型不断重试,30+ 次后需要手动 /stop。

每次 exec 的输出完全相同(都是空),但 hashExecToolOutcome() 仍然为每次生成了不同的 resultHash——这可能是因为 exec 的 status details 中包含了 tailaggregated 字段的细微差异。

5. GitHub Issue 跟踪

这个问题已在 GitHub 上提交并跟踪:

  • Issue #93917:Bug: genericRepeat critical/circuit-breaker never fires when exec results vary slightly
  • PR #97577:fix: make tool loop circuit breaker session-global
  • Issue #97485:feat(agents): add iteration budget for agent loop safety

我们已在 #93917 下补充了复现信息,包括 OpenClaw 2026.6.10 + GLM-5.2 的完整配置和现象描述。

6. 临时缓解措施

在官方修复之前,可考虑以下措施:

  • 降低 warningThreshold:设为 3-5,让 warning 更早触发(虽然模型可能仍忽略)
  • 关注 PR #97577:session-global circuit breaker 可以防住跨工具循环
  • 关注 PR #97485:iteration budget 从根本上限制工具调用轮数
  • 模型选择:不同模型对 warning 的遵从度不同,换模型可能有缓解效果
  • 人工监控:在可预见的死循环场景中保持 /stop 就绪

7. 根因总结

设计缺陷hashExecToolOutcome() 将完整 output 文本纳入 resultHash,导致 exec 结果的任何微小变化都会重置 noProgressStreak。

影响范围:所有使用 exec 工具的场景,只要命令输出有细微变化(时间戳、错误信息、路径顺序等),critical 和 circuit-breaker 就不会触发。

修复方向

  1. 从 exec resultHash 中剥离 volatile 字段(output、tail)
  2. 或采用相似度比较替代精确哈希匹配
  3. 配合 session-global circuit breaker 作为兜底

8. 模型能力差异对比(2026-07-01 补充)

在后续测试中发现,死循环的严重程度与模型能力密切相关。同一个任务场景下,不同模型的表现差异显著:

模型 行为特征 循环次数 是否会换工具 结果
deepseek-v4-flash(火山引擎) 同一个 exec 命令反复调用,不改变策略 166 次 ❌ 很少换工具 654K tokens,手动 /stop
GLM-5.2(智谱) 也会重复调用,但会尝试换命令、换工具 30+ 次 ✅ 会换工具/换参数 仍然会循环,但次数少很多

关键发现:

  • deepseek-v4-flash 在工具连续失败后缺乏”停下来反思”的能力,倾向于用完全相同的参数反复重试,陷入最坏情况的死循环
  • GLM-5.2 虽然也会循环,但会尝试不同的命令和参数(比如换 grep 为 find、换路径),说明它有一定的”换策略”意识,只是不足以完全跳出循环
  • 更强的模型(如 GPT-5.5)可能根本不会陷入循环——在 3-5 次失败后就会主动停下来向用户报告问题

这说明死循环是两层问题叠加:

  1. OpenClaw 层:loop detection 的 critical/circuit-breaker 对 exec 失效(resultHash 问题),缺少 session 级别的硬上限——这是系统性缺陷,所有模型都受影响
  2. 模型层:部分模型在工具连续失败后缺乏策略切换能力——这是模型能力差异,不同模型表现不同

如果只修 OpenClaw 层(加上 session-global circuit breaker 或 iteration budget),所有模型都安全。但如果模型本身能力强,可能根本不需要 loop detection 兜底。

实践建议:在 OpenClaw 官方修复 loop detection 之前,选择”工具失败后容易换策略”的模型(如 GLM-5.2、GPT-5.5)可以显著降低死循环的风险和严重程度。避免在生产环境中使用 deepseek-v4-flash 执行需要多次工具调用的复杂任务。


三、supportsTools: false 与死循环的关系(2026-07-01 补充)

在分析过程中,我们发现了一个值得关注的问题:之前因 DeepSeek-v4-flash 工具调用不匹配导致大量 400 错误,在 openclaw.json 中禁用了其工具支持:

{
  "id": "deepseek-v4-flash",
  "compat": { "supportsTools": false }
}

今天的死循环发生时,session 使用的正是这个配置了 supportsTools: false 的 deepseek-v4-flash。这引发了我们的疑问:禁用工具支持是否反而加剧了死循环问题?

1. supportsTools: false 的实际效果

通过源码分析,supportsTools: false 的实际效果如下:

层面 行为 是否符合预期
API 请求 不传 tools 字段给模型 API ✅ 符合
工具构建 tools: toolsEnabled ? toolsRaw : [],传给 runner 的工具为空 ✅ 符合
System Prompt 仍然包含完整的工具描述(”你可以用 exec 工具…”) ❌ 不符合

关键问题effectiveToolsAllow(决定 system prompt 中列出哪些工具)不依赖 toolsEnabled,而是来自 params.toolsAllow。因此即使工具被禁用,system prompt 仍然告诉模型”你有 exec、read、write 等工具可用”。

2. DSML Recoverer 分析

OpenClaw 有一个 DeepSeek DSML Tool Call Recoverer,能从模型文本输出中解析 DSML 格式的工具调用并执行。我们最初假设这是死循环的根因,但源码分析排除了这个假设:

shouldFilterDeepSeekDsmlText(compat) 的判断条件是 compat.thinkingFormat === "deepseek",而 handai/deepseek-v4-flash 的配置里没有 thinkingFormat: "deepseek",所以 DSML recoverer 不会生效。

3. 矛盾点

如果 supportsTools: false 确实完全禁用了工具调用,那 deepseek-v4-flash 不可能反复执行 exec 命令。但实际观察到的现象是:模型确实陷入了工具调用死循环

可能的解释:

  • 模型在文本中模拟工具调用:模型收到 system prompt 说有工具可用,但 API 请求里没有 tools 定义,于是在文本输出中生成类似工具调用的格式。虽然 DSML recoverer 不生效,但可能存在其他文本解析路径。
  • 触发了 fallback:deepseek-v4-flash 调用失败后,OpenClaw 的 fallback 机制切换到有工具能力的模型(如 deepseek/deepseek-v4-flash 或 alibaba/deepseek-v4-flash),由 fallback 模型执行了工具调用。
  • 其他未发现的机制:可能存在我们尚未发现的工具调用解析路径。

4. 设计缺陷

无论死循环的具体触发路径是什么,system prompt 与实际工具可用性不一致本身就是一个设计缺陷:

  • System prompt 告诉模型”你有 exec、read、write 等工具可用”
  • 但 API 请求里没有 tools 定义
  • 模型被诱导去”使用”实际不存在的工具
  • 这种行为在弱模型(如 deepseek-v4-flash)上更容易导致问题

正确的做法应该是:当 supportsTools: false 时,system prompt 中也应该移除工具描述,或明确告知模型”当前模式下工具不可用”。

5. 待验证问题

  1. supportsTools: false 时,system prompt 仍包含工具描述是否合理?
  2. 是否存在 DSML 之外的文本工具调用解析路径?
  3. 死循环时 session 实际使用的模型是哪个?是 deepseek-v4-flash 本身,还是 fallback 到了其他模型?

这些问题需要进一步验证,欢迎在评论区讨论或到 GitHub Issue #93917 跟进。

四、Loop Detection 实际生效验证(2026-07-01 补充)

在之前的分析中,我们指出 Loop Detection 的 critical/circuit-breaker 对 exec 工具失效(因为 hashExecToolOutcome() 将完整 output 纳入 resultHash,exec 输出的微小变化导致哈希每次不同)。但今天的测试提供了一个新的观察角度。

1. 现象:browser 工具的 Loop Detection 正常工作

今天在执行一个浏览器自动化任务时,由于 targetId 不匹配,agent 反复用相同参数调用 browser 工具的 navigateact 操作。网关日志中记录了大量 Loop Detection 警告:

时间 警告内容 调用次数
14:42:59 Loop warning: browser called 12 times with identical arguments 12
14:43:06 Loop warning: browser called 13 times with identical arguments 13
(持续递增)
14:46:45 Loop warning: browser called 18 times with identical arguments 18

当次数达到 20 次时,系统执行了 Critical 级别的拦截:

CRITICAL: Called browser with identical arguments and identical outcomes 20 times. Session execution blocked to prevent runaway loops.

Session 被成功阻止,不再继续执行重复调用。这说明 对于 browser 工具,Loop Detection 的 warning → critical → block 全链路正常工作

2. 为什么 browser 有效而 exec 无效?

关键差异在于 resultHash 的稳定性

工具 相同参数时的结果 resultHash 是否稳定 Critical 是否触发
browser 相同参数 → 相同结果(页面状态不变) ✅ 稳定 ✅ 触发
exec 相同命令 → 输出可能有微小差异(时间戳、路径顺序等) ❌ 不稳定 ❌ 不触发

browser 工具的返回值是结构化的 JSON(页面 URL、snapshot 等),当参数完全相同时,结果也完全相同,resultHash 保持一致,noProgressStreak 能正确累加到 criticalThreshold。而 exec 工具的输出包含命令的 stdout/stderr,即使命令相同,输出的微小差异(如 ls -la 的时间戳)会导致 resultHash 每次不同。

3. 关于 supportsTools: false 的重要发现

在之前的章节中,我们分析了 supportsTools: false 配置可能导致 system prompt 与实际工具可用性不一致的问题。今天的测试进一步确认了一个关键事实:

当 deepseek-v4-flash 配置了 supportsTools: false 时,它根本不会调用任何工具(包括 browser、exec 等),因此 Loop Detection 机制永远不会被触发。

这解释了为什么之前认为”Loop Detection 没有起作用”——不是因为配置问题,而是因为 deepseek-v4-flash 禁用了工具调用,agent 根本不会发起工具调用循环。今天的测试使用了 GLM-5.2(工具调用启用),agent 频繁调用 browser 工具,Loop Detection 立即正常工作。

换句话说:

  • deepseek-v4-flash(supportsTools: false):不调用工具 → 不触发 Loop Detection → 但也不执行实际任务(陷入文本层面的重复)
  • GLM-5.2(supportsTools: true):调用工具 → 触发 Loop Detection → Critical 拦截成功阻止循环

4. 结论与修正

对之前分析的两个修正:

  1. Loop Detection 配置是生效的——至少对 browser 工具完全正常。exec 工具的 resultHash 问题仍然存在(详见第二章),但这不影响其他工具的检测。
  2. 之前”没有看到 Loop Detection 日志”的原因——不是配置问题,而是 deepseek-v4-flash 的 supportsTools: false 导致工具调用根本不发生。切换到有工具能力的模型后,Loop Detection 立即表现出正常行为。

这一发现进一步印证了第三章的结论:supportsTools: false 的影响远超预期——它不仅可能导致 system prompt 不一致,还掩盖了 Loop Detection 机制本身的正常运作。在排查”防护机制是否生效”类问题时,首先需要确认当前模型是否实际具备工具调用能力。

作者: Jack.shang

jack.shang 程序员->项目经理->技术总监->项目总监->部门总监->事业部总经理->子公司总经理->集团产品运营支持