爆火的 Loop Engineering,三个文件让 Claude Code 循环验证代码直到全绿
爆火的 Loop Engineering,三个文件让 Claude Code 循环验证代码直到全绿
JeecgBoot AI专题研究 | 从单次交付到闭环验证的 AI 编程新范式探索
单次交付的根本缺陷
用 AI 写代码已经是很多开发者的日常,但一个隐藏的问题始终没有被解决:验证这件事仍然压在人身上。
你描述需求,AI 生成代码,你肉眼扫一遍觉得没问题——于是收工。问题是,肉眼扫不到的地方太多了。代码能运行,不代表测试全过;测试全过,不代表类型推断没有隐患;类型没错,不代表格式规范符合团队标准。更常见的场景是:你发现 AI 写的代码有问题,贴回去让它改,改完引入新问题,来回几轮之后你已经不确定是在收敛还是在扩散。
这不是 AI 不够聪明,而是工作流设计本身就缺了一个环节:自动化验证的闭环。
Loop Engineering 的核心思路就一句话:把写代码和验证代码分拆给两个专职 Agent,用一个编排器循环调度,查到全绿才停。验证从"你的工作"变成"系统的工作"。

核心设计:硬隔离而非软约束
Loop Engineering 的实现只需要三个文件加一个配置,但在动手之前必须理解一个关键设计决策:执行和验证为什么必须是两个 Agent,而不是同一个 Agent 的两步操作?
原因出乎意料地简单:写代码的 Agent 往往高估自己的输出质量。让同一个 Agent 写完代码再自查,它有很大概率觉得没问题——因为它刚刚写的,思路还热着。两个专职 Agent 的分工从根本上消除了这个偏差:builder 只负责写和改,checker 只负责跑检查和报告,双方没有利益冲突。
更重要的是,这种隔离不是靠提示词约束实现的,而是通过 tools 字段的差异做到工具层面的硬隔离:

