返回文章列表

Pi 技术分析系列 · 开篇 · 上篇

日期:2026-10-16

一个编码 Agent 接到“读取 README 并总结”这样的任务后,究竟发生了什么?

模型需要知道有哪些工具可以使用,执行程序需要把工具调用变成文件读取,文件内容需要重新进入上下文,最终回答还要显示给用户。任务稍微复杂一些,就会遇到新的问题:用户在运行中补充了要求,工具返回了错误,历史超过上下文窗口,或者程序在执行中退出。

这些问题决定了编码 Agent 的实际行为。模型提供生成与决策能力,围绕模型的执行系统则决定请求怎样组织、动作怎样发生、结果怎样保存,以及下一步何时开始。

Pi 提供了这样一套执行系统。这组上下篇先建立整体架构地图,再沿着执行、状态和恢复的关系分析它的设计。上篇介绍 Pi 的组成、启动、任务循环与会话职责;下篇继续讨论上下文、能力接入、持久运行,以及这些机制的验证与取舍。后续专题会沿着这张地图,分别深入具体问题。

1. Pi 是什么:模型之外的执行系统

Pi 将自己定位为一个精简、可扩展的 Agent harness。这里的 harness 可以理解为“围绕模型组织工作的执行框架”:它向模型提供上下文和工具声明,接收模型响应,执行工具,再决定是否发起下一次请求。

对于使用者,Pi 可以表现为终端里的编码助手。对于开发者,它也提供可以嵌入其他程序的能力。当前版本有几种主要使用形态:

使用形态

与外部交互的方式

适合承载的工作

交互式 CLI

在终端输入任务,观察流式回答与工具进度

人与 Agent 持续协作

Print 模式

提交提示词,输出结果后退出

一次性脚本任务

JSON 模式

输出结构化事件流

自动化消费运行过程

RPC 模式

通过进程间的结构化请求与事件控制会话

其他应用托管 Pi

TypeScript SDK

在程序中创建会话、提交输入和订阅事件

嵌入宿主应用,自行组织交互

这些入口共享会话与执行能力,因此“编码助手的能力”和“终端里的表现形式”可以分别讨论。终端是一种交互入口;模型请求、工具执行和历史管理还有各自的责任主体。

flowchart LR
    U[用户或宿主应用] -->|提交任务| P[Pi 执行系统]
    P -->|上下文与工具声明| M[模型]
    M -->|文本或工具调用| P
    P -->|调用具体实现| T[文件与命令等工具]
    T -->|执行结果| P
    P -->|事件与最终结果| U

图 1:Pi 与模型、工具和使用者之间的数据流。模型提出工具调用,Pi 执行调用,并把结果用于后续请求。

这张图也说明了能力边界。模型返回一个 read 调用,并不意味着模型已经读到了文件。Pi 必须找到对应工具、校验参数、执行读取,并把结果交回模型。类似地,工具能够执行命令,不意味着执行框架自动提供了操作系统级隔离;当前 Pi 的隔离需要由部署环境或相关扩展承担。1

从这个角度分析 Pi,重点是理解它如何把一次生成变成持续工作的过程。

2. 整体架构:各部分如何组成系统

Pi 的整体结构包含两组值得区分的运行能力:一组支撑当前编码助手,另一组围绕持久任务与恢复展开。它们共享模型等基础能力,但并不组成一条依次经过所有模块的调用链。

当前编码助手的主要关系是:coding-agent 组织产品能力,agent 驱动通用执行循环,ai 提供模型契约与接入能力,tui 承担终端交互。工具还可以通过 MCP 接入,也可以通过 Code Mode 编排。

