三维度 × 四分类——CycleDetector 的完整检测算法

2026-10-06·终止与自愈·循环检测

循环'不是一种模式------至少是三种。完全相同的调用(同一个 grep 重复 7 次)、工具模式重复(read→edit→exec 交替 50 次)、状态回退(做了一圈操作回到起点)。这篇详解 CycleDetector 的完整算法------三个检测维度、工具四级分类、参数变化减半计数、以及 per-tool 阈值覆盖。

为什么不能只检测"完全相同的调用"

"Agent 陷入死循环怎么办"一章讲了为什么 max_turns 不是答案------正确的终止判据是"有没有在循环"。但"循环"本身就是一个模糊的概念。

考虑这个场景:

bash 复制代码
轮次 1:  read("auth.go")
轮次 2:  edit("auth.go", patch_v1)
轮次 3:  exec("go build")          → 编译错误 A
轮次 4:  read("auth.go")
轮次 5:  edit("auth.go", patch_v2)
轮次 6:  exec("go build")          → 编译错误 B
轮次 7:  read("auth.go")
轮次 8:  edit("auth.go", patch_v3)
轮次 9:  exec("go build")          → 编译错误 C
...重复 10 次

没有一次调用和前一次"完全相同"------每次的 patch 不同、每次的编译错误不同。Identical Call 检测器不会触发。

但从全局看,Agent 在 read → edit → exec 的模式中循环了 10 次。每次都改 auth.go,每次编译都报不同的错------它在"打地鼠":修了错误 A 引入了错误 B,修了 B 引入了 C,修了 C 又引出了 A。

这是循环吗? 答案取决于"10 次"是否超出了合理范围。对于一个简单函数------5 次迭代应该够了;对于一个涉及 15 个文件的大型重构------30 次迭代可能是正常的。

"循环"不是一种模式------是多种模式的统称,每种需要不同的检测策略和不同的阈值。


三个检测维度

CycleDetector 同时维护三个独立的 streak 计数器------每个维度捕捉一种不同的循环形态。

维度一:Identical Call Streak------完全相同的调用

最确定的循环信号:工具名 + 参数 + 返回结果完全相同。

检测方式:对 (工具名, 参数JSON) 计算 hash,对返回结果计算 hash。两个 hash 与上一次都相同 → streak +1;任一不同 → streak 重置为 1。

bash 复制代码
grep("TODO", "src/")  → hash(params)=a1b2, hash(result)=c3d4  streak=1
grep("TODO", "src/")  → hash(params)=a1b2, hash(result)=c3d4  streak=2 ✓
grep("TODO", "src/")  → hash(params)=a1b2, hash(result)=c3d4  streak=3 ✓
grep("TODO", "src/")  → hash(params)=a1b2, hash(result)=c3d4  streak=4 → Warn
grep("TODO", "src/")  → hash(params)=a1b2, hash(result)=c3d4  streak=5 → Term

为什么阈值这么低(默认 Warn=3, Term=5): 完全相同的调用产出完全相同的结果------第二次就已经没有信息增益了。连续做 5 次完全一样的事情,几乎不可能是有意义的行为。

为什么要同时检查参数和结果: 只检查参数会误判"合法的轮询"------比如 exec("git status") 每次参数相同但结果可能不同(代码在被修改)。同时检查结果 hash 确保"不仅做了同样的事,还得到了同样的答案"------这才是真正的死循环。

维度二:Tool Category Streak------工具类别连续计数

检测同一类工具(exploration 或 action)被连续调用的次数。

所有工具被分为两大类:

  • Exploration(探索类) :read、grep、search、find 等------不改变系统状态
  • Action(操作类) :edit、write、exec、delete 等------改变系统状态

当同一类工具被连续调用时 streak +1;切换类别时 streak 重置。

bash 复制代码
read("a.go")     → exploration streak=1
read("b.go")     → exploration streak=2
grep("func")     → exploration streak=3
read("c.go")     → exploration streak=4
...连续读了 30 个文件
read("z.go")     → exploration streak=30 → Warn(默认 30)
...继续读
read("zz.go")    → exploration streak=50 → Term(默认 50)

为什么探索类阈值更高(默认 30/50 vs 操作类 15/25): 连续读 30 个文件可能是正常的------比如 Agent 在理解一个大型代码库的架构。但连续执行 15 次写操作而中间没有任何读取或验证------这更可能是在盲目修改。

为什么操作类阈值更低: 操作会改变系统状态。错误的操作需要被撤销。连续操作意味着连续的状态变更------每一次变更都增加了"如果出错需要回滚"的成本。

维度三:Per-Tool Overrides------特定工具独立阈值

某些工具的合理重复次数和通用阈值差距很大。Per-tool override 允许为特定工具设置独立的 Warn/Term 值:

