01 — 整体架构与包依赖
问题
一个生产级 Agent 系统涉及 LLM 调用、工具执行、状态管理、会话持久化、终端渲染、多进程协作等多个关注点。如果把它们全部塞进一个包,代码会迅速变成无法维护的巨型模块。
Pi 的解法是 严格分层:每一层只依赖下一层,职责边界清晰。
11 个包的职责
| 包名 |
npm 包 |
职责 |
层级 |
telemetry |
pi-telemetry |
厂商中立的遥测契约(span、属性、schema 类型推断) |
基础设施 |
ai |
pi-ai |
多 provider LLM 统一接口、模型管理、认证 |
LLM 层 |
agent |
pi-agent-core |
Agent 运行时:主循环、工具调用、状态机、compaction |
运行时层 |
protocol |
pi-protocol |
传输中立的二进制 RPC 协议(CBOR + 长度前缀帧) |
通信层 |
session-backends |
pi-session-backend-sqlite-node |
SQLite 会话持久化实现 |
存储层 |
coding-agent |
pi-coding-agent |
交互式编码 Agent CLI(工具、扩展、系统提示、运行模式) |
产品层 |
tui |
pi-tui |
终端 UI 库(差分渲染、组件模型、键盘输入) |
UI 层 |
chord |
chord |
应用组合运行时(服务、复制状态、RPC、插件) |
分布式层 |
server |
— |
服务端会话路由(基于 protocol) |
分布式层 |
client |
— |
客户端连接(基于 protocol) |
分布式层 |
evals |
— |
评估测试框架 |
测试 |
依赖关系图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| ┌─────────────┐ │ telemetry │ 基础设施(零依赖) └──────┬──────┘ │ TelemetryContext ┌──────▼──────┐ │ ai │ LLM 抽象层 └──────┬──────┘ │ Model, StreamFn, Usage, uuidv7 ┌──────▼──────┐ │ agent │ Agent 运行时核心 └──┬───┬───┬──┘ ┌───────────┘ │ └───────────┐ │ │ │ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────────┐ │ protocol │ │ session- │ │ coding-agent │ │ │ │ backends │ │ │ └──────┬──────┘ └─────────────┘ └──┬───┬───┬──────┘ │ │ │ │ ┌──────▼──────┐ ┌────────┘ │ └────────┐ │ server │ │ │ │ │ client │ ┌────▼───┐ ┌─────▼───┐ ┌─────▼────┐ └─────────────┘ │ tui │ │ chord │ │ evals │ └────────┘ └─────────┘ └──────────┘
|
关键依赖方向:上层依赖下层,不允许反向依赖。telemetry 是最底层的零依赖包,coding-agent 是最上层的产品包。
分层架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| ┌──────────────────────────────────────────────────────────┐ │ 产品层 (coding-agent) │ │ AgentSession · 工具(read/bash/edit/write) · 扩展系统 │ │ 运行模式(Interactive/Print/RPC/JSON) · 系统提示构建 │ ├──────────────────────────────────────────────────────────┤ │ UI 层 (tui) │ │ 差分渲染 · 组件模型 · 键盘输入 · Markdown · 编辑器 │ ├──────────────────────────────────────────────────────────┤ │ 运行时层 (agent) │ │ Agent Loop · 工具执行 · Harness 状态机 · Compaction │ │ Lane · Session · Hook · 事件总线 │ ├──────────────────────────────────────────────────────────┤ │ LLM 层 (ai) │ │ Provider 抽象 · Models 管理 · Auth · 流式调用 · 重试 │ ├──────────────────────────────────────────────────────────┤ │ 通信层 (protocol) │ 存储层 (session-backends) │ │ CBOR 编码 · 帧封装 │ SQLite · Schema · Fork │ ├──────────────────────────────────────────────────────────┤ │ 基础设施 (telemetry) │ │ Span 契约 · Schema 类型推断 · Noop/Memory 实现 │ ├──────────────────────────────────────────────────────────┤ │ 分布式层 (chord, server, client) │ │ Facet · Service · ReplicatedState · SessionRouter │ └──────────────────────────────────────────────────────────┘
|
一个请求的完整生命周期
以用户在终端输入 “读取 README.md 并总结” 为例,追踪数据如何流经各层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| 用户输入 "读取 README.md 并总结" │ ▼ ┌─ coding-agent (产品层) ────────────────────────────────────┐ │ InteractiveMode 接收键盘输入 │ │ → packages/coding-agent/src/modes/interactive/ │ │ AgentSession.prompt(userMessage) │ │ → packages/coding-agent/src/core/agent-session.ts:1159 │ │ buildSystemPrompt() 组装系统提示 │ │ → packages/coding-agent/src/core/system-prompt.ts:28 │ │ Agent.prompt(userMessage) │ │ → packages/coding-agent/src/core/sdk.ts (Agent 构造) │ ├───────────────────────────────────────────────────────────┤ │ agent (运行时层) │ │ agentLoop() 启动主循环 │ │ → packages/agent/src/agent-loop.ts:32 │ │ runLoop() 内层循环: │ │ 1. 调用 streamFn 发送上下文给 LLM │ │ 2. LLM 返回流式响应 + toolCall(读取文件) │ │ 3. 执行 tool: read("README.md") │ │ 4. 将 tool result 加入上下文 │ │ 5. 再次调用 streamFn (LLM 看到文件内容后总结) │ │ 6. LLM 返回纯文本(无 toolCall) → 循环结束 │ ├───────────────────────────────────────────────────────────┤ │ ai (LLM 层) │ │ Models.stream(model, context, options) │ │ → packages/ai/src/models.ts:672 │ │ applyAuth() 解析认证 (API key / OAuth) │ │ → packages/ai/src/models.ts:641 │ │ Provider.stream() 调用具体 provider API │ │ → packages/ai/src/providers/ │ │ 返回 AssistantMessageEventStream │ ├───────────────────────────────────────────────────────────┤ │ session-backends (存储层) │ │ 每轮结束后 Storage.commit() 持久化消息 │ │ → packages/session-backends/sqlite-node/src/storage.ts │ │ 写入 entries 表 + 更新 usage_ledger │ ├───────────────────────────────────────────────────────────┤ │ tui (UI 层) │ │ InteractiveMode 订阅 AgentSession 事件 │ │ 流式渲染 assistant 消息 + tool 执行状态 │ │ → packages/coding-agent/src/modes/interactive/ │ └───────────────────────────────────────────────────────────┘
|
事件流(简化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| agent_start → turn_start → message_start (用户消息) → message_end → [调用 LLM] → message_start (assistant 流式消息) → message_update (流式内容...) → message_end → tool_start (read 工具) → tool_end (工具结果) → turn_end → turn_start (第二轮) → message_start (assistant 总结) → message_update (流式内容...) → message_end → turn_end → agent_end
|
关键设计决策
1. StreamFn 契约:错误不抛异常,在流中表达
StreamFn 类型(packages/agent/src/types.ts:28)的契约明确规定:不能 throw 或返回 rejected promise,所有失败必须通过流中的协议事件 + stopReason: "error" 表达。
为什么:Agent loop 是一个长期运行的循环,如果 LLM 调用抛异常,循环会被打断,正在进行的工具调用和状态可能不一致。将错误放入流中,让 loop 以统一的方式处理”正常响应”和”错误响应”,保证状态机不中断。
2. 双层 Agent 架构:Agent vs AgentHarness
Pi 有两套 Agent 实现:
- 经典 Agent(
agent.ts + agent-loop.ts):简单的 while 循环,适合理解概念
- AgentHarness(
harness/ 目录):状态机驱动,支持持久化恢复、多 lane、compaction
为什么:经典 Agent 容易理解但不支持崩溃恢复。Harness 将每次状态转换都持久化,进程崩溃后可以从上次检查点恢复。代价是复杂度大幅增加(drive/ 目录有 12 个阶段处理器文件)。
3. 权威数据 vs 投影缓存
SQLite schema 中,entries 表是权威数据,branch_entries 是投影缓存。两者通过触发器保证一致性。
为什么:分支遍历需要按顺序读取大量 entry,如果每次都从 entries 表的 parent_id 链遍历,性能很差。branch_entries 预计算了分支的线性顺序,但它是可重建的缓存——即使损坏,也能从 entries 重建。
4. 传输中立协议
protocol 包定义了消息格式和 CBOR 编码,但不关心传输方式(WebSocket、stdio、Unix socket)。
为什么:同一套协议可以用于本地进程间通信(experimental server/worker)和远程连接,无需修改消息层代码。
学习要点
- Pi 的分层是严格的单向依赖:上层可以调用下层,下层不知道上层存在
coding-agent 是组装层——它不实现 Agent 逻辑,而是把 agent、ai、tui 的能力组合成产品
- 理解”一个请求的生命周期”是阅读后续文档的地图
- 两层 Agent 架构(经典 vs Harness)是这个项目最重要的设计决策之一
下一篇:02-LLM 抽象层-pi-ai — 从底层开始,看 LLM 接口如何统一