builder 的 tools 包含 Write 和 Edit,能修改代码;checker 的 tools 只有 Read、Grep、Glob、Bash,从工具可见性层面保证了它物理上无法修改任何文件。就算 checker 的提示词里没写"不能改代码",它也改不了——因为工具根本就没给它。
三个文件搭建 Loop Engineering 完整闭环
整套系统的所有配置放在项目根目录的 .claude/ 子目录下,三个文件各司其职。
文件一:builder.md(只写代码)
放在 .claude/agents/builder.md:
---
name: builder
description: 负责编写和修复代码。用于实现任务或修复 checker 发现的失败。
tools: Read, Write, Edit, Glob, Grep, Bash
model: sonnet
---
你只负责构建和修复,不做其他任何事情。
## 接到任务时
1. 先读项目的 AGENTS.md、README、package.json(或等效配置文件),
理解架构分层和编码约定。不了解项目约定就动手,白跑的循环比读文档
花的时间多得多。
2. 确认任务涉及的文件范围。如果需要跨层修改,先想清楚依赖方向是否允许。
3. 写一行任务简报:目标、涉及文件、完成标准。然后开始实现。
## 接到修复请求时
1. 逐条阅读 checker 报告的失败项,每条失败都要读到 file:line。
2. 定位根因。区分症状和病因:测试失败是症状,代码逻辑错误是病因。
修病因,不要修症状。
3. 一次只修一个根因。如果 checker 报了 3 个失败,但它们可能是同一个
根因引起的,先修最可能的那个,跑一遍检查看是否连带解决其他的。
4. 不要顺手重构不相关的代码。循环验证的场景下,每一行多余改动都可能
引入新问题,让下一轮 checker 报出意料之外的失败。
## 红线
- 绝不弱化测试来让它通过。修代码,不是修测试。
- 绝不通过删除、注释、跳过失败的检查来达到通过。
- 绝不在没有跑过检查的情况下声称已修复。
## 汇报格式
修改完成后,先本地跑一遍 checker 会执行的命令,确认通过再汇报。
汇报格式:
改了什么:<一句话>
修改文件:<file1>, <file2>, ...
本地检查结果:<通过/失败>
文件二:checker.md(只跑检查)
放在 .claude/agents/checker.md:
---
name: checker
description: 运行所有检查并报告失败项。在 builder 之后调用。绝不修改代码。
tools: Read, Grep, Glob, Bash
model: sonnet
---
你只检查,绝不修复。
## 发现检查命令
不要假设检查命令。先读 package.json 的 scripts 字段(或等效配置),
找出项目实际使用的检查命令。常见模式:
- test: `npm test` / `pnpm test` / `vitest run`
- lint: `eslint .` / `oxlint .` / `biome check`
- 类型: `tsc --noEmit` / `vue-tsc --noEmit`
- 格式: `prettier --check` / `format:check`
如果项目有聚合检查命令(如 `pnpm check` = test + lint + tsc + format),
优先跑聚合命令,它能一次性覆盖所有检查项。
如果项目有额外检查(依赖守卫、deadcode 检测、安全扫描等),也要跑。
这些检查往往能抓到测试和 lint 抓不到的问题。
## 执行
按顺序运行所有检查命令。每项检查的完整输出都要保留,不要只保留最后
一行的 pass/fail。失败的检查往往需要看中间输出才能定位根因。
## 报告格式
- 全部通过:输出 "ALL GREEN",然后逐项列出每项检查的名称和通过证明
(如 "test: 848 passed, 0 failed")。不要只说全过了。
- 任何失败:输出 "FAILED",然后逐条列出:
`file:line - 什么坏了 - 哪个检查抓到的`
如果同一文件有多个失败,合并列出。如果多个失败可能是同一根因,
标注疑似同源。
## 红线
- 绝不意译失败信息。复制真实错误输出的关键行。builder 要根据你的报告
来修复,模糊的报告会浪费整整一轮循环。
- 绝不因为看起来是小问题而省略失败项。你没修过的问题,builder 也不知道。
- 绝不自己尝试修复。你只负责报告,修复是 builder 的事。
文件三:循环编排器(/loop 命令)
放在 .claude/commands/loop.md,自动注册为斜杠命令 /loop:
---
description: 循环运行 builder 和 checker,直到所有检查通过
argument-hint: <task>
allowed-tools: Read, Grep, Glob, Bash, Task
model: sonnet
---
以循环方式执行此任务:$ARGUMENTS
## 第 0 步:对齐目标
写一行任务简报:目标、涉及文件、完成标准。
这一行会传给 builder 和 checker,确保三者对齐。
## 循环
1. 派 builder 实现任务(或修复上一轮的失败)。
2. 派 checker 运行所有检查。
3. 如果 checker 说 ALL GREEN:停止,向我展示 diff 和检查结果。
4. 如果 checker 说 FAILED:把 checker 的完整失败报告原样转发给 builder,
不要自己解读或过滤。builder 需要原始错误信息来定位根因。
5. 回到第 1 步。
## 轮次管理
- 最多 5 轮。每轮开始时公开声明 "Cycle N/5"。
- 如果同一失败连续出现两次,停止循环。builder 可能在瞎猜,
不是在修复。把情况报告给我。
- 如果修复导致之前通过的检查失败,停止循环。在拆东墙补西墙。
停止条件在 CLAUDE.md 中。严格遵循。
编排器有一条隐性关键原则值得单独强调:失败报告必须原样转发,禁止二次解读。编排器的天然倾向是帮忙总结,但总结会丢失行号、堆栈轨迹、中间输出——而这些恰恰是 builder 定位根因必需的信息。一次"好心总结"可能让整轮循环白跑。
停止规则写进 CLAUDE.md
把以下内容追加进项目根目录的 CLAUDE.md,让三个 Agent 都能看到刹车条件:
## Loop stop rules
### 停止条件
循环在以下任一条件成立时停止:
1. ALL GREEN:所有检查通过。停止,附上每项检查的通过证明。
2. 轮次用尽:达到 5 轮上限。停止,报告仍失败的项、每轮尝试了什么、为什么没成功。
3. 同一失败连续两轮:builder 在猜,不是在修。停止,升级给我。
4. 回归:修复导致之前通过的检查失败。停止,说明改了什么导致了回归。
5. 无实质进展:连续 2 轮失败项数量没有减少。停止,可能任务范围过大,
需要拆分成更小的子任务。
6. 疑似超出能力边界:builder 反复尝试但失败原因涉及它无法访问的外部依赖
或环境问题。停止,报告阻塞点。
### 红线
- 永远不在没有 checker 输出的情况下报告成功。
- 永远不弱化、删除、跳过检查来达到 ALL GREEN。
- 永远不修改 checker 的工具白名单。
### 升级协议
停止并升级给我时,必须携带以下信息:
- 当前轮次(Cycle N/5)
- 仍失败的项列表
- 每项已尝试过的修复方法
- 你的判断:为什么继续循环不会解决问题
六条停止条件覆盖了实战中最常见的卡死模式:通过了(停)、跑完了(停)、在猜(停)、回归了(停)、没进展(停)、超出边界(停)。升级协议要求编排器在刹车时必须携带结构化的诊断报告,你拿到之后直接决策,不需要自己翻历史。
接入 step-3.7-flash:让循环跑起来不卡
三个文件就位后,还需要一个支撑循环高频运转的模型。修改 ~/.claude/settings.json:
{
"env": {
"ANTHROPIC_AUTH_TOKEN": "你的 StepFun API Key",
"ANTHROPIC_BASE_URL": "https://api.stepfun.com/step_plan",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "step-3.7-flash",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "step-3.7-flash"
},
"model": "sonnet"
}

