Pi / 01
Pi 是什么:定位、架构与 Agent Loop
为什么这个问题值得关注
把 Pi 只理解成“终端里的聊天工具”,会看不到它真正有价值的部分:一套可以单独使用、嵌入应用、替换模型并扩展运行行为的 Agent Harness。反过来,把它描述成“什么都不做的四包小内核”,又会漏掉 Session、Compaction、RPC、SDK 和项目资源加载这些已经存在的能力。
更可靠的理解方法是追踪一次完整工具往返:谁产生意图,谁执行环境动作,哪条结果成为下一轮模型事实,以及哪一层负责把这些对象保存下来。
本文基于官方仓库 commit
a470b121。包和 API 会变化,职责边界比数量更稳定。
Pi 解决什么问题
Pi 的官方定位是 minimal agent harness。翻译成工程职责,它需要完成:
- 把用户目标、项目上下文和可用工具组织成模型请求;
- 接收流式模型事件,把 tool call 转成真实工具执行;
- 将 tool result 追加回消息序列,继续下一轮模型请求;
- 保存 Session,使任务可以恢复、分支和压缩;
- 通过 Skill、Extension、Prompt Template 和 Package 适配不同工作流;
- 通过 TUI、JSON、RPC 和 SDK 服务不同宿主。
Pi 同时明确不内置 MCP、subagent、权限弹窗、plan mode、todo 和 background bash。这些不是“做不到”,而是官方不愿替所有用户规定唯一实现。你可以用 Extension 或 Package 增加它们,也可以继续使用 CLI、容器和 tmux 等外部能力。
当前源码中的职责层
当前 monorepo 对外发布五个主要包。下图是职责图,不是逐函数调用图:
flowchart TD
U["用户目标"] --> CA["pi-coding-agent<br>会话、工具、资源与交互"]
CA --> CORE["pi-agent-core<br>状态、事件与 Agent Loop"]
CORE --> AI["pi-ai<br>Provider 与统一消息协议"]
AI --> P["模型 Provider"]
CORE --> TOOLS["read、write、edit、bash"]
TOOLS --> CORE
TUI["pi-tui<br>终端渲染"] --> CA
CA --> TEL["pi-telemetry<br>遥测契约与适配"]
| 包 | 稳定职责 | 不应误解为 |
|---|---|---|
@earendil-works/pi-ai |
模型目录、统一消息、流式事件、Provider 适配 | 完整 Agent Runtime |
@earendil-works/pi-agent-core |
有状态 Agent、工具执行、事件流和 Loop | Coding Agent 产品外壳 |
@earendil-works/pi-coding-agent |
CLI、Session、资源加载、内置工具、Extension、SDK/RPC | 只有 TUI 的命令行程序 |
@earendil-works/pi-tui |
差分终端渲染和交互组件 | 模型决策层 |
@earendil-works/pi-telemetry |
厂商无关的遥测契约和适配器 | 安全审计系统 |
仓库里还存在 protocol、client、server、session backend 等内部或专项包。因此,学习当前源码时不应再用固定“四包架构”概括全部实现。
一次 README 请求为什么会调用模型两次
用户要求“读取 README 并概括项目”时,模型第一次并不知道文件内容。最小闭环如下:
用户消息
-> 第一次模型调用
-> assistant message 提出 read(toolCallId=call_1)
-> Agent Loop 执行 read
-> tool result 返回文件内容(toolCallId=call_1)
-> 第二次模型调用
-> assistant message 给出最终回答
这里有三种不同责任:
- 模型负责提出动作:tool call 只表示“模型想读取”。
- Loop 负责调度动作:根据工具名定位实现、校验参数、处理并发与取消。
- 工具负责产生环境事实:tool result 才能证明读取成功、失败或被中止。
toolCallId 把意图与结果配成一对。如果只保存 tool call 和最终文本,却丢掉 tool result,系统无法证明文件真的被读取过。这也是《动手学 Pi》序章最值得保留的观察方法:先问每条事实由谁产生,再判断它是否应该进入长期 transcript。
运行事件和持久消息不是一回事
pi-agent-core 对外发送的事件比教材的七个抽象里程碑更完整。一次带工具的请求大致经过:
agent_start
turn_start
message_start / message_update / message_end
tool_execution_start / tool_execution_update / tool_execution_end
message_start / message_end # toolResult message
turn_end
turn_start # 带着工具结果再次请求模型
message_start / message_update / message_end
turn_end
agent_end
这些事件适合驱动 TUI、日志和外部宿主,但并非全部都要进入模型上下文。长期事实主要保存在结构化消息和 Session entry 中:
user:用户目标和后续指令;assistant:完整文本、thinking 和 tool call;toolResult:与 call id 配对的结果、错误标记和 details;- Session 中的模型切换、compaction、branch summary、label 与 Extension entry。
“UI 上显示过”不等于“下一轮模型能看到”,“最终回答提到成功”也不等于“工具事实证明成功”。这两个区分是调试 Agent Loop 的起点。
并行工具带来的顺序问题
当前 pi-agent-core 默认支持并行工具执行。并行时需要区分两个顺序:
tool_execution_end按真实完成时间发出,快工具可能先结束;- 持久化的 tool result 仍按 assistant message 中 tool call 的原始顺序追加。
这种设计同时满足实时 UI 和稳定 transcript。若直接把“完成事件顺序”当作“消息顺序”,相同任务可能在每次运行中产生不同的上下文,测试和恢复都会变得不稳定。
工具失败也必须形成配对结果。参数无效、工具不存在、实现抛错、超时和取消不能让 call 悬空,否则后续模型既不知道发生了什么,也无法可靠恢复。
Pi 不只在终端中运行
| 模式 | 入口 | 适合场景 | 宿主额外责任 |
|---|---|---|---|
| Interactive | pi |
人持续在环的开发任务 | 审批、观察和最终验证 |
| Print / JSON | pi -p、--mode json |
Shell 脚本、CI、事件采集 | 退出码、结构化输出和超时 |
| RPC | pi --mode rpc |
Python、Go、编辑器或独立进程集成 | JSONL framing、进程生命周期、取消 |
| SDK | createAgentSession() |
Node.js 应用内嵌 | Session、凭据、工具与并发管理 |
TUI 只是 pi-coding-agent 的一个产品入口。判断 Pi 是否能嵌入你的系统,应查看 RPC/SDK 契约,而不是根据“terminal coding harness”推断它只能在终端使用。
用 JSON 模式观察真实闭环
在临时仓库中执行一个只读任务:
pi --mode json "读取 README.md,只报告第一行" 2>/dev/null \
| jq -c 'select(.type | startswith("tool_execution"))'
你应当看到同一个 toolCallId 出现在 tool_execution_start 和 tool_execution_end。然后把文件名改成不存在的路径,观察结束事件和 tool result 如何表达错误。
这个实验不要求记住所有事件名。它要验证的是四个不变量:
- tool call 与 result 能配对;
- 工具错误不会伪装成正常文本;
- 结果会进入下一轮模型请求;
- 运行结束后,文件系统状态仍需独立验证。
从哪里继续读源码
| 问题 | 当前源码入口 |
|---|---|
| Agent 事件、工具并发与 Loop | packages/agent/src/ |
| Message、Model、Provider 与流式协议 | packages/ai/src/types.ts、packages/ai/src/api/ |
| CLI 到 Session 的组合入口 | packages/coding-agent/src/main.ts |
| Session 与 JSONL entry | packages/coding-agent/src/core/session-manager.ts |
| SDK 创建会话 | packages/coding-agent/src/core/sdk.ts |
| Extension 类型与事件 | packages/coding-agent/src/core/extensions/types.ts |
源码路径比博客中的包数量更适合作为事实锚点。升级后若文章结论与类型定义冲突,应以当前类型、测试和可执行行为为准。
最小内核的收益与代价
Pi 的收益不是“功能最少”,而是工作流偏好较少写死在核心里。你可以只使用 pi-ai,也可以通过 SDK 嵌入 Session,或者用 Extension 修改工具和事件行为。
代价同样明确:
- 没有内置 sandbox,进程权限就是最大影响范围;
- Extension 与 Package 是同进程可执行代码,需要自行审计;
- 很多产品能力有多种社区实现,团队必须选择并维护组合;
- Provider 和扩展 API 迭代较快,固定版本与回归测试不可省略。
因此,“可扩展”不自动等于“生产可用”。生产系统仍要回答隔离、审计、恢复、升级和评测由谁负责。
小结
- Pi 是可组合 Agent Harness,不只是终端聊天界面。
- 当前主线由
pi-ai、pi-agent-core、pi-coding-agent、pi-tui和pi-telemetry等包分工完成。 - tool call 是模型意图,tool result 才是环境事实,两者必须用 call id 配对。
- 运行事件服务 UI 与宿主,Session 和消息保存可恢复事实,二者不能混用。
- Pi 原生支持 Session、JSON、RPC 和 SDK,但安全隔离仍需由外部系统提供。
下一篇建议继续看:pi-ai:统一多 Provider 的适配层设计