flowchart TB
    subgraph CLI[编码助手主线]
        CA[coding-agent<br/>会话策略与产品装配]
        AG[agent<br/>运行循环与工具调度]
        UI[tui<br/>终端交互]
        MCP[mcp<br/>外部工具协议]
    end
    subgraph DUR[持久运行体系]
        ENV[env<br/>远程执行环境]
        D[durable<br/>会话与任务运行时]
        CH[chord<br/>状态与对象契约]
    end
    AI[ai<br/>统一模型接口]
    CM[codemode<br/>脚本工具执行]
    TM[telemetry<br/>观测契约]
    CA --> AG
    CA --> AI
    CA --> UI
    CA --> MCP
    CA --> CM
    AG --> AI
    ENV --> D
    ENV --> CH
    D --> AI
    D --> CH
    D --> CM
    AI --> TM

图 2:主要内部运行时依赖,箭头从使用方指向被依赖方。它表达结构依赖,不表示一次请求的执行顺序。行为评估模块 evals 单列在下表中,不作为运行时调用链的一层。

完整来看,这个版本有 11 个模块:

模块

核心责任

与整体系统的关系

coding-agent

会话策略、配置、扩展、内置工具、CLI 与 SDK

把通用能力组织成编码助手

agent

模型与工具循环、运行状态、输入队列和事件

承载一次运行的通用控制流

ai

消息类型、模型注册、认证、供应商与协议适配、流式响应

为上层提供统一模型接口

tui

终端输入、组件、布局、屏幕更新

把会话事件转成终端交互

mcp

MCP 客户端、传输、工具发现与调用

将外部服务接入工具体系

codemode

在 QuickJS 环境中运行模型生成的 JavaScript

让一次脚本执行组织一组工具调用

durable

持久会话、文档状态、任务调度、提交与恢复

用可保存的任务状态承载 Agent 工作

chord

对象契约、可观察 JSON 状态、取消上下文、跨进程通信和热更新

为需要这些能力的运行时与应用提供基础设施

env

本地及远程执行环境、连接与 daemon

将 Durable 的文件和命令操作转交具体机器

telemetry

观测接口、类型化 schema 和参考适配器

允许宿主接入自己的观测系统

evals

编码助手的行为评估、实验执行与报告

检查实际任务表现,而非参与每次请求

这里最重要的结构事实是:当前 coding-agent 直接装配 Agent 与 SessionManager,没有把 durable 作为这条主链的底层。 durable 自己组织生成任务与工具任务,并直接使用 ai 请求模型。2

这种组织允许开发者在不同层次使用 Pi:可以复用模型接入,可以复用通用循环,也可以直接使用完整编码会话。对持久任务有需求的应用,则可以使用 Durable 的运行机制。它们解决的问题彼此关联,生命周期与状态语义却需要分别理解。

3. 系统如何启动:把依赖装配成一个会话

一个能够工作的编码会话,需要同时知道模型是什么、工作目录在哪里、哪些工具可用、有哪些扩展,以及是否存在需要恢复的历史。启动过程的作用,就是把这些输入组织成一致的运行环境。

在 CLI 路径中,启动入口先完成基础设置,再进入 main()。随后创建与工作目录相关的服务,包括配置、模型运行时和资源加载器。资源加载器发现技能、提示词模板、主题和扩展;模型运行时负责模型与认证等能力。

flowchart TB
    IN[CLI 参数与工作目录] --> S[创建目录相关服务]
    CFG[配置与模型认证] --> S
    RES[扩展、技能与模板] --> S
    S --> SDK[createAgentSession<br/>装配会话依赖]
    HIST[SessionManager<br/>新会话或已有历史] --> SDK
    SDK --> AG[Agent<br/>通用执行对象]
    AG --> AS[AgentSession<br/>编码会话策略]
    SDK --> AS
    AS --> RT[AgentSessionRuntime<br/>持有当前会话及相关服务]
    RT --> MODE[交互、Print / JSON 或 RPC 模式]

图 3:CLI 启动时的装配过程。箭头表示构建所需的输入或构建结果;后续请求不会每次都重走这条启动流程。

createAgentSession() 是重要的装配位置。它选择或恢复模型,准备消息转换和模型流函数,创建 Agent,再把 Agent、历史管理器、配置、资源和自定义工具交给 AgentSession。3

