Loop Engineering 落地 pi:从六块积木到可跑驱动器
前两篇把 Loop Engineering 的概念、演进谱系、案例都拆完了。这篇不再讲”它是什么”,回答一个更落地的问题:怎么让你手上的 agent 真的跑出一个 loop?
被解剖的对象是 pi——一个最小化的终端 coding harness。选它有两个原因:一是它的文档和 SDK 公开可读,所有 API 都能在源码里核实;二是它代表了一类”故意不做子代理、故意不做 plan mode”的克制型 harness,正适合用来演示 Loop Engineering 的核心论点——loop 不是产品功能,是你写的一个驱动器。
一、先认清一件事:pi 没有”loop”功能,是故意的
pi 的 README 里有一句开宗明义的话:
Pi ships with powerful defaults but skips features like sub agents and plan mode. Instead, you can ask pi to build what you want or install a third party pi package that matches your workflow.
这恰恰是 Loop Engineering 的本意。回想第二篇里 Addy Osmani 的判断——“一旦看出 Codex app 和 Claude Code 的六块积木形状相同,就不再争论用哪个工具,而是去设计跨工具都能跑的 loop”。pi 把这句话推到极致:它连子代理和工作树都不替你做,但把做这些事需要的每一个钩子都暴露成了 API。
所以”让 pi 跑出 loop”这句话,翻译成工程语言就是:写一个大约 80 行的驱动程序,把 pi 的原语按 Loop Engineering 的五动作(Discovery → Handoff → Verification → Persistence → Scheduling)串起来。下面逐块拆。
二、六块积木在 pi 里的对照表
把第二篇的六块积木逐一映射到 pi 的原生 API(每一条都可在 pi 的 docs/ 和 examples/ 里核实):
| 积木 | pi 的对应物 | 出处 |
|---|---|---|
| Automation(调度触发) | 扩展的 session_start 钩子 + pi.exec 跑 cron;或外部 fs.watch 监听触发文件 → pi.sendMessage({triggerTurn:true}) 注入一轮 turn | examples/extensions/file-trigger.ts |
| Worktree(并行不踩脚) | pi 不内置,但扩展里能 pi.exec("git", ["worktree","add",...]);git-checkpoint.ts 已示范每轮 git stash create 做检查点 | examples/extensions/git-checkpoint.ts |
| Skill(项目知识) | pi 原生实现 Agent Skills 标准,SKILL.md 按需加载(progressive disclosure),/skill:name 强制调用 | docs/skills.md |
| Connector(MCP / 外部工具) | pi 的 MCP gateway(mcp 工具)+ 扩展 pi.registerTool() 把任意外部 API 包成模型可调工具 | docs/extensions.md Custom Tools |
| Sub-agent(maker/checker) | pi 不内置子代理,但两条路:① SDK 开第二个 createAgentSession() 换模型 + 只读工具当 checker;② 扩展 registerTool 内部直接 complete() 调第二个模型——handoff.ts 就是这招 | examples/extensions/handoff.ts、examples/sdk/01-minimal.ts |
| Memory(跨 session 状态) | session 本身就是落盘 JSONL 的记忆(SessionManager.create(),/tree//fork/compaction 分支摘要);外加磁盘 STATE.md,appendEntry()/getBranch() 可程序读写 | docs/sessions.md、docs/session-format.md |
| 积木 | pi 内置? | 补法 |
|---|---|---|
| Automation | 半内置(有钩子无调度) | cron + 一个 file-trigger 扩展 |
| Worktree | 无 | 扩展里 pi.exec("git worktree ...") |
| Skill | ✅ 原生 | 直接写 SKILL.md |
| Connector | ✅ 原生(MCP) | 配 MCP server 或 registerTool |
| Sub-agent | 无 | 第二个 session / complete() |
| Memory | ✅ 原生 | SessionManager + STATE.md |
一句话总结这张表:缺的只有 Sub-agent 和 Worktree 两块,且都是”扩展里几行代码”能补上的——pi 的设计哲学就是不替你做,但把钩子全给你。
三、驱动 loop 的三条路
pi 有三种非交互模式(见 docs/usage.md),任选一种当 loop 引擎:
| 方式 | 命令 / API | 适合 |
|---|---|---|
| RPC 模式 | pi --mode rpc,stdin/stdout 跑 JSONL:prompt / get_state / get_entries / fork / set_model / bash + 事件流 agent_end / tool_execution_end | 跨语言驱动(Python / bash 都能写),最轻量,推荐做生产 loop |
| JSON 事件流 | pi --mode json | 一次性 print + 结构化事件,适合简单一问一答 |
| SDK 内嵌 | createAgentSession() + session.prompt() + session.subscribe() | TypeScript 原生,verifier 能直接 complete() 换模型,推荐做原型 |
下面先给 SDK 版骨架(最短、最贴近第二篇那段 Python 的结构);但实测时发现 SDK 在自定义 provider 环境下有坎(见本节末「实测」),生产跑改用 RPC 版——两者逻辑相同。
四、最小可跑骨架(SDK 版)
和第二篇那段 Python 一一对应,只是把驱动器从 Anthropic SDK 换成 pi SDK。三个结构性选择保持不变:客观反馈信号、独立 verifier、磁盘状态接力。
// pi-loop.ts — 在 pi 上跑出 Loop Engineering 的最小骨架import { createAgentSession, SessionManager, AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";import { readFileSync, writeFileSync, existsSync } from "node:fs";import { execSync } from "node:child_process";
const GOAL = process.argv[2] ?? "fix: single quote in company name crashes bill save";const STATE = "STATE.md";const RUBRIC = "uv run pytest passes (all green); no syntax errors; diff <= 200 lines";const MAX_ITERS = 20;
// 反馈信号——loop 的命脉(客观二元判定,不是 agent 自评)function feedback() { try { const out = execSync("uv run pytest", { encoding: "utf-8", cwd: process.cwd() }); return { pass: /passed/.test(out) && !/failed/.test(out), out }; } catch (e: any) { return { pass: false, out: String(e) }; }}const loadMemory = () => existsSync(STATE) ? readFileSync(STATE, "utf-8") : "(no prior)";const saveMemory = (i: number, v: string) => writeFileSync(STATE, `\n## iter ${i}\nverdict: ${v}\n`, { flag: "a" });
// 认证与模型注册表——maker 和 checker 共用一份const authStorage = AuthStorage.create();const modelRegistry = ModelRegistry.create(authStorage);// 用 registry.find 拿模型(接受任意字符串 id;getModel 要字面量联合类型,不实用)const resolveModel = (id: string) => modelRegistry.find("anthropic", id)!;
// 生成器——一个落盘的 pi session(session.jsonl 就是它的记忆外存)const { session: maker } = await createAgentSession({ cwd: process.cwd(), sessionManager: SessionManager.create(process.cwd()), tools: ["read", "bash", "edit", "write"], model: resolveModel(process.env.LOOP_MAKER_MODEL ?? "claude-opus-4-8"), authStorage, modelRegistry,});
for (let i = 0; i < MAX_ITERS; i++) { const { pass, out } = feedback(); if (pass) { console.log(`done in ${i} iterations`); break; }
// maker:读 STATE.md 接力上一轮,做最小修复 await maker.prompt(`Goal: ${GOAL}
Prior attempts and rejected approaches (from STATE.md):${loadMemory()}
Latest test feedback:${out}
Diagnose the ROOT cause (not symptom), make the smallest fix,do not refactor unrelated code.`);
// 验证器——独立 session + 换模型 + 只读工具,绝不让 maker 批自己的作业 const verdict = await verifier(GOAL, RUBRIC, out); saveMemory(i, verdict); // 落盘——agent 会忘,仓库不会 if (verdict.startsWith("PASS")) { console.log(`passed at iter ${i}`); break; }}maker.dispose();
async function verifier(goal: string, rubric: string, testOut: string) { const { session: checker } = await createAgentSession({ sessionManager: SessionManager.inMemory(), // 干净上下文,不继承 maker 的对话 tools: ["read", "grep"], // 只读——checker 不改代码,只评分 model: resolveModel(process.env.LOOP_CHECKER_MODEL ?? "claude-haiku-4-5"), // 故意换一个模型 authStorage, modelRegistry, }); const chunks: string[] = []; const unsub = checker.subscribe(e => { if (e.type === "message_update" && e.assistantMessageEvent.type === "text_delta") chunks.push(e.assistantMessageEvent.delta); }); await checker.prompt(`Goal: ${goal}Rubric: ${rubric}Test output:${testOut.slice(0, 4000)}
Does the current repo meet the rubric? Reply ONLY 'PASS' or 'FAIL: <reason>'.`); unsub(); checker.dispose(); return chunks.join("");}三个结构性选择为什么这样写
-
feedback()跑pytest—— 这是客观二元信号。没有它就只是链式调用,loop 永远不知道”做完了没有”。第二篇引用过那句:“A loop is not ‘run the agent again on a schedule.’ That is just a more expensive way to fail repeatedly.”——反馈信号就是区分这两者的那条线。 -
verifier()是独立 session + 只读工具 + 换模型 —— 这是 maker/checker 分家在 pi 上的落地。三个缺一不可:- 独立 session(
SessionManager.inMemory())→ checker 不继承 maker 的对话,不会”被自己说服”; - 只读工具(
tools:["read","grep"])→ checker 不能改代码,只能评分; - 换模型(
claude-haiku-4-5)→ 不同模型有不同的偏见,交叉验证更可靠。
这等价于
handoff.ts里用complete()起一个干净上下文的做法——它注释里写得很直白:“Instead of compacting (which is lossy), handoff extracts what matters”——宁可起干净 session 也不用有损压缩。 - 独立 session(
-
STATE.md+SessionManager.create()(落盘) —— 跨 session 接力。“模型会忘,仓库不会”在 pi 上的具体落地:session.jsonl 是 maker 的长期记忆,STATE.md 是跨轮的进度看板。明天接着今天跑,而不是从零重复。
实测:SDK 在自定义 provider 下的坎
上面 SDK 骨架是原型,但实际跑时遇到一个坎:pi 的自定义 provider(如 alibaba-cloud,由 pi-alibaba-models 这个 package 在 session_start 时 pi.registerProvider() 注册)只在 pi 进程完整启动后才可用。用 SDK 的 createAgentSession() 时,modelRegistry.find("alibaba-cloud", ...) 在 session 创建前返回 undefined——provider 还没注册。结果是 session 起来了、agent_end 触发了,但 text_delta 全空,模型根本没被调。
解法是改走 RPC:让 pi 进程自己加载 packages、注册 provider、用 settings.json 的默认模型。RPC 下 pi --mode rpc 是完整启动的 pi,package 扩展会跑、provider 会注册、模型会真被调。这条路线在 ~/.local/share/mini-agent 里已经验证可跑通(它的 tasks/pi_rpc.py 就是这么驱动的)。还有一个 verifier 的坑:prompt 走 argv 会让 pi -p 崩溃(SIGKILL),必须走 stdin。下面 RPC 版的骨架就是实测能跑通的版本。
RPC 版的可跑骨架
不想嵌入 Node?把上面 SDK 调用换成给 pi --mode rpc 进程写 JSONL,逻辑完全相同:
# 启动一个落盘的 maker sessionpi --mode rpc --name loop-maker --session-dir .pi/loop
# 驱动器(Python/bash 都行)给 stdin 写一行,等 stdout 的 agent_end 事件echo '{"id":"r1","type":"prompt","message":"Goal: ..."}' | ...echo '{"type":"get_entries"}' # 读进度echo '{"type":"fork"}' # 起一个 checker 分支echo '{"type":"set_model","provider":"alibaba-cloud","model":"qwen3.7-max"}'get_entries / fork / set_model / bash 这些 RPC 命令覆盖了上面所有 SDK 调用,agent_end 事件就是驱动循环继续的信号。RPC 模式的好处是驱动器可以用任何语言写、且不受 SDK 的 provider 注册时机问题影响;坏处是 verifier 要起第二个进程(或 fork 一个新 session 再 set_model)。
五、补两块 pi 没内置的积木
Automation:让 loop 自己定时转
一个 ~20 行扩展 + cron,直接复刻 file-trigger.ts:
import * as fs from "node:fs";import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) { pi.on("session_start", () => { fs.watch("/tmp/pi-loop-trigger", () => { // cron 每小时 echo "run" > 此文件 const goal = fs.readFileSync("/tmp/pi-loop-trigger", "utf-8").trim(); if (goal) pi.sendMessage( { customType: "cron-loop", content: goal, display: true }, { triggerTurn: true }, // 触发一轮 turn ); }); });}配合 echo "fix flaky test X" > /tmp/pi-loop-trigger(或 crontab 定时),就是”系统自己发现工作并触发”。pi 的 sendMessage + triggerTurn:true 是把外部事件变成 agent turn 的标准入口——file-trigger.ts 的注释原话是 “Useful for external systems to send messages to the agent”。
Worktree:并行 agent 不踩脚
在 turn_start 钩子里开 git worktree,git-checkpoint.ts 的同款 pi.exec:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) { pi.on("turn_start", async (_e, ctx) => { // 给每个并行 task 一个独立 checkout,互不踩文件 await pi.exec("git", ["worktree", "add", "-B", `loop-${ctx.sessionId}`, "../wt-loop"]); });
pi.on("session_shutdown", async () => { await pi.exec("git", ["worktree", "remove", "../wt-loop", "--force"]); });}git-checkpoint.ts 已经示范了在 turn_start 里 pi.exec("git",["stash","create"]) 的模式——worktree 只是把它从 stash 换成 worktree add,机械碰撞的解决思路完全一致。
六、把四条”静默成本”落成具体检查点
第二篇结尾说过 loop 的四个静默成本(验证债 / 理解腐化 / 认知让渡 / token 爆炸)。在 pi 上,它们各自有对应的机械防线:
| 静默成本 | pi 上的机械防线 |
|---|---|
| 验证债 | verifier 强制只读 + 换模型;handoff.ts 的”宁可起新 session 不用有损压缩” |
| token 爆炸 | MAX_ITERS + feedback() 提前退出 + STATE.md 让下一轮从进度接力而非重跑历史——三道闸 |
| 理解腐化 | pi 的 /tree + 分支摘要让你回看每个被否的分支;定期从”生产模式”切回”发现模式”亲自跑一轮 |
| 认知让渡 | Skill 文档(SKILL.md)把”当年那个事故后定下的规矩”写下来——判断下放,但权威留在写文档的人手里 |
七、和 Claude Code / Codex 的对应
第二篇给过一张”Claude Code 里的六块积木对应物”表。pi 作为最克制的 harness,对应关系是这样的:
| 积木 | Claude Code | Codex app | pi |
|---|---|---|---|
| Automation | /loop、cron、hooks、GitHub Actions | Automations tab、/goal | 扩展 session_start + cron + sendMessage |
| Worktree | git worktree、--worktree | 内置 per-thread | pi.exec("git worktree")(扩展里手动) |
| Skill | SKILL.md | SKILL.md | SKILL.md(原生) |
| Connector | MCP | MCP | MCP gateway(原生) |
| Sub-agent | .claude/agents/ | .codex/agents/ | 第二个 session / complete()(手动) |
| Memory | CLAUDE.md、STATE.md、看板 | Markdown、Linear | session.jsonl(原生)+ STATE.md |
可以看出 pi 的策略:原生提供”信息层”(Skill / Connector / Memory),把”编排层”(Automation / Worktree / Sub-agent)留给你。这和 Addy Osmani说的”看出形状相同就不再争工具”是一致的——pi 干脆把编排层完全交出,逼你把 loop 显式写出来,而不是依赖某个 /loop 内置命令的黑盒。代价是多写 ~80 行驱动器,收益是 loop 的每一步都看得见、改得动。
八、一个完整可跑的例子
光有骨架不够,需要一个能真跑起来的最小场景。下一节给出完整项目结构(含一个故意写坏的 bill.py 让 loop 去修)。核心文件三件:
pi-loop/├── pi-loop.ts # 上面那段驱动器├── extensions/│ ├── cron-loop.ts # Automation 扩展│ └── worktree-per-turn.ts # Worktree 扩展├── skill/│ └── SKILL.md # 项目约定固化("最小修复、不重构无关代码"等)├── app/│ ├── bill.py # 故意写坏的:单引号公司名崩 save│ ├── test_bill.py # 会被 loop 反复跑的客观信号│ └── STATE.md # 跨轮进度(loop 自己读写)└── package.json运行:npx tsx pi-loop.ts "fix: company name with single quote crashes bill save"——你会看到 maker 反复跑 test → 改代码 → checker 评分 → STATE.md 接力,直到 pytest 全绿或撞 MAX_ITERS。
这个项目已经从骨架落地成可跑代码,下一节给出完整结构——三件扩展加 SKILL.md 都是可直接 pi --skill ./skill 和放进 ~/.pi/agent/extensions/ 挂载的。
九、总结:loop 是写出来的,不是开出来的
回到开头那句——pi 没有”loop”功能,是故意的。把这篇压缩成一句:
在 pi 上跑 loop = 写一个驱动器,用
createAgentSession()当 maker、第二个只读 session 当 checker、STATE.md+ 落盘 session 当记忆、cron + file-trigger 扩展当调度、pi.exec("git worktree")当并行隔离。pi 原生给你 Skill / Connector / Memory 三块信息层积木,编排层那三块(Automation / Worktree / Sub-agent)你用 ~80 行写出来。
这 80 行里有三条不能省的命脉,和前两篇一脉相承:
- 反馈信号(
feedback()跑pytest)——没有它,loop 只是在更快地烧 token。 - 独立 verifier(只读 + 换模型 + 干净上下文)——agent 不能批自己的作业。
- 磁盘状态(
STATE.md+ 落盘 session)——模型会忘,仓库不会。
用 Addy Osmani 收尾第二篇的那句话收尾这篇,因为它在 pi 上格外贴切:“Build the loop. But build it like someone who intends to stay the engineer, not just the person who presses go.” pi 把那颗”启动键”都省了,逼你亲手搭——这恰恰是 Loop Engineering 最诚实的形态。
参考来源
- pi 官方文档:README、
docs/skills.md、docs/extensions.md、docs/sessions.md、docs/session-format.md、docs/rpc.md、docs/usage.md - pi 示例代码:
examples/sdk/01-minimal.ts、examples/sdk/02-custom-model.ts、examples/extensions/handoff.ts、examples/extensions/file-trigger.ts、examples/extensions/git-checkpoint.ts - Addy Osmani《Loop Engineering》(Substack)——“loop engineering sits one floor above the harness”
- 第二篇《Loop Engineering(循环工程):从概念到实战》——五动作六积木、五杠杆、腾讯案例
- 第一篇《Loop Engineering 深度解析》——与 Harness/Context/Hermes Engineering 的四层对比
- Agent Skills 标准——pi 原生实现的 skill 格式