今天在使用 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 的死循环不是理论风险,而是真实会发生的问题。关键认知:
/stop只能中断当前 turn,不能修复根因- 模型重复调用同一命令时,系统不会自动干预
- 无人值守的 Agent 可能白白消耗大量 tokens
- 选择更稳定的模型 + 建立监控是最现实的防线
在 Agent 真正可靠之前,有人值守仍然是必要的安全网。
二、深入源码分析(2026-07-01 更新)
在初次发布本文后,我们对 OpenClaw 源码进行了深入分析,发现死循环的根因比表面现象更复杂。以下是完整分析过程。
1. 死循环类型:大循环 vs 调度器自循环
最初我们怀疑这是 OpenClaw 调度器层面的”自激循环”——调度器自己反复调用同一个工具,不经过大模型。但经过源码分析确认:这是标准的 OpenClaw → 大模型 → OpenClaw 大循环。
调度器层面没有 auto-retry/auto-continue 机制。每次 exec 工具调用都必须由大模型发起:
- 模型发出 tool call(exec)
- OpenClaw 执行命令,结果返回给模型
- 模型收到结果后,决定再次调用 exec
- 循环往复
之所以感觉”速度快”,是因为 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 中包含了 tail 或 aggregated 字段的细微差异。
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 就不会触发。
修复方向:
- 从 exec resultHash 中剥离 volatile 字段(output、tail)
- 或采用相似度比较替代精确哈希匹配
- 配合 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 次失败后就会主动停下来向用户报告问题
这说明死循环是两层问题叠加:
- OpenClaw 层:loop detection 的 critical/circuit-breaker 对 exec 失效(resultHash 问题),缺少 session 级别的硬上限——这是系统性缺陷,所有模型都受影响
- 模型层:部分模型在工具连续失败后缺乏策略切换能力——这是模型能力差异,不同模型表现不同
如果只修 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. 待验证问题
supportsTools: false时,system prompt 仍包含工具描述是否合理?- 是否存在 DSML 之外的文本工具调用解析路径?
- 死循环时 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 工具的 navigate 和 act 操作。网关日志中记录了大量 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. 结论与修正
对之前分析的两个修正:
- Loop Detection 配置是生效的——至少对 browser 工具完全正常。exec 工具的
resultHash问题仍然存在(详见第二章),但这不影响其他工具的检测。 - 之前”没有看到 Loop Detection 日志”的原因——不是配置问题,而是 deepseek-v4-flash 的
supportsTools: false导致工具调用根本不发生。切换到有工具能力的模型后,Loop Detection 立即表现出正常行为。
这一发现进一步印证了第三章的结论:supportsTools: false 的影响远超预期——它不仅可能导致 system prompt 不一致,还掩盖了 Loop Detection 机制本身的正常运作。在排查”防护机制是否生效”类问题时,首先需要确认当前模型是否实际具备工具调用能力。