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 实现:

  • 经典 Agentagent.ts + agent-loop.ts):简单的 while 循环,适合理解概念
  • AgentHarnessharness/ 目录):状态机驱动,支持持久化恢复、多 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 逻辑,而是把 agentaitui 的能力组合成产品
  • 理解”一个请求的生命周期”是阅读后续文档的地图
  • 两层 Agent 架构(经典 vs Harness)是这个项目最重要的设计决策之一

下一篇:02-LLM 抽象层-pi-ai — 从底层开始,看 LLM 接口如何统一