事情是这样的。
你跟 Agent 说:「把项目里所有的 console.log 替换成 logger.info」。
听起来像是个实习生十分钟就能干完的活。结果两小时后你回来一看,它还在那儿:
text
scss
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 账单已经烧到 8 美元,文件内容一个字没变。
这不是段子,这是 Agent 出厂设置。今天聊聊怎么给它装保险丝。
先看看 Agent 是怎么把自己玩死的
三种经典死法:
死循环:反复调用同一个工具,做同样的事。像极了找不到钥匙却在同一个口袋摸十遍的你。
Token 烧穿:无限续写。你让它改个日志,它给你写了一篇关于日志系统哲学意义的散文。
输出截断:模型说着说着被掐断了,但它自己不知道,下一轮接着从「总之综上所述」开始,前言不搭后语。
下面逐个拆。
保险丝一:工具死循环检测
OpenClaw 的思路很朴素:给每次工具调用打指纹。
text
scss
指纹 = SHA256(工具名 + 序列化后的参数)
光看参数还不够,因为 Agent 可能参数一样但结果在变(比如轮询等待)。所以还要记录结果指纹------同样的调用、同样的参数、同样的结果,才算「无进展」。
基于这个框架,四种检测器层层加码。
检测器 1:通用重复检测
同一个工具调用、同样的参数,触发 10 次阈值。
动作:警告,不阻断。
为什么不直接砍?因为有些场景确实需要重试。比如网络抖动、文件锁竞争。10 次是给「合理重试」留的余量,超过这个数基本可以断定它在原地打转。
就像你妈喊你起床,前三次是提醒,第十次就开始掀被子了------但还没动手。
检测器 2:无进展轮询检测
不光看参数,还要看结果。
如果状态长时间没变化,说明这个工具调用对这个任务是无效的。Agent 应该去干点别的,而不是在这儿刷 CD。
举个反例:
text
scss
get_build_status() → "building"
get_build_status() → "building"
get_build_status() → "building"
...
参数一样,结果一样,纯纯的浪费。这时候该做的是 sleep 或者换个思路,而不是把同一个接口刷到冒烟。
检测器 3:Ping-Pong 检测
这个最阴间。两个工具交替调用,像是在打乒乓球:
text
erlang
read_file → write_file → read_file → write_file → ...
单独看每一次调用,参数可能都在变,结果也在变,前面两个检测器抓不住。
但把序列拉长一看------这俩在互相喂球,谁也没得分。
检测逻辑:两个工具交替执行,且交替后的结果和上一轮相同,判定为死循环。
检测器 4:全局熔断
上面三种情况,本质上都是「无进展」。
只要累计 30 次无进展,强制停止,没有例外。
这是最后一道闸。不管你在干嘛,不管你看起来多努力,只要 30 次没推动任务前进,就给我下来。
有点像电路里的空气开关。前面几层是保险丝,这个是总闸。
保险丝二:Token 预算控制
死循环是行为问题,Token 烧穿是资源问题。
解法:每次任务预算好 Token,不能超。
但不是简单粗暴地到点就砍,而是分阶段干预。
90% 阈值:注入提示
当消耗达到预算的 90%,往上下文里塞一条消息:
text
erlang
已完成 Token 目标的 87%(26170/30000),继续工作------不要总结
注意最后四个字:不要总结。
因为模型有个坏习惯,一看到快没额度了,就开始「综上所述,我已完成...」然后洋洋洒洒写一堆废话,把最后的预算全花在表演上。
这条提示就是在告诉它:别整虚的,干活。
递减回报检测
光看总量还不够,还得看增量效率。
text
续写第一次:+3000 Token 正常
续写第二次:+2000 Token 正常
续写第三次:+400 Token 增量很小
续写第四次:+200 Token 连续两次递减,停止
逻辑很直白:如果每次续写带来的新内容越来越少,说明模型在挤牙膏。再给它预算也是浪费,直接停。
这就像追剧。第一集信息量爆炸,第二集还行,第三集开始注水,第四集你已经在刷手机了。这时候正确的做法是弃剧,而不是继续充会员。
保险丝三:输出截断恢复
前两个保险丝管的是「别乱花钱」,这个管的是「钱花了但话没说完」。
每个模型都有 max_output_tokens 参数。Claude 默认是 8192 个 token。如果模型要说的话超过这个限制,就会被硬截断。
问题是:模型自己不知道。
它以为自己在正常输出,下一轮接着从「总之综上所述」开始,结果前言不搭后语。
Claude Code 的处理方式分三步:
第一步:8k 不够,提到 64k
简单粗暴。既然默认值太小,那就把天花板抬高。
但这只是缓解,不是根治。再大的窗口也有满的时候。
第二步:注入恢复消息
一旦检测到输出被截断,往上下文里塞一条:
text
输出 Token 限制被触发,直接从断点继续------不要道歉,不要回顾你在做什么。
如果是说到一半被截断了,从那个思路接着说,把剩下的工作拆成更小的块。
这段话信息量很大:
- 不要道歉:模型被截断后第一反应是「抱歉,我之前的输出被中断了」------纯浪费 token
- 不要回顾:别重新总结已经说过的内容
- 从断点继续:直接接上
- 拆成更小的块:防止下一次又被截断
本质上是教模型「被截断后该怎么办」的 SOP。
第三步:认栽
3 次恢复都不行,那就把不完整的结果返回给用户,标记输出被截断。
该认怂就认怂。硬撑只会烧更多 token,还拿不到完整结果。
三根保险丝,各管一段
回头看:
| 保险丝 | 管什么 | 核心手段 |
|---|---|---|
| 工具死循环检测 | 行为异常 | 指纹 + 4 层检测器 + 全局熔断 |
| Token 预算控制 | 资源失控 | 预算 + 90% 提示 + 递减回报检测 |
| 输出截断恢复 | 输出异常 | 提高上限 + 恢复消息 + 认栽 |
三者不是孤立的。死循环会烧 token,烧 token 可能导致输出被截断,截断后的恢复消息又可能触发新一轮循环。
所以真正的工程做法是三层同时上,互相兜底。
Agent 这东西,能力越强,翻车的方式越花哨。保险丝不是为了限制它,是为了让它在正确的方向上跑得更久。
毕竟,一个不会原地打转、不会烧穿预算、不会说到一半断片的 Agent,才是能真正交付任务的 Agent。
下次再看到 Agent 对着同一个文件读读写写,记得它不是在努力,它只是缺一根保险丝。