扩展包分层与依赖选择
按职责边界把 Pi 拆成模型层、Agent 运行时和编码智能体三层,只装当前用得上的那一层。
扩展包分层与依赖选择
打开 Pi 的 package.json,你会看到一长串 @earendil-works/pi-*。它们不是并列的功能开关,而是分层的。
分层的意思是:pi-ai 不认识智能体,pi-agent-core 不认识终端,pi-coding-agent 才认识文件编辑。这条边界决定了你能替换什么、能复用什么,以及——更重要的是——你需不需要某一层。
很多人在这一步犯的错是"看起来完整":把能装的包全装上。结果是打包体积失控、攻击面扩大、升级被一个根本没用到的实验性包绑架。
这篇讲清楚三件事:每个包的一句话职责和不负责什么;实际依赖方向决定了什么会连带被装上;以及按你的真实需求该怎么选。读完你能写出一个"只包含真正需要的那几层"的 package.json,并解释每一项为什么在。
读这篇需要的前置只有一条:知道 npm 依赖树的基本概念(直接依赖、传递依赖、dependencies 与 devDependencies 的区别)。
三个核心层
Pi 的主干是三层,箭头表示依赖方向:
# 方框从上到下是依赖方向:上层依赖下层
┌─────────────────────────────────────────────┐
│ @earendil-works/pi-coding-agent │ 编码智能体:CLI + SDK
│ 交互式编码工具、会话管理、扩展系统、资源发现 │
├─────────────────────────────────────────────┤
│ @earendil-works/pi-agent-core │ Agent 运行时
│ 工具执行循环、事件流、队列、上下文转换 │
├─────────────────────────────────────────────┤
│ @earendil-works/pi-ai │ 模型层
│ 多 provider 接入、认证解析、流式接口、用量统计 │
└─────────────────────────────────────────────┘下层不知道上层。pi-ai 里没有"智能体"这个概念,pi-agent-core 里没有"终端"也没有"会话文件"。这条约束是 Pi 能被各种宿主复用的原因。
pi-ai:模型层
一句话:把"和某个模型说话"这件事封装成统一接口。
它提供 Models 集合、provider 工厂、模型目录、认证解析、流式事件和用量统计。四十多个内置 provider 都在这一层,认证方式各不相同(API key、OAuth、环境变量、云凭据),这一层把它们统一成同一套调用。
它不负责:工具执行、循环编排、会话持久化、终端界面。
一个入口细节值得注意:pi-ai 的 chat 目录只收录支持工具调用的模型,因为工具调用是 agentic 工作流的基础。图像和分类模型走各自的能力目录。
pi-agent-core:Agent 运行时
一句话:在模型之上跑一个会自己转的工具调用循环。
它提供 Agent 类、agentLoop 低层接口、AgentTool 工具模型、beforeToolCall / afterToolCall / finishTurn 控制点、steering / follow-up 队列。它依赖 pi-ai,但不依赖终端。
它不负责:模型目录和凭据管理(那是模型层)、会话文件、扩展系统、CLI。
pi-coding-agent:编码智能体
一句话:把上面两层组装成一个能改代码的终端智能体。
它提供 pi 命令行、内置编码工具(read、bash、edit、write、grep、find、ls、powershell)、会话树与压缩、扩展系统、Skills、提示模板、项目信任、MCP 与 codemode 内置扩展,以及 SDK。
它不负责:通用的模型目录管理(那在 pi-ai)、通用智能体循环(那在 pi-agent-core)。
一个容易误解的点:SDK 不等于 Coding Agent。 SDK 嵌在同一个包里,但它是 Coding Agent 的编程入口。你用 SDK 时,createAgentSession() 默认会做资源发现、读设置、加载凭据——这些是 Coding Agent 的行为。
其余各包
这些包不是"可选功能",是各自独立的关注点。
注意 pi-mcp 的自我描述:它"不依赖官方 MCP SDK,也不依赖其他 pi 包"。你可以只用它接 MCP。
pi-telemetry 的取向也很特别——只定义契约和 schema,不带 exporter,也没有"当前 span"这样的全局状态。它附带一个内存参考实现(InMemoryTelemetryContext),但真正的上报要你自己提供 adapter。遥测上下文是显式传递的,这是为了让 pi 包之间的遥测不互相污染。
依赖关系
实际依赖方向(来自各包 package.json):
这张表解释了一个关键事实:装 pi-coding-agent 会自动带来 pi-agent-core、pi-ai、pi-codemode、pi-mcp、pi-tui。 你没法只要 Coding Agent 而不要 MCP 客户端。反过来,pi-mcp、pi-codemode、pi-tui、pi-telemetry、chord 都是叶子包,单独使用不牵动其余部分。
顺带说明 pi-evals 的位置:它同时依赖 pi-ai 和 pi-coding-agent,所以它属于"要写行为评估才装"的那一类,位置比 pi-durable 更靠主干。
输入、输出、状态、责任边界
每个包都要能说清楚这四件事,否则它就是个黑箱。
一个能说明问题的对比:pi-ai 的状态在进程内,pi-durable 的状态在存储里。 差别不是功能多寡,是"进程崩了会怎样"。
选型决策表
按你的实际需求选,不要按"以后可能用得上"选。
三种最小依赖组合
只做对话。 只需要模型层:
{
"dependencies": {
"@earendil-works/pi-ai": "^1.3.0"
}
}dependencies 放运行时真正要用的包;devDependencies 放只在开发期用的(类型检查器、测试框架)。版本前缀 ^ 表示允许同主版本的次版本和补丁版本升级,~ 只允许补丁版本。
provider 不是独立的包,它从 @earendil-works/pi-ai/providers/anthropic 这样的子路径引入,同一个包就够。代码长这样:
import { createModels } from "@earendil-works/pi-ai/models";
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
// createModels() 返回空集合,必须逐个 setProvider();没用到的不会进打包产物
const models = createModels();
models.setProvider(anthropicProvider());
const model = models.getModel("anthropic", "claude-sonnet-4-6");
if (!model) throw new Error("模型未找到或该 provider 未注册");
// 用 AbortController 掌握取消时机
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 30_000);
const stream = models.streamSimple(
model,
{
systemPrompt: "你是一个助手。",
messages: [{ role: "user", content: "你好", timestamp: Date.now() }],
},
{ signal: controller.signal },
);
try {
// 流上失败不抛异常,而是走 error 事件
for await (const event of stream) {
if (event.type === "text_delta") {
process.stdout.write(event.delta);
} else if (event.type === "error") {
console.error(event.reason === "aborted" ? "请求已取消" : "请求失败", event.error.errorMessage);
}
}
// 拿到终态消息,成功与否看 stopReason,而不是看有没有抛异常
const response = await stream.result();
if (response.stopReason === "aborted") {
console.warn("拿到了部分内容:", response.content);
}
} finally {
// 一定要清掉定时器
clearTimeout(timer);
controller.abort();
}四个细节:createModels() 返回空集合,必须 setProvider() 逐个注册;getModel() 返回 Model 或 undefined,不检查就是运行时空指针;取消靠 AbortController 的 signal 传进请求选项,中止后的响应 stopReason 是 "aborted";clearTimeout 必须在 finally 里清掉,否则定时器会让进程挂住不退出。
嵌入现有应用。 加上 Agent 运行时:
{
"dependencies": {
"@earendil-works/pi-ai": "^1.3.0",
"@earendil-works/pi-agent-core": "^1.3.0"
}
}pi-agent-core 本身会传递依赖 pi-ai,这里把 pi-ai 也显式列出来,是因为你的代码直接 import 了它的符号——直接依赖要写进 dependencies,不要依赖传递依赖恰好暴露出来。版本前缀 ^ 表示允许同主版本的次版本和补丁版本升级;要不要锁死精确版本,取决于你能否接受上游的破坏性变更。
要接 MCP 但不想要 CLI。 pi-mcp 是独立包:
import { McpClient, StdioTransport } from "@earendil-works/pi-mcp";
// 传输层决定 MCP server 怎么起:这里用 stdio,让 Pi 拉起一个子进程
const transport = new StdioTransport({
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
});
// 客户端身份由宿主给:name 和 version 是 MCP 握手时要自报家门的信息
const client = new McpClient({
name: "my-client",
version: "1.0.0",
// roots 声明这次会话允许 server 看到的根目录,是给 server 的边界提示
roots: [{ uri: "file:///workspace", name: "workspace" }],
});
try {
// 必须先连上才能列工具:未连接的 client 拿不到任何工具
await client.connect(transport);
const tools = await client.listTools();
const result = await client.callTool("search", { query: "MCP" });
console.log(tools.length, result);
} finally {
// 漏掉 close() 连接不会干净断开
await client.close();
}client.close() 一定要放在 finally 里——它会关闭传输、标记连接已关闭,并唤醒所有挂起的请求。
反例:不要为了"看起来完整"把不需要的包装进来
下面这个 package.json 看起来专业、覆盖全面、像是"把 Pi 的能力都接进来了":
{
"dependencies": {
"@earendil-works/pi-ai": "^1.3.0",
"@earendil-works/pi-agent-core": "^1.3.0",
"@earendil-works/pi-coding-agent": "^1.3.0",
"@earendil-works/pi-mcp": "^1.3.0",
"@earendil-works/pi-codemode": "^1.3.0",
"@earendil-works/pi-tui": "^1.3.0",
"@earendil-works/pi-durable": "^1.3.0",
"@earendil-works/pi-telemetry": "^1.3.0",
"@earendil-works/chord": "^1.3.0"
}
}如果这个项目只需要"一个能回答问题的 HTTP 接口",这段配置会带来什么:
反过来的正确做法是从最小集合开始:
{
"dependencies": {
"@earendil-works/pi-ai": "^1.3.0"
}
}然后按真实需求往上加。每加一个包,都能说清"哪个需求逼我加的"。
这里有一条官方支持的做法可以直接用:pi-ai 的 provider 工厂是子路径导入,每个 provider 一个模块,不导入其它 provider。需要代码分割时,各家的 SDK(@anthropic-ai/sdk、openai、@google/genai)会留在懒加载的 chunk 里,第一次请求到该 API 时才加载。对应的三条规则:
判断标准很简单:如果一个包在你的代码里没有被 import,它就不该在 dependencies 里。
三类信息要分清
改造这类文章时有一条容易被忽略的原则:明确区分下面三类东西。看不懂时,先问"这句话属于哪一类"。
第三类最容易变成"官方推荐"。本文的选型决策表是教学设计——它基于官方包清单和依赖关系推导,但不是 Pi 官方发布的选型指南。遇到和官方文档冲突的地方,以官方文档为准。
同理,样例代码里的 ^1.3.0 这类版本写法是教学选择,不是官方要求。你实际安装的版本要以你用的发布渠道为准。
你现在能做什么
- 说出 Pi 每个包的一句话职责,以及它不负责什么(模型层不跑循环、Agent 层不读终端、Coding Agent 不自己实现 LLM 协议)。
- 查各包的
package.json,画出当前的依赖方向图,并回答"装pi-coding-agent会连带装进哪五个包"。 - 按你的真实需求,从决策表里选出该装哪些包,并写出理由。
- 写出一个"最小练习只引入所需层"的
package.json,并解释每一项为什么在。 - 用
grep检查你的项目里实际 import 了哪些@earendil-works/pi-*符号,把没被 import 的包从dependencies里移出去。 - 用
createModels()+setProvider()写一个只依赖pi-ai的流式对话程序,并在finally里释放流。 - 用
pi-mcp独立接一个 MCP server,在finally里close()掉客户端。 - 识别出一个反例:一个看起来更完整、实际更糟的依赖组合,并说清它的四个具体代价。