AI agent 在循环、不进展怎么办

Agent 一直试同一个修法,或在两种状态之间反复——快速打断的方法。

典型的”agent 死循环”长这样:Claude Code 或 Cursor agent 跑了 12 轮 npm test,每轮都改同一处 expect(...).toBe(...)、跑、失败、再改回去;或者 Aider 在 git diffgit restore 之间来回横跳。屏幕上 token 一直在烧,进度条没动。这种循环 95% 不是模型”不会修”,而是它陷入了一个反馈坏掉的小闭环——测试 flaky、约束不够、或它读到的上下文和它要改的代码不一致。这篇讲怎么 30 秒内识别循环、怎么用一句话打破它,以及怎么从一开始就预防。

常见原因

按命中率从高到低排序。

1. 它一直跑的测试本身是 flaky

最常见的循环触发器:测试里有时间戳、随机数、网络请求或 Date.now(),导致同样的代码每次跑结果不一样。Agent 看到红,改一处;变绿;下一轮又红——它以为是自己改坏了,于是回滚——又绿——再改——又红。

Run 1: ✓ pass
Run 2: ✗ fail (snapshot mismatch — timestamp drift)
Run 3: ✓ pass (after agent "reverted")
Run 4: ✗ fail

如何判断:把 agent 停下来,你自己手动连跑 3 次同一条命令。如果不动代码也能复现”绿-红-绿”,问题就在测试,不在 agent。

2. Prompt 有歧义,agent 在两种解读之间反复横跳

你说”修复登录 bug”,但代码里有 OAuth 和 magic link 两条路径。Agent 改完 OAuth 看到 magic link 测试挂,转头改 magic link,OAuth 又挂——它在两个互斥目标之间反复横跳。

如何判断:看 agent 最近 5 步动作的 diff,如果改的是两个不同文件的不同函数,并且每次改完都”撤销上一次”,就是 prompt 太宽。

3. 计划写错了但 agent 不肯重新规划

Agent 一开始写了个”先改 schema,再改 API,最后改 UI”的计划。Schema 那步其实走不通(比如数据库已经有数据),但 agent 死守计划,只在 schema 这一步反复改 migration 文件,不肯回头重新规划。

如何判断:让 agent 输出当前 plan + 现在卡在第几步。如果它说”还在 step 2”超过 5 轮,就该手动让它重新规划。

4. 上下文窗口被旧错误塞满,agent 看不到全局

Claude Code 或 Codex 在长会话里,上下文里前 80% 都是”上一次失败”的 log,agent 一直在拟合那些旧错误,看不到当前代码已经变了。

如何判断:开个新会话,把当前代码贴进去,重新描述目标。如果新会话一次就改对了,就是上下文污染。

5. Agent 在 mock 和真实实现之间打转

它写了一个 mock 让测试过,下一轮发现 mock 不真实,去改真实实现,结果原来的测试又挂,于是回去改 mock——循环。

如何判断:搜 diff 里有没有 jest.mockvi.mockMagicMocksinon.stub 反复出现。

6. 工具调用失败但 agent 没意识到

Bash 工具返回了非零 exit code,但 agent 把它当 stdout 解析,以为命令”成功了只是输出不对”,于是再跑一遍。

如何判断:看最近 3 次工具调用的 exit code,如果都是非 0 但 agent 没在 chat 里提”命令失败了”,就是它没识别到失败。

最短修复路径

按收益排序。前两步通常 60 秒内就能打破循环。

Step 1:立刻停下,回看最近 10 步动作

按 Esc(Claude Code)或点 Stop(Cursor / Codex),不要让它继续烧 token。然后在聊天里要它输出最近的动作总结:

列出你最近 10 步做了什么操作(文件路径 + 改动摘要 + 命令 + 结果)。
不要解释,只要表格。

90% 情况下你会立刻看到它在两个文件之间来回切,或在同一行反复改。

Step 2:用一句话约束打破循环

最有效的反循环 prompt 模板:

停。不要再改 X。
当前真实情况是 Y。
下一步只做一件事:Z。
不要碰其他文件。

具体例子:

循环类型约束 prompt
反复改测试期望值”不要再动测试。测试是对的,去改实现让它符合测试。“
反复加 / 删 mock”保留真实实现。把 mock 全删了,跑集成测试看真实失败信息。“
反复改 schema migration”schema 不动。回到 step 1,从只改 UI 层开始。“
在两个文件之间横跳”只允许编辑 src/auth/login.ts,其他文件全部只读。“

Step 3:把测试或验证条件钉死

如果 agent 一直在追一个 flaky 测试,先把测试稳定下来再让它继续:

# 连跑 5 次同一个测试,看是否稳定
for i in {1..5}; do npm test -- --testNamePattern="login flow"; done

如果有 2 次失败,先 mock 时间 / 网络 / 随机源:

// vitest setup
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-01-01'));

测试稳定后再让 agent 继续修。

Step 4:开新会话,喂干净上下文

如果以上都不行,最后一招是 reset 上下文:

  1. 复制当前文件的完整内容(不是 diff)。
  2. 开新会话。
  3. 用这个模板重新启动:
目标:[一句话目标]
当前代码(已经改过 N 轮):
[贴完整文件]
当前失败的测试 / 错误:
[贴完整错误]
约束:不要改测试,不要改配置,只允许改 src/X.ts。
先输出你的计划,等我批准再动手。

新会话不会被旧错误污染,往往一次就过。

Step 5:设硬性 iteration cap

让循环不要再发生。Claude Code 可以在 CLAUDE.md 里加:

## Agent 行为约束
- 单个任务最多 5 轮 build/test 迭代
- 第 3 轮还没过,主动停下输出现状让用户决策
- 不允许在同一个文件的同一段代码上反复改 ≥ 3 次

Cursor 用 .cursorrules、Aider 用 .aiderrules、Codex 用 AGENTS.md,写法同理。

预防建议

  • 在 CLAUDE.md / AGENTS.md / .cursorrules 里设 iteration cap,比如”同一个测试连挂 3 次就停下问人”
  • 不只看 agent 最终输出,要看它的 reasoning / action trace,循环模式往往要看 trace 才发现
  • 把 flaky 测试隔离到单独的 flaky.test.ts,别让 agent 在不稳定的反馈下迭代
  • 长任务每 5 轮强制 agent 重新输出”当前 plan + 当前进展”,避免它失忆
  • 给 agent 明确的”完成标准”——不要说”修好登录”,要说”npm test -- auth 全过”
  • 出现循环就开新会话重启,不要试图在污染的上下文里继续

相关阅读

标签: #AI 编程 #排查 #排查