Agent 获得的是执行所需的契约。例如,模型请求通过注入的 streamFn 完成。编码会话可以在这个函数外组织认证、请求参数和缓存相关策略,通用循环则继续围绕统一消息和事件运行。

AgentSessionRuntime 管的是另一个生命周期问题:当前会话被替换时,哪些关联对象必须一起更换? 切换或导入历史可能改变有效工作目录。如果只替换消息列表,工具仍使用旧目录,或者资源仍来自旧项目,就会出现状态不一致。

因此,运行时对象持有当前 AgentSession 和目录相关服务,并保存重建它们的工厂。替换会话时,它会结束原会话的活动运行,发出关闭事件,释放旧会话,再创建新会话并让外层入口重新绑定。创建失败会向调用方传播,由交互层决定怎样展示错误。3

从设计上看,依赖装配与请求执行分开,能让会话能力被不同入口复用;而会话替换作为完整生命周期处理,能避免工作目录、资源和历史只更新了一部分。

4. 一次任务如何执行:从用户输入到最终回答

回到“读取 README 并总结”。下面假设模型选择调用一次 read,然后返回总结。这是用来解释机制的示例;真实模型也可能选择其他工具,或发起更多请求。

sequenceDiagram
    actor User as 用户
    participant Mode as 交互入口
    participant Session as AgentSession
    participant Runner as Agent / AgentLoop
    participant Model as 模型接口
    participant Tool as read 工具
    User->>Mode: 读取 README 并总结
    Mode->>Session: prompt(text)
    Session->>Session: 输入处理、资源展开与运行准备
    Session->>Runner: prompt(messages)
    Runner->>Session: prepareRequest
    Session-->>Runner: 当前会话上下文
    Runner->>Model: 上下文与工具声明
    Model-->>Runner: assistant 消息,包含 read 调用
    Runner-->>Session: message_end
    Session->>Session: 分发事件并保存消息
    Runner->>Tool: 校验参数后 execute
    Tool-->>Runner: 文件内容或错误结果
    Runner-->>Session: 工具事件与结果消息
    Session->>Session: 保存工具结果
    Runner->>Model: 带工具结果的下一次请求
    Model-->>Runner: 最终总结
    Runner-->>Session: 消息、turn_end 与 agent_end
    Session-->>Mode: 会话事件与收尾结果
    Mode-->>User: 展示总结

图 4:一次读文件任务的主要调用与事件。图中把流式输出合并为消息,实际交互界面会在运行过程中持续接收更新;第二次请求也会经过请求准备。

输入首先进入 AgentSession.prompt()。会话层会检查扩展命令,让 input handler 有机会处理输入,再展开技能命令和提示词模板。如果 Agent 已经在运行,输入会依据指定行为进入 steering 或 follow-up 队列。

准备好新的运行后,AgentSession 调用 Agent.prompt()。后者建立运行生命周期,把当前消息、工具和循环配置交给 runAgentLoop()。循环请求模型,提取模型响应中的工具调用,执行工具,并把结果用于下一轮请求。4

整个过程需要区分四个尺度:

概念

本文采用的含义

示例中的数量

用户输入

用户提交的一项要求

“读取 README 并总结”这一条输入

模型请求

向模型发起的一次生成请求

第一次请求读文件,第二次生成总结

Turn

一次模型响应及其随后的工具执行

示例中有两个 turn

Run

从输入驱动的一串 turn 到最终回答的过程

示例中是一段完整运行

在具体实现里,会话层还可能在底层 Agent 结束后启动重试或恢复。因此,底层一次循环结束、整个会话处理完成,也需要分别判断。

这段流程体现了 Agent 的基本反馈结构:工具结果改变后续请求的输入,后续请求再决定是否继续行动。任务能够跨越一次模型生成,就是因为执行系统维持了这个反馈过程。

5. Agent 循环:怎样决定继续、等待和结束

5.1 两层循环分别处理什么

runLoop() 的内层循环处理工具调用和 steering 消息;外层循环在自然停止点检查 follow-up。两类输入进入运行的时机不同,因此需要分开处理。