工具 默认 Warn 默认 Term 为什么
exec 15 30 编译→测试循环天然需要重复 exec
exec:readonly 25 40 只读 exec(如 grep、cat)风险更低
edit 10 20 编辑同一文件多次是正常的重构模式
write 10 20 写新文件的重复通常意味着重复创建
apply_patch 10 20 类似 edit
plan 8 15 反复规划意味着决策不清------不是在执行

override 优先于通用阈值 ------exec 被归类为 action(通用阈值 15/25),但 override 把它调高到 15/30。这反映了 exec 在编程场景中的特殊性------"改代码 → 编译 → 修错 → 再编译"是正常的工作模式。


工具四级分类 Fallback

CycleDetector 需要知道一个工具是 exploration 还是 action------这决定了用哪套阈值。分类通过四级 fallback 确定:

第一级:硬编码 map(~20 个内置工具)

引擎内置的标准工具有确定的分类:

工具 分类 理由
read / grep / search_files exploration 不改变状态
edit / write / apply_patch action 改变文件
exec action 可能改变状态
exec:readonly exploration 只读命令
plan exploration 规划不产生副作用
memory exploration 读写记忆不算系统状态变更
delegate_task action 启动子 Agent 是重量级操作

第二级:ExtraCategories(消费方声明)

消费方可以在配置中为自定义工具声明分类:

bash 复制代码
CycleDetectConfig{
  ExtraCategories: {
    "search_database":   CategoryExploration,
    "send_notification": CategoryAction,
    "list_pages":        CategoryExploration,
    "write_block":       CategoryAction,
  }
}

这比让框架猜更可靠------只有消费方知道 search_database 是只读的还是有副作用的。

第三级:ToolMeta 自动推断

如果工具没有在前两级被分类,检查工具自身声明的元数据:

  • ReadOnly=true → exploration
  • Dimension="perceive" → exploration
  • 其他 → 进入第四级

第四级:默认 action

未分类的工具默认为 action(更严格的阈值)。 这是一个 fail-safe 设计------不确定时选择更安全的选项。一个被误分为 exploration 的 action 工具可以在高阈值下跑很多次才触发;被误分为 action 的 exploration 工具只是更快触发(产出一条纠正消息),但不会导致真正的损失。

错误分类的代价是不对称的 ------把 action 当 exploration(阈值过高)可能让危险操作多跑 30 次;把 exploration 当 action(阈值过低)只是早一点触发自愈纠正。不确定时选阈值低的那个。


Halving:区分"盲目重复"和"在尝试不同方法"

CycleDetector 最精巧的机制是 halving ------当工具被标记为 ProgressAware(进度感知)时,参数发生变化会使 streak 计数减半。

为什么需要 halving

不加 halving 的问题:

bash 复制代码
edit("auth.go", patch_v1)  → streak=1
edit("auth.go", patch_v2)  → streak=2   ← 参数不同!模型在尝试新方案
edit("auth.go", patch_v3)  → streak=3   ← 又不同!还在尝试
...
edit("auth.go", patch_v20) → streak=20  → Term ← 但每次都是不同的修改!

模型在 auth.go 上尝试了 20 种不同的修改方案------这不是"盲目重复",是迭代改进。但纯计数把它当成了和"同一个 patch 重复 20 次"一样的事。

Halving 怎么工作

对 ProgressAware 工具------参数变化时 streak /= 2(最小值为 1):

bash 复制代码
edit("auth.go", patch_v1)  → streak=1
edit("auth.go", patch_v2)  → streak=1   ← 参数变了,1/2=0.5→1
edit("auth.go", patch_v2)  → streak=2   ← 完全相同
edit("auth.go", patch_v3)  → streak=1   ← 参数变了,2/2=1
edit("auth.go", patch_v3)  → streak=2   ← 完全相同
edit("auth.go", patch_v3)  → streak=3   ← 完全相同
edit("auth.go", patch_v4)  → streak=2   ← 参数变了,3/2=1.5→2

效果:如果模型每次都尝试不同的参数------streak 几乎不增长,不会触发阈值。如果模型在某些参数上重复------streak 增长,最终触发。

这区分了两种行为:

行为模式 streak 增长速度 触发可能性
每次不同参数(迭代改进) 极慢(每次 /2) 极低
同一参数重复 2-3 次再换(部分重复) 中等 中等
完全相同的参数反复重复(死循环) 快(线性增长) 高

哪些工具是 ProgressAware

默认的 ProgressAware 工具是会改变系统状态的写操作------edit、write、apply_patch。因为对这些工具来说"参数在变"是最重要的进展信号------不同的 patch 意味着不同的修改尝试。

消费方可以通过 ExtraProgressAware 声明额外的进度感知工具。


完整的检测流程

把三个维度、四级分类、halving 组合在一起:

bash 复制代码
每次工具调用完成后:

1. 确定工具分类
   ├─ 硬编码 map → 找到分类
   ├─ ExtraCategories → 找到分类
   ├─ ToolMeta (ReadOnly/Dimension) → 推断分类
   └─ 默认 action

