官方资料:Pi 项目(原 badlogic/pi-mono) · Agent Core · 核心循环源码 · 会话文档 · 扩展文档
本文按 2026-10-15 官方 main 的文档与源码整理;English Studio 部分是工程建议,不代表已经落地。
先建立心智模型
Pi 是一个最小、可扩展的 Agent Harness,而不是把所有功能放进一个超级 Agent。它用少量通用机制提供模型调用、工具循环和事件;上层应用再用扩展、技能、提示词和会话管理组成具体产品。
| 层次 | 负责什么 | 相关包 |
|---|---|---|
| 模型协议 | 多模型消息和流式适配 | pi-ai |
| 执行引擎 | Agent 状态、Loop、工具、消息队列、事件流 | pi-agent-core |
| 产品能力 | 会话树、资源装配、技能、扩展和终端交互 | pi-coding-agent |
| 外部入口 | CLI、print / JSON、RPC 和 SDK | 使用方 |
Pi Coding Agent 默认采用 read、write、edit、bash 四类基本工具;子 Agent、计划模式等不被强制写进每个实例,可按需通过扩展和包增加。官方 README 明确提出这种取舍。
一、真正理解 Pi 的双层 Agent Loop
Agent 并不是“一次提问、一次回答”:模型可以返回工具调用,工具结果再回填上下文,模型继续决策。Pi 的 runLoop() 采用两层循环:
- 内层循环:围绕模型回复、工具结果和 steering(运行中途的补充/改向指令)继续推进。
- 外层循环:内层已经可以停止时,检查 follow-up 队列,有后续任务则再运行,否则结束。
为什么需要 steering 和 follow-up 两种队列?
| 机制 | 什么时候生效 | 学习场景 |
|---|---|---|
steering |
当前运行还未完成,在下一个合适的轮次介入 | “先别解释,改成出一道练习题” |
follow-up |
当前任务自然完成后再处理 | “等语法分析完,再整理成生词卡” |
它们是输入调度机制,不等于同时运行的两个 Agent,更不是自动化业务状态机。
一个 Turn 的关键环节:拼装待处理消息 → prepareRequest → 模型流式输出 → 校验和执行工具 → 结果写回 → finishTurn 与 turn_end。出现错误或取消时可以提前结束,而不是保证所有工具都执行完。
下面是为理解源码而做的逻辑伪代码,不是 Pi 原封不动的实现:
while (true) { // 外层:在当前工作结束时检查 follow-up
let toolsOwed = true;
while (toolsOwed || pending.length > 0) {
appendMessages(pending);
const answer = await callModel();
const results = await executeTools(answer.toolCalls);
appendToolResults(results);
toolsOwed = shouldContinueFromTools(results);
pending = await getSteeringMessages();
}
pending = await getFollowUpMessages();
if (pending.length === 0) break;
}真实源码还有请求准备、异步事件、并发工具、取消处理和终止控制。它表达的重点是:执行调度属于宿主代码;大模型只提出下一步操作。
二、消息管线:存储的消息不一定都发送给模型
Pi 区分两种消息类型:
AgentMessage是应用中的消息,可以包含 UI 数据和自定义事件。Message是模型能够理解的角色消息。
每次模型请求前,transformContext() 可整理历史或注入内容,然后 convertToLlm() 将 AgentMessage 转成模型可接受的格式。
| 阶段 | 输入到输出 | 意义 |
|---|---|---|
| 原始消息 | AgentMessage[] |
会话及 UI 业务信息 |
transformContext |
AgentMessage → AgentMessage | 裁剪、筛选、注入 |
convertToLlm |
AgentMessage → Message | 转成模型协议 |
| Provider | Message → stream | 生成模型回复 |
例如 English Studio 的“已保存一张词汇卡”可以在应用会话里保留完整结构,模型下一轮只需要读取“本次保存了一个复习词汇”即可。会话持久化内容与本轮模型上下文必须分开设计。
事件流与工具执行
Pi 发布 agent_start、turn_start、message_start/update/end、tool_execution_start/update/end、turn_end、agent_end 等事件,上层 UI 可以分别展示“模型回答中”“工具执行中”“任务已结束”。
目前 Agent Core 支持并发工具模式;并发完成顺序可能与原始工具列表不同,但最终 toolResult 消息仍按模型来源顺序组织。beforeToolCall / afterToolCall 分别负责准入、阻断与结果后处理。具体约定参见 Agent Core。
三、Session:为什么它不等于模型上下文?
Pi Coding Agent 默认把会话持久化为 JSONL,并将历史组织成可分叉的树。用户当前选择的活动分支决定下一次模型请求使用哪段历史。
| 操作 | 行为 | 历史是否保留 |
|---|---|---|
/tree |
在当前会话树中切换分支 | 保留其它分支 |
/fork |
从较早的用户消息创建新的会话 | 保留原会话 |
/clone |
把活动分支复制为新会话 | 保留原会话 |
/compact |
生成历史摘要,控制上下文长度 | 原始日志仍保留 |
自动 Compaction 控制模型上下文规模,而不是清空全部学习历史。真正用于下一轮的,只是活动分支、保留的最近消息和相关摘要。
四、Extension:如何不修改 Loop 就增加能力?
Pi 扩展是 TypeScript 模块,能通过 ExtensionAPI 注册模型可调用工具、命令、事件处理器、模型提供方和 UI 能力。
| 扩展点 | 用途 |
|---|---|
pi.registerTool() |
加模型工具 |
pi.registerCommand() |
加用户命令 |
pi.on("tool_call") |
允许、修改或拒绝一次工具调用 |
pi.on("tool_result") |
处理工具执行结果 |
before_agent_start |
在新运行前补充上下文 |
pi.appendEntry() |
保存扩展自己的 Session 记录 |
例如限制敏感工具(示例围绕官方 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 的建议 |
|---|---|
| Provider 与 Loop 分离 | Rust Harness 统一接多种 LLM |
| 状态消息与模型消息分离 | 学习记录存结构化数据,按需投影成 Prompt |
| Steering / Follow-up | 分清“打断当前任务”与“安排下一任务” |
| Session Tree | 支持会话分支、恢复和历史摘要 |
| Extension | 安全审计、工具准入、临时上下文检索 |
| 业务逻辑保持在应用层 | 语法专项 Workflow 由确定性状态机控制 |
例如 Listening Agent 发现语法薄弱点,可以先保存结构化学习事件;用户决定进入专项训练后,应用层再启动 Grammar Agent。不能因为 Pi 具有 follow-up,就把它当成跨页面、跨天的 Workflow 引擎。
总结:记住三个结论
- 运行:内层 Loop 处理工具与 steering,外层 Loop 在自然结束时查看 follow-up。
- 上下文:Session 是可追溯的历史树,而模型上下文是它的选择性投影。
- 扩展:把功能加入工具和 Extension,不要为不同 Agent 复制一套循环。
进一步阅读:Pi Agent Core · Session 文档 · Extension 文档 · yoagent 技术笔记 · DeepSeek Harness 技术笔记