核心架构与设计哲学
本文档深入分析 Pi 的架构设计模式,理解这些模式不仅能读懂 Pi 的代码,还能举一反三到其他 LLM Agent 项目。
一、Provider 抽象层:用一套接口统一 30+ LLM 提供商
文件:packages/ai/src/api-registry.ts、packages/ai/src/types.ts、packages/ai/src/providers/
设计问题
LLM 提供商各有不同的 API:
- OpenAI 有 Chat Completions API 和 Responses API
- Anthropic 用 Messages API,消息格式完全不同
- Google 用 Generative AI API,结构又不一样
- 还有 Bedrock、Mistral、Groq...
如果不做抽象,每支持一个提供商就要写一套调用逻辑,编码智能体就无法跨提供商切换。
解决方案:三层抽象
┌─────────────────────────────────────────────────┐
│ 第一层:Model(模型元数据) │
│ │
│ interface Model<TApi extends Api> { │
│ id: string; // "gpt-4o-mini" │
│ name: string; // "GPT-4o Mini" │
│ api: TApi; // "openai-completions" │
│ provider: Provider; // "openai" │
│ contextWindow: number; // 128000 │
│ reasoning: boolean; // true/false │
│ input: ('text'|'image')[]; │
│ cost: CostInfo; // token 价格 │
│ compat?: Compat; // 兼容性标志 │
│ } │
│ │
│ ★ 类型参数化:Model<'openai-completions'> │
│ 确保模型与 API 的绑定在编译期检查 │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 第二层:ApiProvider(API 提供商) │
│ │
│ interface ApiProvider<TApi extends Api> { │
│ api: TApi; │
│ stream: (model, context, options) => EventStream│
│ streamSimple: (...) => EventStream │
│ } │
│ │
│ ★ 每种 API 类型一个 Provider 实现: │
│ - openai-completions → 一个 Provider │
│ - anthropic-messages → 一个 Provider │
│ - google-generative-ai → 一个 Provider │
│ │
│ ★ Provider 在 api-registry 中注册: │
│ registerApiProvider("openai-completions", { │
│ stream: streamOpenAICompletions, │
│ streamSimple: streamSimpleOpenAICompletions │
│ }) │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 第三层:统一调用接口(stream / complete) │
│ │
│ // 调用者不需要知道底层是哪个 API │
│ const response = stream(model, context, opts); │
│ │
│ // 内部路由: │
│ // 1. 根据 model.api 查找注册的 ApiProvider │
│ // 2. 调用 provider.stream(model, context, opts) │
│ // 3. 返回统一的 AssistantMessageEventStream │
└─────────────────────────────────────────────────┘
关键设计决策
1. 事件流抽象
所有提供商的流式响应被统一为一组事件类型:
// 无论底层是 OpenAI SSE、Anthropic SSE、还是 Google 流式
// 调用者看到的都是这些事件:
type Event =
| { type: 'start'; partial: AssistantMessage }
| { type: 'text_start'; contentIndex: number }
| { type: 'text_delta'; delta: string; contentIndex: number }
| { type: 'text_end'; content: string; contentIndex: number }
| { type: 'thinking_start'; contentIndex: number }
| { type: 'thinking_delta'; delta: string }
| { type: 'thinking_end'; content: string }
| { type: 'toolcall_start'; contentIndex: number }
| { type: 'toolcall_delta'; delta: string; partial: AssistantMessage }
| { type: 'toolcall_end'; toolCall: ToolCall }
| { type: 'done'; reason: StopReason; message: AssistantMessage }
| { type: 'error'; reason: 'error' | 'aborted'; error: AssistantMessage };
这意味着 Agent Loop 代码完全不需要关心是哪个提供商。它只处理这 12 种事件。
2. 模型元数据生成
模型列表不是手写的,而是通过脚本从各提供商的 API 自动生成:
packages/ai/scripts/generate-models.ts
→ 拉取各提供商的最新模型元数据
→ 生成 packages/ai/src/models.generated.ts
3. 兼容性处理
不同 OpenAI 兼容的 API 有细微差异,通过 compat 字段处理:
interface OpenAICompletionsCompat {
supportsStore?: boolean; // 是否支持 store 字段
supportsDeveloperRole?: boolean; // 是否支持 developer 角色
supportsReasoningEffort?: boolean; // 是否支持 reasoning_effort
maxTokensField?: 'max_completion_tokens' | 'max_tokens';
requiresThinkingAsText?: boolean; // 是否将 thinking 转为 text
thinkingFormat?: 'openai' | 'deepseek' | 'qwen' | ...;
}
30+ 提供商只需要维护这个 compat 配置,不需要修改 Agent Loop 代码。
二、事件驱动架构
文件:packages/ai/src/utils/event-stream.ts、packages/agent/src/types.ts
Pi 的整个架构是事件驱动的。从 LLM 流式响应到 TUI 更新,从工具执行到扩展触发,全部通过事件传递。
事件类型层次
AgentEvent (agent-core 层的事件)
├── agent_start — Agent 开始处理
├── agent_end — Agent 处理完成
├── turn_start — 一轮 LLM+工具 开始
├── turn_end — 一轮结束
├── message_start — 消息开始
├── message_end — 消息结束
├── message_update — 消息更新(流式 delta)
│ └── assistantMessageEvent — 嵌套 LLM 原始事件
├── tool_call_start — 工具调用开始
├── tool_call — 工具调用
├── tool_result — 工具结果
├── context_compact — 上下文压缩
└── auto_retry_start / end — 自动重试
事件流示例
用户输入 "解释这个文件" 后的事件序列:
1. agent_start
2. turn_start
3. message_start { role: "user", content: "解释这个文件" }
4. message_end
5. message_start { role: "assistant" } ← 空消息占位
6. message_update { delta: "好的" } ← 流式文本
7. message_update { delta: ",让" }
8. message_update { delta: "我来" }
9. ... (更多 delta)
10. message_end ← 消息完成
11. tool_call_start { name: "read" } ← 工具调用开始
12. tool_call { name: "read", args: ... }
13. tool_result { content: "文件内容..." }
14. turn_end
15. turn_start ← 第二轮
16. message_start { role: "assistant" }
17. message_update { delta: "这个文件..." }
18. ... (更多 delta)
19. message_end ← done, stopReason: "stop"
20. agent_end { messages: [...] }
TUI 监听这些事件并实时更新界面:
message_update → 更新流式文本显示
tool_call → 显示工具调用状态
tool_result → 显示工具输出
agent_end → 隐藏 Working 指示器,恢复编辑器
扩展事件
除了 Agent 内部事件,扩展系统有自己的事件体系:
// 扩展可以监听的事件
type ExtensionEvent =
| 'input' // 用户输入
| 'before_agent_start' // Agent 调用前
| 'message_end' // 消息结束
| 'tool_call' // 工具调用
| 'tool_result' // 工具结果
| 'before_provider_request' // LLM 请求前
| 'after_provider_response' // LLM 响应后
| 'agent_end' // Agent 结束
| 'session_shutdown'; // 会话关闭
扩展可以在这些事件的任何一环拦截、修改、或增强行为。
三、TUI 差分渲染
文件:packages/tui/src/tui.ts
核心类:TUI extends Container
为什么需要差分渲染
传统终端 UI 每帧清除整个屏幕并重新渲染。这在终端环境中有两个问题:
- 闪烁:清屏 → 重绘,肉眼可见
- 性能:每次输出几百行 ANSI 转义码,终端处理慢
Pi 的解决方案是差分渲染:
- 保存上一帧的输出(
previousLines: string[])
- 渲染当前帧
- 逐行对比,只输出变化的行
实现
// tui.ts 核心字段
class TUI extends Container {
private previousLines: string[] = []; // 上一帧输出
private renderTimer: NodeJS.Timeout | undefined;
private static readonly MIN_RENDER_INTERVAL_MS = 16; // ~60fps
start(): void {
const loop = () => {
this._render(); // 渲染
this.renderTimer = setTimeout(loop, TUI.MIN_RENDER_INTERVAL_MS);
};
loop();
}
private _render(): void {
// 1. 收集所有组件的渲染输出
const lines = this._collectAllLines();
// 2. 与上一帧对比
const diff = this._computeDiff(this.previousLines, lines);
// 3. 只输出变化的部分
for (const change of diff) {
this.terminal.cursorTo(change.row, 0);
this.terminal.clearLine();
this.terminal.write(change.text);
}
// 4. 保存当前帧
this.previousLines = lines;
}
}
组件接口
// tui.ts: Component 接口
interface Component {
render(width: number): string[]; // 渲染为行数组
handleInput?(data: string): void; // 处理按键
invalidate(): void; // 清除缓存,强制重绘
}
所有 UI 元素都是 Component:
Text — 纯文本行
Container — 子组件容器(递归渲染)
Editor — 编辑器(含光标、选区)
Markdown — Markdown 渲染
Loader — 加载动画("Working...")
SelectList — 选择列表(模型选择、设置等)
Overlay 系统
Pi TUI 支持模态覆盖层:
// 在现有内容上弹出选择器
ui.showOverlay(new ModelSelectorComponent(), {
anchor: 'center',
width: 60,
height: 20,
});
Overlay 渲染流程:
- 渲染基础内容(header + chat + editor + footer)
- 在基础内容上叠加 overlay
- 差分对比时包含 overlay 区域
- Overlay 关闭时只更新被覆盖的区域
四、扩展系统
文件:packages/coding-agent/src/core/extensions/runner.ts
核心类:ExtensionRunner
扩展是什么
Pi 的扩展是一个 TypeScript 模块,通过 ExtensionFactory 函数创建。扩展可以:
- 注册自定义工具 —
pi.registerTool({ name: "deploy", ... })
- 注册斜杠命令 —
pi.registerCommand({ name: "deploy", ... })
- 监听事件 —
pi.on("tool_call", handler)
- 拦截输入 —
pi.on("input", handler)
- 修改系统提示 — 在
before_agent_start 中修改
- 创建 UI 组件 — 在编辑器上下方渲染自定义组件
- 注册快捷键 — 自定义按键绑定
扩展加载流程
1. discoverAndLoadExtensions()
├── 扫描 ~/.pi/agent/extensions/ (全局)
├── 扫描 .pi/extensions/ (项目)
├── 扫描已安装的 Pi Packages
└── 加载 CLI --extensions 指定的路径
2. 对每个扩展:
├── import(filePath)
├── 调用 ExtensionFactory(cwd, agentDir)
└── 注册工具、命令、事件监听
3. ExtensionRunner 汇总所有扩展
├── 工具注册到 ToolRegistry
├── 命令注册到 SlashCommands
└── 事件监听注册到事件总线
扩展深入:输入拦截机制
当用户输入文本时,扩展可以在多个层级拦截和修改:
用户输入 "@review src/index.ts"
│
▼
┌────────────────────────────────────────────┐
│ 扩展 input 事件拦截 │
│ │
│ extension_1: on('input') │
│ → 检查是否需要处理 │
│ → 返回 { action: 'pass' } 跳过 │
│ │
│ extension_2: on('input') │
│ → 检测到 @review 前缀 │
│ → 返回 { action: 'handled' } │
│ → 扩展自己处理这个输入 │
│ → 输入不再发送给 LLM │
└────────────────────────────────────────────┘
输入拦截的三种返回值:
实际应用示例:
// 自动将 #issue 标记转换为 GitHub 链接
pi.on('input', (event) => {
const match = event.text.match(/#(\d+)/);
if (match) {
const issueUrl = `https://github.com/${repo}/issues/${match[1]}`;
return {
action: 'transform',
text: event.text.replace(/#\d+/, `[Issue #${match[1]}](${issueUrl})`),
};
}
return { action: 'pass' };
});
扩展深入:工具注册
扩展注册的工具与内置工具完全平等,LLM 可以自由调用:
pi.registerTool({
name: 'database-query',
description: 'Execute a read-only SQL query',
parameters: Type.Object({
query: Type.String({ description: 'SQL query to execute' }),
}),
// 工具渲染器(可选):控制工具在 TUI 中的显示
renderCall: (args) => `🔍 查询: ${args.query}`,
renderResult: (result) => `📊 结果: ${result.rowCount} 行`,
execute: async (args, context) => {
// context 包含当前会话信息
const rows = await db.query(args.query);
return {
content: [
{
type: 'text',
text: JSON.stringify(rows, null, 2),
},
],
};
},
});
工具执行流程:
LLM 返回 ToolCall { name: 'database-query', arguments: { query: '...' } }
│
▼
ToolRegistry 查找工具定义
│
▼
TypeBox schema 校验参数
│
▼
执行扩展的 execute 函数
│
▼
返回 ToolResult → 写入上下文 → LLM 继续
扩展深入:UI 组件注册
扩展可以在编辑器的上方或下方渲染自定义 UI 组件:
pi.on('session_start', (event, ctx) => {
// 在编辑器上方添加一个状态指示器
ctx.ui.setWidget('above', {
render: (width) => {
const status = getProjectStatus();
return [`📁 ${status.branch} | ✅ ${status.tests} tests passing`];
},
invalidate: () => true, // 始终重新渲染
});
});
UI 组件位置:
┌─────────────────────────────────┐
│ header (Logo + 快捷键提示) │
├─────────────────────────────────┤
│ chatContainer (聊天消息) │
├─────────────────────────────────┤
│ widgetContainerAbove ← 扩展组件 │
├─────────────────────────────────┤
│ editor (编辑器) │
├─────────────────────────────────┤
│ widgetContainerBelow ← 扩展组件 │
├─────────────────────────────────┤
│ footer (状态栏) │
└─────────────────────────────────┘
扩展上下文(v0.78.1+)
v0.78.1 为扩展提供了更丰富的上下文信息:
ctx.mode — 运行模式感知
扩展可以检测当前的运行模式,根据不同模式适配行为:
pi.on('before_agent_start', (event, ctx) => {
switch (ctx.mode) {
case 'tui':
// 交互模式:可以使用丰富的 UI 组件
ctx.ui.setStatus('正在处理...');
break;
case 'rpc':
// RPC 模式:作为其他程序的子进程运行
// 避免使用 UI 组件,专注于数据处理
break;
case 'json':
// JSON 事件流模式:输出结构化事件
break;
case 'print':
// 打印模式:一次性输出
break;
}
});
四种运行模式:
ctx.getSystemPromptOptions() — 系统 Prompt 检查
扩展命令可以检查当前的基础系统 Prompt 输入,用于审计或条件逻辑:
pi.registerCommand({
name: 'audit-prompt',
description: '显示当前系统 Prompt 的组成部分',
execute: async (args, ctx) => {
const options = ctx.getSystemPromptOptions();
// 检查系统 Prompt 包含哪些部分
const parts = [];
if (options.includeCodingInstructions) parts.push('编码指令');
if (options.includeProjectContext) parts.push('项目上下文');
if (options.includeToolDescriptions) parts.push('工具描述');
return `当前系统 Prompt 包含:${parts.join('、')}`;
},
});
这对于调试和理解 Agent 的行为非常有用——你可以检查系统 Prompt 是否包含特定的指令或上下文。
扩展生命周期
扩展的完整生命周期:
1. 发现阶段
└── 扫描扩展目录,加载模块
2. 注册阶段
├── registerTool() → ToolRegistry
├── registerCommand() → SlashCommands
├── registerShortcut() → Keybindings
└── on() → EventListeners
3. 会话启动
└── session_start 事件触发
├── 初始化 UI 组件
└── 注册快捷键
4. 运行阶段
├── input 事件 → 拦截/修改用户输入
├── before_agent_start → 修改系统 Prompt
├── tool_call → 监听工具执行
├── message_end → 处理 LLM 响应
└── agent_end → Agent 完成
5. 会话关闭
└── session_shutdown 事件触发
└── 清理资源
扩展 API 示例
// 一个完整的扩展示例
export default function extension(cwd: string) {
return {
name: 'my-extension',
setup(pi: ExtensionAPI) {
// 1. 注册工具
pi.registerTool({
name: 'deploy',
description: 'Deploy the current project',
parameters: Type.Object({
environment: Type.Enum({ staging: 'staging', production: 'production' }),
}),
execute: async (args) => {
const result = await exec(`deploy --env ${args.environment}`);
return { content: [{ type: 'text', text: result.stdout }] };
},
});
// 2. 注册斜杠命令
pi.registerCommand({
name: 'status',
description: 'Show deployment status',
execute: async () => {
const status = await getDeploymentStatus();
return `当前部署状态: ${status}`;
},
});
// 3. 监听事件
pi.on('tool_call', (event) => {
console.log(`Tool called: ${event.toolName}`);
});
// 4. 拦截用户输入
pi.on('input', (event) => {
if (event.text.startsWith('# ')) {
return { action: 'transform', text: `[COMMENT] ${event.text}` };
}
return { action: 'pass' };
});
// 5. 修改系统 Prompt
pi.on('before_agent_start', (event, ctx) => {
// 根据运行模式适配
if (ctx.mode === 'tui') {
ctx.ui.setStatus('准备部署...');
}
});
},
};
}
五、工具系统
文件:packages/coding-agent/src/core/tools/
工具架构
LLM 请求 ToolCall { name: "read", arguments: { path: "package.json" } }
│
▼
ToolRegistry.get("read")
│
▼
ToolDefinitionWrapper.execute(args, context)
│
├─ 1. validate(args) ← TypeBox schema 校验
├─ 2. checkPermissions() ← 检查工具权限
├─ 3. emit(tool_call) ← 通知 TUI / 扩展
├─ 4. execute() ← 实际执行
│ fs.readFileSync("package.json")
├─ 5. truncate(result) ← 截断过长输出
└─ 6. emit(tool_result) ← 返回结果
内置工具
工具输出的截断与累积
LLM 的上下文窗口有限,工具输出需要智能截断:
// truncate.ts
const MAX_OUTPUT_TOKENS = 12000;
function truncateOutput(
output: string,
maxTokens: number,
): {
text: string;
wasTruncated: boolean;
tokensUsed: number;
} {
// 智能截:不截断在字符中间
// 保留头部和尾部(让 LLM 看到开头和结尾)
// 中间用 "... (output truncated)" 替代
}
六、会话格式(JSONL)
文件:packages/coding-agent/src/core/session-manager.ts
Pi 的会话是每行一条 JSON 的格式(JSONL):
{"type":"session_start","id":"abc123","cwd":"/path/to/project","timestamp":1234567890}
{"type":"message","message":{"role":"user","content":[{"type":"text","text":"hello"}],"timestamp":1234567891}}
{"type":"message","message":{"role":"assistant","content":[{"type":"text","text":"Hi! How can I help?"}],"timestamp":1234567892}}
{"type":"session_end","timestamp":1234567893}
为什么用 JSONL 而非 JSON 数组?
- 流式写入:不需要等会话结束才写文件
- 崩溃恢复:即使进程崩溃,已写入的行仍然有效
- 增量读取:可以继续附加,不需要重写整个文件
- 大文件友好:不需要一次性加载到内存
七、设计哲学总结
1. 关注点分离
pi-ai → LLM 通信(怎么跟大模型说话)
pi-agent-core → 智能体逻辑(怎么循环、怎么调用工具)
pi-coding-agent → 应用层(CLI、扩展、会话、TUI 集成)
pi-tui → 终端 UI(怎么在终端里显示和交互)
每个包都有清晰的边界和接口。
2. 事件驱动而非回调地狱
❌ 回调嵌套(难以追踪)
llmStream.on('text', (text) => {
ui.update(text, () => {
tool.execute(args, (result) => {
llmStream.continue(result, () => {
// ...
});
});
});
});
✅ 事件流(线性、可组合)
for await (const event of stream) {
emit(event); // 所有监听者收到
}
3. 类型安全贯穿
// Model 类型参数化确保 API 匹配
const model = getModel('openai', 'gpt-4o-mini');
// ^ Model<'openai-completions'>
// stream() 根据 model.api 推断 options 类型
stream(model, context, {
// TypeScript 知道这里是 OpenAICompletionsOptions
maxTokens: 4096,
});
4. 扩展优于修改
不鼓励修改核心代码。通过扩展系统:
- 添加自定义工具
- 注册斜杠命令
- 监听/修改事件
- 创建 UI 组件
5. 渐进式复杂度
简单使用:pi "解释这个文件" → 自动选择模型、自动认证
进阶使用:pi --model anthropic/claude → 指定模型
高级使用:自定义扩展、自定义工具 → 完整控制
下一步
回顾: