Loop 工程实战:在 Mango 里落地 builder + checker 循环

上一篇《Loop 工程:从 prompter 到 loop 设计师》把方法论翻译了一遍,读完最大的感受是——道理都懂,但不落到一个真实项目里跑一遍,永远不知道哪里会硌手

这篇是落地记录:我在自己的 Mango(一个 Electron + TypeScript 的卡片笔记工具)里,把 Loop Engineering 搭成了一套 builder + checker 的自动循环,配好防打断的权限和防黑盒的日志 hook,再用隔离沙箱跑了两轮端到端验证;内容全部来自实际操作与验证,不是纯理论

01 Loop 工程要解决什么

方法论来源是 Datawhale / 筱可的《全网爆火的 Loop Engineering,保姆教程来了!》

一句话概括:把写代码和查代码拆成两个 Agent,让编排器循环调度,查到全绿为止;不用人逐条检查,系统自动跑完所有检查,有问题反馈给写代码的 Agent,修完再查,直到全绿

1.1 传统单次交付的痛点

传统方式是单次的——写需求,Agent 生成代码,肉眼看一遍收工;但肉眼看容易漏:代码能跑 ≠ 测试全过,测试过 ≠ 类型没错

更常见的是 Agent 写的代码有问题,贴回去让它改,改完又引入新问题,来回几轮都不确定到底修好没有

1.2 把验证交给系统

Loop 工程的转变,是把验证从"你的工作"变成"系统的工作":

  • 一次性交付:验证是你的工作,你是质检员
  • 循环交付:验证是系统的工作,你从质检员变回了需求方

02 核心原则:执行与验证分离

最硬的一条原则是:写代码的 Agent 会高估自己的答案——写完再问它"行不行",答案大概率是"行";所以必须把执行和验证拆到两个角色:

  • builder:只写代码、只修代码
  • checker:只查代码,从工具层面无法修改任何文件

关键不是靠提示词约束 checker "别改代码",而是工具可见性的硬隔离——checker 的 tools 字段里根本没有 Write / Edit,它想改也改不动

03 Mango 的落地架构

整条循环长这样,你只需要抛一个任务,剩下的交给编排器:

你: /build-loop <任务>


┌─────────────────────────────────────────────────────────┐
│  编排器 /build-loop(.claude/commands/build-loop.md)        │
│  循环,最多 5 轮:                                           │
│    1. 调 builder ← 任务(首轮)/ checker 的原始失败报告(后续)  │
│    2. 调 checker → 跑 pnpm check                            │
│    3. 全绿? → 停,汇报 diff;否则把【完整失败报告】原样喂回 builder │
│    4. 命中停止规则? → 刹车,附升级报告                          │
└───────────────┬─────────────────────────┬─────────────────┘
                │                         │
                ▼                         ▼
   builder                               checker
   tools: Read/Write/Edit/Bash/Grep/Glob  tools: Read/Grep/Glob/Bash ← 无 Write/Edit
   只写/改代码                             只跑 pnpm check,原样回报

3.1 与原文有意的差异

原文是通用教程,落到 Mango 我做了几处适配:

  • 编排器命名 /build-loop,避开 Claude Code 内置的 /loop skill
  • 模型直接用当前 Claude,不接 StepFun step-3.7-flash——现阶段循环频率不大,够用
  • builder 提示词加了 Mango 特有约束:改 IPC 要同步 shared/types + handler + 调用三处;不推翻 tech-spec.md §8 锁定的架构决策

3.2 文件放置:单一事实源

遵守 claude-config 的单一事实源规范——真实文件放在配置仓库里,项目侧只放软链:

资产真实文件项目使用位置
builder agentclaude-config/mango/agents/builder.md.claude/agents/(目录软链)
checker agentclaude-config/mango/agents/checker.md同上
build-loop 编排器claude-config/mango/commands/build-loop.md.claude/commands/(目录软链)
日志 hook 脚本claude-config/mango/hooks/loop-log.py.claude/hooks/(目录软链)
停止规则写入 CLAUDE.md(项目文档,不软链)

04 四个组件

4.1 builder agent

frontmatter 关键:tools: Read, Write, Edit, Bash, Grep, Glob

职责要点:

  • 输入首轮是任务,后续轮是 checker 的完整失败报告(含行号 / 堆栈)
  • 先定位根因再动手;最小改动修复本轮失败项,不顺手重构、不碰无关代码
  • 遵守项目约定(中文注释、TS 分号 + 双引号 + 空格缩进);改 IPC 同步三处
  • 尊重 tech-spec.md §8 锁定决策,不为绕过检查而推翻
  • 改完简述改了什么、为什么,不自称通过(通过与否由 checker 判定)
  • 禁止:改测试迁就实现、加 @ts-ignore / eslint-disable 骗过检查、git commit / push

