官方资料:Pi 项目(原 badlogic/pi-mono) · Agent Core · 核心循环源码 · 会话文档 · 扩展文档
本文基于 2026-10-15 官方 main 分支的文档与源码整理;涉及 English Studio 的部分为工程架构建议,不代表已落地实现。
先建立心智模型
Pi 的定位是一个极简、可扩展的 Agent Harness(执行底座),而非把所有特性塞进单一实例的“全能型超级 Agent”。它仅通过少量正交的通用机制提供模型调用、工具循环与事件分发;上层应用则通过组合扩展(Extension)、技能(Skill)、提示词(Prompt)和会话管理,装配出具体的业务产品。
| 架构层次 | 核心职责 | 对应包名 / 模块 |
|---|---|---|
| 模型协议层 | 屏蔽多厂商 API 差异,统一标准消息结构、工具 Schema 与流式输出 | pi-ai |
| 执行引擎层 | 纯逻辑 Headless 运行时,维护 Agent 状态、双层 Loop、工具调度、消息队列与事件流 | pi-agent-core |
| 终端渲染层 | 保留模式组件树与增量差分渲染引擎,提供独立于业务逻辑的终端交互界面 | pi-tui |
| 产品装配层 | 串联底层核心,提供内置工具、JSONL 会话树、资源装配(Skill / Prompt)、扩展系统与多模式入口 | pi-coding-agent |
| 外部接入层 | 面向终端用户或宿主程序的 Interactive CLI、Print / JSON、RPC 协议与 TypeScript SDK | 宿主应用 / 使用方 |
在工具集设计上,Pi Coding Agent 默认仅内置 read、write、edit、bash 四类基础原语;诸如子 Agent(Sub-agent)、计划模式(Plan Mode)等高阶能力并不会硬编码进每个实例,而是按需通过扩展和外部包动态挂载。官方 README 明确阐述了这一克制的工程取舍。
模块间的联系:依赖拓扑与协作链路
要真正读懂 Pi 的源码,首先需要看清各个包之间的单向依赖关系与运行期协作链路。Pi 严格遵循“底层无感上层、上层装配底层”的分层原则:
1. 装配工具、上下文与钩子
2. 订阅生命周期事件
4. 发起标准 Message[] 流式推理
3. 驱动视图更新
各模块在运行期的核心协作可分为四条脉络:
- 严格的纯逻辑隔离(
pi-agent-core仅依赖pi-ai):pi-agent-core是一个完全无头(Headless)的纯逻辑运行时,它绝不反向依赖pi-coding-agent或pi-tui。它不知道“终端界面”的存在,也不关心“JSONL 文件如何落盘”。当需要与大模型交互时,它仅将转换后的标准Message[]交给pi-ai,由pi-ai屏蔽 OpenAI、Anthropic、Google 等不同供应商的流式协议差异。 - 自顶向下的装配注入(
pi-coding-agent→pi-agent-core): 在启动阶段,pi-coding-agent扮演“组装工厂”的角色。它扫描本地的AGENTS.md、SYSTEM.md、Skills 与 Prompt 模板,连同内置的四大基础工具(read/write/edit/bash)以及从 Session 树恢复的活动分支历史,一并注入给pi-agent-core实例,并挂载transformContext与工具拦截钩子。 - 自底向上的事件广播(
pi-agent-core→Session/pi-tui/RPC):pi-agent-core的事件流是整个系统的神经中枢。在runLoop()执行期间,所有模型流式增量、工具调用状态、轮次起止都以事件(Event)形式向外广播:Session Manager订阅事件,在每轮对话或工具结果产出时追加写入 JSONL 节点,并在上下文逼近阈值时触发自动压缩(Auto Compaction)。pi-tui(或在非终端模式下的 RPC / JSON 输出流)订阅相同的事件流,将文本增量与工具执行进度实时差分渲染到终端屏幕上。
- 横向切面的动态介入(
Extension System贯穿全链路):pi-coding-agent中的扩展系统通过统一的ExtensionAPI横跨多个模块:- 向
pi-ai注入新的自定义模型 Provider; - 向
pi-agent-core注册自定义工具、挂载beforeToolCall/afterToolCall拦截器,或通过steering/follow-up队列干预循环方向; - 向
Session Manager写入扩展专属的状态记录(appendEntry); - 向
pi-tui挂载自定义终端弹窗、状态栏或交互式问答组件。
- 向
一、真正理解 Pi 的双层 Agent Loop
Agent 的运行并非简单的“单次提问、单次回答”:模型可能发起工具调用,宿主执行工具并将结果回填至上下文后,模型再据此做下一步决策。为此,位于 pi-agent-core 中的 runLoop() 采用了精巧的双层循环结构:
- 内层循环(Inner Loop): 围绕模型响应、工具执行结果以及
steering(运行中途的补充或改向指令)持续推进当前任务,直至无需调用工具且无中途插队指令。 - 外层循环(Outer Loop): 当内层循环自然收敛并准备停止时,检查
follow-up队列;若存在待处理的后续任务则开启新一轮内层循环,否则彻底结束本次运行。
为什么需要区分 steering 和 follow-up 两种队列?
| 调度机制 | 介入时机 | 典型学习场景示例 |
|---|---|---|
steering |
当前任务尚未完成,在下一个安全的轮次边界立即介入 | “先别长篇解释了,直接改成出一道练习题” |
follow-up |
当前任务完整收敛并自然结束后,再顺序触发 | “等这段语法分析完,再帮我整理成生词卡” |
需要注意,这两者本质上是 pi-agent-core 内部的单 Agent 输入调度队列,既不代表两个并行运行的 Agent,也不是跨阶段的自动化业务状态机。在交互模式下,pi-coding-agent 将用户的快捷键(如 Enter 与 Alt+Enter)分别映射为向这两个队列投递消息。
单个 Turn(轮次)的标准生命周期依次为:拼装待处理消息 → prepareRequest → 调用 pi-ai 获取模型流式输出 → 校验并执行工具 → 工具结果写回上下文 → 触发 finishTurn 与 turn_end 事件。若运行中途发生异常或收到取消信号,循环会提前安全终止,而不会强行保证所有待执行工具跑完。
以下代码是为还原核心控制流而提炼的逻辑伪代码(并非 Pi 源码的逐行复刻):
while (true) { // 外层循环:在当前任务自然结束时检查 follow-up 队列
let toolsOwed = true;
while (toolsOwed || pending.length > 0) { // 内层循环:推进工具调用与 steering 指令
appendMessages(pending);
const answer = await callModel(); // 内部经转换管线后调用 pi-ai
const results = await executeTools(answer.toolCalls);
appendToolResults(results);
toolsOwed = shouldContinueFromTools(results);
pending = await getSteeringMessages();
}
pending = await getFollowUpMessages();
if (pending.length === 0) break;
}真实的生产源码还涵盖了请求预处理、异步事件广播、并发工具调度、Abort 取消信号及异常终止控制。但这段伪代码揭示了最核心的架构边界:执行调度权完全归属于宿主代码(pi-agent-core),大模型(通过 pi-ai)仅负责推理并提议下一步动作。
二、消息管线:落盘存储的消息不等于发送给模型的上下文
为了解耦“业务状态”与“模型协议”,Pi 在 pi-agent-core 与 pi-ai 的交界处严格区分了两种消息结构:
AgentMessage(属于pi-agent-core/pi-coding-agent):应用层消息,除了对话文本外,还可携带 UI 渲染状态、扩展自定义数据及业务元数据。Message(属于pi-ai):底层大模型协议所能理解的标准角色消息(如user、assistant、toolResult)。
在每次向 pi-ai 发起请求前,管线会先调用 transformContext() 对历史消息进行裁剪、过滤或动态注入,随后再通过 convertToLlm() 将 AgentMessage 投影为 pi-ai 可接受的标准 Message 格式。
| 管线阶段 | 所属模块边界 | 数据形态转换 | 工程意义 |
|---|---|---|---|
| 原始状态 | pi-coding-agent / pi-agent-core |
AgentMessage[] |
承载完整的会话历史、扩展状态与 UI 业务数据 |
transformContext |
pi-coding-agent 注入 → pi-agent-core 调用 |
AgentMessage[] → AgentMessage[] |
历史窗口裁剪、Compaction 摘要替换、动态上下文注入 |
convertToLlm |
pi-agent-core → pi-ai 边界 |
AgentMessage[] → Message[] |
剥离应用层私有字段,转换为标准模型协议格式 |
| Provider 调用 | pi-ai |
Message[] → Stream |
适配具体模型厂商 API,发起推理请求并接收流式响应 |
以 English Studio 为例:当系统生成一条“已保存词汇卡”的记录时,应用层会话需要持久化完整的卡片数据结构以供前端渲染;但在下一轮推理时,只需向模型投影一句轻量的“本次会话已保存 1 个复习词汇”即可。会话的持久化结构与单轮推理的模型上下文必须解耦设计。
事件流与并发工具执行
pi-agent-core 在运行期会依次派发 agent_start、turn_start、message_start/update/end、tool_execution_start/update/end、turn_end、agent_end 等细粒度生命周期事件。上层 UI(如 pi-tui 或宿主前端)可直接订阅这些事件,精准驱动“模型思考中”“工具执行中”“任务已完成”等交互状态。
在工具调度方面,Agent Core 原生支持并发工具执行模式:尽管多个工具异步完成的时间先后可能与模型输出的调用顺序不一致,但最终写回上下文的 toolResult 消息依然严格按照模型发起调用的原始顺序组装,以保证上下文的确定性。此外,beforeToolCall 与 afterToolCall 钩子分别承接调用前的准入拦截与调用后的结果清洗。详细规范可参阅 Agent Core 文档。
三、Session:为什么持久化会话不等于模型上下文?
Session 管理位于 pi-coding-agent 层。它通过订阅 pi-agent-core 的消息事件,默认采用 JSONL 格式将交互历史持久化到本地,并在逻辑上通过 id 与 parentId 指针将其组织为一棵可分叉的会话树(Session Tree)。用户当前所处的“活动分支(Active Branch)”,决定了下一次模型请求将回溯哪一条路径上的历史记录。
| 会话指令 | 执行行为 | 原有历史是否保留 |
|---|---|---|
/tree |
在当前会话树的不同分支间自由切换,更新 pi-agent-core 的当前上下文路径 |
完整保留所有分支 |
/fork |
从某个历史用户消息节点分叉,创建独立新会话文件 | 完整保留原会话 |
/clone |
将当前活动分支完整克隆为一个新会话 | 完整保留原会话 |
/compact |
调用模型生成结构化历史摘要,通过 transformContext 控制上下文窗口开销 |
原始 JSONL 日志依然保留 |
自动压缩(Auto Compaction)机制清晰体现了 pi-coding-agent 与 pi-agent-core 的配合:当 Session Manager 检测到 Token 接近上下文上限时,会将早期历史归纳为摘要节点;在下一轮运行前,只把“摘要节点 + 最近保留的活跃消息”组装进 pi-agent-core,而原始 JSONL 文件中的完整学习轨迹分毫无损。
四、Extension:如何在不侵入核心 Loop 的前提下扩展能力?
Pi 的扩展机制位于 pi-coding-agent 层,本质上是标准的 TypeScript 模块。开发者可通过 ExtensionAPI 在不改动 pi-agent-core 核心循环一行代码的前提下,将自定义逻辑挂载到各个模块的衔接点上:
| 核心扩展点 | 作用的底层模块 | 典型用途 |
|---|---|---|
pi.registerProvider() |
pi-ai |
注册自定义模型提供商或动态路由模型(Virtual Model) |
pi.registerTool() |
pi-agent-core |
向模型注册可调用的自定义工具(或覆盖内置工具) |
pi.on("tool_call") |
pi-agent-core |
桥接至 beforeToolCall,在工具执行前进行权限审计、参数改写或直接阻断 |
pi.on("tool_result") |
pi-agent-core |
桥接至 afterToolCall,对工具返回的结果进行脱敏、截断或二次加工 |
before_agent_start |
pi-agent-core |
在新一轮 Agent 运行启动前动态注入外部上下文(如按需 RAG 检索) |
pi.appendEntry() |
Session Manager |
向当前 JSONL Session 树追加扩展专属的自定义持久化记录 |
pi.registerCommand() |
pi-coding-agent / pi-tui |
注册用户级斜杠命令,或挂载自定义终端交互界面 |
以下示例展示了如何利用最小化的官方 API 拦截高危工具调用:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.on("tool_call", async (event) => {
if (event.toolName === "delete_learning_records") {
return { block: true, reason: "删除学习记录属于高危操作,需先获得用户显式授权" };
}
});
}五、English Studio 可以直接借鉴哪些架构思想?
将 Pi 的模块划分与设计哲学映射到 English Studio 的工程实践中,可以提炼出以下清晰的模块对标与演进方向:
| Pi 的模块与原理 | English Studio 的架构落地建议 |
|---|---|
pi-ai 与 pi-agent-core 分离 |
在 Rust 后端中拆分独立的 llm-provider crate 与无状态的 agent-runtime crate,彻底隔离厂商协议与执行循环 |
pi-agent-core 的 Headless 定位 |
核心执行循环不绑定 WebSocket 或特定前端协议,仅向外输出标准异步 EventStream,同时服务于 Web 端、桌面端与后台批处理任务 |
| 应用层消息与模型消息分离 | 学习记录全量落库为结构化业务事件(对应 AgentMessage),推理前按需通过投影函数裁剪为精简 Prompt(对应 Message) |
steering / follow-up 双队列 |
在交互层严格区分“打断并修正当前讲解”与“追加本轮结束后的衍生练习” |
pi-coding-agent 的 Session Tree |
支持练习中途的追问分支(不污染主线进度)、断点恢复以及长周期学习历史的滚动摘要 |
| Extension 生命周期钩子 | 将安全审计、高危工具准入、按需词库/语法库检索沉淀为可插拔的运行时中间件(Middleware / Hook) |
| 业务编排保留在应用层 | 跨阶段的语法专项训练 Workflow 由应用层确定性状态机驱动,而非依赖底层 Agent Loop 自由发挥 |
举例来说,当 Listening Agent 在听力练习中识别出用户的某个语法薄弱点时,正确的模块协作方式是:由底层 Loop 调用工具将该薄弱点持久化为一条结构化的“学习事件”;待用户明确选择进入专项突破后,再由上层应用状态机装配专门的 Prompt 与工具集,启动新的 Grammar Agent 会话。切忌因为 pi-agent-core 具备 follow-up 队列,就把单次会话循环当成跨页面、跨周期的业务 Workflow 引擎来滥用。
总结:记住四个核心结论
- 模块边界:
pi-ai管多模型协议,pi-agent-core管无头执行循环与事件分发,pi-tui管终端增量渲染,pi-coding-agent负责将会话树、扩展系统与工具装配成完整产品。 - 运行调度: 内层 Loop 聚焦于推进工具调用与响应
steering实时改向;外层 Loop 则在当前任务自然收敛后,负责消费follow-up延续任务。 - 上下文管理: Session 是全量、可分叉、可追溯的持久化历史树,而单次请求的模型上下文只是这棵树经过
transformContext与convertToLlm后的选择性投影。 - 能力扩展: 新功能应通过注册工具与挂载 Extension 钩子来横向切入,切勿为了不同业务角色的 Agent 而重复造轮子编写多套执行循环。
进一步阅读:Pi Agent Core · Session 文档 · Extension 文档 · yoagent 技术笔记 · DeepSeek Harness 技术笔记