2. 更新 Identical Call Streak
   ├─ hash(工具名+参数) == 上次 && hash(结果) == 上次
   │   → identical_streak++
   └─ 不同 → identical_streak = 1

3. 更新 Category Streak
   ├─ 分类 == 上一次分类
   │   ├─ ProgressAware && 参数变化
   │   │   → category_streak /= 2 (min 1)
   │   └─ 否则
   │       → category_streak++
   └─ 分类不同 → category_streak = 1

4. 查找阈值
   ├─ ToolOverrides 有该工具 → 用 override 阈值
   └─ 否则 → 用分类的通用阈值

5. 判断信号
   ├─ identical_streak >= IdenticalCallTermThreshold → CycleTerm
   ├─ category_streak >= CategoryTermThreshold → CycleTerm
   ├─ identical_streak >= IdenticalCallWarnThreshold → CycleWarn
   ├─ category_streak >= CategoryWarnThreshold → CycleWarn
   └─ 未达任何阈值 → 无信号

6. CycleTerm 进入两阶段协议
   ├─ 首次(该维度未自愈过)→ 自愈(注入纠正消息 + 重置 counter)
   └─ 二次(该维度已自愈过)→ HITL → 超时终止

关键:三个维度独立运行。 Identical Call 触发自愈不影响 Category Streak 的状态------两者互不干扰。一个维度的自愈机会不被另一个维度消耗。


编程场景的阈值调优

不同的 Agent 产品场景需要不同的阈值配置。编程场景是最需要精细调优的------因为"改代码→编译→修错→再编译"是天然的重复模式。

截至本文写作时几种编程 Agent 的阈值范围(包括实际产品和我们的配置):

维度 通用默认值 编程场景推荐值 理由
Exploration Warn/Term 30 / 50 50 / 100 大代码库分析 40+ 文件很正常
Action Warn/Term 15 / 25 25 / 50 大重构 edit 同一文件 20+ 次正常
Identical Call Warn/Term 3 / 5 4 / 7 保持低------完全相同的调用极少有意义
exec override 15 / 30 30 / 60 编译-测试循环
edit override 10 / 20 30 / 60 同一文件反复编辑

委派子 Run 可以使用更严格的阈值------子 Agent 的任务范围通常更窄,合理的迭代次数更少。


你现在能做什么

1. 至少实现 Identical Call 检测。 对 (工具名, 参数JSON) 做 hash 比较,连续 N 次相同 → 终止或告警。N=5 是合理起点。实现量 ~20 行代码。

2. 区分探索类和操作类工具。 读文件/搜索 = 探索(高阈值),写文件/执行命令 = 操作(低阈值)。不确定的默认为操作。

3. 写操作加 halving。 对 edit/write/patch 类工具------参数变化时 streak /= 2。这一个机制就能消除大多数"正常迭代被误判为循环"的误报。

4. 给高频工具设独立阈值。 exec(编译测试)和 edit(代码修改)的合理重复次数远高于通用阈值。用 per-tool override 而非提高全局阈值。

5. 监控 Warn 但不一定 Term。 Warn 阈值触发时注入一条提示给模型------"你可能在重复操作"。很多时候 Warn 就够了------模型收到提示后自行换策略。Term 是最后手段。


引用来源

本文所有数据均可通过以下链接验证。

相关推荐
小七-七牛开发者1 小时前
Agent 支线知识:Personal Agent 如何理解个人意图?
ai·agent·ai智能体·长期记忆·personalagent
熊猫钓鱼>_>1 小时前
鸿蒙PC构建实战:hvigor modelVersion 降级 + schema 清理 + ArkTS 编译三连坑完整复盘
开发语言·华为·ai编程·harmonyos·openharmony·数据·ai生成
骇客野人1 小时前
从大模型对话聊天都人工智能智能体干活到如今的AI编程的思考以及未来发展趋势展望
人工智能·ai编程
小虎AI生活2 小时前
59.3k Stars 的 diagram-design 实战:接入 WorkBuddy,让 AI 按 4px 网格自动出架构图
aigc·ai编程
jerrywus3 小时前
Skill不就是prompt吗?我拿它做了一段南京新街口微缩景观动画
chatgpt·agent·claude
枫叶丹43 小时前
AI 提出科学假设之后:从数据筛选到实验验证
人工智能·chatgpt·开源·agent·codex
深蓝AI3 小时前
Epic 摊牌了:Claude 和 Codex 在写 UE6 的代码,十年没修掉的 bug 被 AI 扫光
程序员·ai编程
阿里云云原生3 小时前
Qoder × AgentLoop:智能体规模化应用,敏感数据保护如何跟上?
agent
咚咚咚小柒3 小时前
【前端】LangChain.js学习
前端·javascript·人工智能·typescript·langchain·vue·ai编程