第 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 的积木省掉的事
fetch + SSE 解析 + tool_calls 累积pi-ai四种 wire 协议、30+ provider、怪癖兼容、认证解析、token 统计
runAgent while 循环pi-agent-coreAgent参数校验、并行工具执行、事件流、steering 队列、abort 语义
toolRead/Write/Edit/Bashpi-coding-agentcreateCodingTools()pi 的工具本体:截断、diff、图片、串行化,开箱即用

注意第三个包的角色:pi-coding-agent 不只是 CLI,它把工具实现、会话管理、压缩等全部作为库导出——pi CLI 本身就是用这些导出拼起来的。我们用它的 createCodingTools() 拿到的,就是 pi 自己用的那四个工具,一行不缺。

重构:四块拼一个 agent

1. 模型层(pi-ai):注册全部内置 provider,按环境变量选模型。认证不用管——每个 provider 自己解析环境变量(ANTHROPIC_API_KEYDEEPSEEK_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 树。这就是分层设计的回报:每一层都是可选的,你只为需要的复杂度付费

三层方案对照

裸写(第 02–07 章)pi-ai + agent-corecoding-agent SDK
代码量~500 行~100 行~20 行
理解深度逐字节理解理解接口与事件理解组装
provider 数量1 种协议30+30+
会话/压缩手写简版不管(留给你)内置完整版
适用学习、完全定制自己的产品级 agent嵌入 pi 的全部能力

第 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 长成你的样子