flowchart TB
    START[准备本轮消息与请求] --> MODEL[请求模型并接收响应]
    MODEL --> ERR[模型响应错误或取消?]
    ERR -->|是| STOPERR[完成本轮并结束循环]
    ERR -->|否| HAS[包含工具调用?]
    HAS -->|是| TOOLS[执行工具批次并收集结果]
    HAS -->|否| FIN[finishTurn 与 turn_end]
    TOOLS --> FIN
    FIN --> ENDQ[finishTurn 明确要求结束?]
    ENDQ -->|是| STOP[发出 agent_end]
    ENDQ -->|否| INNER[需要工具续问或有 steering?]
    INNER -->|是| NEXT[prepareNextTurn]
    INNER -->|否| FOLLOW[有 follow-up?]
    FOLLOW -->|是| NEXT
    FOLLOW -->|否| CONT[显式要求继续?]
    CONT -->|是| NEXT
    CONT -->|否| STOP
    NEXT --> START

图 5:循环的主要控制分支。“需要工具续问”取决于工具批次结果;普通批次会继续,全部结果要求终止时可以跳过续问。图中的等待发生在模型或工具调用中,队列检查本身不会无期限等待新输入。

Steering 适合修改当前任务的方向。例如用户补充“只看安装说明”,消息会在正常的输入检查点进入上下文。工具已经执行中的时候,它通常要等当前响应的工具批次完成,再影响后续请求。

Follow-up 表示当前工作自然结束后再做的事情。例如“总结完后,再检查许可证”。只要当前循环还需要执行工具或处理 steering,follow-up 就不会抢先进入。

另一个边界是准备请求期间的排队行为。prepareNextTurn 可能包含压缩等耗时操作,循环会在约定条件下补查 steering;prepareRequest 则不会自行额外轮询队列。这让队列进入 turn 的位置保持明确。5

5.2 并行执行与稳定上下文怎样同时成立

默认情况下,一个响应中的工具调用可以并行执行。如果全局配置要求串行,或者这个批次有工具声明 executionMode: "sequential",整个批次就改为串行。

在并行模式里,预处理按顺序完成,通过预处理的工具随后并行运行。工具完成后可以立刻发出完成事件;但最终写入上下文的工具结果仍按模型原先给出的调用顺序组织。

sequenceDiagram
    participant L as AgentLoop
    participant A as 工具 A
    participant B as 工具 B
    participant E as 事件订阅者
    Note over L,B: 模型按 A、B 顺序发出调用
    par 执行 A
        L->>A: execute
    and 执行 B
        L->>B: execute
    end
    B-->>L: B 先完成
    L-->>E: B 的 tool_execution_end
    A-->>L: A 后完成
    L-->>E: A 的 tool_execution_end
    L->>L: 按 A、B 顺序组织 ToolResultMessage

图 6:执行完成顺序与上下文消息顺序分离。示例中 B 先完成,但结果消息仍保持 A、B 的顺序。

这个选择同时满足了两个目标:界面可以尽早显示已经完成的工作,模型历史也不会随着运行耗时变化而改变排列。代价是循环必须区分“执行完成事件”和“最终结果消息”,不能把两者当作同一种输出。

5.3 结束是一个协议

一次没有工具调用的回答通常会走向自然停止;循环也允许其他明确的结束方式。如果一个工具批次的所有最终结果都要求 terminate: true,就可以跳过自动工具续问。混合批次中只有部分结果要求终止,不会因此结束整个批次后的运行。

finishTurn 可以进一步返回继续或结束决定。如果要求继续,但没有工具结果、steering 或 follow-up 自然触发下一次请求,循环会补上一轮仅基于当前上下文的请求。

错误也有不同层次。工具执行失败通常会成为错误工具结果,供模型后续处理;模型响应的 stopReason 为 error 或 aborted 时,当前循环完成本轮并结束。如果模型输出因为长度限制被截断,里面的工具参数可能不完整,循环会把该响应中的工具调用转成失败结果,而不是直接执行它们。5

