Orkas Orkas
首页 博客 架构
架构

循环检测不等于停滞检测:抓住那些不重复却在原地打转的 Agent

Orkas 有三层循环防线。它们都抓不住一个卡住的强模型,因为三层问的都是「你在不在重复做同一件事」——而卡住的模型恰恰不重复。这篇讲这个盲区、我们从 BEACON 借来的「输出侧」进展定义,以及在动手改之前我们打算先量什么。

这是长程 Agent 设计手记系列的第一篇,起因是我们精读了 BEACON(浙江大学,arXiv:2605.06078)。

要让一个 Agent 扛得住长任务,我们认为有两件事必须做好:

  • 它能持续往目标推进。这件事又拆成三块:上下文压缩(忘掉什么才是安全的)、里程碑设计(怎么知道一步真的完成了)、防止打转(卡住了要有人发现)。
  • 它能反思并持续改进。

这篇讲防止打转,因为这是我们代价最大的一个。

一句话版本 一个不重复但在原地打转的 Agent,照样需要被抓住 停滞检测就在 Orkas 里,所以一次长任务会告诉你它卡住了,而不是安静地把剩下的预算花完。
下载 Orkas — 免费

先说现象

我们收到过用户反馈,内部也发现过:一些超长任务跑到一半就不往前走了。

看日志,agent 一直在——读文件、搜索、跑命令,一刻没停。但半小时过去,什么都没推进。

我们最初归因于上下文丢失:压缩把「这条路已经试过」这条记录丢了,所以又去试一遍。把代码和日志对着看之后才发现,这只是一半。

我们本来就有防线,而且有三层

层级判据阈值
精确重复工具名 + 规范化参数,逐字节相同LOOP_WARN=3 警告 / LOOP_HARD=5 强制停止
近似重复除易变的 id、timestamp 字段外相同NEAR_DUP_LOOP_WARN=6 / NEAR_DUP_LOOP_HARD=12
自旋收敛压缩 ≥2 次 烧掉 ≥75% 工具轮次预算SPIN_CONVERGENCE_MIN_COMPACTIONS=2
SPIN_CONVERGENCE_TOOL_LOOP_RATIO=0.75

第三层的注释原文写着:Compound "may be spinning after context loss" signal。有人早就预见到了。所以问题从来不是没做,而是做出来的东西抓不到它。

盲区:三层全是「输入侧」检测

三层共用的签名是工具名 + 规范化参数。它只回答一个问题:你在不在重复做同一件事?

但一个卡住的强模型根本不会重复调用。它会这样走:

读文件 A → grep X → 换个行号范围再读 A
  → 跑个略有差别的命令 → 读文件 B → 回头再 grep …

每一次调用签名都不同,三层检测器全部静默。而这整段时间里,可验证的状态变化是 0:没有文件被真正改写,没有命令产出新结果,没有任何不可逆的事情发生。

循环检测不等于停滞检测。前者看输入,后者必须看输出。

论文给的正是这个输出侧定义

BEACON 的段内奖励:

r_t = R_ms · γ^(t_k − t)    若这一段以里程碑收尾
    = 0                     否则

一个没有以里程碑收尾的段,拿到的是精确的 0,不管它执行了多少个动作。动作数量根本不进入公式。这是我们见过的「忙 ≠ 有进展」最干净的形式化。

基线那一层更狠。基线取的是组内平均每步回报,所以一段走了 8 步而组内平均 5 步,整段的 advantage 一齐偏负,惩罚还随超出程度递增。打转不只是没奖励,是被主动惩罚,而且是比例性的。

论文 Figure 8 里有条失败轨迹,最后两个动作拿到完全相同的 −2.20。那个「最后一个里程碑之后再也够不到下一个」的尾段,就是打转的数学形态。而它很常见:完成了至少一个子目标但最终失败的轨迹,稳定占 39%~47%

一组消融数据否定了「按步数切」

切分方式成绩相对基线 72.8
随机切 5 段74.2+1.4
真实里程碑91.4+17.2

按任意步数切分几乎没有价值,按真实结构切才有价值。

回头看我们第三层的判据——消耗了 75% 的工具轮次预算——正是一个「任意步数」触发器。它问的是你烧了多少,不是你达成了什么。正确的形态应该是:

✗  if 步数 > N                     → 干预
✓  if 步数 > N 且 零验证里程碑      → 干预

后者不会误伤一个正常推进的长任务,因为这种任务每隔一段就会有里程碑。前者会。

还有一个正反馈环

把常数放在一起看:上下文到 82% 触发压缩;而自旋收敛要压缩满两次、再加烧掉 75% 轮次预算才响。

打转 → 上下文填满 → 触发压缩 → 持久状态被摘要掉
     → 重新推导丢失的东西 → 更多打转

自旋检测器是通过「观察到压缩发生了很多次」来判断的,而压缩正是造成失忆的那一步。它检测的是下游症状,还要等这个环转满两圈才响。

更麻烦的是,它的干预方式是提醒模型回锚到自己的持久状态。可如果持久状态恰恰就是被压缩掉的那部分,模型根本没有东西可以回锚。这是在 prompt 层修一个 state 层的问题。

我们要加的:输出侧停滞检测

补第四层,由两个计数器构成。两者都从宿主已经记录的工具观测里机械派生,都不需要模型判断。

距上一个已验证里程碑的步数。最简单的进展度量,配合上面那个复合判据使用,不单独用。

去重后的新增状态变化数。两者中更有价值的一个:

  • 读文件时内容 hash 与此前一致,就不算新信息——换个行号范围读同一个文件,hash 不会变。
  • 命令名、退出码、输出 hash 三者都与此前一致,不算新信息。
  • 写入后的 after-hash 等于 before-hash,说明根本没写进去。

这个计数器专抓签名匹配漏掉的那种情况:动作各不相同,信息增益为零。判据是机械的,不需要任何语义理解。

然后,压力应该是连续的,而不是一次性提醒:

把计数器摆进上下文,让模型看见
  → 强制修订计划(承认此路不通)
    → 问用户
      → 中止,但保留已达成的里程碑

最后一级很重要。如果这次运行注定要停,那也该带着已经挣到的成果停——这正是论文量到的那 39%~47% 被白白丢掉的部分进度。

论文帮不上的地方

BEACON 是训练方法。它让训练出的策略不容易游走,但它本身没有运行时的检测和干预机制。它提供的是「进展」的定义,不是控制器。阈值怎么定、阶梯怎么升、什么时候中止,都得我们自己设计。

它的 per-step 基线还需要「组内平均段长」作参照。而生产环境里,同一个用户任务通常只跑一次,没有这个 group。能用的替代只有历史同类任务的统计,噪声大得多,也完全没有论文里那条方差隔离的保证。

第一步是量,不是改

在动任何决策逻辑之前,我们想先埋点,回答一个现在答不上来的问题:线上发生的那些停滞,有多少是失忆型,有多少是无梯度型?

  • 多数伴随信息增益归零、但调用签名各不相同 → 无梯度型。现有三层结构上就抓不到,补输出侧检测才是解法。
  • 多数伴随重读已被压缩掉的内容 → 失忆型。该修的是压缩时保留什么。

这两个结论对应的投入完全不同。先量比先设计划算,而且埋点几乎是免费的——那些观测数据本来就存在。

上面所有内容都压在一个概念上,而这篇一直在用它却没定义过:已验证的里程碑。下一篇就讲这个。为什么「模型说这步做完了」不构成它真的做完了的证据、产品里其实已经有的两半为什么一直没接上,以及一条论文里没有的判据:看不可逆,别看重要。