API Key 在阶跃星辰开放平台(platform.stepfun.com)申请。把 sonnet 和 haiku 两个别名都映射到 step-3.7-flash,整套 Loop Engineering 的三个 Agent 就都会走同一个模型。
为什么选 step-3.7-flash? Loop Engineering 对模型有一个非常规的要求:在短时间内被高频调用,每次都要快速给出准确判断。这和"一次对话解决所有问题"的单轮场景完全不同。
step-3.7-flash 在循环调度场景下有三个具体优势:
- 400 TPS 推理速度:每轮等待时间极短,5 轮循环下来模型推理总耗时可能不到喝一口水的时间。换 50-100 TPS 的模型,同样 5 轮多出好几分钟纯推理等待——等久了人就会忍不住手动干预,而手动干预恰恰破坏了 Loop Engineering 的意义。
- 可配置推理强度:支持 low/medium/high 三档推理强度,定位代码错误用 medium 档就够,不需要每轮都拉满。
- Step Plan 订阅制:循环模式下 token 消耗量远超单轮对话,按量计费很容易失控。Flash Pro 档位每月 8000M Credit,高频调用场景基本覆盖。
真实战场:给 Step-Realtime-CLI 贡献四个 PR
理论够了,看看实际跑起来是什么感觉。以给阶跃星辰开源项目 Step-Realtime-CLI 提 PR 为例,全程使用 Claude Code + step-3.7-flash 的 loop 模式,连续处理了四个方向的问题,这个项目的 pnpm check 命令串联了六项检查(test + lint + dep-guard + deadcode + tsc + format:check),天然充当了 checker 的检查集。
问题一:WSL 环境下的 Bun 路径污染
clone 项目跑 setup.sh,WSL 里直接报错——command -v bun 找到了 PATH 里 Windows 版本的 Bun(路径在 /mnt/c/...),用它构建出来的二进制在 Linux 里根本跑不了。
/loop 修复 setup.sh 在 WSL 下误用 Windows Bun 的问题
builder 写了 resolve_bun() 函数:优先读 STEP_BUN_BIN 环境变量,检测到 /mnt/* 路径时警告并跳过,回退到 $HOME/.bun/bin/bun 等 Linux 原生路径。checker 跑 pnpm check,一轮全绿,提了 PR #37。
问题二:启动时崩溃——空 chunk 与缺失构建
step-cli error: OpenTUI runtime did not export createLocalTuiClientApp()
builder 读 tsdown.config.ts 发现两个问题:rolldown 把 local-tui-app.ts tree-shake 掉了导致产出 chunk 为空;同时 agent-sdk 包根本就没有构建步骤,dist/index.js 不存在。补上 preserve entry 配置和 agent-sdk 构建步骤,checker 一轮通过,提了 PR #28。
问题三:TUI 刷屏——系统消息泄漏到展示层
TUI 转录面板持续出现 “Delegation reminder” 这类系统消息,这是插件通过 beforeModelRequest 注入的内部消息,不应该呈现给用户。
/loop 修复插件注入的 system message 泄漏到 TUI 转录面板的问题
builder 给 SystemMessage 加了 hidden?: boolean 标志,TUI 渲染器和剪贴板导出跳过带 hidden 标记的消息,涉及 12 个文件。第一轮 checker 发现 deadcode 检测报了一个未使用的导出,builder 修掉,第二轮全绿,提了 PR #32。
问题四:格式检查不过
准备提 PR 时 pnpm check 的 format:check 报格式问题。这种 Prettier 格式修复不需要 loop,builder 直接跑 prettier --write,checker 确认通过,提了 PR #38。
四个 PR 总共跑了约 6 轮循环(PR #32 跑了 2 轮,其余各 1 轮)。整个过程中人工介入的唯一一次:在 PR #32 涉及 12 个文件时,确认了一下改动范围是否在预期之内。失败发现、根因定位、修复实现、验证通过——全是 loop 自己完成的。
踩坑录:三个会让你多跑轮次的细节
坑一:builder 有顺手重构的冲动。 跑 PR #32 时,builder 修完 system message 泄漏,顺手调整了不相关的类型导出,结果下一轮 checker 报出 deadcode 警告,白跑一轮。提示词里明确写了"不要重构不相关的代码",但 Agent 读完整个代码库之后总是忍不住——这也是停止规则里"回归"刹车条件存在的原因,在 builder 拆东墙补西墙时及时强制停止。
坑二:checker 的报告精度决定循环效率。 有一次 checker 只贴了最后一行错误信息,没带上下文。builder 找不到根因,瞎猜了修法;第二轮 checker 同样只贴最后一行,又浪费一轮。在 checker 提示词里加上"保留每项检查的完整输出"后才解决。checker 的报告是 builder 唯一的信息来源,报告模糊一轮,整个循环就白跑一轮。
坑三:编排器会自发"压缩"失败信息。 编排器拿到 checker 的报告后,有一种天然的倾向是先自己理解一遍再转述给 builder——转述过程中行号丢了、堆栈轨迹没了,只剩一句话的概括。builder 拿到的是二手解读而不是原始错误,定位效率大幅下降。提示词里必须明确写"不要解读或过滤,原样转发",否则编排器觉得自己在帮忙,实际上在制造信息损耗。
总结
Loop Engineering 本质上是在工作流设计层面解决了一个长期存在的问题:AI 写代码的过程中,验证从来不是内置的。
搭建完成后,你和 Agent 的协作方式发生了根本改变。以前:描述需求 → 等结果 → 人工验证 → 反馈 → 再等结果,验证是你的工作。现在:输入 /loop <任务> → 等循环结束 → 审 diff → 决定要不要提 PR,验证是系统的工作。
你从质检员变回了需求方。
本文为 JeecgBoot AI 专题研究系列文章。
更多推荐


所有评论(0)