3498 words
17 minutes
Loop Engineering 落地 pi:从六块积木到可跑驱动器

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}) 注入一轮 turnexamples/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.tsexamples/sdk/01-minimal.ts
Memory(跨 session 状态)session 本身就是落盘 JSONL 的记忆(SessionManager.create()/tree//fork/compaction 分支摘要);外加磁盘 STATE.mdappendEntry()/getBranch() 可程序读写docs/sessions.mddocs/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("");
}

三个结构性选择为什么这样写#

  1. feedback()pytest —— 这是客观二元信号。没有它就只是链式调用,loop 永远不知道”做完了没有”。第二篇引用过那句:“A loop is not ‘run the agent again on a schedule.’ That is just a more expensive way to fail repeatedly.”——反馈信号就是区分这两者的那条线。

  2. 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 也不用有损压缩

  3. STATE.md + SessionManager.create()(落盘) —— 跨 session 接力。“模型会忘,仓库不会”在 pi 上的具体落地:session.jsonl 是 maker 的长期记忆,STATE.md 是跨轮的进度看板。明天接着今天跑,而不是从零重复。

实测:SDK 在自定义 provider 下的坎#

上面 SDK 骨架是原型,但实际跑时遇到一个坎:pi 的自定义 provider(如 alibaba-cloud,由 pi-alibaba-models 这个 package 在 session_startpi.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,逻辑完全相同:

Terminal window
# 启动一个落盘的 maker session
pi --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

~/.pi/agent/extensions/cron-loop.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

~/.pi/agent/extensions/worktree-per-turn.ts
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_startpi.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 CodeCodex apppi
Automation/loop、cron、hooks、GitHub ActionsAutomations tab、/goal扩展 session_start + cron + sendMessage
Worktreegit worktree--worktree内置 per-threadpi.exec("git worktree")(扩展里手动)
SkillSKILL.mdSKILL.mdSKILL.md(原生)
ConnectorMCPMCPMCP gateway(原生)
Sub-agent.claude/agents/.codex/agents/第二个 session / complete()(手动)
MemoryCLAUDE.md、STATE.md、看板Markdown、Linearsession.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 行里有三条不能省的命脉,和前两篇一脉相承:

  1. 反馈信号feedback()pytest)——没有它,loop 只是在更快地烧 token。
  2. 独立 verifier(只读 + 换模型 + 干净上下文)——agent 不能批自己的作业。
  3. 磁盘状态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 官方文档:READMEdocs/skills.mddocs/extensions.mddocs/sessions.mddocs/session-format.mddocs/rpc.mddocs/usage.md
  • pi 示例代码:examples/sdk/01-minimal.tsexamples/sdk/02-custom-model.tsexamples/extensions/handoff.tsexamples/extensions/file-trigger.tsexamples/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 格式
Loop Engineering 落地 pi:从六块积木到可跑驱动器
https://sgjki547.top/posts/loop-engineering-落地-pi-从积木到可跑驱动器/
Author
SGJki
Published at
2026-07-12
License
CC BY-NC-SA 4.0