Agent 的三大翻车现场:死循环、Token 烧穿、输出截断,我用三道保险丝给它上了堂课

0 阅读7分钟

前言:一个 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