Agent 陷入死循环了怎么办?指纹 + 滑动窗口 + 分层熔断

0 阅读5分钟

上一篇聊了上下文压缩,那是「空间」问题;这篇聊一个「时间」问题——Agent 为什么会卡在原地打转,以及怎么让它自己停下来。

前言

如果你在学习Agent或者对function感兴趣的话,一定见过这样的场景:Agent 调一次工具,结果不满意,于是又调一次同样的工具、同样的参数,结果还是一样的,可它就是不换思路,一遍遍重复,直到把 token 耗尽。

这就是 Agent 最经典的问题之一——死循环。根本在于模型是无状态的:它不知道「我上一轮已经这么干过了、还没用」,它只会看着当前的上下文继续接龙。所以「意识到自己在重复」这件事,只能从外部注入

这篇就讲清楚两件事:怎么检测死循环,检测到之后怎么办。核心就是三个关键词——指纹、滑动窗口、分层熔断

一、先看清:死循环长什么样

把 Agent 的死循环归个类,最常见的是两种:

  1. 原地踏步型:同一个工具、同一组参数,反反复复调用,不管结果如何就是不停。
  2. 来回横跳型:在 A、B 两个操作之间交替,比如「读文件 → 报错 → 再读同一个文件 → 再报错」,两个动作互相把对方顶回去。

要检测这两种模式,第一步是给每一次工具调用算一个「指纹」。

二、指纹:把一次调用压成一段哈希(或者你想加密成其他的)

指纹要回答一个问题:这次调用,和之前哪次调用是「同一件事」?

思路很朴素:工具名 + 参数的规范字符串,做一次哈希:

const hash = `${toolName}:${sha256(stableStringify(params)).slice(0, 16)}`

取舍点:这里有个特别容易忽略的细节——参数必须规范化之后再哈希。JSON 里 {a:1, b:2}{b:2, a:1} 是两个不同的对象,但对 Agent 来说其实是「同一个调用」。如果直接 JSON.stringify,两个 key 顺序不同就会算出不同的指纹,导致漏检。所以要先对对象的 key 排序,再序列化,保证「语义相同 → 指纹相同」。

三、滑动窗口:只记住「最近」的历史

光有指纹还不够,还得记住历史。但不能全记——Agent 可能跑几十上百轮,全记下来既占内存,又会让「很久以前的一次重复」误触发检测。

所以用一个滑动窗口,只保留最近 30 轮(或者你自己自定义):

const HISTORY_SIZE = 30
history.push({ toolName, argsHash, timestamp })
if (history.length > HISTORY_SIZE) history.shift()  // 丢掉最老的

取舍点:窗口大小是个权衡。太小,检测不出「慢速重复」;太大,又容易误伤。30 轮是一个经验值——足够覆盖「同一件事反复折腾」的典型场景,又不至于把早该翻篇的历史翻出来。

四、三种检测器:重复、横跳、无进展

有了指纹和历史,就可以上检测逻辑了。这里其实是三个独立的检测器,分别对应三种「卡住」:

① 原地踏步:同一调用出现太多次。 直接数窗口里「工具名 + 参数指纹」完全相同的调用出现了几次:

const recentCount = history.filter(
  h => h.toolName === toolName && h.argsHash === argsHash
).length

② 来回横跳:两个操作交替。 从尾部往前找,看是不是 A、B、A、B 这样的交替模式。

③ 无进展:调用相同,结果也相同。 这是最狠的一种——不但调用一样,连结果都一样,说明这一轮是彻彻底底的白做。实现上要额外给每次调用记一个「结果指纹」,然后数「相同调用、相同结果」连续出现了几次。

取舍点:为什么要分成三种,而不是一个「重复计数」一锅端?因为它们的危险程度不一样。「原地踏步」可能是模型在「多试几次」;「来回横跳」是思路卡死;「无进展」是结果都证明没用了还在试——最后一种最该立刻停。分开检测,才能分层响应。

五、分层响应:先劝,再停

检测到之后,不是一刀切直接停,而是分两层:

const detection = detect(toolName, input)
if (detection.level === 'critical') {
  shouldBreak = true     // 硬熔断:停止整个循环
} else {
  messages.push({        // 软提醒:注入一句提示
    role: 'user',
    content: '[系统提醒] 你可能陷入了重复,请换一个思路。'
  })
}

具体是这样一个梯度(次数都是作者自定义的,没有一定的标准,看使用场景而论):

阈值级别动作
≥ 5 次warning注入提示,劝模型换思路
≥ 8 次critical直接熔断,停止循环

取舍点:为什么「警告」是往上下文塞一句提醒,而不是直接停?因为模型是有概率性的,偶尔重复一两次未必是真卡死,硬停会误伤正常的重试。所以先给它一个自我纠偏的机会——软约束先行,硬熔断兜底。这就是给无状态函数加「负反馈」的核心:它自己根本没有「我在重复」的意识,只能靠外部提醒。

顺带一提,这个熔断只是「循环级」的一道闸。再往外,还有两道硬闸兜底:最多跑 N 轮(超过就退),以及 token 预算(烧超了就强制停)。三层叠起来,才能保证 Agent 无论怎么使用,都不会真的失控。

结语

死循环检测,本质上是给 Agent 补上一块它天生缺失的东西——自我意识。模型不知道自己在重复,我们就用「指纹」替它记住历史,用「检测器」替它判断卡没卡,用「熔断」替它踩下刹车。