Featured image of post Pi Agent 深度拆解:TypeScript 多包架构下的 Steering + Follow-up 双队列设计

Pi Agent 深度拆解:TypeScript 多包架构下的 Steering + Follow-up 双队列设计

如果你一直在关注我前两天的博客——Grok Build 的 Rust 巨构和 OpenCode 的 Effect TS 体系——今天的主角是 Pi Agent@earendil-works/pi)。

Pi 是一个 TypeScript 多包单体仓库项目,由 5 个核心包组成,共计 859 个 TypeScript 源文件。它的定位和 OpenCode 类似(都是 TypeScript 编码 Agent),但在架构设计上走了完全不同的路。

TL;DR:Pi 在「用户交互能力」上远超同类。它的 Steering + Follow-up 双队列设计,是 Grok Build 和 OpenCode 都没有的工程创新。


一、五层包的依赖关系

Pi 的包结构比 OpenCode 的 20+ 包更精简,但每层的抽象更严格:

@earendil-works/pi-orchestrator → RPC 子代理编排
    ↑
@earendil-works/pi-coding-agent → 编码 Agent CLI
    ↑
@earendil-works/pi-agent-core → 无状态 Agent Loop 引擎
    ↑
@earendil-works/pi-ai → 30+ LLM Provider 统一接口
    ↑
@earendil-works/pi-tui → 终端 UI 库

对比一下前两篇的架构:

项目 包数量 语言 Agent 数量 会话存储
Grok Build 50+ crate Rust 1 (MvpAgent) ACP 协议 + 磁盘序列化
OpenCode 20+ 包 TypeScript/Effect 2 (build/plan) Drizzle SQLite
Pi Agent 5 个包 TypeScript N/A (用户配置) JSONL 树形文件

Pi 的包最少,但它的 Agent Loop 设计 反而是三者中最复杂的。


二、Agent Loop:Steering vs Follow-up 双队列

这是 Pi 最值得关注的架构创新。读 packages/agent/src/agent-loop.ts,它的核心循环是一个双层结构:

// 外层循环:处理 follow-up 消息
while (true) {
    let hasMoreToolCalls = true;

    // 内层循环:处理工具调用和 steering
    while (hasMoreToolCalls || pendingMessages.length > 0) {
        // step 1: 处理 pending steering 消息
        // step 2: 调用 LLM
        // step 3: 执行工具(并行或串行)
        // step 4: shouldStopAfterTurn 检查
        // step 5: 获取新的 steering
    }

    // 检查 follow-up 队列
    // 若有 → 继续外层循环
    // 若无 → break
}

Steering 和 Follow-up 有什么区别?

  • Steering:在当前 turn 内注入的消息。用户可以在 Agent 思考时发一条"等一下,换个思路",它被排到 pending 队列,在当前 LLM 调用结束后立即处理。这是「打断并纠正」机制。
  • Follow-up:在当前 turn 结束后注入的消息。Agent 即将停止时,如果外部有新的请求(比如子 Agent 返回结果),它被注入到下一轮。这是「追加任务」机制。

对比 Grok Build:Grok Build 没有 Steering 机制。用户一旦提交 prompt,Agent 会一直跑到工具调用结束或达到最大迭代次数。用户在中途发送的消息会被排到新 turn 中,而不是注入到当前 turn。

这种差异的本质原因是:Architecture influences interaction design.

  • Grok Build 用 ACP 协议 + 网关模式:用户通过 session/prompt 提交请求,Agent 执行完毕后通过 PromptResponse 返回。协议层面就是「一问一答」。
  • Pi Agent 在进程内运行:Agent Loop 和 Steering 队列在同一个进程空间,天然支持 mid-turn injection。

三、30+ LLM Provider 的抽象层

Pi 的 @earendil-works/pi-aipackages/ai/src/)拥有我在开源编码 Agent 中见过的最完整的 Provider 抽象层。

types.ts 中定义了核心类型:

interface Model {
    id: string;
    provider: string;
    description?: string;
    // 认证方式
    authType?: "api-key" | "oauth" | "cert" | "none";
    // 是否支持流式、工具调用、并行工具等
    capabilities?: ModelCapabilities;
}

注册机制在 providers/all.ts 中,每个 Provider 一个文件:

providers/
├── anthropic.ts       # Claude 系列
├── openai-codex.ts    # Codex 系列
├── openai.ts          # OpenAI 系列
├── deepseek.ts        # DeepSeek
├── google-vertex.ts   # Google Vertex AI
├── fireworks.ts       # Fireworks
├── openrouter.ts      # OpenRouter
├── cerebras.ts        # Cerebras
├── mistral.models.ts  # Mistral
├── minimax.ts         # MiniMax
├── moonshotai-cn.models.ts  # 月之暗面
└── ... (30+ providers)

对比 Grok Build:Grok Build 的 Provider 层在 xai-grok-sampler crate 中,核心是 Retry + Backoff + AuthScheme 三层。它不需要 30+ Provider 是因为 Grok Build 通过 ACP 协议的 backend_search 机制复用 xAI 服务器端的 model catalog。

对比 OpenCode:OpenCode 的四轴 LLM 路由体系(Protocol/Endpoint/Auth/Framing)比 Pi 更抽象,但 Pi 在 Provider 覆盖面上更广——尤其是对中文厂商的支持(MiniMax、月之暗面、小米、零一万物等),这是 Pi 的一个明显差异化。


四、工具系统:定义优先 + 双层注册

Pi 的工具系统在 packages/agent/src/types.ts 中定义了核心类型体系:

ToolDefinition → AgentTool → Agent
     ↓              ↓
  schema (JSON)   runtime implementation

ToolDefinition:工具的定义(名称、描述、参数 Schema),由 LLM 调用方消费。 AgentTool:工具的实现(validate → execute),由 Agent 执行方消费。

两层注册流程:

1. 注册 ToolDefinition(定义)→ 注册到 Agent
2. 注册 AgentTool(实现)→ 注册到 Agent
3. Agent Loop 中:LLM 选择工具 → Agent 匹配 ToolDefinition → Agent 调用对应的 AgentTool

Pi 支持两种工具执行模式:

  • 串行执行:一个工具调用返回后再下一个
  • 并行执行:多个工具调用同时执行(在 LLM 支持并行工具调用的情况下)

对比 Grok Build:Grok Build 的工具系统使用 ToolBridge + FinalizedToolset,通过 xai-tool-runtime crate 实现 类型安全的 Resource 注入。每个工具声明它需要的资源(Terminal、Cwd、SessionEnv 等),由系统注入。Pi 没有这一层——工具直接通过 input 参数获取依赖。


五、会话管理:JSONL 树形文件

Pi 的会话存储在 packages/coding-agent/src/agent-session.ts(3283 行!)中。它的存储格式是 JSONL 树形文件

~/.pi/sessions/
├── {session_id}/
│   ├── config.json           # 会话配置
│   ├── turns.jsonl           # 核心数据:每个 turn 一行 JSON
│   └── branches/
│       └── {branch_id}/
│           ├── config.json
│           └── turns.jsonl

树形分支:Pi 支持从历史中的任何 turn 创建分支,这个分支可以独立发展。这是一个被严重低估的功能——在复杂的调试场景中,你想回到 10 分钟前的某个决策点重新探索,而不丢失那之后的分析结果。

对比 Grok Build:Grok Build 的会话通过 ACP 协议管理,5 个生命周期状态(Working → IdleResident → Dormant → Completed → DeadFailed),不支持分支

对比 OpenCode:OpenCode 用 Drizzle SQLite 做持久化,支持 V2 Session 的 durable prompt admission——先持久化再执行。也不支持分支

JSONL 树形 + 分支回滚,Pi 在会话管理的灵活度上独树一帜。


六、子代理:RPC 进程级编排

Pi 的子代理通过 packages/orchestrator/ 实现,和其他编码 Agent 最大的不同在于:它启动的是独立的 Pi 进程,通过 RPC 协议通信。

主 Pi 进程 → 启动子 Pi 进程(独立 Node/Bun 进程)
  │  RPC 协议(JSON-RPC over stdio/Unix socket)
  ▼
子 Pi 进程 → 拥有自己的 Agent Loop、工具、会话

这种方式和 Grok Build 的 SubagentCoordinator(进程内 spawn_local)完全不同:

| 维度 | Pi (RPC 进程) | Grok Build (进程内) | OpenCode (???) |——|————-|——————-| | 隔离性 | ✅ 完全隔离,崩溃不影响主进程 | ❌ 同一进程,panic 可能级联 | | 资源开销 | ❌ 每个子 Agent 一个完整进程 | ✅ 共享进程资源 | | 通信方式 | JSON-RPC over stdio/socket | ACP 消息通道 | | 嵌套深度 | 无限制 | 1 层 | | 工具继承 | 从零启动,需重新注册 | 继承父进程的 ToolBridge |

