Xinwei Xiong · 2026 年 7 月 20 日
24 分钟 · 11896 字 · | EN

独立开发者的 Loop Engineering:验证器、状态与安全自动化

Loop Engineering 把反复提示变成可验证、可停机的工程系统。本文结合真实仓库实践,拆解状态分层、确定性闸门、评分器、只读验证、权限梯度与无人值守护栏,并厘清 Claude Code /goal 与 Codex Goals 的机制差异,帮助独立开发者在保留判断力的前提下长期安全放大产能。

一张象征验证器、状态与安全自动化的安静控制台

用得越熟,为什么反而越累

先说一个我自己撞了很久才想明白的怪现象。

刚开始用 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 完事?因为数字能做三件退出码做不到的事

  1. 能设预算:loop 的停机条件可以写成"整体分 ≥ 85",而不是"全绿"——全绿这个要求在真实项目里经常达不到,于是你要么放弃 loop,要么把闸门降级成摆设。
  2. 能看趋势:连续三轮分数不变,就是"无进展"的确定性信号,可以直接触发停机(后面讲护栏时会再提)。
  3. 能定位:分数是分项加出来的,掉分时你知道掉在哪一项。

写 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 分不出这两者。

你分得出。


参考

读者回响

加入讨论

新文章写好,先寄给你

每有新文章,寄一封信到你的邮箱。双重确认,随时退订。