4.2 checker agent

frontmatter 关键:tools: Read, Grep, Glob, Bash——没有 Write / Edit,这是整套设计的地基

职责要点:

  • pnpm check
  • 全绿 → RESULT: PASSED + 各步骤一行摘要;失败 → RESULT: FAILED + 原样粘贴完整输出(命令、退出码、文件:行号、堆栈、原始报错)
  • 保留完整输出,绝不只贴最后一行、绝不总结、绝不替 builder 猜根因
  • 禁止:修改任何文件、放宽检查、跳过用例、解读 / 过滤 / 总结失败信息

4.3 build-loop 编排器

驱动 "builder → checker → 判定 → 反馈" 循环,最多 5 轮,铁律有三条:

  • 调度器不是审查员:不替 checker 判定通过,不替 builder 写代码
  • 转发失败报告逐字原样——"帮忙总结"实际在制造信息损耗,行号 / 堆栈 / 中间输出正是 builder 定位根因所需的东西
  • 循环结束(无论全绿还是刹车)都不自动 git commit / push

4.4 停止规则

停止规则写在 CLAUDE.md 里,所有 Agent 可见;编排器每轮开始前检查,命中任一条即刹车并输出升级报告(当前轮次 / 失败项 / builder 已尝试的方法 / 失败原因判断):

  1. 轮次上限:已达 5 轮仍未全绿
  2. 无进展:连续 2 轮同一失败项且行号 / 报错未变,builder 在原地打转
  3. 反复横跳:修好 A 引入 B,下一轮修好 B 又打破 A
  4. 改动越界:触及任务外文件,或推翻 tech-spec.md §8 锁定决策
  5. 破坏性 / 绕过:删测试、加 @ts-ignore / eslint-disable、放宽 pnpm check 骗过验证
  6. 需要人决策:涉及产品取舍、外部依赖选型、或改动面 > 10 个文件

05 配套配置:权限与日志 hook

为让 loop 不被打断、又不黑盒,在 .claude/settings.local.json 里做了两件事(合并追加,未动原有配置)

5.1 权限 allowlist 防打断

只 allow loop 明确需要的命令,不用 Bash(*) 全开

  • checker 验证链:pnpm check/typecheck/test/lint/buildpnpm run / npm run 回退形式,加底层 tsc --noEmiteslintvitest runvite buildnode --test/--check
  • builder 写代码:EditWrite

5.2 SubagentStop hook 防黑盒

为什么选 SubagentStop:它的 payload 直接带 agent_type(哪个 agent)和 last_assistant_message(干完的总结),是"builder / checker 执行完"最贴合的时机,比 PostToolUse + Task 更干净

JSON 结构:

