Skip to content

🐣 小a的入职第一天

概念篇学完,小a正式入职 pi 工坊。老z递给它一张工牌:"从今天起,你要拆解整个工坊。先搞清楚——这工坊到底有几间房?每间房干嘛的?"

小a兴冲冲跑去数,回来报告:"报告师傅,有……呃,我没数清。"

老z乐了:"别急,我带你一间间走。"

从这一章起,我们正式进入 Part II 源码篇。和概念篇不同——这一篇会大量贴 pi 的真实源码。每个论断都有代码支撑,绝不瞎编。


16.1 工坊全景:9 间房

先看 pi 这个 monorepo 拆成了几个包。我们直接数源码:

📄 源码证据 packages/ 目录下的 package.json(共 9 个包)

真实描述干嘛的
pi-aiUnified LLM API with automatic model discovery and provider configuration统一多厂商 LLM 调用(第 3 章讲的"翻译局")
pi-agent-coreGeneral-purpose agent with transport abstraction, state management, and attachment support通用 Agent 运行时(第 7 章的 Loop 真身)
pi-coding-agentCoding agent CLI with read, bash, edit, write tools and session management编码 Agent 产品(最终成品 CLI)
pi-tuiTerminal User Interface library with differential rendering终端 UI 库(差分渲染)
pi-protocolTransport-neutral CBOR protocol for remote pi sessions远程会话的二进制协议
pi-serverexperimental server package for pi远程 Agent 服务器
pi-clientTransport-neutral client for remote pi sessions over framed CBOR bytes远程会话客户端
pi-evals(无描述)评测体系
pi-storage-sqlite-nodeNode sqlite storage backend for pi-agent-core sessions会话的 SQLite 持久化后端

🧙 注意第 9 个包的特殊之处:

前 8 个都是 packages/包名/ 的结构,唯独 pi-storage-sqlite-node 藏在 packages/storage/sqlite-node/ 里。它是 pi-agent-core 的可选存储后端——agent-core 的会话能存成 SQLite,靠的就是它。

为什么单独成包?因为 SQLite 依赖原生模块,不该让核心包(agent-core)强制背上这个依赖。这是"可选依赖"的设计——核心保持纯净,重依赖按需加载。


16.2 房间之间的关系:依赖图

光知道有几间房不够,还得知道它们谁依赖谁。这是理解整个架构的关键。

📄 源码证据 各包 package.json 的 dependencies(只看内部依赖)

                    pi-coding-agent(成品 CLI)
                   ↗   |   |   |   ↖
                  /    |   |   |     \
        pi-agent-core  |   |   |    pi-tui
              ↓        |   |   |
            pi-ai ←────┘   |   |
                            |   |
              pi-client ←───┘   |
                  ↓             |
            pi-protocol ←───────┘

              pi-server ──→ pi-coding-agent, pi-ai, pi-protocol

整理成文字版依赖关系:

依赖谁
pi-ai(不依赖内部包,是地基)
pi-agent-core→ pi-ai
pi-tui(不依赖内部包,独立地基)
pi-protocol(不依赖内部包,独立地基)
pi-client→ pi-protocol
pi-coding-agent→ pi-agent-core, pi-ai, pi-client, pi-protocol, pi-tui
pi-server→ pi-ai, pi-coding-agent, pi-protocol
pi-evals(devDeps 里依赖 pi-ai, pi-coding-agent,用于跑评测)

🧙 看出门道了吗?三个关键认知:

  1. pi-ai 是地基:agent-core 依赖它,coding-agent 依赖它。整个体系的基石。
  2. pi-tui 和 pi-protocol 是独立地基:它们不依赖任何内部包,可以单独被别的项目复用。
  3. pi-coding-agent 是集大成者:它依赖 5 个包,把所有零件组装成最终产品。

16.3 依赖铁律:只能向下,不能向上

仔细看依赖图,你会发现一个规律:箭头都是"从上往下"(从成品指向地基),从不反过来。