最后,agent_end 只表示底层循环不再产生新事件。Agent 仍会等待订阅者完成事件处理,再清理运行状态并变为空闲。把“收到结束事件”当成“所有相关工作都已完成”,会忽略这段收尾过程。

6. 状态与会话:三个核心对象如何分工

Pi 中的 Agent、AgentSession 和 SessionManager 名称相近,但分别承担不同责任。

对象

持有或组织的内容

主要责任

Agent

运行中的消息、工具、模型选择、流式消息、待完成调用与队列

驱动通用循环,接收事件并更新运行状态

AgentSession

编码助手的配置、资源、扩展、压缩与恢复策略

把用户输入和产品行为接到循环边界

SessionManager

历史记录、记录索引、当前叶节点与会话文件

保存历史,并构建当前分支的上下文

这三个对象通过方法调用和事件协作。会话层调用 Agent 发起运行;Agent 接收循环事件,先更新自己的状态,再依次等待订阅者处理。AgentSession 是订阅者之一,它把事件交给扩展和外层入口,并在消息完成等边界写入历史。4

flowchart TB
    LOOP[AgentLoop 产生事件] --> AG[Agent.processEvents<br/>更新运行状态]
    AG --> AS[AgentSession 接收事件]
    AS --> EXT[扩展事件处理器]
    EXT --> PUB[公开会话事件订阅者]
    PUB --> PERSIST[message_end 等边界<br/>写入 SessionManager]
    PERSIST --> RETURN[当前订阅处理返回]

图 7:当前 CLI 会话中一类事件的处理顺序。公开会话通知位于消息持久化之前;公开监听器的返回并不等价于 Durable 的存储提交协议。其他事件有各自的处理分支。

Agent 关注执行规则,例如哪些工具调用尚未结束、何时读取 steering、什么时候可以空闲。AgentSession 则关注编码产品的策略,例如提示词如何展开、是否需要压缩、失败后是否重试,以及扩展是否要求继续工作。

这也是生命周期 hook 有用的原因。prepareRequest 可以在每次请求前安装当前会话的正式上下文;prepareNextTurn 可以在后续请求前刷新工具和提示词,或触发压缩;finishTurn 则给会话层一个在整轮完成后决定继续行为的位置。

从设计上看,这个边界让通用循环保持可复用,同时允许编码助手不断增加会话行为。相应的成本是:运行状态、持久历史和请求上下文之间必须有明确的同步点。事件顺序与 hook 顺序因此成为架构契约,不能只被当作通知细节。

从一次运行走向长期会话

沿着输入、模型请求、工具执行和事件收尾,可以看清 Pi 怎样组织一次任务。Agent、AgentSession 与 SessionManager 的分工,也说明了运行状态、产品策略和历史记录各自的位置。

接下来需要回答的是:历史怎样变成本次模型可见的上下文,新的工具和扩展怎样接入,以及进程退出后未完成工作如何处理。这些问题把一次运行连接到长期会话与持久任务。

下篇接着分析这些边界,并把测试、观测与行为评估放回整体架构中。

研究范围与源码依据

本文基于 Pi 的固定提交 4954d760504c11df9b2cbdc040346107b6a8153d。文中的“当前”均指这个版本,源码链接也固定到同一提交。

上篇依据模块依赖、CLI 与 SDK 装配、Agent 循环、会话事件和历史写入的静态阅读展开。本次没有安装 Pi 运行依赖,没有执行其测试,也没有进行真实模型联调。图中的任务、工具完成顺序和具体执行过程是解释机制的示例,不是运行实录。

引用

依据

支撑的分析

1

项目定位与使用形态

Pi 的定位、入口与执行环境边界

2

coding-agent 依赖、Durable 依赖、Env 依赖

主链与持久运行体系的结构关系

3

会话装配、运行时替换、CLI 入口

启动、装配与会话生命周期

4

Agent 运行对象、AgentSession

输入进入循环、状态与事件协作

5

Agent 循环

请求、工具执行、队列、转换与结束协议

6

AgentSession

会话策略、请求投影与事件持久化