设计启发¶
这一章把前面分析过的源码设计抽象成可迁移的工程模式。它不依赖 Pi 本身,适合做其他 agent runtime、coding agent、插件系统时参考。
每条设计启发背后的具体源码入口见源码索引。
1. Agent Loop 要小¶
Pi 最值得借鉴的一点是把 agent-loop.ts 做得很小。它只处理:
- 消息进入上下文。
- 模型流式响应。
- 工具调用和工具结果。
- steering/follow-up 队列。
- 生命周期事件。
所有产品特性都放在外层。这样 loop 可以被 CLI、SDK、RPC、测试和未来产品复用。
2. 自定义消息要有转换边界¶
coding agent 一定会产生不属于 LLM 协议的消息:
- shell command output。
- 压缩摘要。
- 分支摘要。
- 扩展私有消息。
- UI-only notification。
Pi 的做法是允许 AgentMessage 扩展,但在 provider 边界统一 convertToLlm()。这比一开始就把所有消息压扁成 user/assistant 更灵活,也比让 provider 知道产品消息更干净。
3. 会话存储应保留结构¶
很多 agent 会把会话存成线性聊天数组。Pi 用 JSONL append-only tree:
- 可以 fork。
- 可以切换 leaf。
- 可以保留原始历史。
- compaction 不需要删除旧消息。
- branch summary 有自然位置。
如果你的 agent 需要探索式开发、回滚、分支对比,树形会话比数组更适合。
4. 上下文不是历史¶
Pi 把“历史如何存储”和“模型看到什么”分开:
- 历史是 JSONL entry。
- 模型上下文是
buildSessionContext()生成的视图。 - 压缩只是改变视图,不改写原始历史。
这是长会话 agent 的关键原则。
5. 工具执行要兼顾三种顺序¶
工具系统至少有三种顺序:
- preflight 顺序:校验、权限、hook。
- execution 顺序:可以并行以提高速度。
- context 写回顺序:必须稳定可重放。
Pi 的设计把这三种顺序分开处理,尤其适合多 tool call 的模型。
6. 扩展事件需要语义化¶
不要只提供一个通用 emit()。不同事件应该有不同规则:
- 有的事件可 cancel。
- 有的事件可 block。
- 有的事件可替换 message。
- 有的事件串行累积 result。
- 有的事件只是通知。
Pi 的 ExtensionRunner 明确区分这些事件,使扩展强大但不混乱。
7. 插件上下文要可失效¶
支持 reload、switch session、fork 的系统里,插件很容易持有旧 context。Pi 在 dispose 时让旧 extension context 失效,并要求扩展在新 session 回调中继续工作。
这是插件系统稳定性的关键细节。
8. Provider 差异要显式建模¶
“OpenAI-compatible” 往往只是大体兼容。Pi 的 compat 配置把差异写成模型/provider metadata:
- role 支持差异。
- tool result 格式差异。
- thinking 参数格式差异。
- cache control 差异。
- session affinity 差异。
这比到处写 if/else 更容易维护。
9. 功能不一定都进核心¶
Pi 明确不内置 MCP、sub-agent、plan mode、permission popup、todo、background bash,而是让扩展实现。这个取舍背后是一个产品架构原则:
核心只提供稳定能力边界,工作流由插件组合。
这能减少核心复杂度,也给用户更大的工作流自由度。
10. 摘要要保留工程上下文¶
对 coding agent 来说,摘要不能只有“聊了什么”。Pi 的 compaction summary 还保留:
- 目标。
- 约束。
- 已完成/进行中/阻塞。
- 关键决策。
- 下一步。
- 关键上下文。
- read files。
- modified files。
这是让另一个 LLM 继续工作的最低有效上下文。
一句话总结¶
Pi 的优秀设计不是某个单点技巧,而是边界感:模型协议、agent loop、产品会话、扩展系统、工具执行、上下文压缩各自有清晰职责,并通过少数稳定接口连接起来。