Loop Engineering 实战:用 Claude Code 工程化一个会自己验收、会自己停的 AI 循环
原文首发于公众号「开发者效率局」,欢迎关注获取更多技术干货。
2026 年 6 月,Addy Osmani、Peter Steinberger、Anthropic 的 Boris Cherny 几乎前后脚把同一个观点推上热搜:别再一句句给 Coding Agent 喂提示词了,去设计「喂提示词给 Agent 的那个系统」。 这就是 Loop Engineering------把「人肉 while 循环」替换成真正的 while 循环。
这篇讲三件事:一个循环由哪五个零件组成、四种循环各自该用在哪、以及为什么真正的分水岭是那个没人愿意写的 Verifier。最后用 Claude Code 从头工程化一个「夜里自己修小 bug」的循环。
一个循环的解剖:五件套
任何循环拆开都是同样五个零件:
- Goal ------一个可以被程序判定 的「完成」定义。「优化得更好」不是 Goal,「
npm test全绿且tsc --noEmit无报错」才是。 - Prompter------生成并发出「下一条指令」。
- Reader------捕获并解析这一轮输出(自动化场景让它吐 JSON,用 jq 读)。
- Verifier ------独立检查达没达标。最容易做错、最值钱。
- Controller------决定停 / 重试 / 带修正再来,并管「最多试 N 次」的刹车。
四种循环
| 类型 | 触发 | 场景 | Claude Code |
|---|---|---|---|
| Heartbeat | 固定短间隔 | 盯 CI / 部署 | /loop 5m |
| Cron | 到点跑 | 夜间批处理 | 定时任务 |
| Hook | 事件触发 | 测试失败 | Stop hook |
| Goal | 没达标就跑 | 修到测试绿 | Stop hook + 计数器 |
实战:工程化一个「夜里自己修小 bug」的循环
我们用 Claude Code 从头搭一个真实循环:每晚把项目里积压的小 bug 修掉一批,全程无人值守,修完测试绿了自己停,第二天早上你来 review diff。
Step 1:把 Goal 写成一份「循环规格」
先把目标、范围、验收标准落成一个文件,让循环每轮都读它:
markdown
<!-- .claude/loops/nightly-fix.md -->
# 夜间修复循环
## 目标(可判定)
- `npm test` 全绿
- `npx tsc --noEmit` 无报错
- `npm run lint` 无 error(warning 允许)
## 工作范围(blast radius)
- 只允许改 src/ 下的文件
- 不准动 migrations/、不准改 package.json 依赖
- 单轮只改一个 bug,改完立刻验收
## 记忆
- 进度写到 .claude/loops/progress.md,记录「已修 / 跳过 / 卡住」
那段 blast radius(爆炸半径) 是关键:无人值守循环最怕它在你睡觉时把范围外的东西也「顺手优化」了。先圈地,再放它跑。
Step 2:Prompter + Controller------用 Stop hook 让它「不达标不准停」
Stop hook 在 Claude 每次准备结束回复时触发,可以返回「拦截」决定把它摁回去继续干:
python
#!/usr/bin/env python3
# .claude/hooks/nightly-stop.py ------ Goal 循环的控制器 + 验收触发器
import json, sys, subprocess, pathlib
data = json.load(sys.stdin)
# 救命刹车 1:Claude 被 hook 强制续跑时会带这个标志,防止 hook 自己递归
if data.get("stop_hook_active"):
sys.exit(0)
# 救命刹车 2:自己的次数上限,怎么改都修不好就放它停(别烧一整晚 token)
counter = pathlib.Path(".claude/loops/attempts")
tries = int(counter.read_text()) if counter.exists() else 0
if tries >= 8:
counter.unlink(missing_ok=True)
sys.exit(0)
# Verifier:跑一遍真实的验收命令,而不是问 Claude「你做完了吗」
checks = [
["npm", "test"],
["npx", "tsc", "--noEmit"],
]
failed = [c for c in checks if subprocess.run(c, capture_output=True).returncode != 0]
if not failed: # 全部达标 → 放它停
counter.unlink(missing_ok=True)
sys.exit(0)
counter.write_text(str(tries + 1)) # 没达标 → 摁回去继续
print(json.dumps({
"decision": "block",
"reason": f"以下验收未通过:{[c[0] for c in failed]}。"
f"按 .claude/loops/nightly-fix.md 的范围继续修,一次只改一个,改完把进度写进 progress.md。"
}))
这段同时是 Controller (决定停还是续)、Verifier (真去跑 npm test)、Prompter (reason 就是下一轮指令)。两个 sys.exit(0) 少一个,循环就可能在你睡着时变成烧钱机器。
Step 3:Cron + headless------让循环每晚自己起床
无人值守用 headless 模式(claude -p,跑完即退),挂到 crontab 每晚自动起:
bash
#!/usr/bin/env bash
# scripts/nightly-fix.sh(crontab:0 23 * * * 起)
set -euo pipefail
# until:测试不绿就再来一轮------最外层的 Goal 循环
attempt=0
until npm test >/dev/null 2>&1; do
attempt=$((attempt+1))
[ "$attempt" -gt 8 ] && { echo "试了 8 轮仍未通过,留给人工"; break; }
claude -p "读取 .claude/loops/nightly-fix.md,挑一个还没修的 bug 修掉,严格遵守工作范围" \
--allowedTools "Bash,Read,Edit,Grep" \
--permission-mode acceptEdits \
--max-turns 30 \
--output-format json | jq -r '.result // "(无输出)"'
done
echo "✅ 夜间循环结束,diff 已就绪,早上 review"
无人值守脚本里 --allowedTools 和 --permission-mode 必须显式给 ,否则它会卡在权限询问上死等。至此五件套齐活:Goal(规格文件)、Prompter(提示词 + reason)、Reader(jq 读 JSON)、Verifier(npm test / tsc)、Controller(until + 计数器 + Stop hook)。
全文最重要的一节:Verifier 才是分水岭
菜鸟和 Loop Engineer 的区别不在 Prompter,在 Verifier。两个致命反模式:
- 让干活的人给自己判卷:在循环里问 Claude「你修好了吗」,它几乎总说「修好了」。Verifier 必须独立于 Worker------要么用确定性检查(测试 / 类型 / lint / diff),要么换一个全新上下文的 subagent 专门挑刺。
- 验收标准不可判定:「优化到我满意」没法被程序判真假,循环只会空转烧钱。Loop Engineer 的功夫一大半花在把模糊目标翻译成可判定信号。
可信度排序:确定性检查 > 独立 subagent 验收 > 同一个 Agent 自评(约等于没验收)。
如果觉得有帮助,欢迎点赞收藏 👍
更多技术文章,关注公众号「开发者效率局」,每周二/四更新。