Pi 技术分析系列 · 开篇 · 下篇
分析日期:2026-10-17
认识 Pi(上):整体架构与任务执行介绍了 Pi 的整体架构,并沿一次任务梳理了启动、模型请求、工具执行和事件收尾。三个核心对象的分工由此明确:Agent 驱动运行,AgentSession 组织编码会话策略,SessionManager 保存历史并构建上下文。
本篇接着讨论这些对象怎样支撑持续工作。历史需要选择和转换,才能成为下一次模型请求的材料;工具、扩展和模型适配需要接入已有流程;任务跨越进程生命周期时,还需要可保存的执行状态与恢复规则。
我们从“历史怎样成为上下文”开始,再把能力接入、持久运行和验证体系连接起来,最后归纳上下篇呈现出的设计取舍,并给出后续专题的问题地图。
1. 历史如何成为上下文:消息、分支、投影与压缩
1.1 三种表示服务于三个问题
完整历史需要解释“发生过什么”;运行时消息需要表达“应用现在处理什么”;模型请求需要表达“模型这次应该看到什么”。Pi 为这些问题保留了不同表示。
SessionEntry 是历史记录。除消息外,它还可以保存模型切换、思考等级、压缩、分支摘要、标签和上下文编辑等信息。记录带有 id 与 parentId,因此历史能形成树,而不只是一个不断增长的消息数组。
AgentMessage 是运行时消息。它包含模型消息,也允许应用扩展自定义类型。编码助手可以记录命令执行、压缩摘要和自定义消息,而不要求每一种应用概念都直接成为供应商协议的一部分。
Message 是 ai 的统一模型消息契约,包含 system、user、assistant 和 toolResult。供应商适配器再把这种统一表示转成实际 API 请求。3
flowchart TB
H[SessionEntry 历史树] -->|选择当前分支| P[会话投影]
C[压缩记录与上下文编辑] -->|影响投影结果| P
P -->|生成| A[AgentMessage 数组]
A --> T[transformContext]
T --> CONV[convertToLlm]
CONV --> M[统一 Message 数组]
M --> API[供应商请求表示]图 1:历史到模型请求的数据转换。箭头表示数据加工;它不意味着原始历史被每一步直接覆盖。
1.2 分支与压缩改变的是请求视角
SessionManager 用 JSONL 保存记录,并用父子关系和当前叶节点确定活动分支。构建上下文时,系统先沿当前分支选择记录,再根据压缩和上下文编辑生成消息投影。
flowchart LR
R[共同历史] --> Q[某次用户输入]
Q --> A[回答 A]
Q --> B[回答 B]
B --> N[当前后续消息]
N --> V[当前上下文沿 B 分支构建]图 2:会话树的示意。图中的边表示历史继承关系;选择 B 分支后,请求上下文不会自动混入 A 分支的回答。
压缩会引入摘要和保留范围,使后续模型请求不必携带全部旧消息。历史文件仍然保留用于追溯的记录。上下文编辑也通过对应记录影响投影,而不是简单把当前内存数组当作唯一事实。
因此,压缩不只是“从数组前面删除一些消息”。它同时涉及完整历史、当前分支、被保留内容和发给模型的摘要,必须维持这些表示之间的对应关系。
当前编码会话在每次请求前,通过 prepareRequest 从 SessionManager 构建正式投影,并保留当前可执行工具集合。随后通用循环应用上下文转换,再调用模型。这让请求所见内容有一个明确来源,而不是任意沿用某次 UI 更新后的数组。2
1.3 转换边界怎样隔离应用概念
以下代码节选来自 streamAssistantResponse(),保留了从运行时消息到模型请求的关键顺序:
let messages = context.messages;
if (config.transformContext) {
messages = await config.transformContext(messages, signal);
}
// Convert to LLM-compatible messages (AgentMessage[] → Message[])
const llmMessages = await config.convertToLlm(messages);
const llmContext = normalizeContext({ messages: llmMessages });来源:Agent 循环中的上下文转换。
transformContext 仍在应用消息层工作;convertToLlm 则负责把应用消息映射为模型消息。例如,命令执行记录可以转成用户消息,压缩摘要可以转成带有说明文字的上下文,明确排除的内容不会进入请求。
工具也有两种对应表示:运行时保留可执行工具,消息中的 system 记录保存模型可见的工具声明及变更。declareToolChanges() 用声明差量表达工具集合变化,使“模型被告知可以调用什么”和“宿主能够执行什么”有可追踪的联系。
这一层设计的价值是把应用历史与供应商协议隔开。代价是转换本身必须得到验证:错误过滤可能使模型丢失信息,错误排序可能破坏调用和结果的关系,错误声明可能让模型请求一个宿主不能执行的工具。
2. 能力如何接入:工具、扩展、模型与交互界面
2.1 工具连接声明与真实动作
一个工具同时面向模型和宿主。模型看到名称、说明和参数 schema;宿主掌握 execute()、取消信号、执行结果以及显示所需的信息。
flowchart TB
DEF[名称、说明与参数 schema] -->|声明给模型| M[模型生成 toolCall]
M --> FIND[按名称查找工具]
FIND --> VALID[准备和校验参数]
VALID --> BEFORE[beforeToolCall]
BEFORE --> EXEC[execute]
EXEC --> AFTER[afterToolCall]
AFTER --> RESULT[工具结果消息与事件]图 3:通用 Agent 的正常工具路径。查找失败、参数无效、hook 阻止和取消会走对应错误分支,未在图中展开。
以 read 为例,工具实现需要解析路径,检查可读性,区分文本与图片,对文本处理偏移量和行数,再限制返回内容的大小。图片还需要根据模型能力与图像处理结果生成对应内容。对模型来说,它得到的是统一工具结果;具体文件系统操作仍由工具实现承担。4
这种划分让执行循环可以调度不同工具,但也说明工具契约不能只包含一个函数名。参数描述影响模型如何发起调用,schema 决定宿主如何解释输入,结果形态影响下一轮模型能够理解什么。
2.2 Skill、模板与 Extension 改变的层次不同
机制 | 主要载体 | 对运行的影响 |
|---|---|---|
Skill | 指令文本与相关资源 | 向模型提供某类工作的知识或步骤; |
Prompt Template | 可展开的提示词文本 | 把命令与参数组织成输入内容 |
Extension | 宿主执行的代码 | 注册工具、命令和模型能力,处理事件,改变输入、上下文或交互 |
Skill 影响模型如何工作,Extension 则可以直接参与执行系统。两者都能改变最终行为,但作用位置和生命周期不同。例如,一份 Skill 可以要求模型遵循某种检查步骤;一个 Extension 可以在工具执行前检查调用并阻止它。
ResourceLoader 负责发现和加载资源,ExtensionRunner 负责调度扩展处理器。发现资源与执行扩展分开,使资源来源、重新加载和运行期事件可以分别处理。5
2.3 MCP 扩展工具来源,Code Mode 改变调用组织方式
MCP 提供连接外部工具服务的协议。Pi 的 MCP 能力负责建立连接、发现工具、发送调用,并把返回内容转成模型可以消费的文本或图片等形式。对 Agent 循环而言,这些外部能力仍然可以表现为工具。
Code Mode 关注的是另一件事:让模型生成一段 JavaScript,由脚本组合工具调用。脚本运行在 QuickJS 环境中,通过注入的工具能力完成工作。嵌套调用的中间结果不必逐条进入模型上下文,模型主要接收脚本输出和返回结果。6
例如,脚本可以读取几个配置文件,筛选所需字段,再返回一份汇总。这样一次模型决策能够组织一组程序化操作。相应的复杂度进入脚本执行与嵌套工具管理,包括超时、取消、错误、结果大小和调用可见性。
因此,MCP 和 Code Mode 可以协作:前者提供工具来源,后者组织调用过程。它们不会自动替代会话层对历史、上下文和运行状态的管理。
2.4 统一模型接口怎样容纳供应商差异
编码会话注入 Agent 的 streamFn 会调用 ModelRuntime.streamSimple()。模型运行时准备认证和请求参数,再调用相应 Provider。独立使用 ai 时,也可以通过 createModels() 创建模型注册与调用入口。
flowchart TB
L[AgentLoop] -->|调用 streamFn| R[ModelRuntime 或 Models]
R -->|选择模型与准备认证| P[Provider]
P -->|统一消息转协议请求| API[供应商 API 实现]
API -->|流式响应转统一事件| R
R -->|AssistantMessageEventStream| L图 4:模型接入的请求与响应路径。ModelRuntime 是编码会话中的模型管理实现;其他宿主可以使用 Models 接口组织自己的接入。
Provider 将模型目录、认证和 API 实现组织在一起。API 层负责实际协议,包括请求参数、传输、内容块与流式事件之间的映射。以上层统一消息和事件为边界,Agent 循环就不必直接解析每一家供应商的响应格式。7
统一接口也不意味着所有模型完全相同。模型能力、思考等级、输入限制和传输差异仍然需要在相关层表达。这里的设计问题,是把共同行为放在哪里,并让不可消除的差异有清楚的位置。
2.5 界面消费事件,而不是承担模型循环
交互模式把用户输入交给会话,并订阅会话事件。文本增量更新回答组件,工具事件更新进度与结果,界面再请求 TUI 渲染。TUI 提供组件、布局、输入处理和屏幕更新能力。8
Print 模式可以只输出最终文本;JSON 模式可以输出事件;RPC 模式允许宿主程序控制会话。它们对显示和控制的要求不同,但都可以建立在会话能力上。
这使交互方式能够变化,而运行规则有相对集中的实现位置。与此同时,流式事件也成为公开接口的一部分:它们的含义、顺序和完成边界会影响宿主怎样展示进度,以及什么时候认为任务已经结束。
3. 持久运行体系:Durable、Chord 与 Env
3.1 保存对话与恢复工作是两个问题
假设工具已经执行了一个外部动作,但程序在记录结果前退出。重启后,系统可能知道模型曾要求调用这个工具,却不知道动作是否完成。
会话历史能够帮助恢复上下文,但要恢复未完成工作,还需要保存任务意图、执行阶段、检查点和恢复规则。这是 Durable 所关注的问题。当前版本将其标记为实验性 API;它拥有独立于 CLI Agent 循环的运行机制。9
3.2 持久运行中的几个基本概念
概念 | 含义 | 为什么需要 |
|---|---|---|
Harness | 打开存储并运行会话与任务的运行时 | 统一管理调度与生命周期 |
Conversation | 一条会话及其不可变历史记录 | 确定输入、上下文和工作的归属 |
Submission | 提交给会话的输入或写入请求 | 表示一个已接收、可以等待结果的请求 |
Entry | 一条不可变历史记录 | 保存用户输入、模型回答、工具结果等事实 |
Document | 在提交中修改的类型化 JSON 状态 | 保存配置、输入队列、运行状态与业务状态 |
Task | 带检查点的持久状态机 | 让工作能够等待、继续和恢复 |
Commit | 原子提交边界 | 把相关记录、文档变更和任务状态一起保存 |
一个输入通常会驱动生成任务。生成任务请求模型,必要时创建工具任务并等待结果,然后交接给后续生成工作,直到输入得到回答。
flowchart TB
SUB[接收 Submission] --> C1[提交用户 Entry 与运行状态]
C1 --> G1[GenerationTask<br/>准备并请求模型]
G1 --> C2[提交 assistant Entry 与工具任务]
C2 --> T[ToolTask<br/>执行工具工作]
T --> C3[提交工具结果与任务状态]
C3 --> G2[后续 GenerationTask]
G2 --> C4[提交最终回答并完成 Submission]图 5:包含一次工具调用的 Durable 运行示意。箭头表示工作推进;提交节点标出持久状态边界。运行过程还可以保存流式进度,并非只有图中的最终结果才会提交。
GenerationTask 从 prepare 阶段开始,在明确阶段中处理上下文、模型请求和后续工作。工具调用则成为独立 ToolTask,具有自己的状态与归属。等待工具完成可以表现为可保存的任务状态,而不只是留在进程内存中的一个 Promise。10
3.3 恢复依赖检查点与重放约定
工具执行前,Durable 会先提交执行意图,其中包含最终参数和重放策略。如果进程在执行途中退出,恢复路径会检查记录下来的策略与当前工具声明。只有两者都允许安全重放,工具才会重新执行;否则形成中断错误结果。10
flowchart TB
I[提交工具意图<br/>参数与 replay 策略] --> E[执行外部动作]
E --> R[提交工具结果]
E -.->|执行阶段进程中断| RE[重启后读取检查点]
RE --> SAFE{已保存策略与当前工具<br/>都声明 safe?}
SAFE -->|是| AGAIN[重新执行工具]
SAFE -->|否| INT[生成 interrupted 结果]
AGAIN --> R图 6:工具执行中断后的恢复分支。重放意味着重新执行,不意味着系统已经自动证明外部动作只发生一次。
如果工具会发送消息、创建资源或提交支付,仅仅把 replay 设为 safe 并不会消除重复副作用。工具仍然需要幂等键、结果查询或其他业务约束。持久运行时提供的是保存意图和实施恢复规则的位置。
Durable 还用任务所有权表达父子工作关系。前台子任务会参与父任务的等待和取消;后台任务可以成为不同的生命周期边界。这让子 Agent、并发子任务和取消传播能够围绕已保存状态组织,但具体语义需要与任务定义和所属关系一起分析。
存储后端也有差别。Memory 适合不落盘的运行;JSONL 使用追加文件;SQLite 和 Cloudflare Durable Object SQLite 提供对应的持久存储适配。是否能抵抗进程退出、主机故障或掉电,仍取决于后端与刷新配置,不能仅凭“使用 Durable”给出同一种保证。9
3.4 Chord 与 Env 分别补上什么
Chord 提供类型化对象契约、可观察 JSON 状态、取消 Context、跨进程连接与热更新等能力。它的完整能力范围比 Durable 的一次模型请求更广;不能把“Durable 依赖 Chord”理解成每次生成都会用到 Chord 的全部机制。11
Env 则将执行环境抽象落实到机器。Durable worker、存储和认证可以留在本地,文件与命令操作通过连接交给远程 daemon。这里既有 TypeScript 客户端,也有在目标机器运行的 Rust 程序。远程环境与本地环境实现同类执行契约,使运行时可以在不改变模型循环目标的情况下更换动作发生的位置。11
flowchart LR
subgraph HOST[运行时所在机器]
W[Durable worker]
ST[存储]
AUTH[模型认证与接入]
CLIENT[RemoteExecutionEnv]
W --> ST
W --> AUTH
W --> CLIENT
end
subgraph REMOTE[执行所在机器]
DAEMON[pi-env daemon]
OS[文件与进程]
DAEMON --> OS
end
CLIENT -->|连接与操作协议| DAEMON图 7:远程执行的部署示意。箭头表示访问或调用关系;具体传输可以由连接实现提供,SSH 是其中一种使用方式。
Durable、Chord 和 Env 共同展示了另一组设计问题:工作怎样保存、状态怎样被观察、取消怎样传播、动作在哪里执行。它们与编码助手有关联,却不能被简化成给 SessionManager 换一个数据库。
4. 如何验证理解:测试、观测与行为评估
架构分析需要区分“控制流符合约定”和“真实任务完成得好”。Pi 的测试、观测和评估分别提供不同证据。
4.1 模拟模型能够验证哪些事实
通用循环通过 streamFn 请求模型,因此测试可以注入确定的响应,而不依赖在线模型。在现有 echo 工具测试中,第一次模拟响应要求调用 echo,工具返回结果,第二次模拟响应给出最终文本。12
这条测试可以检查:模型是否被调用两次,工具是否收到预期参数,结果是否出现在返回消息中,以及相关结果信息是否经过 hook 处理。它让一次完整工具反馈过程具有可重复的输入与输出。
同一组测试还覆盖上下文转换顺序、工具完成顺序与结果消息顺序、排队消息进入循环的时机,以及结束与继续的边界。这些场景直接对应前文的架构契约。
flowchart LR
FAKE[确定的模拟模型响应] --> TEST[控制流测试]
TEST --> CONTRACT[验证消息、事件与状态约定]
RUN[真实模型与工具运行] --> OBS[观测记录]
OBS --> DIAG[解释耗时、用量与失败]
TASK[任务集与评估条件] --> EVAL[行为评估]
EVAL --> OUT[比较实际任务表现]图 8:三类验证提供不同证据。连线表示证据产生过程;控制流测试通过,不能替代真实模型联调或任务效果评估。
4.2 Telemetry 提供契约,宿主决定后端
telemetry 提供显式传递的观测上下文、span 契约、类型化 schema 和内存参考实现。它不自带一个默认导出后端,也不依赖全局“当前 span”状态。应用可以提供自己的适配器,接到日志、OpenTelemetry 或其他系统。13
这意味着观测能力可以作为边界契约融入调用过程,而不要求所有使用 Pi 的应用采用同一种平台。真正能够观测到哪些阶段,还需要看相应调用路径是否创建并传递了观测上下文。
4.3 Evals 关注任务表现
evals 用于编码助手的行为评估。当前实现区分宿主环境中的评估与文档增益实验。文档增益实验会在有文档、无文档的隔离条件下运行用例,再配对结果形成报告。13
这里回答的问题已经从“事件顺序正确吗”变成“给定模型、工具和材料,任务结果怎样”。模型、输入样本、环境和评分方式都会影响结论。因此,一篇整体架构分析可以确认评估机制的存在和组织方式,却不能据此声称 Pi 在某类任务上表现更好。
5. 整体设计判断与后续研究地图
结合上篇的执行链与本文的上下文、扩展和恢复机制,可以形成几项初步判断。这些判断是基于源码结构与行为契约的分析,不代表对作者全部设计动机的推断。
5.1 通用循环与产品策略分开,复用以契约为代价
Agent 循环保留模型请求、工具执行、队列和生命周期等共同机制,编码会话通过 hook、资源和事件组织产品行为。这样的结构允许其他宿主复用执行能力。
相应的代价是,调用顺序和状态边界必须清楚。哪些 hook 可以改变下一次请求,哪些事件意味着消息已经完成,什么时候能够开始下一项工作,都不能由接入方自行猜测。
5.2 事件连接执行与交互,也增加了顺序要求
相同运行过程能够服务终端、JSON 和 RPC,说明事件是重要的连接方式。工具完成事件与结果消息分开,也是为了同时满足显示与上下文需求。
但事件不是无成本的解耦。异步订阅、持久化时机、工具结果顺序和空闲判定,都会影响外部观察到的行为。后续分析需要把事件看作协议,检查它与真实状态的对应关系。
5.3 历史与上下文分开,使长期会话能够改变视角
历史树保存发生过的事情,会话投影决定本次请求看见什么。分支、摘要和上下文编辑因此能在保留追溯能力的同时改变后续生成。
这也把复杂度集中到投影和转换边界。压缩是否保留工具调用关系、分支是否恢复正确模型选择、自定义消息是否被正确转换,都会直接影响后续模型行为。
5.4 持久任务让恢复成为运行模型的一部分
Durable 保存任务意图、检查点与所有权,使恢复和取消可以围绕明确状态展开。这种结构适合研究持续运行、子任务和跨进程恢复。
代价是持久状态本身需要版本、迁移、重放和生命周期规则。持久化范围扩大,并不会自动消除业务副作用的不确定性;它让这些问题具有可以讨论和验证的边界。
5.5 后续文章沿哪些问题展开
作为系列开篇,这组上下篇建立整体位置。后续按以下问题逐篇展开,每篇会把一个机制讲到能够推演其行为,并把结论放回整体架构中。
后续主题 | 核心问题 | 对应架构位置 |
|---|---|---|
Agent 循环与生命周期 | 一次运行怎样推进,什么条件下继续或结束? |
|
工具执行与并发 | 参数、hook、错误、取消和结果顺序怎样协作? | Agent 循环与工具实现 |
输入队列与运行中干预 | steering 和 follow-up 怎样进入已有工作? | Agent 与 AgentSession |
会话历史与分支 | 历史树怎样保存、切换和恢复? | SessionManager |
上下文投影与压缩 | 模型看见哪些信息,摘要怎样影响后续行为? | 会话投影、转换与压缩 |
扩展与资源生命周期 | 扩展如何注册、运行、重载,并参与会话? | ResourceLoader 与 ExtensionRunner |
MCP 与 Code Mode | 外部工具怎样接入,脚本怎样组合嵌套调用? | 工具接入与编排 |
模型适配与流式协议 | 统一消息怎样映射不同供应商,错误和用量如何返回? | ModelRuntime、Provider 与 API |
TUI、RPC 与 SDK | 同一个会话怎样支持不同交互入口? | 会话接口与交互层 |
Durable 调度与恢复 | 检查点、等待、重放和所有权怎样决定恢复行为? | Harness 与 TaskScheduler |
Chord 与执行环境 | 状态怎样同步、代码怎样更新、动作怎样转交远程机器? | Chord 与 Env |
测试、观测与评估 | 怎样分别证明机制正确、解释运行、衡量任务效果? | 测试、Telemetry 与 Evals |
这条研究路线的目标,是逐步建立能够解释整个系统行为的模型:面对一条输入、一次工具错误、一项上下文变化或一次进程中断,能够指出谁负责处理、状态在哪里变化,以及下一步为什么会发生。
研究范围与源码依据
这组上下篇基于本地 Pi 源码,版本固定为 4954d760504c11df9b2cbdc040346107b6a8153d。文中的“当前”均指这个版本。引用使用同一提交的源码链接,避免后续分支变化使结论失去对应依据。
引用 | 依据 | 支撑的分析 |
|---|---|---|
请求、工具执行、队列、转换与结束协议 | ||
会话策略、请求投影与事件持久化 | ||
三种消息表示、分支和压缩 | ||
工具声明与文件读取实现 | ||
技能、模板与扩展的接入边界 | ||
外部工具和脚本编排 | ||
模型注册、认证与协议映射 | ||
事件与交互入口 | ||
持久运行、存储和恢复边界 | ||
检查点、重放与任务推进 | ||
状态基础设施与远程执行 | ||
echo、消息转换、顺序与队列场景 | ||
观测契约与行为评估 |
回看整体架构与执行链:认识 Pi(上):整体架构与任务执行。