第 08 章:用 pi 的官方积木重构 mini-pi
第 01–07 章,我们徒手实现了一个 coding agent 的全部核心:loop、工具、流式、会话、压缩。这一章换个姿势——用 pi 官方发布的三个包,把同样的事重做一遍。你会看到每一层抽象到底替我们省掉了什么,以及为什么 pi 的分层设计让"拼一个自己的 agent"变成几十行代码的事。
先说结论:裸写版约 500 行;pi 栈版约 100 行,能力还更强。完整代码在 code/mini-pi/。
积木清单
npm install @earendil-works/pi-ai @earendil-works/pi-agent-core @earendil-works/pi-coding-agent
注意第三个包的角色:pi-coding-agent 不只是 CLI,它把工具实现、会话管理、压缩等全部作为库导出——pi CLI 本身就是用这些导出拼起来的。我们用它的 createCodingTools() 拿到的,就是 pi 自己用的那四个工具,一行不缺。
重构:四块拼一个 agent
1. 模型层(pi-ai):注册全部内置 provider,按环境变量选模型。认证不用管——每个 provider 自己解析环境变量(ANTHROPIC_API_KEY、DEEPSEEK_API_KEY……),也可以用 MINI_PI_API_KEY 显式覆盖。
src/main.ts(模型层)
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
const provider = process.env.MINI_PI_PROVIDER ?? "anthropic";
const modelId = process.env.MINI_PI_MODEL ?? "claude-sonnet-4-5";
const models = builtinModels();
const model = models.getModel(provider, modelId);
if (!model) {
console.error(`Unknown model: ${provider}/${modelId}`);
process.exit(1);
}
2. 运行时(pi-agent-core):Agent 类接管了整个 loop——第 02 章的 while、第 04 章的流式累积和 abort、工具参数校验、并行执行,全部内置:
src/main.ts(agent)
import { Agent } from "@earendil-works/pi-agent-core";
import { convertToLlm, createCodingTools } from "@earendil-works/pi-coding-agent";
const agent = new Agent({
initialState: {
systemPrompt: SYSTEM_PROMPT, // 与裸写版同款提示词
model,
tools: createCodingTools(process.cwd()), // pi 的 read/bash/edit/write 本体
},
convertToLlm, // pi-coding-agent 的消息转换(AgentMessage[] → LLM Message[])
getApiKey: async () => process.env.MINI_PI_API_KEY,
});
convertToLlm 是第 02 章埋下的概念的现身:loop 内部流转的是 pi 自己的 AgentMessage(带 usage、stopReason、thinking 块),发给 LLM 前才转成各家 wire 格式——这个转换函数由 coding-agent 提供。
3. 渲染:订阅事件。第 04 章我们手写 onText 回调;pi 的 loop 对一切发事件,UI 只是消费者——这正是 pi 四种运行模式能共用一个核心的原因:
src/main.ts(渲染)
agent.subscribe((event) => {
switch (event.type) {
case "message_update":
if (event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
break;
case "tool_execution_start":
console.log(`\x1b[36m[${event.toolName}]\x1b[0m`);
break;
// ……tool_execution_end / message_end 略,见 code/mini-pi/src/main.ts
}
});
4. REPL 与中断:agent.prompt(text) 驱动一轮;agent.abort() 中止,部分结果自动留在 agent.state.messages 里——第 04 章我们手动实现的语义,现在是默认行为。
src/main.ts(REPL 核心)
rl.on("line", (line) => {
const text = line.trim();
if (!text || running) return rl.prompt();
running = true;
agent.prompt(text).finally(() => {
running = false;
rl.prompt();
});
});
rl.on("SIGINT", () => {
if (running) return agent.abort();
rl.close();
});
跑起来:MINI_PI_PROVIDER=deepseek MINI_PI_MODEL=deepseek-v4-flash DEEPSEEK_API_KEY=sk-... npx tsx src/main.ts。
模型 ID 从哪来
pi-ai 内置了每个 provider 的模型目录(由 OpenRouter 与 models.dev 生成)。可以用 models.getModels(provider) 列出,或跑 npx tsx -e "import {builtinModels} from '@earendil-works/pi-ai/providers/all'; console.log(builtinModels().getModels('anthropic').map(m=>m.id))" 查看。
再上一层:coding-agent SDK
Agent 之上还有一层。pi-coding-agent 的 SDK 把会话管理、自动压缩、AGENTS.md 加载、技能/扩展发现也组装好了——也就是 pi CLI 去掉 TUI 的样子。官方快速开始(docs/sdk.md):
src/sdk-demo.ts
import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
const modelRuntime = await ModelRuntime.create();
const { session } = await createAgentSession({
sessionManager: SessionManager.inMemory(),
modelRuntime,
});
session.subscribe((event) => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
});
await session.prompt(process.argv[2] ?? "What files are in the current directory?");
二十行,得到一个"完整版 pi"的会话:session.prompt / steer / followUp / compact / abort 俱全,上下文接近上限时自动压缩,SessionManager 换成文件模式就是第 06 章的 JSONL 树。这就是分层设计的回报:每一层都是可选的,你只为需要的复杂度付费。
三层方案对照
第 09 章会在这条路上继续走:自定义工具、事件钩子,以及 pi 的扩展哲学如何让你在不 fork 的情况下改造一切。
本章要点
- pi 的三个包就是本教程三阶段的各自终点:LLM 层、loop 层、产品层,每层都可以独立替换。
createCodingTools() 给你 pi 的工具本体;Agent 给你 pi 的 loop 本体——mini-pi 因此与 pi 架构同构。
- 事件流是 loop 与 UI 之间唯一的契约;渲染打字机效果和实现 JSON/RPC 模式,只是不同的订阅者。
- 验证状态:
code/mini-pi 已通过 tsc --noEmit(包版本 0.80.10);端到端运行需要你自己的 API key。
下一章:第 09 章:进阶——让 agent 长成你的样子。