🧙 依赖铁律:上层依赖下层,下层绝不依赖上层。

  • pi-ai(地基)→ 不知道 pi-agent-core 的存在
  • pi-agent-core → 不知道 pi-coding-agent 的存在

为什么这么严格? 因为如果下层依赖上层,就会形成循环依赖——改一处,牵动整条链,最后谁也动不了。单向依赖让每个包能独立编译、独立测试、独立发布。

一个真实的例子:agent-core 怎么"不认识" ai?

这里有个精妙的设计。回忆第 7 章——agent 的循环要调 LLM,那它不就得依赖 pi-ai 吗?

它确实依赖 pi-ai(看依赖表),但它"用得很有分寸":

🧙 agent-core 不直接调用 pi-ai 的具体函数,而是依赖一个抽象的"流函数"接口(streamFn)。pi-ai 实现了这个接口,agent-core 只认接口、不认实现。

这叫依赖倒置——agent-core 定义"我需要一个能流式调 LLM 的函数",pi-ai 来填这个坑。这样 agent-core 理论上能接任何实现 streamFn 的东西(比如测试用的假实现)。

这个设计我们在第 18 章拆 agent-core 时会详细看到。


16.4 三个设计原则(后面每章都在印证)

老z让小a记住三个原则,因为 Part II 后面每一章都是在印证它们:

原则一:关注点分离(Separation of Concerns)

🧙 每一层只干一件事,不越界。

  • pi-ai 只管"怎么调 LLM"(不管 agent 逻辑)
  • pi-agent-core 只管"agent 怎么循环"(不管具体工具)
  • pi-coding-agent 只管"编程场景的产品化"(不管 LLM 细节)
  • pi-tui 只管"怎么画界面"(不管 agent 在干嘛)

好处:改一层不影响别的层。换 LLM 厂商?只动 pi-ai。改循环逻辑?只动 agent-core。换界面?只动 tui。

原则二:依赖倒置(Dependency Inversion)

🧙 上层定义接口,下层实现接口。上层不依赖具体实现。

刚才讲的 streamFn 就是例子。还有 coding-agent 不直接依赖具体的 LLM 调用,而是依赖 agent-core 的抽象。

好处:可替换、可测试(用假实现测真逻辑)。

原则三:可独立发布(Independent Publishing)

🧙 每个包都是独立的 npm 包,能被别的项目单独复用。

你写个自己的项目,只想用 pi 的 LLM 统一调用?npm install @earendil-works/pi-ai 就行,不用装整个 pi。只想用终端 UI 库?单独装 pi-tui。

好处:生态共享,不强迫用户装一坨用不到的东西。


16.5 为什么是 monorepo?

小a有个疑问:"拆成 9 个包,为什么不做成 9 个独立仓库,非要放一个 monorepo 里?"

🧙 monorepo 的好处:

  1. 协同改动:改了 pi-ai 的接口,能在一个仓库里同时改所有依赖它的包(agent-core、coding-agent),保证一致性。分散在多仓库,改一个要同步 N 个 PR。
  2. 统一构建/测试:一套构建配置(npm workspaces),一条命令构建所有包。
  3. 版本同步:发布时各包版本能对齐,避免"ai 升级了但 agent-core 没跟上"。

pi 用的是 npm workspaces(看根 package.json 的 workspaces 配置)+ TypeScript 的 paths 路径映射(看 tsconfig.json 的 compilerOptions.paths)。

📄 源码证据package.json + tsconfig.json

json
// package.json(简化,实际是 7 项数组)
{
  "workspaces": [
    "packages/*",
    "packages/storage/*",           // ← 第 9 个包(storage/sqlite-node)靠这条被发现
    "packages/coding-agent/examples/extensions/*"
  ]
}

// tsconfig.json 用 compilerOptions.paths 做包间路径映射(不是 references)
{
  "compilerOptions": {
    "paths": { "@earendil-works/*": ["packages/*/src"] }
  }
}

