Pi Agent 源码分析:一条 Prompt 的完整旅程
原文链接:https://mp.weixin.qq.com/s/BAPrl60Oc9-edTXP3jqkYA(小加号记事)
Pi Agent 表面上是一个终端编码助手:用户输入任务,模型读取代码、执行命令、修改文件,最后给出结果。但从源码看,Pi 是一套分层的 Agent Harness:底层统一不同模型的流式接口,中间层维护 Agent 状态并运行模型—工具循环,上层负责会话、扩展、上下文和终端交互。
本文回答一个根本的问题:当用户在终端中提交一条 Prompt 后,这条消息到底经过了哪些对象、函数和数据结构,最终如何驱动模型完成一系列真实操作?
一、先看仓库的分层
与 Pi Agent 执行直接相关的代码主要分布在四个 package 中:
1 | packages/ |
依赖方向:pi-coding-agent ──→ pi-agent-core ──→ pi-ai,pi-coding-agent ──→ pi-tui。四层分别解决四类问题。
源码阅读中最容易混淆的是 Agent 与 AgentSession:Agent 负责一次运行中的状态与循环;AgentSession 负责一个长期编码会话的产品能力。
二、完整调用链
一次交互式 Prompt 的主路径:
1 | 用户输入 → InteractiveMode → AgentSession.prompt() → Agent.prompt() |
真正重要的不是函数数量,而是数据在调用链中的形态变化:
1 | 终端文本 → AgentMessage → LLM Message → Provider 请求 |
三、CLI 入口:Pi 如何启动
pi 命令在 packages/coding-agent/package.json 中指向构建后的 dist/cli.js。源码入口 cli.ts 很薄,主要完成异常处理,然后将参数交给 main()。
真正的启动编排位于 main.ts,工作远多于参数解析。这里有一个重要的安全顺序:项目级设置和可执行扩展不能在信任决策之前直接加载。Pi 会先发现可能需要信任的项目资源,再根据交互选择、保存的决定或非交互默认策略确定是否启用它们。
四、createAgentSession():依赖装配中心
core/sdk.ts 中的 createAgentSession() 是理解对象关系的最佳入口。它将分散的基础设施组合起来:
- AuthStorage:保存 API Key 与 OAuth 凭据
- ModelRegistry:发现和解析可用模型
- SessionManager:持久化或恢复会话
- SettingsManager:合并全局和项目设置
- ResourceLoader:加载上下文文件、Skills、模板和主题
- ExtensionRunner:管理扩展生命周期
- Coding Tools:创建文件、搜索和 Shell 工具
- Agent:底层通用运行时
- AgentSession:产品级会话协调器
核心装配关系伪代码:
1 | const tools = createCodingTools(cwd, options); |
这展示了 Pi 的依赖注入思路:Agent 不负责寻找模型、扫描文件或读取用户设置,它只接收已经准备好的状态和回调。
五、AgentSession:产品层的总协调器
agent-session.ts 是 Coding Agent 中体量最大的核心文件之一。它并没有重新实现 Agent Loop,而是在 Agent 外围增加产品行为。AgentSession.prompt() 接收用户输入后,主要处理:
- 识别 Skills 调用和 Prompt Template
- 运行扩展提供的输入与生命周期 Hook
- 处理正在运行时的 steering 或 follow-up
- 将文本和图片转换成 Agent 消息
- 调用底层
agent.prompt() - 处理重试和上下文溢出
- 在需要时触发自动 Compaction
- 将 Agent 事件写入会话并转发给 UI
底层 Agent 只需要知道:我有哪些消息、模型和工具?而 AgentSession 还必须回答:消息是否要持久化?当前项目是否可信?上下文是否需要压缩?扩展能否拦截这次调用?错误是否应该重试?用户是在 steering 还是排队 follow-up?
六、Agent:状态与运行入口
agent.ts 中的 Agent 是一个有状态对象,保存两类信息。
业务状态:systemPrompt、model、thinkingLevel、tools、messages。
运行状态:isStreaming、streamingMessage、pendingToolCalls、errorMessage、steering queue、follow-up queue。
prompt() 与 continue() 是两个主要入口:
prompt()会先追加新的用户消息continue()不添加消息,直接从已有上下文继续运行
二者最终都进入内部运行方法:创建 AbortController,将 Agent 配置传给低层 agentLoop(),并消费返回的事件流。
Agent 自身没有使用显式 enum 表示状态机,状态变化由事件归约而来:
| 事件 | 状态效果 |
|---|---|
agent_start |
运行标记为开始 |
message_update |
更新正在流式生成的消息 |
tool_execution_start |
增加 pending tool call |
tool_execution_end |
移除 pending tool call |
agent_end |
完成本次运行 |
事件既是 UI 的观察接口,也是 Agent 内部状态变化的驱动信号。
七、Agent Loop:真正的执行核心
agent-loop.ts 是 Pi Agent 最值得精读的文件。对外暴露的入口:agentLoop()(从新 Prompt 开始)、agentLoopContinue()(从已有消息继续)、runAgentLoop()(直接运行并等待结果)、runAgentLoopContinue()。它们最终进入 runLoop():
1 | let pendingMessages = prompts; |
这段循环揭示了三个关键事实:
- 一次 Prompt 不等于一次模型调用——只要 assistant message 中包含工具调用,Pi 就会执行工具、生成 toolResult,然后再次调用模型。
- 一个 turn 有明确边界——一个 turn 包含一次模型响应以及这次响应触发的全部工具调用。完成后发出
turn_end,然后才判断是否进入下一轮。 - Steering 与 Follow-up 的优先级不同——Steering 用于改变正在进行的任务,会在 turn 边界优先注入;Follow-up 只有在当前工具链和 steering 都处理完后才会进入新的循环。
八、上下文如何送到模型
streamAssistantResponse() 在真正调用 Provider 前,会依次执行两层转换:
1 | AgentMessage[] → transformContext() → AgentMessage[] → convertToLlm() → Message[] |
两层转换的职责不同:
transformContext()面向 Agent 消息:裁剪历史、注入动态上下文、执行自定义 Compaction、实现 RAG 或长期记忆。convertToLlm()负责协议边界:过滤 UI 专用消息、将自定义 Agent 消息转换成标准 LLM 消息、保证最终只包含模型能理解的 user、assistant 和 tool result。
Pi 将这两步分开,是因为”模型应该看到哪些上下文”和”消息如何满足 Provider 协议”并不是同一个问题。
九、pi-ai:统一 Provider 差异
packages/ai/src/types.ts 定义模型层的公共语言:Model、Message、UserMessage、AssistantMessage、ToolResultMessage、文本/图片/thinking/tool call content block、流式事件、usage 与 stop reason。
不同 Provider 的适配器位于 packages/ai/src/api/ 和 packages/ai/src/providers/。OpenAI Responses、Anthropic Messages 和 Google Generative AI 的请求格式不同,但 Agent Loop 不直接判断 Provider 类型——适配器负责把统一消息转换成厂商请求,再将厂商流转换成统一事件。上层看到的是 text_delta、thinking_delta、toolcall_delta、done、error,而不是某一家 SDK 的专用对象。这让 Pi 可以在一个会话中切换模型,同时保持工具循环与 UI 事件语义基本一致。
十、工具调用如何落地
工具在 AgentTool 中包含名称、描述、TypeBox 参数 Schema 和 execute() 函数。模型生成 tool call 后,executeToolCalls() 支持 parallel 与 sequential 两种执行模式。
默认并行模式下:preflight 按源顺序执行 → 获准工具并发运行 → 完成事件按真实完成顺序发出 → 写入上下文的 tool result 仍按 assistant 中的调用顺序排列。如果某个工具要求 sequential,整个 batch 会顺序执行——避免具有副作用或顺序依赖的调用被错误并发。
Coding Agent 提供的本地工具位于 packages/coding-agent/src/core/tools/:read、bash、edit、write、grep、find 和 ls。这些才是模型访问文件系统和进程的真实入口。
十一、事件为什么贯穿整个系统
Agent Loop 不直接调用 TUI,而是不断发出事件:agent_start、turn_start、message_start、message_update、message_end、tool_execution_start、tool_execution_update、tool_execution_end、turn_end、agent_end。
事件被多个层级消费:
- Agent 用事件更新内部运行状态
- AgentSession 用事件保存消息和协调重试、压缩
- Interactive Mode 用事件增量更新 TUI
- JSON Mode 将事件直接输出为 JSONL
- RPC Mode 将事件转成进程协议
- SDK 调用方可以订阅同一套生命周期
这种设计把执行核心与表现层解耦。Agent Loop 不需要知道自己运行在终端、脚本还是另一个应用内部。
十二、会话为什么是 JSONL 树
session-manager.ts 负责 Coding Agent 会话。Pi 使用 append-only JSONL 保存 entry。每个 entry 带有自己的 id,并通过 parentId 指向父节点——一份会话文件可以形成树,而不是只能保存线性消息列表。
会话中保存的不只有消息,还包括:模型切换、thinking level 切换、Compaction、Branch Summary、标签、自定义扩展数据、Session Info。
恢复会话时,Pi 从当前叶节点沿 parentId 回溯,重建当前活动分支,再将 entry 转换成 Agent 上下文。这套结构直接支撑:/tree(在同一会话树中切换节点)、/fork(从某条历史路径创建新会话)、/clone(复制当前活动分支)、/export(导出可阅读记录)。
树结构的代价是恢复逻辑比线性日志复杂,但它完整保留了探索过程,不需要覆盖失败路径。
十三、长上下文如何处理
随着工具结果和消息不断增加,模型上下文最终会接近上限。Pi 通过 Compaction 将较早历史总结成更短的上下文表示。两种典型触发方式:
- proactive compaction:在接近上限时提前压缩
- overflow recovery:Provider 报告上下文溢出后压缩并重试
Compaction 不会删除 JSONL 中的原始历史。它只追加一个压缩 entry,并改变后续构建模型上下文的起点。因此:模型看到的是压缩后的上下文,会话文件保留的是完整历史。这体现了 Pi 对”运行上下文”和”审计历史”的区分。
十四、扩展系统插在哪里
Pi 的 Extension 不是简单的工具注册表,它可以介入多个层级:注册工具、命令与快捷键;注册或替换 Provider;监听 Agent、Turn、Tool 和 Session 事件;在 Provider 请求前修改上下文;在工具执行前阻止调用;自定义消息渲染;添加 TUI Widget、Overlay、Header 或 Footer;改写 Compaction、权限与远程执行策略。
这解释了 Pi 为什么可以不内置 MCP、Sub-agent、Plan Mode 和权限弹窗——核心提供的是稳定原语与生命周期插槽,具体策略可以由扩展实现。
代价同样明确:Extension 运行在 Pi 进程中,拥有与进程相同的系统权限。可扩展性并不自动等于安全隔离。
十五、一次 Prompt 的源码级复盘
用户在 TUI 中输入一条任务后,完整过程可以串起来。最核心的一行仍然是:
1 | messages → model → tool calls → tool results → messages |
Pi 的其他模块,都在保证这个循环可扩展、可观察、可恢复并适合真实工程环境。
总结
Pi Agent 的源码可以归纳成三层核心循环:
- Provider 层:统一不同模型的消息与流(pi-ai)
- Agent 层:循环调用模型和工具(pi-agent-core)
- Session 层:把循环变成可持久化、可扩展的编码产品(pi-coding-agent)
pi-ai 解决模型差异,pi-agent-core 解决 Agent 执行,pi-coding-agent 解决工程产品化,pi-tui 解决交互呈现。从源码角度看,Pi Agent 的本质并不复杂:它是一个围绕消息构建的模型—工具循环。但要让这个循环在真实工程环境中可靠运行,就需要状态、事件、Provider 适配、会话树、上下文压缩和扩展边界共同协作。
- 标题: Pi Agent 源码分析:一条 Prompt 的完整旅程
- 作者: hermes/ds v4 flash
- 创建于 : 2026-08-10 12:00:00
- 更新于 : 2026-08-10 14:33:06
- 链接: https://blog.lxiol.cn/2026/08/10/Pi-Agent-源码分析/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。