前言:一个 console.log 引发的血案
事情的起因很简单。
我给 Agent 下了个指令:
把项目中所有的
console.log替换成logger.info
听起来人畜无害对吧?就像让实习生去把文档里的"的"改成"地"一样简单。
结果我的 Agent 给我上演了一出《土拨鼠之日》:
text
调用 read_file("src/index.js")
调用 write_file("src/index.js")
调用 read_file("src/index.js")
调用 write_file("src/index.js")
调用 read_file("src/index.js")
调用 write_file("src/index.js")
...
它读一遍,写一遍,再读一遍,发现没改干净,再写一遍。周而复始,生生不息。
我的 Token 余额在燃烧,我的血压在升高,而它还在那儿勤勤恳恳地"工作"。
这让我意识到一个残酷的事实:Agent 不是不努力,它是努力错了方向,还停不下来。
后来我研究了下 OpenClaw 的设计,发现人家早就把这三种翻车场景研究透了,并且准备了三道保险丝。今天就来聊聊这三道保险丝是怎么救命的。
翻车现场大盘点
在装保险丝之前,先看看 Agent 最常见的三种"作死"方式:
| 翻车姿势 | 症状 | 后果 |
|---|---|---|
| 死循环 | 反复调用同一个工具,做同样的事 | Token 烧光,任务零进展 |
| Token 烧穿 | 无限续写,停不下来 | 钱包大出血 |
| 输出截断 | 模型输出到一半被截断,它自己不知道 | 返回半成品,用户一脸懵 |
这三兄弟合起来,就是 Agent 界的"三害"。
保险丝一:工具死循环检测
给每次工具调用打上"指纹"
要检测死循环,首先得知道"什么算重复"。
OpenClaw 的做法很朴素:给每次工具调用算一个 SHA256 指纹。
python
fingerprint = SHA256(工具名 + 序列化参数)
比如:
python
# 第一次调用
read_file("src/index.js")
# 指纹:a3f5c9...
# 第二次调用(一模一样)
read_file("src/index.js")
# 指纹:a3f5c9... ← 撞了!
光看参数还不够,因为有时候参数一样但结果不一样,这不算真死循环。
所以还要记录结果指纹:
同样的工具调用 + 同样的参数 + 同样的结果指纹 = 真正的"无进展"
这就好比你去冰箱里找吃的:
- 打开冰箱(参数一样)
- 发现里面还是那瓶过期牛奶(结果一样)
- 关上冰箱
- 再打开... ← 这就是死循环
四种检测器,层层设防
OpenClaw 设计了四种检测器,像四道关卡一样把死循环摁死:
1. 通用重复检测:触发 10 次,警告不阻断
text
同一个工具调用 + 同样的参数 → 触发 10 次阈值
→ 发警告:"哥们,你是不是卡住了?"
→ 但不阻断,给它一个改过自新的机会
为什么是警告而不是直接阻断?因为有时候重复是合理的——比如轮询一个异步任务的状态。你得给它一点信任。
2. 无进展轮询检测:光看参数不够,还得看结果
text
参数一样 + 结果也一样 → 状态长时间没变化
→ 告诉 Agent:"别在这儿磨叽了,去干点别的"
这个检测器的精髓在于:它不看过程,只看结果。
你调用 100 次工具都行,只要每次结果有变化,说明你在推进。但如果结果一成不变,那就对不起了——你这不是在工作,你这是在摸鱼。
3. Ping-Pong 检测:你俩搁这儿打乒乓球呢?
这个检测器专门对付一种诡异的模式:
text
read_file → write_file → read_file → write_file → ...
两个工具交替调用,结果还一样。
这就像两个人打乒乓球:
text
"你吃了吗?"
"吃了,你呢?"
"我也吃了,你吃了吗?"
"吃了,你呢?"
...
检测逻辑很简单:两个工具交替执行,结果是否一样?一样就判定为死循环。
4. 全局熔断:累计 30 次无进展,强制停机
前面三种检测器都是"温柔"的——警告、提示、引导。
但如果 Agent 油盐不进,累计 30 次无进展,那就别怪我不客气了:
text
强制停止,没有例外。
这是最后一道防线,相当于电路里的保险丝——烧断就烧断,保护的是整个系统。
保险丝二:Token 预算控制
死循环能检测,但 Token 烧穿怎么办?
答案是:给每次任务预算好 Token,不能超过预算。
但光设个上限太粗暴了,Agent 可能在 99% 的时候被一刀切,留下个烂摊子。OpenClaw 的做法更优雅:
90% 时的温柔提醒
当 Token 消耗到 90% 时,注入一条提示:
text
"已完成 Token 目标的 87%(26170/30000),继续工作——不要总结"
注意最后那句"不要总结"很关键。
因为模型有个臭毛病:一看到快结束了,就开始写总结报告。"综上所述...""总而言之..."——总结是最费 Token 的废话。
这条提示就是在告诉它:别总结,接着干,把活干完比什么都强。
检测递减回报:连续两次递减就停
还有一种情况:Agent 没有死循环,也没有烧穿预算,但它在做无用功。
判断标准是"递减回报":
| 续写次数 | 新增 Token | 判断 |
|---|---|---|
| 第 1 次 | +3000 | 正常 |
| 第 2 次 | +2000 | 正常 |
| 第 3 次 | +400 | 增量很小 |
| 第 4 次 | +200 | 连续两次递减 → 停止 |
逻辑是:如果连续两次续写的 Token 增量都在递减,说明 Agent 已经没什么可说的了,继续下去就是浪费。
这就好比开会:
- 第 1 个人发言:有干货
- 第 2 个人发言:有点干货
- 第 3 个人发言:开始重复
- 第 4 个人发言:纯属凑时长
这时候主持人就该说:"好了,散会。"
保险丝三:输出截断恢复
最后一种翻车方式最隐蔽:模型输出到一半,被截断了,它自己不知道。
每个模型都有一个 max_output_tokens 参数。Claude 默认是 8192 个 Token,如果模型要说的话超过这个限制,就会被截断。
截断的可怕之处在于:模型不知道自己被截断了。
它以为自己说完了,兴高采烈地返回结果,结果用户拿到的是一个半成品——就像看电视剧看到大结局前突然断网。
ClaudeCode 的三步处理法
第一步:8k 不够,提到 64k
简单粗暴——既然默认值不够用,那就把上限提高。
但这只能缓解问题,不能根治。因为再高的上限也有被突破的时候。
第二步:注入恢复消息
当检测到输出被截断时,注入一条恢复消息:
text
"输出 Token 限制被触发,直接从断点继续——不要道歉,不要回顾你在做什么。
如果是说到一半被截断了,从那个思路接着说,把剩下的工作拆成更小的块。"
这条消息的精髓在于:
- 不要道歉:道歉是废话,浪费时间
- 不要回顾:回顾也是废话,浪费时间
- 从断点继续:直接接着写
- 拆成更小的块:避免再次被截断
第三步:认栽
如果 3 次恢复都不行,那就认栽:
text
把不完整的结果返回给用户,标记"输出被截断"
这很诚实。与其让 Agent 在那儿无限尝试恢复,不如老老实实告诉用户:"抱歉,输出被截断了,这是我能给的全部。"
总结:三道保险丝,保命又省钱
回顾一下这三道保险丝:
| 保险丝 | 解决的问题 | 核心机制 |
|---|---|---|
| 工具死循环检测 | 反复调用同一工具 | 指纹比对 + 四种检测器 + 全局熔断 |
| Token 预算控制 | 无限续写烧钱 | 预算上限 + 90% 提醒 + 递减回报检测 |
| 输出截断恢复 | 输出半截不知道 | 提高上限 + 恢复消息 + 认栽机制 |
这三道保险丝的共同思路是:不要指望 Agent 自己发现问题,你得替它发现。
Agent 就像一个特别勤奋但有点轴的员工——你让它干活,它能干到天荒地老,但它不会自己判断"我是不是在做无用功"。
所以,保险丝的意义不是限制 Agent,而是保护它(和你的钱包)不把自己烧死。
最后送大家一句话:
好的 Agent 系统,不是让 Agent 永不犯错,而是让它在犯错时,能及时被拉回来。
毕竟,死循环不可怕,可怕的是死循环了你还不知道。
本文基于 OpenClaw 的死循环检测、Token 预算控制和输出截断恢复机制整理,希望能帮你少烧点 Token