Pi 的 RPC 模式更安全但更重。Grok Build 的进程内模式更高效但隔离性差——这和两者语言的选择一脉相承:Rust 的类型系统让进程内并发安全得多,而 TypeScript 需要用进程隔离来弥补。


七、扩展系统 vs 插件系统

Pi 的扩展系统比 Grok Build 和 OpenCode 的插件系统更轻量,但覆盖了完整的 Agent 生命周期事件。

packages/coding-agent/src/extensions/types.ts(1682 行)定义了所有扩展点:

  • onBeforeTurn / onAfterTurn:Turn 前后钩子
  • onToolCall / onToolResult:工具调用钩子
  • onUserMessage / onAssistantMessage:消息钩子
  • onConfigLoad / onConfigSave:配置钩子
  • onSessionCreate / onSessionDestroy:会话生命周期钩子

扩展可以:

  • 注册新工具(通过 extensions.registerTool()
  • 注册新命令(如 /help/search
  • 修改系统提示词
  • 拦截和修改 LLM 响应
  • 添加自定义 UI 组件(TUI 模式下)

对比 Grok Build:Grok Build 的 Hooks 系统在 xai-grok-agent 中,支持 before_turnafter_turn,通过 TurnHook trait 实现。Pi 的扩展系统更丰富,而 Grok Build 的 Hooks 更专注于安全和合规。


八、安全问题:没有内置权限系统

Pi 的 README 明确说明:

Pi does not include a built-in permission system for restricting filesystem, process, network, or credential access.

它给出的替代方案是外部容器化:

  1. Gondolin 扩展:在 Linux micro-VM 中运行工具
  2. Plain Docker:整个 Pi 进程跑在 Docker 容器中
  3. OpenShell:基于策略的沙箱

对比 Grok Build:Grok Build 有完整的 xai-grok-workspace 权限系统 + xai-grok-sandbox 沙箱 + permission::manager(6295 行的大模块)。它是三者中唯一有原生权限模型的。

对比 OpenCode:OpenCode 的 PermissionV1.Ruleset 在 Agent 定义中指定,支持细粒度的 allow/deny。

Pi 在安全性上最弱,但这也反映了它的目标用户群不同——Pi 更面向「个人开发者个人使用」,而 Grok Build 设计之初就考虑了企业级安全。


九、和 Grok Build 的深层对比

从代码层面,两者在四个关键维度上有本质差异:

9.1 并发模型

  • Grok Build:Tokio Actor 模型 + LocalSet,!Send 类型安全
  • Pi Agent:EventEmitter + async/await,无共享状态

9.2 工具系统

  • Grok BuildToolBridge + FinalizedToolset,类型安全的 Resource 注入,支持 MCP 工具和本地工具统一注册
  • Pi AgentToolDefinition + AgentTool 双层注册,定义优先,不支持 MCP

9.3 协议

  • Grok Build:ACP 协议,支持 5 种部署模式(TUI/Headless/Stdio/Server/Leader)
  • Pi Agent:纯 CLI 工具,无标准化协议层

9.4 验证系统

  • Grok Build:Skeptic 验证 + Goal 系统 + Doom Loop 检测(最完善)
  • Pi Agent:无验证系统

写在最后

Pi Agent 是一个定位清晰的个人开发工具。它在架构设计上有两个真正的创新:

  1. Steering + Follow-up 双队列:让用户可以在 Agent 思考过程中介入,这是其他编码 Agent 没有做到的。
  2. JSONL 树形会话存储 + 分支:为探索式编程提供了天然的「反悔」机制。

但它也有明显的短板:没有权限模型、没有标准化协议、没有验证系统。如果你把它和 Grok Build 对比,它们解决的问题不同——Grok Build 是「企业级 AI 编码基础设施」,Pi 是「个人开发者的编码助手」。

如果你想深入了解,Pi 的 @earendil-works/pi-ai Provider 抽象层是三者中最值得学习的——30+ Provider 的统一接口设计,对于任何需要支持多模型的项目都有参考价值。

相关阅读:

GitHub: https://github.com/earendil-works/pi

By AI博士 万戈