用得越熟,为什么反而越累
先说一个我自己撞了很久才想明白的怪现象。
刚开始用 Claude Code 的时候,效率提升是肉眼可见的:一个下午干完过去两天的活。用熟之后,效率还在涨,但一天结束时的疲惫感也在涨——因为我整天都在做同一件事:看它跑完、判断对不对、想下一句该说什么、再敲回车。
一天下来我敲了几百次回车。每一次都不难,但每一次都要我在场。
这个姿势的名字叫人在环里(human in the loop)。它在 2024、2025 年是唯一选项,因为模型不够稳,你必须每一步都盯着。但到了 2026 年,它变成了瓶颈本身——不是模型的瓶颈,是你的瓶颈。你能同时盯几个会话?三个?五个?那就是你产能的天花板,跟模型有多强没关系。
Boris Cherny(Claude Code 的负责人)今年把这件事说得很直白:
我已经不 prompt Claude 了。我有一堆 loop 在跑,它们负责 prompt Claude、负责想下一步做什么。我的工作是写 loop。
Peter Steinberger 那句更早、传得更广:
你不该再给 coding agent 写提示词了。你该设计那个替你写提示词的循环。
这两句话被 Addy Osmani 在 6 月的一篇文章里正式命名成了 loop engineering(循环工程)。听起来像又一个新词,但它指的其实是一个很具体的动作:把你脑子里那些隐性职责——调度、验收、状态管理、判断停不停——一件件挪进一个显式的系统。
prompt engineering 优化的是一次回答。loop engineering 优化的是多次回答朝一个可验证的结果收敛。
这篇文章不打算再讲一遍"什么是 loop"。我想拆的是中间那层——从"知道该写循环"到"循环真的能在你睡觉时跑",隔着一整层工程。这层工程我自己在这个博客仓库里从头搭过一遍,踩过的坑、留下的文件、失败的地方,我都摊开讲。
loop 的最小定义只有三样
先把噪音去掉。市面上讲 loop 的文章一上来就是 ReAct 五步、编排框架、多 agent 拓扑,把人绕晕。剥到底,一个能跑的 loop 只需要三样东西:
┌─────────────────────────────────────────────┐
│ │
│ ①做一件事 ──▶ ②检查结果 ──▶ ③还没到? │
│ ▲ │ │
│ └──────────────────────────────┘ │
│ │
│ 到了 → 停下,交给人 │
└─────────────────────────────────────────────┘
一件事、一次检查、一条停机条件。
所有的工程量都压在②和③上。①是模型的事,你管不着也不用管;②和③是你的事,而绝大多数失败的 loop,都是在这两处偷了懒。
偷懒的典型长这样:目标写成"把首页优化一下"。这是个方向,不是任务——它没有完成态。于是 loop 会把首页重写八遍,每版都不一样,没有一版明显更好,最后它自信地报告"已完成",而你花了真金白银买到了一台昂贵的 slop machine:有动作,没进展。
我在《给 AI 任务,别给方向》 里讲过这条原则的单轮版本。放进 loop 里,它的破坏力被放大了几十倍——因为单轮聊歪了你当场就知道,loop 跑歪了要到第二天早上看账单才知道。
所以好目标的判据只有一条:它的成功能不能被检查?
- ✅
test/auth 下全部测试通过,且 tsc 无报错—— 能检查,跑一下就知道 - ✅
data/seo/ 下最新快照里所有 PSI 分数 ≥ 90—— 能检查,读文件比数 - ❌
把这个模块重构得更优雅—— 不能检查,永远不知道该停 - ❌
提升文章的 SEO 表现—— 不能检查,也不知道多久算到
写不出可检查的条件,说明这件事还不该进 loop,它该先经过你的判断,被切成能检查的小块。
我的第一个 loop:一个 bash while 和三个文件
最朴素的 loop 实现是 Geoffrey Huntley 在 2025 年 7 月命名的 Ralph 技巧——名字来自《辛普森一家》里那个无知、执着、乐观的小孩 Ralph Wiggum,因为这个循环也是这样:不管发生什么都傻乎乎地继续。它的纯粹形态就一行:
while :; do cat PROMPT.md | claude -p --dangerously-skip-permissions ; done
看着很糙。但它藏着一个真正重要的设计:每一轮都是全新的上下文窗口。
这不是缺陷,是特性。上下文会腐烂——跑到第 40 轮时,窗口里塞满了前 39 轮的试错、废弃方案、失败的命令,模型会被自己的历史带偏。Ralph 的做法是干脆每轮推倒重来,让状态活在磁盘上,而不是活在上下文里。
用一句我很喜欢的话说:agent 会忘,仓库不会。
我把这套东西落到了这个博客仓库的 scripts/ralph/ 下,跑了一个完整的项目——把文章详情页重做成中英双主题的三栏排版,一共 9 个 user story,全程无人值守。目录长这样:
scripts/ralph/
├── ralph.sh # 循环本体:跑 N 轮,检测完成信号,跨轮归档
├── CLAUDE.md # 每轮喂进去的那份指令(loop body)
├── prd.json # 契约层:结构化 backlog,每个 story 带验收标准和 passes
├── progress.txt # 沉淀层 + 流水层:顶部是蒸馏出的模式,下面是追加日志
└── .last-branch # 换分支时自动归档上一轮的运行记录
循环体:每轮做什么,被写死成十步
CLAUDE.md 是每轮塞进模型的那份指令。它的关键不在"聪明",在确定——十个步骤,每步都不给自由发挥的余地:
1. 读 prd.json
2. 读 progress.txt(先看顶部的 Codebase Patterns)
3. 确认在 PRD 指定的分支上,不在就切/建
4. 挑出优先级最高的、passes: false 的那一个 story
5. 只实现这一个 story
6. 跑质量检查(typecheck / lint / test)
7. 发现可复用模式就写回 CLAUDE.md
8. 检查通过才提交,message 格式固定
9. 把该 story 的 passes 改成 true
10. 把这轮的进展追加到 progress.txt
注意第 4 步和第 5 步:一轮只做一件事。这是 Ralph 最容易被忽略的纪律。上下文重置本质上是一种粗暴的范围控制——杀掉上下文、干干净净地开始、只做一件事。你要是让它一轮做三件事,第三件一定是糊的。
第 6 步和第 8 步是这个 loop 的闸门:检查不过就不许提交。这一条把"每轮都有进展"变成了"每轮都有可验证的进展"。
状态分三层,别混在一个文件里
我一开始把所有状态塞在一个 markdown 里,跑了十几轮就发现问题:文件涨到几千行,agent 每轮读它就烧掉大半个上下文,还越读越糊涂。
后来拆成了三层,这个拆法我觉得是这套东西里最有复用价值的部分:
┌──────────────────────────────────────────────────────┐
│ 契约层 prd.json │
│ 结构化、定长、机器可读 │
│ 每个 story: {id, title, acceptanceCriteria[], passes}│
│ ← agent 从这里取任务,往这里写完成 │
├──────────────────────────────────────────────────────┤
│ 沉淀层 progress.txt 顶部的 ## Codebase Patterns │
│ 蒸馏后的可复用知识,定长,会被主动压缩 │
│ "PaperMod 的 .dark 类在 body 上,不在 html 上" │
│ ← agent 每轮先读这里,避免重复踩坑 │
├──────────────────────────────────────────────────────┤
│ 流水层 progress.txt 下方的追加日志 │
│ 只追加不改写,无限增长,换分支时整体归档 │
│ ← 给人看的审计线索,agent 一般不需要读全 │
└──────────────────────────────────────────────────────┘
三层的区别是增长方式:契约层随任务数增长但有界,沉淀层被人为压住不许长(长了就合并、删旧),流水层随时间无限长但可以整体归档。agent 每轮必读前两层,流水层只在需要溯源时翻。
沉淀层是复利的来源。翻我那份 progress.txt 的顶部,跑了 9 轮之后它自己攒出了这些:
## Codebase Patterns
- PaperMod 把 .dark 类加在 body 上(不是 html)——tokens.css 里的暗色选择器必须用 body.dark
- JS 静态文件必须放 static/js/,assets/js/ 下的 Hugo 不直接 serve
- Playwright 的 force:true click 不触发隐藏元素的 JS 事件,要用 page.evaluate(() => el.click())
- custom.css 里有 .dark .post-content h2 的固定颜色覆盖,需要更高优先级规则压过去
这四条没有一条是我告诉它的,全是它自己撞出来的。而且第 1 条撞了两次——第一次写进沉淀层的版本还写错了(写成 html.dark),第二次撞到才修正。这本身也说明了一件事:沉淀层需要被验证,不然它会把错误也复利下去。
停机:三重保险,不能只信模型的自述
ralph.sh 里判断"跑完了没有"用了三道:
for i in $(seq 1 $MAX_ITERATIONS); do
OUTPUT=$(claude --dangerously-skip-permissions --print < "$SCRIPT_DIR/CLAUDE.md" 2>&1 | tee /dev/stderr) || true
if echo "$OUTPUT" | grep -q "<promise>COMPLETE</promise>"; then
echo "Ralph completed all tasks!"
claude --dangerously-skip-permissions --print "/ship-pr"
exit 0
fi
sleep 2
done
exit 1 # 撞到迭代上限,非零退出
- 模型自述:grep 一个约定的完成标记。这是最弱的一道,因为模型会撒谎。
- 迭代上限:
MAX_ITERATIONS兜底,撞到就非零退出。这是最硬的一道,因为它不依赖任何判断。 - 契约校验:
prd.json里所有 story 的passes是否全为true——这是可以被第三方独立检查的,我在外面再跑一个jq就能确认。
只有第三道是真正可信的。前两道一个太软一个太粗,但组合起来能覆盖大部分情况:模型撒谎时被契约拦住,模型卡死时被上限拦住。
产品原语已经追上来了
2026 年最大的变化不是模型,是那个 bash while 循环变成了产品功能。你现在不用再手搓 Ralph 了——两家的 CLI 都把它做成了原语。
/goal:跨轮次跑到条件成立
Claude Code 从 v2.1.139 起提供 /goal。你写一个完成条件,它就一轮一轮跑下去,直到条件成立才把控制权还给你:
/goal test/auth 下全部测试通过且 lint 干净;每轮必须跑 npm test 和 npm run lint
并把输出贴出来;或在 20 轮后停止
它和手搓 Ralph 的关键差别在于谁来判定完成。官方文档说得很清楚:/goal 本质上是一个会话级的 prompt-based Stop hook——每轮结束后,把条件和对话记录发给你配置的 small fast model(默认 Haiku),它返回一个 yes/no 加一句理由。“no” 会把理由作为下一轮的指引带回去。
也就是说:写代码的那个模型,不负责给自己打分。 这正是 maker-checker 的思路,被做进了停机条件本身。
Codex 那边是同一个概念的镜像实现:/goal 在 CLI 0.128.0(2026 年 4 月)作为实验特性出现,0.133.0(5 月)转正,之后的版本加上了 rollout token 预算、手机端远程监督,以及一个只读的 verifier 子代理。
两家收敛到同一个形状,这件事本身比任何一家的实现都重要——它说明这不是某个厂商的产品偏好,是这类系统的必然结构。
三种"让会话继续"的方式,别用混
官方文档里有一张表我觉得值得背下来,因为选错了会很难受:
| 方式 | 下一轮什么时候开始 | 什么时候停 |
|---|---|---|
/goal | 上一轮结束就开始 | 一个独立模型确认条件成立 |
/loop | 一个时间间隔到了 | 你手动停,或模型自认为做完了 |
| Stop hook | 上一轮结束就开始 | 你自己的脚本或提示词说了算 |
选择依据是什么东西该启动下一轮:
- 有明确终点、要一口气干完 →
/goal - 需要定期巡检、本来就没有终点(比如每小时看一眼监控) →
/loop - 判定标准是确定性的、能用脚本表达(退出码、文件存在、diff 为空) → Stop hook
第三条最容易被忽略。/goal 的评估器是个语言模型,它会犯语言模型会犯的错;而 Stop hook 可以挂一个 shell 脚本,npm test 退出码是 0 就是 0,不存在"看起来通过了"。
一条硬约束,推出一条设计规则
官方文档里有一句话我认为是整篇文档最重要的,但几乎没人展开讲:
评估器不调用工具,所以它只能判断 Claude 已经在对话里呈现出来的东西。
这句话的意思是:评估器看不见你的文件系统,看不见你的 CI,看不见任何没有被打印到对话里的东西。 它只读 transcript。
由此推出一条很实操的设计规则——loop 必须把证据说出声:
❌ /goal 所有测试通过
(模型可能压根没跑测试,只是说了句"应该没问题了",
评估器读到这句话,可能就判 yes)
✅ /goal 所有测试通过。每轮必须实际执行 `npm test`,
并把完整输出(含 exit code)贴进回复;只有当输出里
出现 0 failing 时才算达成
差别在于后者把验证动作和证据形态都写进了条件里。评估器不能自己去查,那就逼执行者把证据端到它面前。
这条规则往外推,其实是《给你的 AI 工作流装上质量门禁》 里那句"每个模块都要先问:这里出问题,我怎么知道"的 loop 版本——在无人值守的场景里,没被打印出来的成功,等于没有成功。
验证器才是瓶颈,不是模型
如果这篇文章你只记一句话,我希望是这句:在一个 loop 里,瓶颈永远是验证器,不是模型。
模型每半年强一次,验证器不会自己变强。你的 loop 能跑多久、能不能放心走开,取决于那个说"到了"的东西有多可信。
我把验证器分成三级阶梯,能用上一级就绝不用下一级:
第一级 确定性闸门 ────────── 退出码、编译结果、schema 校验
不会自欺,成本近似为零,但只能回答"过/不过"
第二级 打分脚本 ──────────── 自己写的 scorer,输出一个数字
能回答"好了多少",可以设预算、可以看趋势
第三级 LLM 判官 ──────────── 语言模型读结果给判断
能评"写得好不好"这类没有确定性定义的东西,
但它会犯语言模型的错,是最后手段
第一级:确定性闸门
npm test 的退出码、tsc 有没有报错、hugo build 是不是 0 warning、git status 干不干净。这些东西不会自欺,这是它们唯一但压倒性的优势。
我那个 Ralph 循环里,CLAUDE.md 第 6 步和第 8 步就是这一级:检查不过不许提交。跑了 9 轮,没有一次提交是坏的——不是因为模型多可靠,是因为闸门在那儿。
第二级:打分脚本,被严重低估的一层
这一级我觉得是大多数人漏掉的。它介于"过/不过"和"让 AI 评一评"之间:你自己写一个脚本,把系统的健康度算成一个数字。
我在仓库里写了两个:
scripts/score-site-build-health.mjs # 构建健康度
scripts/score-content-search-integrity.mjs # 内容/搜索索引一致性
后者做的事很具体:重新生成一遍内容索引,跟仓库里已提交的两份索引文件逐字节比对,一致的每份给 20 分,满分 40;再叠上工作流覆盖度、垃圾文件检查等几项。
function compareIndexes(expected) {
const expectedJson = normalizeIndexPayload(expected); // 剔掉 generatedAt 这种噪声字段
const files = trackedIndexFiles.map((filePath) => ({
filePath,
matches: fs.existsSync(filePath) &&
normalizeIndexPayload(readJson(filePath)) === expectedJson,
}));
const score = files.filter((f) => f.matches).length * 20;
return { score, max: 40, files };
}
为什么值得多花这个功夫,而不是直接 diff 完事?因为数字能做三件退出码做不到的事:
- 能设预算:loop 的停机条件可以写成"整体分 ≥ 85",而不是"全绿"——全绿这个要求在真实项目里经常达不到,于是你要么放弃 loop,要么把闸门降级成摆设。
- 能看趋势:连续三轮分数不变,就是"无进展"的确定性信号,可以直接触发停机(后面讲护栏时会再提)。
- 能定位:分数是分项加出来的,掉分时你知道掉在哪一项。
写 scorer 的成本很低——就是把你本来会用眼睛扫的那几项写成代码。但它把"我感觉还行"变成了"85 分,比昨天低 5 分,掉在索引一致性上"。这个转换是 loop 能不能无人值守的分水岭。
第三级:LLM 判官,能不用就不用
有些东西确实没有确定性定义——“这段文案通顺吗”、“这个改动符合项目风格吗”、“这个 SEO 建议靠不靠谱”。这时候才轮到 LLM 判官。
用的时候有两条纪律:
第一,判官要问证据,不要问感觉。 「你觉得完成了吗」这个问法会得到几乎恒定的 yes。要问的是「贴出你执行的命令和完整输出,指出输出里哪一行证明了条件成立」。
第二——这条我认为是整个 maker-checker 里最容易被搞砸的——验证器的权限必须严格小于执行器。
理由很朴素:给了写权限的 checker,会顺手改一下让检查通过。它不是故意作弊,是因为它的目标函数是"让条件成立",而修改比修复容易。于是 maker-checker 悄悄退化成了 maker-maker,你以为有两道防线,其实一道都没有。
Codex 在 0.142+ 里把 verifier 子代理明确做成只读的,我觉得不是巧合,是这个坑踩得够多之后的收敛。你自己配子代理时也应该照办:
# .codex/agents/verifier.toml —— 示意
name = "verifier"
description = "只读验证器:对照验收标准检查产出,只报告不修改"
tools = ["read", "grep", "bash:readonly"] # 关键:没有写权限
Claude Code 侧对应的是 .claude/agents/ 下的子代理定义,同样把工具集收窄到只读。
一线的人怎么搭:三套系统,一个骨架
讲完原理,看几个真实系统。有意思的是它们表面差别很大,剥开之后是同构的。
Steinberger:一份指令,一堆 skill,一条铁律
Peter Steinberger 一个人维护一套三十万行的 TypeScript/React 生态(Web、Chrome 扩展、CLI、Tauri 桌面端、Expo 移动端),日常开 3–8 个并行实例。他的系统是三个决定叠出来的:
单一事实源。 他不给 Codex 和 Claude 各写一份规则,而是维护一个 agent-scripts/AGENTS.MD,把 ~/.codex/AGENTS.md、~/.claude/CLAUDE.md 全部 symlink 指过去。下游每个仓库只放一个指针文件——“动手前先读那份共享指令(不存在就跳过)",共享块绝不复制进下游仓库。
这里有个细节值得单说:全局用 symlink,下游用指针,是两种机制解决两个问题。 symlink 让文件享有完整优先级(Claude Code 里 @import 进来的内容在冲突时会输给直接写的规则),而下游要进 git、要被 clone、要同时指挥两个工具,所以用纯文本指令加"不存在就跳过"的优雅降级。
可复用单元是 skill,不是 prompt。 每个 skill 是 skills/<name>/SKILL.md,配套脚本放 scripts/ 下。他甚至写了个 validate-skills 挂成 git hook 强制格式。
一条永不委派的铁律。 review 干净、CI 绿、有 proof——这几道 land 前的闸门永远是他自己把关,从不委派、从不跳过。Codex 只有在他决定要 land 且闸门通过之后,才被允许去跑 rebase/merge 这些机械步骤。
Boris Cherny:朴素得惊人
Claude Code 的负责人反复强调自己的配置"vanilla 到不好意思讲”:
- 并行用 5 个独立 checkout,标签编号 1–5,系统通知告诉他哪个要输入
- 每个任务先进 Plan mode 磨计划,满意了再切自动接受编辑
- 每天重复几十次的动作固化成 slash command,check 进 git 全队共享(招牌是一个
/commit-push-pr) CLAUDE.md当团队错题本,约 2.5k token,谁踩坑就把教训写回去- MCP 接真实工具(Slack / BigQuery / Sentry),
.mcp.json进仓库
一个都不花哨。但你把它和 Steinberger 的对齐看,会发现是同一张骨架。
Osmani 的五件套
Addy Osmani 把这张骨架总结成五个原语加一个记忆:automations(定时触发发现和 triage)、worktrees(并行隔离)、skills(沉淀项目知识)、connectors/MCP(接真实工具)、subagents(一个想一个查),加上第六样——外置状态(markdown 或 issue board)。
他特别指出:这五样现在两个产品里都有了,名字不同能力相同。所以"用哪个工具"这个问题基本可以退场了,剩下的问题是你的 loop 设计得怎么样。
抽掉人名,剩下六件
① 版本化的单一事实源 CLAUDE.md / AGENTS.md,下游放指针
② 重复动作固化成手艺 skill / slash command,进 git
③ 并行用 worktree 隔离 每个后台任务可追踪、可见
④ 一道绝不委派的闸门 CI 绿 + 人肉 review + proof,守在 land 之前
⑤ 工具接到真实环境 MCP / 连接器,让反馈是真的
⑥ 教训写回指令文件 让纠正复利,而不是消失在单个会话里
第 ⑥ 条是唯一一条讲"时间"的。前五条决定 loop 今天能不能跑,第六条决定它三个月后是更聪明了还是原地踏步。
把内循环接到外循环:三段式权限梯度
前面讲的都是内循环——你在场,或者至少你的机器开着。真正的杠杆在外循环:定时触发、事件触发、你关了电脑它还在跑。
但这里有个很多人一步跨太大的地方:别把一个能改代码的大 loop 直接扔进无人值守。
我在这个博客仓库里最后收敛出来的形态,是把它按权限切成三段,每段的权力严格递增,段与段之间由人或由确定性条件连接:
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ ① 采集 │ │ ② 分析 │ │ ③ 修复 │
│ seo-snapshot │───▶│ seo-analyze │───▶│ seo-fix │
├───────────────┤ ├───────────────┤ ├───────────────┤
│ 每天定时 │ │ 每天定时 │ │ 手动单点触发 │
│ 无 AI │ │ AI 只读 │ │ AI 可写 │
│ 纯脚本抓数据 │ │ contents: read│ │ 范围白名单 │
│ 提交 json 快照 │ │ 只往 issue 说话│ │ 只开 PR │
└───────────────┘ └───────────────┘ └───────────────┘
│
▼
merge 永远是人
第一段:采集,不带 AI
每天定时跑几个脚本,把 Google Search Console、PageSpeed Insights、CrUX 的数据抓下来,写成 data/seo/gsc-YYYY-MM-DD.json 这样的快照提交进仓库。
这一段刻意不放 AI。 采集是确定性的活,让 AI 干只会引入不确定性和成本。而且快照进 git 之后,就有了时间序列——第二段才有"今天 vs 昨天"可比。
第二段:分析,AI 只读,而且只会说话
这一段是 AI 值班员。它每天读最新的两份快照,比对差异,把发现写成一条评论发到一个滚动的追踪 issue 里。
关键在于它的权限被卡死在两处:
permissions:
contents: read # ← 令牌级:它根本改不了源码
issues: write
# ← 提示词级:再说一遍
Dry-run mode: read the SEO snapshots, write findings to the
tracking issue, do NOT touch the source tree.
一层是令牌,一层是提示词。我在注释里管这叫 “belt to that suspenders”——腰带加背带。权限控制永远要在机制层做,提示词只是补充;反过来(只写在提示词里)等于没做。
它输出什么也被限死了:PSI 分数变化超过 5 分要点名 URL 和大概率的元凶指标、GSC 里 impressions ≥ 20 且 CTR < 2% 的页面算标题候选、位置 8–20 的新查询算便宜的机会。全文控制在 600 词以内。
输出格式被约束住,是让 AI 值班员可用的前提。 不约束的话,它会给你一篇每天都很像、每天都读不完的散文,两周后你就不看了——loop 就死了,死于噪音。
第三段:修复,可写,但门开得极窄
这一段能改代码、能开 PR,所以它有三道锁:
第一道,只能手动触发。 没有 schedule,只有 workflow_dispatch。我读了当天的观察,挑一条我认可的建议,手动点一次。判断权在我,执行权才给它。
第二道,范围白名单。 触发时传一个 scope_hint,提示词把它当 allowlist;另外硬性拉黑一批目录:
Do NOT touch these unless explicitly named in the task:
layouts/**, .github/**, scripts/**, config.yml,
package.json, netlify.toml, data/seo/**
.github/** 在黑名单里这件事值得多说一句:不许 AI 改自己的工作流。 这是自动化系统里最基本的自指防线——一个能修改自己触发条件和权限的 loop,等于没有护栏。
第三道,溯源检查。 提示词的第一步是去读那个 issue,确认操作者给的任务确实对应 issue 里的某条建议;对不上就停下、发条评论说明找不到、然后退出。这一步把"AI 自由发挥"堵死了:它的每个改动都必须能追溯到一条它自己昨天写下的、我今天认可过的观察。
最后,merge 永远是人。 我在博客重做的复盘 里写过这条边界,到今天没变过:98.4% 的脚手架可以交出去,1.6% 的判断不能。
为什么切三段比一个大 loop 好
值得把这件事讲透,因为直觉上"一个 loop 从头干到尾"更省事。
第一,权限最小化落到了段级。 一个大 loop 必须拿到并集权限(能读数据、能写 issue、能改代码),于是分析阶段的一个幻觉就有机会变成源码里的一个改动。切开之后,②段拿着 contents: read,它连想改都改不了。
第二,失败被隔离。 ①段挂了,②段读到旧快照会说"数据窗口只有一天"(提示词里明确要求它这么说,而不是编数据)。②段挂了,③段根本不会被触发。而一个大 loop 里,前半段的失败会被后半段当成事实继续往下跑。
第三,人被放在了信息最富、成本最低的那个点上。 我不需要读代码 diff 就能判断"这条建议值不值得做"——那是②段的输出,一段中文。而如果是一个大 loop,我看到的是一个已经写好的 PR,判断成本高得多,而且会有沉没成本带来的心理压力(它都写好了,merge 了吧)。
第三条是我觉得最反直觉、也最重要的一条。自动化设计里,“人在哪一步介入"比"人介不介入"重要得多。
几个值得组合的打法
上面这些零件可以互相搭。挑几个我验证过、或者从机制上一定成立的组合。
Ralph × /goal:两种 loop 分工,不是二选一
它们的差别在上下文策略,恰好互补:
| Ralph(外层 bash) | /goal(会话内) | |
|---|---|---|
| 上下文 | 每轮清空 | 跨轮保留 |
| 适合 | 任务多、每个任务窄 | 单任务深、需要连续推理 |
| 状态 | 必须全在磁盘上 | 部分可以留在会话里 |
| 失败模式 | 忘掉重要背景 | 上下文腐烂、被自己的历史带偏 |
所以合理的组合是嵌套:外层 Ralph 负责"取下一个 story、清空重来”,内层每个 story 用 /goal 跑到它自己的验收标准成立。
# 示意:外层管调度和上下文重置,内层管单个任务的收敛
while :; do
STORY=$(jq -r '[.userStories[] | select(.passes==false)]
| sort_by(.priority) | .[0] // empty' prd.json)
[ -z "$STORY" ] && break
claude -p --output-format stream-json --verbose \
"/goal 完成这个 story 并让所有验收标准成立,
每条标准都要贴出验证命令和输出;或在 12 轮后停止。
Story: $STORY"
done
/goal × Stop hook:软判官 + 硬闸门
官方文档说 /goal 是"会话级的 prompt-based Stop hook"的封装。既然如此,你完全可以自己再挂一个 script-based Stop hook,让两者叠加:
/goal的 LLM 评估器判断语义条件(“重构完成且没有引入新的公开 API”)- 你的 Stop hook 脚本判断确定性条件(
npm test退出码、git diff --stat是否为空、scorer 分数是否 ≥ 阈值)
任何一道说"不行",loop 就继续。软判官负责"意思对不对",硬闸门负责"事实成不成立"。别指望一个语言模型同时干好这两件事。
本地内循环 × Actions 外循环:便宜的跑快,贵的跑准
我的分工是这样的:
- 本地 Ralph:迭代快、反馈紧、成本可控、能随时 Ctrl+C。适合还在探索阶段的活。
- GitHub Actions:跨设备、关掉电脑还在跑、有完整审计日志、权限能在令牌层强制。适合已经稳定、需要定时发生的活。
判断标准很简单:这件事的形状还在变吗? 还在变就留本地;已经稳定成"每天做一次同样的事",就搬进 Actions,顺便把权限收窄。
maker-checker × 权限不对称 × worktree
三样一起上才完整:
maker → 实现者,有写权限,在自己的 worktree 里
checker → 验证器,只读,对照验收标准和测试,只报告不修改
worktree → 隔离,最坏情况是 git worktree remove,不污染主线
少任何一样都会漏:只有 maker-checker 没有权限不对称,checker 会作弊;有权限不对称没有 worktree,几个并行 maker 会互相踩;有 worktree 没有 checker,你只是并行地产出了几份不可信的东西。
护栏清单
无人值守的 loop 必须带护栏。下面这几条是我认为一条都不能省的:
硬迭代上限。 最简单也最有效。MAX_ITERATIONS 或者条件里写 or stop after 20 turns。它不依赖任何判断,所以永远有效。
token / 成本预算。 从第一天就设死上限,不要等看到账单再设。loop 的成本是乘法结构:轮数 × 每轮上下文 × 子代理数量,任何一项失控都会放大好几个数量级。Osmani 在他那篇文章里专门加了免责声明说必须小心 token 成本、用量差异可以极大——写这个的人是 Google 的工程负责人,他都要专门提醒,说明这不是小概率事件。
无进展检测。 连续 N 轮出现下面任一情况就停下汇报:git diff 为空、scorer 分数不变、评估器给出的理由和上一轮几乎一样。这一条要靠打分脚本才做得干净——这也是我前面说第二级验证器被低估的原因之一。
破坏性操作的白名单,不是黑名单。 允许列表远比禁止列表安全,因为你想不全所有该禁的东西。前面 seo-fix 那个 scope_hint 就是这个思路。
回滚成本要接近零。 每个 loop 跑在自己的分支或 worktree 上,最坏情况是 git branch -D。做到这一点,你对 loop 的容忍度会高很多,反而敢让它跑更久。
审计线索。 流水层日志、Actions 的运行记录、每个改动能追溯到哪条信号。出问题时你需要回答"它为什么这么干",答案必须在文件里,不在你记忆里。
会失败的地方,我说三个
写到这里都在讲怎么搭。但有三个问题是loop 越好用、越严重的,得说清楚。
第一,“done” 是一个声明,不是一个证明。 就算你的验证器设计得再好,它验的也只是你想到的那些条件。loop 在无人值守时既在做事,也在无人值守地犯错。我自己的经验是,跑完之后我仍然要读 diff——不是每一行,但主干必须读。省掉这一步的那几次,无一例外后面加倍还回来了。
第二,理解力会腐蚀。 loop 越顺,你没写过的代码就越多,你以为你懂的和实际存在的之间的差距就越大。Osmani 把这个叫 comprehension debt(理解债),我觉得很准。它的可怕之处在于没有报警——直到某天你需要在 loop 帮不上忙的地方动手,才发现自己在自己的项目里成了外人。
我给自己的对策是:loop 只用在我已经懂的领域加速,不用在我不懂的领域替我理解。同一个 loop,两个人搭得一模一样,一个用来在懂的地方跑更快,一个用来逃避理解——工具分不出这两者,只有你自己知道。
第三,验证器本身要花钱、要维护。 子代理各自跑自己的模型和工具调用,token 是实打实多烧的。打分脚本会随项目演化而失真——我那两个 scorer 在仓库结构调整后就得改。验证器不是一次性投资,是需要持续养的资产。 这也是为什么我坚持能用退出码就不用 LLM:退出码不需要养。
一段可以直接粘的指令
最后给一个能立刻用的东西。
把下面这段整个复制,粘进你自己仓库里的 Claude Code 或 Codex,它会把这篇文章讲的骨架在你的项目里落地成真实文件——先勘察再动手,不懂就问你,而不是瞎猜。
你要在这个仓库里搭一套最小可用的 loop 系统。按下面的顺序做,
每一步做完先把结论讲给我听,不要一口气全做完。
【第一步:勘察,只读,不要改任何文件】
1. 判断技术栈,找出确切的构建 / 测试 / lint / 类型检查命令
(读 package.json / Makefile / CI 配置,不要猜)。
2. 找出已有的 CLAUDE.md / AGENTS.md / .claude / .codex 配置。
3. 找出仓库里"我每周会重复做好几次"的那类任务的痕迹
(看最近 50 条 commit 的信息模式)。
4. 把结论写成一份不超过 30 行的报告给我,并明确列出
你没能确定的地方。不要在这一步创建任何文件。
【第二步:等我确认后,创建骨架】
在仓库根目录下创建:
LOOP.md — 循环体指令(每轮喂给 agent 的那份)
.loop/state.json — 契约层:结构化任务列表,
每项含 {id, title, acceptance[], passes}
.loop/patterns.md — 沉淀层:可复用模式,初始为空,
有硬上限 40 行,超了必须合并旧条目
.loop/log.md — 流水层:只追加
LOOP.md 必须写清这十件事:
1) 先读 .loop/patterns.md,再读 .loop/state.json
2) 只挑一个 passes:false 且优先级最高的任务
3) 一轮只做这一个任务,不许顺手做别的
4) 用第一步勘察出的真实命令做验证,不要发明命令
5) 必须把验证命令的完整输出(含退出码)贴进回复
—— 没打印出来的成功等于没成功
6) 验证不过就不许提交,也不许把 passes 改成 true
7) 通过后提交,message 格式:feat: [任务ID] - [标题]
8) 把这轮进展追加到 .loop/log.md
9) 若发现可复用模式,写进 .loop/patterns.md(注意 40 行上限)
10) 不许修改 .github/**、CI 配置、以及 LOOP.md 自身
【第三步:验证器】
写一个 scripts/loop-score.mjs(或与仓库语言一致的等价物):
- 依次跑第一步找到的那些检查命令
- 每项给固定分值,输出 JSON: {score, max, items:[{name, pass, detail}]}
- 退出码:score < 阈值 时返回 1
不要用 LLM 判断任何能用命令判断的东西。
【第四步:maker-checker】
创建一个只读的验证子代理(Claude Code 放 .claude/agents/,
Codex 放 .codex/agents/),要求:
- 工具集只含读取和只读命令,绝对不含写文件和 git 提交
- 职责是对照 acceptance 逐条核对证据并报告,不许自己动手修
- 报告格式:每条标准 → 通过/不通过 → 引用哪一段输出作为证据
【第五步:给我两条可直接跑的命令】
a) 一条内循环命令,用 /goal,条件里必须包含:
具体验收标准、"必须贴出验证输出"、以及一个轮次上限
b) 一条外层脚本,跑 N 轮、每轮重置上下文、检测完成信号、
撞上限则非零退出
【全程铁律】
- 任何拿不准的地方停下来问我,不要猜。
- 不要碰 .github/**、不要改 CI、不要改自己的 LOOP.md。
- 每一步做完先汇报再继续。
这段指令本身就体现了文章里几条原则:先勘察后动手(不许猜命令)、证据必须打印出来、验证器只读、自指防线(不许改自己)、以及每步之间留一个人的确认点。
跑完之后你手上会有:一份循环体指令、三层状态文件、一个打分脚本、一个只读验证子代理,和两条能直接跑的命令。剩下的是你自己的判断——第一个任务选什么。
我的建议是选一个窄到无聊、但成功能被测试判定的活:修一类 flaky test、把某个模块的类型补全、把某套 e2e 跑绿。别一上来就上"重构核心模块"。你要先验证的不是模型能力,是你的验证器可不可信。
你从环里挪到了环外面
回到开头那个问题:为什么用得越熟反而越累?
因为熟练度提升的是你在环里的效率——你敲得更快、判断得更准、能同时盯更多会话。但环还是那个环,你还在里面。
loop engineering 做的事是把你挪出去。你从"每一步都在场的执行者",变成"设计这个环、然后看护栏的人"。这不是变轻松了——Osmani 说得对,loop 设计比 prompt engineering 更难,不是更容易,因为它要求你把过去凭直觉做的判断(这算做完了吗?该停了吗?这个改动能不能信?)全部写成显式的、可执行的规则。
但它是可积累的。你今天写下的一条验收标准,明天还在用;你沉淀进 patterns 的一条坑,三个月后还在替你挡。而你敲的那几百次回车,敲完就没了。
这也是为什么我认为这件事对一个人干活的场景意义最大。团队可以靠人堆并行,一个人不行——一个人的产能天花板就是注意力。把注意力从执行挪到设计,是唯一能突破这个天花板的方向。
最后重复一遍 Osmani 那句我很认同的话:同样一套 loop,两个人搭出来可以得到完全相反的结果。 一个用它在自己深刻理解的领域跑得更快,另一个用它来逃避理解这件事本身。loop 分不出这两者的区别。
你分得出。
参考
- Keep Claude working toward a goal — Claude Code Docs
:
/goal的官方机制说明,包括评估器不调用工具这条关键约束 - Loop Engineering — Addy Osmani :五个原语加一个记忆的框架,以及关于成本与理解债的诚实提醒
- Ralph Wiggum as a “software engineer” — Geoffrey Huntley :Ralph 技巧的原始出处
- Using Goals in Codex — OpenAI Developers :Codex 侧的 goal 实现与只读验证子代理
- 同系列可以接着读:Agent Engineering 全景地图 (loop 外面那 98.4% 的工程量)、给你的 AI 工作流装上质量门禁 (验证器思想的单机版)、给 AI 任务,别给方向 (可检查目标的单轮版本)




读者回响