Appearance
🐣 小a的入职第一天
概念篇学完,小a正式入职 pi 工坊。老z递给它一张工牌:"从今天起,你要拆解整个工坊。先搞清楚——这工坊到底有几间房?每间房干嘛的?"
小a兴冲冲跑去数,回来报告:"报告师傅,有……呃,我没数清。"
老z乐了:"别急,我带你一间间走。"
从这一章起,我们正式进入 Part II 源码篇。和概念篇不同——这一篇会大量贴 pi 的真实源码。每个论断都有代码支撑,绝不瞎编。
16.1 工坊全景:9 间房
先看 pi 这个 monorepo 拆成了几个包。我们直接数源码:
📄 源码证据
packages/目录下的 package.json(共 9 个包)
| 包 | 真实描述 | 干嘛的 |
|---|---|---|
| pi-ai | Unified LLM API with automatic model discovery and provider configuration | 统一多厂商 LLM 调用(第 3 章讲的"翻译局") |
| pi-agent-core | General-purpose agent with transport abstraction, state management, and attachment support | 通用 Agent 运行时(第 7 章的 Loop 真身) |
| pi-coding-agent | Coding agent CLI with read, bash, edit, write tools and session management | 编码 Agent 产品(最终成品 CLI) |
| pi-tui | Terminal User Interface library with differential rendering | 终端 UI 库(差分渲染) |
| pi-protocol | Transport-neutral CBOR protocol for remote pi sessions | 远程会话的二进制协议 |
| pi-server | experimental server package for pi | 远程 Agent 服务器 |
| pi-client | Transport-neutral client for remote pi sessions over framed CBOR bytes | 远程会话客户端 |
| pi-evals | (无描述) | 评测体系 |
| pi-storage-sqlite-node | Node 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,用于跑评测) |
🧙 看出门道了吗?三个关键认知:
- pi-ai 是地基:agent-core 依赖它,coding-agent 依赖它。整个体系的基石。
- pi-tui 和 pi-protocol 是独立地基:它们不依赖任何内部包,可以单独被别的项目复用。
- 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 的好处:
- 协同改动:改了 pi-ai 的接口,能在一个仓库里同时改所有依赖它的包(agent-core、coding-agent),保证一致性。分散在多仓库,改一个要同步 N 个 PR。
- 统一构建/测试:一套构建配置(npm workspaces),一条命令构建所有包。
- 版本同步:发布时各包版本能对齐,避免"ai 升级了但 agent-core 没跟上"。
pi 用的是 npm workspaces(看根 package.json 的 workspaces 配置)+ TypeScript 的 paths 路径映射(看 tsconfig.json 的 compilerOptions.paths)。
📄 源码证据 根
package.json+tsconfig.jsonjson// 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 协议传消息 │
└────────────────────────────────────────┘🧙 看这张图,记住两件事:
- 数据是从上到下流的(用户命令 → 成品 → 运行时 → LLM 调用)
- 每一层只和相邻层说话(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 章)……每章都贴真实源码,讲清"它为什么这么设计"。
课后实验
- 亲自数包:克隆 pi 仓库,在
packages/下数 package.json 的数量,对照本章的 9 个包。看看你能不能找到那个藏在storage/sqlite-node里的第 9 个。 - 画依赖图:根据本章的依赖表,自己画一张依赖箭头图。验证:有没有循环依赖?(应该没有)
- 理解 monorepo:打开根
package.json,找到workspaces配置(注意packages/storage/*这条让第 9 个包被发现);打开tsconfig.json,找到compilerOptions.paths,看@earendil-works/*怎么映射到各包。
下一章:我们从最底层的地基开始拆——pi-ai 怎么用一套接口盖住几十家厂商? → 第 17 章 · pi-ai 源码