注意:第 9 个包 storage/sqlite-node 藏在二级目录,靠 packages/storage/* 这条 workspace 规则才被发现(这也是它"特殊"的原因)。


16.6 一张总图:数据怎么从底层流到成品

最后,老z给小a画了一张"全链路图",展示一次完整的 agent 调用,数据怎么穿越各层:

用户敲命令: pi "帮我改 config.ts"

┌─ pi-coding-agent(成品)──────────────────────┐
│  解析命令 → 加载配置 → 启动 agent-session    │
│        ↓                                      │
│  ┌─ pi-agent-core(Agent 运行时)──────────┐ │
│  │  agent-loop 循环:                      │ │
│  │   ① 准备消息(system+history+问题)     │ │
│  │   ② 调 streamFn ─────────────┐         │ │
│  │   ③ 收到 toolCall             │         │ │
│  │   ④ 执行工具(read/edit/bash)│         │ │
│  │   ⑤ 回喂 → 循环               │         │ │
│  │   ⑥ 输出事件 ──────┐          │         │ │
│  └────────────────────│──────────┘         │ │
│                       │          │         │ │
│  ┌─ pi-tui(界面)│         │         │ │
│  │  接收事件 → 差分渲染到终端 ←───┘         │ │
│  └──────────────────────────────────────────┘│
└──────────────────────────────────────────────┘

                       ↓ (streamFn 的实现)
              ┌─ pi-ai(LLM 调用)────────────────┐
              │  找到 provider(如 anthropic)    │
              │  翻译成厂商格式 → 发 HTTP 请求   │
              │  收流式响应 → 翻译回统一格式     │
              └──────────────────────────────────┘

                       ↓ (如果用远程会话)
              ┌─ pi-server / pi-client / pi-protocol ─┐
              │  把 agent 跑在服务器,客户端只显示     │
              │  用 CBOR 协议传消息                    │
              └────────────────────────────────────────┘

🧙 看这张图,记住两件事:

  1. 数据是从上到下流的(用户命令 → 成品 → 运行时 → LLM 调用)
  2. 每一层只和相邻层说话(coding-agent 不直接调 LLM,它通过 agent-core,agent-core 通过 streamFn 接 pi-ai)

这就是分层的力量:任何一层都能被替换,不影响别的层。


本章小结

🐣 小a的第十六课(Part II 开篇)

┌──────── pi 工坊全景 ────────┐
│                            │
│  • 9 间房(9 个包)         │
│    ai/agent-core/coding-   │
│    agent/tui/protocol/     │
│    server/client/evals/    │
│    storage-sqlite          │
│                            │
│  • 依赖只能向下:           │
│    coding-agent → core     │
│    → ai(地基)             │
│    tui/protocol 独立地基   │
│                            │
│  • 三原则:                 │
│    关注点分离               │
│    依赖倒置(streamFn)     │
│    可独立发布               │
│                            │
│  • monorepo:统一管理 +     │
│    协同改动 + 版本同步      │
└────────────────────────────┘

接下来 7 章:我们自底向上,逐层拆解。从地基 pi-ai 开始(第 17 章),到 agent-core(第 18 章),再到成品 coding-agent(第 20 章)……每章都贴真实源码,讲清"它为什么这么设计"。


课后实验

  1. 亲自数包:克隆 pi 仓库,在 packages/ 下数 package.json 的数量,对照本章的 9 个包。看看你能不能找到那个藏在 storage/sqlite-node 里的第 9 个。
  2. 画依赖图:根据本章的依赖表,自己画一张依赖箭头图。验证:有没有循环依赖?(应该没有)
  3. 理解 monorepo:打开根 package.json,找到 workspaces 配置(注意 packages/storage/* 这条让第 9 个包被发现);打开 tsconfig.json,找到 compilerOptions.paths,看 @earendil-works/* 怎么映射到各包。

下一章:我们从最底层的地基开始拆——pi-ai 怎么用一套接口盖住几十家厂商? → 第 17 章 · pi-ai 源码