用得越熟,为什么反而越累
先说一个我自己撞了很久才想明白的怪现象。
刚开始用 Claude Code 的时候,效率提升是肉眼可见的:一个下午干完过去两天的活。用熟之后,效率还在涨,但一天结束时的疲惫感也在涨——因为我整天都在做同一件事:看它跑完、判断对不对、想下一句该说什么、再敲回车。
一天下来我敲了几百次回车。每一次都不难,但每一次都要我在场。
这个姿势叫人在环里(human in the loop)。需要探索与判断时,人当然应该在场;可一旦工作已经重复、而且结果可以检查,持续盯场就会变成瓶颈。你能同时看几个会话?三个?五个?那就是注意力的上限,更强的模型不会自动把它移走。
**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 # 撞到迭代上限,非零退出
这段脚本当前只有两道停机机制:
- 完成标记:输出出现
<promise>COMPLETE</promise>就结束。它依赖模型自述,因此只能算软信号。 - 迭代上限:达到
MAX_ITERATIONS后非零退出。它不证明任务完成,只负责阻止无限运行。
prd.json 里所有 story 的 passes 是否为 true,确实可以用独立的 jq 命令核对;但这个 contract gate 目前还没有写进 ralph.sh。所以不能说脚本已经用契约挡住了错误完成声明。更稳妥的下一步,是在接受完成标记之前运行独立校验,并让校验失败时继续循环或直接失败退出。
名字相似的 Goal,并不是同一种机制
不同 coding agent 都开始提供“持续做到条件成立”的能力,但不能因为名字相似,就把内部机制写成镜像。
Claude Code /goal:会话内循环,独立评估器读 transcript
Claude Code 的 /goal 为当前会话设置完成条件。每轮结束后,一个独立评估器根据目标和对话记录判断是否继续:
/goal test/auth 下全部测试通过且 lint 干净;每轮必须跑 npm test 和 npm run lint
并把输出贴出来;或在 20 轮后停止
写代码的模型不直接为自己的完成声明盖章,这是 maker-checker 的思路。但这个评估器不调用工具,也不会自行查看仓库;它能看见的是会话里已经呈现的证据。
Claude Code 还提供 /loop 和 Stop hook。三者解决的是不同触发问题:
| 方式 | 下一轮什么时候开始 | 什么时候停 |
|---|---|---|
/goal | 上一轮结束就开始 | 一个独立模型确认条件成立 |
/loop | 一个时间间隔到了 | 你手动停,或模型自认为做完了 |
| Stop hook | 上一轮结束就开始 | 你自己的脚本或提示词说了算 |
- 有明确终点、要一口气干完 →
/goal - 需要定期巡检、本来就没有终点(比如每小时看一眼监控) →
/loop - 判定标准是确定性的、能用脚本表达(退出码、文件存在、diff 为空) → Stop hook
第三条很容易被忽略。语言模型评估器适合语义判断;脚本式 Stop hook 更适合退出码、文件存在或 diff 为空这类确定性条件。
评估器只读 transcript 这一边界,推出一条实用规则:loop 必须把证据说出声。
❌ /goal 所有测试通过
(模型可能压根没跑测试,只是说了句"应该没问题了",
评估器读到这句话,可能就判 yes)
✅ /goal 所有测试通过。每轮必须实际执行 `npm test`,
并把完整输出(含 exit code)贴进回复;只有当输出里
出现 0 failing 时才算达成
后者同时规定了验证动作和证据形态。评估器不能自己查,就让执行者把可核对的证据带到会话里。机制细节以 Claude Code 的 /goal 官方文档
为准。
Codex Goals:线程级持久状态与证据审计
Codex Goals 的边界不同。Goal 是附着在一个 task(thread)上的持久状态:目标、生命周期、预算和进展计数都跟随这项工作。系统在安全的空闲边界继续推进,而不是复刻 Claude Code 的 Stop-hook 评估器。
完成规则也更明确:Goal 被标记为完成之前,需要把目标逐项对照文件、diff、命令输出、测试、基准或生成物等证据。预算耗尽不等于完成。官方说明见 Using Goals in Codex 。
两者都要求目标可衡量,但契约不同:
- Claude Code
/goal是会话级的连续轮次机制,独立评估器读取 transcript。 - Codex Goal 是线程级的持久完成契约,结束前执行证据审计。
无论使用哪一种,完成声明都应该指向另一个人能复核的证据。这也是《给你的 AI 工作流装上质量门禁》 里“这里出问题,我怎么知道”在 loop 场景下的版本。
验证器才是瓶颈,不是模型
如果这篇文章你只记一句话,我希望是这句:当 loop 已经能持续行动时,验证器往往比模型更先成为瓶颈。
模型每半年强一次,验证器不会自己变强。你的 loop 能跑多久、能不能放心走开,取决于那个说"到了"的东西有多可信。
我把验证器分成三级阶梯,能用上一级就绝不用下一级:
第一级 确定性闸门 ────────── 退出码、编译结果、schema 校验
不会自欺,成本近似为零,但只能回答"过/不过"
第二级 打分脚本 ──────────── 自己写的 scorer,输出一个数字
能回答"好了多少",可以设预算、可以看趋势
第三级 LLM 判官 ──────────── 语言模型读结果给判断
能评"写得好不好"这类没有确定性定义的东西,
但它会犯语言模型的错,是最后手段
第一级:确定性闸门
npm test 的退出码、tsc 有没有报错、hugo build 是否成功、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。
下面表达的是能力策略,不是任何产品可直接粘贴的 TOML:
verifier:
可以:读文件、搜索文本、运行批准过的非写入检查
不可以:改文件、改测试、提交、推送、修改自己的评分规则
输出:验收条件 → 通过/不通过 → 引用证据
实际落地时,应先查所用产品当前的权限与子代理文档,再用它真正支持的配置实现这条边界。工具名和 schema 会变,原则不变:checker 只报告,maker 才修复。
抽掉产品名,剩下六件事
工具名和操作界面会变,底层结构相对稳定:
① 版本化的单一事实源 CLAUDE.md / AGENTS.md,下游放指针
② 重复动作固化成手艺 skill / slash command,进 git
③ 并行用 worktree 隔离 每个后台任务可追踪、可见
④ 一道绝不委派的闸门 CI 绿 + 人肉 review + proof,守在 land 之前
⑤ 工具接到真实环境 MCP / 连接器,让反馈是真的
⑥ 教训写回指令文件 让纠正复利,而不是消失在单个会话里
第 ⑥ 条是唯一一条讲"时间"的。前五条决定 loop 今天能不能跑,第六条决定它三个月后是更聪明了还是原地踏步。
把内循环接到外循环:三段式权限梯度
前面讲的都是内循环——你在场,或者至少你的机器开着。真正的杠杆在外循环:定时触发、事件触发、你关了电脑它还在跑。
但这里有个很多人一步跨太大的地方:别把一个能改代码的大 loop 直接扔进无人值守。
我在这个博客仓库里最后收敛出来的形态,是把它按权限切成三段,每段的权力严格递增,段与段之间由人或由确定性条件连接:
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ ① 采集 │ │ ② 分析 │ │ ③ 修复 │
│ seo-snapshot │───▶│ seo-analyze │───▶│ seo-fix │
├───────────────┤ ├───────────────┤ ├───────────────┤
│ 每天定时 │ │ 每天定时 │ │ 手动单点触发 │
│ 无 AI │ │ token 只读 │ │ AI 可写 │
│ 纯脚本抓数据 │ │ contents: read│ │ 提示范围+diff门│
│ 提交 json 快照 │ │ 只往 issue 说话│ │ 只开 PR │
└───────────────┘ └───────────────┘ └───────────────┘
│
▼
merge 永远是人
第一段:采集,不带 AI
每天定时跑几个脚本,把 Google Search Console、PageSpeed Insights、CrUX 的数据抓下来,写成 data/seo/gsc-YYYY-MM-DD.json 这样的快照提交进仓库。
这一段刻意不放 AI。 采集是确定性的活,让 AI 干只会引入不确定性和成本。而且快照进 git 之后,就有了时间序列——第二段才有"今天 vs 昨天"可比。
第二段:分析,token 只读,产物只发到 issue
这一段是 AI 值班员。它每天读最新的两份快照,比对差异,把发现写成一条评论发到一个滚动的追踪 issue 里。
关键在于把权限和行为约束分开看:
permissions:
contents: read # GitHub token 不具备把内容写回仓库的权限
issues: write
# ← 提示词级:再说一遍
Dry-run mode: read the SEO snapshots, write findings to the
tracking issue, do NOT touch the source tree.
contents: read 限制的是 GITHUB_TOKEN 对仓库内容的写回能力,不等于 runner 工作区变成不可写文件系统。进程仍可能修改 checkout,只是不能凭这个 token 直接提交或推送。提示词里的 dry-run 约束是行为边界;如果风险更高,还要配合只读沙箱、diff 检查和不上传产物等机制。权限控制应尽量落在机制层,提示词只作补充。
它输出什么也被限死了:PSI 分数变化超过 5 分要点名 URL 和大概率的元凶指标、GSC 里 impressions ≥ 20 且 CTR < 2% 的页面算标题候选、位置 8–20 的新查询算便宜的机会。全文控制在 600 词以内。
输出格式被约束住,是让 AI 值班员可用的前提。 不约束的话,它会给你一篇每天都很像、每天都读不完的散文,两周后你就不看了——loop 就死了,死于噪音。
第三段:修复,可写,但入口尽量收窄
这一段能改代码、能开 PR,所以我设置了三层控制:
第一道,只能手动触发。 没有 schedule,只有 workflow_dispatch。我读了当天的观察,挑一条我认可的建议,手动点一次。判断权在我,执行权才给它。
第二道,范围提示。 触发时传一个 scope_hint,要求 agent 只处理指定范围;另外明确列出默认不应触碰的目录:
Do NOT touch these unless explicitly named in the task:
layouts/**, .github/**, scripts/**, config.yml,
package.json, netlify.toml, data/seo/**
.github/** 在限制列表里值得多说一句:自动化不应修改自己的工作流。不过,scope_hint 和提示词本身只是软边界,不是系统调用级 allowlist。真正的硬边界还应在运行环境中检查最终 diff,超出允许路径就让任务失败。
第三道,溯源检查。 提示词的第一步是去读那个 issue,确认操作者给的任务确实对应 issue 里的某条建议;对不上就停下、发条评论说明找不到、然后退出。这一步把"AI 自由发挥"堵死了:它的每个改动都必须能追溯到一条它自己昨天写下的、我今天认可过的观察。
最后,merge 永远是人。 我在博客重做的复盘 里写过这条边界,到今天没变过:98.4% 的脚手架可以交出去,1.6% 的判断不能。
为什么切三段比一个大 loop 好
值得把这件事讲透,因为直觉上"一个 loop 从头干到尾"更省事。
第一,权限最小化落到了段级。 一个大 loop 往往需要权限并集(读数据、写 issue、改代码),分析阶段的错误因而更容易蔓延。拆开后,②段的 token 只有 contents: read,不能把源码改动推回仓库;若再配合工作区隔离和 diff gate,边界才算完整。
第二,失败被隔离。 ①段挂了,②段读到旧快照会说"数据窗口只有一天"(提示词里明确要求它这么说,而不是编数据)。②段挂了,③段根本不会被触发。而一个大 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
本地内循环 × Actions 外循环:便宜的跑快,贵的跑准
我的分工是这样的:
- 本地 Ralph:迭代快、反馈紧、成本可控、能随时 Ctrl+C。适合还在探索阶段的活。
- GitHub Actions:跨设备、关掉电脑还在跑、有完整审计日志、权限能在令牌层强制。适合已经稳定、需要定时发生的活。
判断标准很简单:这件事的形状还在变吗? 还在变就留本地;已经稳定成"每天做一次同样的事",就搬进 Actions,顺便把权限收窄。
maker-checker × 权限不对称 × worktree
三样一起上才完整:
maker → 实现者,有写权限,在自己的 worktree 里
checker → 验证器,只读,对照验收标准和测试,只报告不修改
worktree → 隔离,最坏情况是 git worktree remove,不污染主线
三者分别处理不同风险:缺少权限不对称,checker 可能越界修改;缺少 worktree,并行 maker 容易互相干扰;缺少 checker,则并行产物仍没有独立验收。
护栏清单
无人值守的 loop 需要护栏。下面这些是我会优先保留的:
硬迭代上限。 MAX_ITERATIONS 或条件里的 or stop after 20 turns 不依赖语义判断,可以稳定阻止无限续跑,但不能证明任务已经完成。
token / 成本预算。 从第一天就设死上限,不要等看到账单再设。loop 的成本是乘法结构:轮数 × 每轮上下文 × 子代理数量,任何一项失控都会放大好几个数量级。Osmani 在他那篇文章里专门加了免责声明说必须小心 token 成本、用量差异可以极大——写这个的人是 Google 的工程负责人,他都要专门提醒,说明这不是小概率事件。
无进展检测。 连续 N 轮出现下面任一情况就停下汇报:git diff 为空、scorer 分数不变、评估器给出的理由和上一轮几乎一样。这一条要靠打分脚本才做得干净——这也是我前面说第二级验证器被低估的原因之一。
破坏性操作使用机制级 allowlist。 允许列表通常比禁止列表安全,因为你很难想全所有该禁的东西。但提示词里的 scope_hint 只是范围提示;要成为真正的 allowlist,还需要沙箱、工具权限或最终 diff gate 来强制执行。
回滚成本要接近零。 每个 loop 跑在自己的分支或 worktree 上,最坏情况是 git branch -D。做到这一点,你对 loop 的容忍度会高很多,反而敢让它跑更久。
审计线索。 流水层日志、Actions 的运行记录、每个改动能追溯到哪条信号。出问题时你需要回答"它为什么这么干",答案必须在文件里,不在你记忆里。
会失败的地方,我说三个
写到这里都在讲怎么搭。但有三个问题是loop 越好用、越严重的,得说清楚。
第一,“done” 是一个声明,不是一个证明。 就算你的验证器设计得再好,它验的也只是你想到的那些条件。loop 在无人值守时既在做事,也在无人值守地犯错。我自己的经验是,跑完之后我仍然要读 diff——不是每一行,但主干必须读。省掉这一步的那几次,无一例外后面加倍还回来了。
第二,理解力会腐蚀。 loop 越顺,你没写过的代码就越多,你以为你懂的和实际存在的之间的差距就越大。Osmani 把这个叫 comprehension debt(理解债),我觉得很准。它的可怕之处在于没有报警——直到某天你需要在 loop 帮不上忙的地方动手,才发现自己在自己的项目里成了外人。
我给自己的对策是:loop 只用在我已经懂的领域加速,不用在我不懂的领域替我理解。同一个 loop,两个人搭得一模一样,一个用来在懂的地方跑更快,一个用来逃避理解——工具分不出这两者,只有你自己知道。
第三,验证器本身要花钱、要维护。 子代理各自跑自己的模型和工具调用,token 是实打实多烧的。打分脚本会随项目演化而失真——我那两个 scorer 在仓库结构调整后就得改。验证器不是一次性投资,是需要持续养的资产。 这也是为什么我坚持能用退出码就不用 LLM:退出码不需要养。
一段可以直接粘的指令
最后给一个能立刻用的东西。
把下面这段交给你使用的 coding agent。它要求 agent 先确认产品真实支持的命令与权限,再落地骨架,避免把 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】
先阅读当前产品的官方权限与子代理文档。只有产品能强制所需边界时,
才创建验证器:
- 可读取并运行批准过的非写入检查,不可写文件、改测试或提交
- 职责是对照 acceptance 逐条核对证据并报告,不许自己动手修
- 报告格式:每条标准 → 通过/不通过 → 引用哪一段输出作为证据
如果边界无法强制,不要发明配置 schema;改用只读沙箱或人工验证。
【第五步:给我两条可直接跑的命令】
a) Claude Code:给出一条 /goal 内循环命令,条件包含具体验收
标准、必须贴出的验证输出和轮次上限。
Codex:使用它支持的 Goal 接口创建线程级 Goal,采用相同证据
契约;不要假设它也使用 /goal 语法。
b) 一条外层脚本,跑 N 轮、每轮重置上下文、检测完成信号、
撞上限则非零退出
【全程铁律】
- 任何拿不准的地方停下来问我,不要猜。
- 不要碰 .github/**、不要改 CI、不要改自己的 LOOP.md。
- 每一步做完先汇报再继续。
这段指令本身就体现了文章里几条原则:先勘察后动手(不许猜命令)、证据必须打印出来、验证器只读、自指防线(不许改自己)、以及每步之间留一个人的确认点。
跑完之后,你应该得到一份循环体指令、三层状态文件、一个打分脚本、一个受实际权限边界约束的 checker,以及与当前产品匹配的运行方式。剩下的是你自己的判断——第一个任务选什么。
我的建议是选一个窄到无聊、但成功能被测试判定的活:修一类 flaky test、把某个模块的类型补全、把某套 e2e 跑绿。别一上来就上"重构核心模块"。你要先验证的不是模型能力,是你的验证器可不可信。
你从环里挪到了环外面
回到开头那个问题:为什么用得越熟反而越累?
因为熟练度提升的是你在环里的效率——你敲得更快、判断得更准、能同时盯更多会话。但环还是那个环,你还在里面。
loop engineering 做的事是把你挪出去。你从"每一步都在场的执行者",变成"设计这个环、然后看护栏的人"。这不一定更轻松:loop 设计往往比单次提示更难,因为它要求你把过去凭直觉做的判断(这算做完了吗?该停了吗?这个改动能不能信?)写成显式、可执行的规则。
但它是可积累的。你今天写下的一条验收标准,明天还在用;你沉淀进 patterns 的一条坑,三个月后还在替你挡。而你敲的那几百次回车,敲完就没了。
这也是为什么我认为它对独立工作者尤其重要。团队可以用分工换并行,一个人的稀缺资源却始终是注意力。把注意力从重复执行挪到系统设计,是抬高这一天花板的一条有效路径。
同样一套 loop,可能产生相反的结果:一个人用它在理解深的领域跑得更快,另一个人用它逃避理解。loop 分不出这两者。
你分得出。
参考
- Keep Claude working toward a goal — Claude Code Docs
:
/goal、/loop、Stop hook 与 transcript-only 评估边界 - Claude Code tools reference :当前工具名与权限面;不要把本文概念策略当作可粘贴 schema
- Loop Engineering — Addy Osmani :五个原语加一个记忆的框架,以及关于成本与理解债的诚实提醒
- Ralph Wiggum as a “software engineer” — Geoffrey Huntley :Ralph 技巧的原始出处
- Using Goals in Codex — OpenAI Developers :线程级持久状态、生命周期、预算、续跑与证据审计
- 同系列可以接着读:Agent Engineering 全景地图 (loop 外面那 98.4% 的工程量)、给你的 AI 工作流装上质量门禁 (验证器思想的单机版)、给 AI 任务,别给方向 (可检查目标的单轮版本)




读者回响