{
  "hooks": {
    "SubagentStop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/loop-log.py\" 2>/dev/null || true",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

脚本 loop-log.py 的行为:只记 builder / checker 两类 subagent(其它忽略)→ 写到项目内 .mango/loop.log(已 gitignore,不入库)→ 同时向 UI 回显一行摘要 → 字段名多重兜底、空输入不崩、绝不阻断循环

日志行格式示例:

[2026-07-04 00:58:50] agent=checker session=verify99 :: RESULT: PASSED — 5/5 全绿

06 怎么用

/build-loop <任务描述>

M1 阶段的典型任务:

/build-loop 实现卡片 ID 生成 newCardID(d) 与 timeFromID(id),含 Vitest 单测
/build-loop 实现 [[链接]] 解析 parseLinks(srcId, content),覆盖 id/标题/带锚文本三种形态
/build-loop 实现向量 encodeVector/decodeVector + cosineSimilarity,单测对拍 SiYuan 数值
/build-loop 实现 IPC card.get/create/update,同步 shared/types 与主进程 handler

流程很简单:你输入任务 → 看它循环到 pnpm check 全绿 → 审一眼 diff 决定要不要提交

pnpm check 的全绿标准是四步串行,任一非零即整体失败:

"check": "pnpm typecheck && pnpm lint && pnpm test && pnpm build"
// typecheck: tsc --noEmit    lint: eslint . --max-warnings=0
// test: vitest run           build: vite build

顺序有讲究:快的、根因最清晰的先跑(类型错误行号最精确),最重的 build 最后;&& 串行让 checker 拿到第一个失败点

07 验证记录

用隔离沙箱(/tmp,测完即删,不碰真实仓库)跑了两轮端到端验证

7.1 第一轮:cosineSimilarity(RAG 核心函数)

  • 场景:故意埋 bug——只算点积、没除模长、没做长度 / 零向量校验
  • 轮次:checker 判 FAILED(原样贴完整输出)→ builder 只改 cosine.mjs 一个文件、逐条对着失败项修 → checker 复验 PASSED(5/5)→ 编排器停止
  • 结论:闭环走通,职责隔离生效,信息无损传递让 builder 拿到行号一轮修好

7.2 第二轮:parseTags(标签提取,更严苛)

这一轮特意加了难度:2 个 bug(未去重 + 空标签),外加一段"引诱 builder 顺手重构"的诱饵函数 isWordChar

不只信 agent 自述,逐项独立核对:

  • checker 两轮都完整贴 diff / 行号 / 堆栈,零总结,判定准确
  • builder 一轮修好两个 bug
  • 诱饵函数 md5 完全不变0da4dbf9...)——builder 没上钩,守住了"最小改动"铁律
  • checker 复验 PASSED 5/5,循环收敛
  • checker 主动指出沙箱 check 缺 lint / build 步骤,诚实不放水
  • SubagentStop hook 在循环中未触发——这个坑见下节

08 踩坑与经验

8.1 信息无损传递:原文三个坑

原文点出的三个坑,本质是同一件事:信息在 Agent 之间传递时会被"好心"地压缩,而循环效率恰恰依赖信息无损传递

  1. builder 爱顺手改别的:读完一圈代码有冲动把看到的问题一起改,引入 deadcode / 类型错误多花一轮 → 提示词写死"最小改动、不碰无关代码",停止规则 3、4 条兜底(第二轮的诱饵函数专门测了这条)
  2. checker 报告太简会拖垮循环:只贴最后一行、不带上下文,builder 找不到根因瞎猜 → 提示词写死"保留完整输出、原样粘贴"
  3. 编排器爱自作主张总结:拿到失败报告先自己理解再转述,行号 / 堆栈全丢 → 提示词写死"逐字原样转发,不解读不过滤"

8.2 hook 需重载才生效

现象:两轮验证共 6 次 subagent 执行,.mango/loop.log 一个都没生成

定位:手动跑 loop-log.py 证明脚本本身完全正常,能写日志、能回显;真正原因是——SubagentStop hook 是会话中途才写进 settings 的,配置监听器只监听会话启动时已有 settings 文件的目录,中途新增的 hook 不会自动加载

解决:打开一次 /hooks 菜单(或重启 Claude Code)重载配置,此后 hook 才会在真实 /build-loop 里触发

经验:新增 hook 后务必 /hooks 或重启验证一次;不能只靠 pipe-test 脚本,那只证明脚本对,不证明 hook 被 harness 调用

8.3 验证方法论

  • 不只信 agent 自述:agent 的总结描述的是"意图",不是"实际";用 md5 核对诱饵函数、用独立命令复验,才是可信验证
  • 用沙箱隔离:验证 loop 一定在 /tmp 沙箱做,用真实可跑的 pnpm check(本机没 pnpm 就用 shim 转发到 npm + node 内置 --test / --check,零依赖);测完整个目录删掉,绝不污染真实仓库
  • 埋诱饵:想验"builder 守不守规矩",就在待修文件里放一段明显可优化但与失败无关的代码,事后核对它有没有被动过

09 前置依赖与落地顺序

关键前置:必须有可跑的 pnpm check——/build-loop 依赖它,而 Mango 目前还没 package.json(处于设计阶段);所以真正跑起来的前提是 M1 起脚手架时先把 pnpm check(tsc + eslint + vitest + build)搭起来,哪怕先只有一两个占位单测;没有它,checker 无从验证,loop 跑不起来

建议的落地顺序:

  1. /hooks 或重启,让 SubagentStop hook 生效
  2. M1 脚手架:建 package.json + pnpm check 四步 + Electron/Vite 骨架
  3. 用一个真实小任务(如 newCardID + 单测)跑一轮 /build-loop,确认权限不打断、hook 日志落地、循环收敛
  4. 之后每个功能都用 /build-loop <功能> 推进,checker 全绿才算该功能完成

一句话收尾:Loop 工程的核心价值不在于让 Agent 变聪明,而在于让验证变成内置环节;搭好 builder(只写)+ checker(只查、工具硬隔离)+ 编排器(循环 + 逐字转发)+ 停止规则,再配好防打断的权限和防黑盒的日志 hook,你就从质检员变回了需求方

参考资料

方法论来源:《全网爆火的 Loop Engineering,保姆教程来了!》(Datawhale / 筱可)

理论篇:本站《Loop 工程:从 prompter 到 loop 设计师

最后更新时间 2026/7/24 14:23:03