核心架构与设计哲学

本文档深入分析 Pi 的架构设计模式,理解这些模式不仅能读懂 Pi 的代码,还能举一反三到其他 LLM Agent 项目。

一、Provider 抽象层:用一套接口统一 30+ LLM 提供商

文件packages/ai/src/api-registry.tspackages/ai/src/types.tspackages/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.tspackages/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 每帧清除整个屏幕并重新渲染。这在终端环境中有两个问题:

  1. 闪烁:清屏 → 重绘,肉眼可见
  2. 性能:每次输出几百行 ANSI 转义码,终端处理慢

Pi 的解决方案是差分渲染

  1. 保存上一帧的输出(previousLines: string[]
  2. 渲染当前帧
  3. 逐行对比,只输出变化的行

实现

// 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 渲染流程:

  1. 渲染基础内容(header + chat + editor + footer)
  2. 在基础内容上叠加 overlay
  3. 差分对比时包含 overlay 区域
  4. Overlay 关闭时只更新被覆盖的区域

四、扩展系统

文件packages/coding-agent/src/core/extensions/runner.ts 核心类ExtensionRunner

扩展是什么

Pi 的扩展是一个 TypeScript 模块,通过 ExtensionFactory 函数创建。扩展可以:

  1. 注册自定义工具pi.registerTool({ name: "deploy", ... })
  2. 注册斜杠命令pi.registerCommand({ name: "deploy", ... })
  3. 监听事件pi.on("tool_call", handler)
  4. 拦截输入pi.on("input", handler)
  5. 修改系统提示 — 在 before_agent_start 中修改
  6. 创建 UI 组件 — 在编辑器上下方渲染自定义组件
  7. 注册快捷键 — 自定义按键绑定

扩展加载流程

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                    │
└────────────────────────────────────────────┘

输入拦截的三种返回值:

返回值效果
{ action: 'pass' }跳过,让下一个扩展或默认流程处理
{ action: 'handled' }扩展已处理,不再发送给 LLM
{ action: 'transform', text: '...' }修改输入内容后继续处理

实际应用示例:

// 自动将 #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;
  }
});

四种运行模式:

模式触发方式扩展适配建议
tui直接 pi可使用完整 UI 组件
rpcpi --mode rpc避免 UI,专注数据处理
jsonpi --mode json输出结构化事件
printpi -p "prompt"简化输出,避免交互

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)   ← 返回结果

内置工具

工具实现关键特性
readread.ts支持行范围、自动截断、token 计数
writewrite.ts创建/覆盖、目录自动创建
editedit.ts精确文本替换、支持多次编辑
edit-diffedit-diff.ts统一 diff 格式补丁应用
bashbash.ts超时控制、输出截断、pty 支持
grepgrep.tsrg 封装、支持正则、行号输出
findfind.tsfd 封装、支持 glob
lsls.ts目录列表、隐藏文件、排序

工具输出的截断与累积

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. 流式写入:不需要等会话结束才写文件
  2. 崩溃恢复:即使进程崩溃,已写入的行仍然有效
  3. 增量读取:可以继续附加,不需要重写整个文件
  4. 大文件友好:不需要一次性加载到内存

七、设计哲学总结

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  → 指定模型
高级使用:自定义扩展、自定义工具       → 完整控制

下一步

回顾: