Pi 的核心设计决策:把 LLM 通信、Agent 运行时、产品交互 严格分离。
同一个底层引擎,可以长出完全不同形态的产品。
一个 AI Agent 至少需要解决三个问题:
- 如何跟 LLM 对话?(调哪个 API、怎么传参、怎么解析流式输出)
- 如何循环执行?(调 LLM → 执行工具 → 把结果喂回去 → 再调 LLM)
- 如何跟用户交互?(终端 UI、Web UI、Slack Bot、API Server)
如果把这三件事混在一起,换个 LLM 提供商就要改 Agent 逻辑,加个 Web 界面就要重写循环引擎。Pi 的解法是垂直分层:
@startuml
skinparam backgroundColor transparent
skinparam componentStyle rectangle
package "Layer 3 — 产品层" #E3F2FD {
[pi-coding-agent] as L3
note right of L3
终端 CLI、扩展系统、
技能系统、UI 交互
end note
}
package "Layer 2 — Agent 运行时" #FFF3E0 {
[pi-agent-core] as L2
note right of L2
Agent Loop、工具执行、
状态管理、事件发射
end note
}
package "Layer 1 — LLM 通信层" #E8F5E9 {
[pi-ai] as L1
note right of L1
Provider 抽象、模型注册、
流式输出、跨 API 兼容
end note
}
L3 --> L2 : 调用
L2 --> L1 : 调用
L1 -[hidden]-> L2
L2 -[hidden]-> L3
@enduml关键约束:每一层只依赖下面的层,绝不反向依赖。
packages/ai/ — 纯粹的 LLM 调用抽象,不知道"Agent"是什么。
职责:
- 定义统一的
Model、Message、Context类型 - 为 30+ Provider(OpenAI、Anthropic、Google、Bedrock…)提供统一接口
- 处理流式输出,发射
AssistantMessageEvent - 自动处理跨 Provider 兼容问题(ID 长度、thinking block 格式等)
核心类型(packages/ai/src/types.ts):
// 模型定义
interface Model<TApi extends Api> {
id: string; // 如 "claude-opus-4-6"
name: string; // 如 "Claude Opus 4.6"
api: TApi; // 如 "anthropic-messages"
provider: string; // 如 "anthropic"
baseUrl: string;
reasoning: boolean; // 是否支持 thinking
contextWindow: number;
maxTokens: number;
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
}
// 统一消息格式
type Message = UserMessage | AssistantMessage | ToolResultMessage;
// LLM 调用上下文
interface Context {
systemPrompt: string;
messages: Message[];
tools?: Tool[];
}关键函数 — stream():
// 所有 Provider 都通过同一个接口调用
const response = await stream(model, context, options);
for await (const event of response) {
// event.type: "start" | "text_delta" | "toolcall_start" | "done" | ...
}packages/agent/ — Agent 的引擎,不关心用什么 UI 展示。
职责:
- 实现 Agent Loop(双循环引擎)
- 管理 Agent 状态(消息历史、工具列表、模型配置)
- 执行工具调用(并行/顺序)
- 发射生命周期事件
- 提供 Steering(转向)和 Follow-up(追加)消息队列
核心类 — Agent(packages/agent/src/agent.ts):
const agent = new Agent({
streamFn: stream, // 来自 pi-ai
initialState: {
systemPrompt: "You are a helpful assistant.",
model: claudeOpus,
tools: [readTool, writeTool, bashTool],
},
});
// 订阅事件
agent.subscribe((event, signal) => {
if (event.type === "message_update") {
// 实时渲染 LLM 的流式输出
}
if (event.type === "tool_execution_end") {
// 工具执行完毕
}
});
// 发送提示
await agent.prompt("帮我读取 package.json 的内容");
// 中途注入消息(Steering)
agent.steer({ role: "user", content: [{ type: "text", text: "停下来" }], timestamp: Date.now() });核心文件结构:
| 文件 | 职责 |
|---|---|
agent.ts |
Agent 类:状态管理 + 事件分发 |
agent-loop.ts |
runLoop():双循环引擎 |
types.ts |
AgentMessage、AgentTool、AgentEvent 等核心类型 |
stream-fn.ts |
默认的 LLM 调用函数 |
packages/coding-agent/ — 面向开发者的编码 Agent CLI,也是 Pi 的旗舰产品。
职责:
- 终端 UI(基于 pi-tui)
- 扩展系统(Extensions、Skills、Prompt Templates)
- 内置工具(read、write、edit、bash、grep、find、ls)
- 认证与 Provider 管理
- 会话持久化(JSONL)
- 上下文压缩(Compaction)
产品层不等于框架。coding-agent 是建立在 pi-ai + agent-core 之上的一个应用。你完全可以用同样的底层构建自己的 Agent。
OpenClaw 是一个支持 46 个消息渠道(WhatsApp、Telegram、Discord 等)的多渠道 AI 助手。它直接复用了 Pi 的前两层:
@startuml
skinparam backgroundColor transparent
package "Pi Coding Agent" {
[CLI + TUI] as cli
}
package "OpenClaw" {
[Gateway\n(46 channels)] as gw
}
package "pi-agent-core" #FFF3E0 {
[Agent Loop] as loop
}
package "pi-ai" #E8F5E9 {
[LLM API] as llm
}
cli --> loop
gw --> loop
loop --> llm
@enduml这证明了分层设计的价值:
- OpenClaw 没有修改 pi-ai 和 agent-core 的任何代码
- 它只替换了产品层(CLI → Gateway)
- 同一个 Agent 引擎,长出了两个完全不同的产品
一次完整的用户请求,数据在三层之间的流动:
@startuml
skinparam backgroundColor transparent
actor User
participant "coding-agent\n(Layer 3)" as L3
participant "agent-core\n(Layer 2)" as L2
participant "pi-ai\n(Layer 1)" as L1
participant "LLM API" as API
User -> L3: 输入 "读取 foo.ts"
L3 -> L2: agent.prompt("读取 foo.ts")
L2 -> L2: 构建 AgentContext
L2 -> L2: convertToLlm(AgentMessage[] → Message[])
L2 -> L1: stream(model, context, options)
L1 -> API: HTTP POST /v1/messages
API --> L1: SSE stream
L1 --> L2: AssistantMessageEvent[]
L2 -> L2: 解析 toolCall: read({path: "foo.ts"})
L2 -> L3: beforeToolCall hook
L3 --> L2: {block: false}
L2 -> L2: 执行 readTool.execute()
L2 -> L3: afterToolCall hook
L2 -> L2: 工具结果 → ToolResultMessage
L2 -> L1: stream(model, context + toolResult, options)
L1 -> API: HTTP POST(带上工具结果)
API --> L1: SSE stream(最终回复)
L1 --> L2: AssistantMessage
L2 --> L3: agent_end 事件
L3 --> User: 渲染最终输出
@enduml- 能说出 Pi 的三个层各自的职责
- 理解"Layer 1 不知道 Agent 是什么"的含义
- 理解为什么 OpenClaw 可以复用 Pi 的底层
- 能画出一次请求在三层之间的完整数据流
上一章:第零章 · 全景导读
下一章:第二章 · LLM 抽象层 (pi-ai) — 如何用一套接口调 30+ 模型