阶段 6:扩展与技能
本阶段的位置:前五阶段已经有了循环、上下文、工具和模型接入。现在换一个视角:如果一个能力不是所有人都需要,Pi 怎样让它在不改核心的情况下长出来? 源码目录:
$PI_SRC。核心材料:packages/coding-agent/docs/extensions.md、skills.md、prompt-templates.md、mcp.md、codemode.md、packages.md。 一句话主线:核心只保留通用机制;具体能力通过可插拔资源接入。
一、先理解 Pi 为什么坚持“小核心”
跳转到“一、先理解 Pi 为什么坚持“小核心””一个 Agent 产品很容易走向“什么都塞进主程序”:
- 加一个公司内部工具;
- 加一套特殊审批流程;
- 加一个只在某个项目使用的命令;
- 加一种新的模型供应商;
- 加一个团队自己的代码评审规范。
如果这些功能都直接写进核心,最后会得到:
- 启动越来越慢;
- 工具说明越来越长;
- 安全边界越来越难判断;
- 任何一个团队的需求都迫使所有人一起升级;
- 修改一个局部功能,可能影响整个循环。
Pi 的选择是反过来的:
核心:循环、会话、上下文、工具管线、模型接口、事件 ↑ ├── 扩展:可执行的新能力 ├── 技能:按需加载的指令和资料 ├── 提示模板:可复用的输入文本 ├── MCP:已有的外部工具服务 └── Pi Package:把以上资源打包分发所以本章真正要学的不是“怎样多写几个 API”,而是:
遇到一个新需求,应该把它放在哪一层?
二、六类资源,先分清用途
跳转到“二、六类资源,先分清用途”2.1 一张决策表
跳转到“2.1 一张决策表”| 你想要的东西 | 首选方式 | 原因 |
|---|---|---|
| 一段固定的评审/写作/排查流程 | 技能(Skill) | 是指令和资料,不需要写程序,可以按需加载 |
| 一句重复输入的提示词 | 提示模板 | 只是一个可复用命令,最轻 |
| 新的可执行动作、事件拦截或界面 | 扩展(Extension) | 需要真正运行代码 |
| 已经存在的外部工具服务 | MCP | 不必重写工具,只要接协议 |
| 一次脚本里调用多个工具、过滤中间结果 | codemode | 中间结果不必全塞回模型 |
| 把以上几样发给别人安装 | Pi Package | 统一打包、版本和分发 |
2.2 最重要的一条分界:指令 vs 代码
跳转到“2.2 最重要的一条分界:指令 vs 代码”技能和提示模板:告诉模型“怎么做”扩展和 MCP 工具:让系统“真的能做”这个区别不是概念上的,而是安全上的:
- 技能本身主要是文本,不会自动获得执行能力;但它可以指示模型去运行附带脚本。
- 扩展是 TypeScript/JavaScript 代码,直接运行在 Pi 进程里,通常拥有和 Pi 相同的操作系统权限。
- MCP 服务可能是独立进程或远程服务,但工具调用仍然进入 Pi 的工具管线。
- codemode 的脚本运行在更窄的 JS 沙箱里,但它调用的工具本身会产生真实影响。
所以“装一个技能”和“装一个扩展”的风险级别完全不同。
三、扩展:在运行时接入新行为
跳转到“三、扩展:在运行时接入新行为”3.1 扩展是什么
跳转到“3.1 扩展是什么”扩展是一个模块,导出一个默认工厂函数。Pi 加载它时,把 ExtensionAPI 交给这个函数,扩展在工厂里注册能力:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) { pi.registerCommand("hello", { description: "显示一句问候", handler: async (name, ctx) => { ctx.ui.notify(`Hello, ${name || "world"}!`, "info"); }, });}保存为:
~/.pi/agent/extensions/hello.ts开发时可以直接加载:
cd $PI_SRC.\pi-test.ps1 --extension $HOME\.pi\agent\extensions\hello.ts然后在交互界面里输入 /hello。
注意:这里注册的是命令,不是给模型调用的工具。命令由人触发,工具由模型触发,两者不要混。
3.2 扩展能贡献什么
跳转到“3.2 扩展能贡献什么”可以按“它想接入哪一层”来记:
| 贡献点 | API | 典型用途 |
|---|---|---|
| 事件处理 | pi.on() |
观察或修改生命周期、拦截工具、注入上下文 |
| 模型工具 | pi.registerTool() |
增加一个模型可调用的动作 |
| 斜杠命令 | pi.registerCommand() |
增加 /xxx 人工命令 |
| 快捷键 / CLI 开关 | pi.registerShortcut() / pi.registerFlag() |
改变交互或启动参数 |
| 发送消息 | pi.sendUserMessage() / pi.sendMessage() |
主动把内容送入会话 |
| 持久化自定义状态 | pi.appendEntry() |
保存不进入模型上下文的扩展数据 |
| 控制活动工具/模型/思考等级 | pi.setActiveTools() 等 |
根据任务动态切换能力 |
| 注册 provider / 虚拟模型 | pi.registerProvider() / pi.registerVirtualModel() |
接入新模型服务或路由 |
| 注册 MCP server | pi.registerMcpServer() |
在扩展里接入外部工具服务 |
| 自定义渲染 | pi.registerToolRenderer() / ctx.ui |
改变终端显示 |
| 扩展间通信 | pi.events |
多个扩展协作 |
初学时不要试图把所有 API 都记住。先问:
我是要加一个“动作”、一个“命令”、一个“观察者”,还是一个“界面”?
3.3 事件:扩展最常用的接入点
跳转到“3.3 事件:扩展最常用的接入点”Pi 的事件可以按主题分组理解:
| 主题 | 你在关心什么 |
|---|---|
| 信任与资源发现 | 项目能不能加载、有哪些技能和扩展可用 |
| 会话 | 新会话、恢复、切换、压缩、关闭 |
| 提示词与运行 | 输入、开始运行、turn 结束、真正空闲 |
| 消息与上下文 | 消息定稿、上下文转换、自定义消息注入 |
| 工具 | 将调用、调用后修改结果、动态暴露工具 |
| 模型流 | 观察 provider 的原始流事件 |
| 用户输入与 UI | 编辑输入、快捷键、对话框、状态栏 |
几个最重要的边界:
before_agent_start:一次运行开始前,可补充指令或消息;tool_call:工具执行前,可检查、修改参数或拦截;tool_result:工具执行后,可修改结果或补充信息;agent_end:底层循环结束,但可能还有自动工作;agent_before_settle:最后还能追加内容或请求继续;agent_settled:真正空闲,只适合通知,不适合再开启自动工作;session_shutdown:清理资源,必须尽量幂等。
3.4 生命周期:工厂不是“启动一切”的地方
跳转到“3.4 生命周期:工厂不是“启动一切”的地方”扩展工厂可能在某些“只加载资源、不创建会话”的命令里执行。因此:
- 不要在工厂里启动长期进程、socket、watcher 或 timer;
- 需要长驻资源时,在
session_start或真正使用它的命令/工具里启动; - 在
session_shutdown里关闭连接、清理订阅; /reload会替换扩展运行时,旧上下文和旧状态不能继续使用。
这是很多扩展“刚加载没问题,一重载就泄漏”的根源。
3.5 拦截工具:一个实用例子
跳转到“3.5 拦截工具:一个实用例子”危险命令不应该只靠模型自觉。下面是一个简化思路:
export default function (pi: ExtensionAPI) { const dangerous = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i];
pi.on("tool_call", async (event, ctx) => { if (event.toolName !== "bash") return undefined;
const command = String(event.input.command ?? ""); if (!dangerous.some((pattern) => pattern.test(command))) return undefined;
if (!ctx.hasUI) { return { block: true, reason: "危险命令在非交互模式下被阻止" }; }
const approved = await ctx.ui.confirm("允许执行危险命令?", command); return approved ? undefined : { block: true, reason: "用户拒绝执行" }; });}这里体现了一个重要原则:没有界面询问时,默认拦截,而不是默认放行。
四、技能:把“做事方法”按需带进来
跳转到“四、技能:把“做事方法”按需带进来”4.1 技能是什么
跳转到“4.1 技能是什么”一个技能通常是一个目录,里面有 SKILL.md:
review-checklist/├── SKILL.md├── references/│ └── security.md└── scripts/ └── scan.shSKILL.md 开头是元数据:
---name: review-checklistdescription: 审查代码改动时使用,重点检查安全性、错误处理、并发和回归风险。---
# 代码评审清单
1. 先读改动涉及的调用方。2. 检查失败路径,而不只看成功路径。3. 搜索是否已有同类实现。4. 给出可复现的验证步骤。4.2 懒加载是什么意思
跳转到“4.2 懒加载是什么意思”如果系统里有一百个技能,把每个技能全文都塞进上下文,会非常浪费。Pi 的做法是:
启动时:只把每个技能的 name + description + 路径放进系统提示需要时:模型读取对应的 SKILL.md 全文需要资料时:再读 references/ 或运行 scripts/这叫按需加载。它让技能数量可以很多,而当前请求只支付相关技能的成本。
因此 description 写得准不准,直接决定技能能不能被正确选中。只写“帮助处理文档”通常太模糊;写清“什么场景、做什么、何时不要用”才有路由价值。
4.3 技能不算“自动执行”
跳转到“4.3 技能不算“自动执行””技能正文是给模型的指令,不会因为文件存在就自动运行。技能可以指导模型去调用工具、读取附带文件或运行脚本,但真正的动作仍要经过工具管线。
这也意味着:审阅陌生技能时要看它的附属脚本和指令。它可能要求模型执行高风险操作。
4.4 技能、提示模板和扩展怎么选
跳转到“4.4 技能、提示模板和扩展怎么选”| 问题 | 技能 | 提示模板 | 扩展 |
|---|---|---|---|
| 主要是文字指令吗? | 是 | 是 | 不是 |
| 需要多个文件、脚本、参考资料吗? | 很适合 | 不适合 | 可以 |
| 需要可执行的新工具吗? | 不能直接提供 | 不能 | 可以 |
| 需要按任务自动选择吗? | 可以(靠 description) | 通常由人输入命令 | 可以监听事件 |
| 成本 | 低,按需加载 | 最低 | 中到高,需要代码信任 |
一句话:
能写成流程说明的,先写技能;只有必须执行代码时,才写扩展。
五、MCP:把外部工具接进同一条管线
跳转到“五、MCP:把外部工具接进同一条管线”5.1 MCP 解决什么问题
跳转到“5.1 MCP 解决什么问题”MCP 可以理解成“工具服务的通用插头”。已经有人把 GitHub、数据库、文件系统、内部系统封装成 MCP server 时,你不需要再写一个 Pi 专用工具,只要配置连接。
Pi 的工具管线 │ ├── 内置 read/bash/edit/write ├── 扩展注册的工具 └── MCP server 暴露的工具对模型来说,它们最终都是工具调用;对权限、校验、截断和会话回填来说,也应该走同一条流水线。
5.2 怎么接
跳转到“5.2 怎么接”cd $PI_SRC.\pi-test.ps1 mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem ..\pi-test.ps1 mcp list用户级配置通常在 ~/.pi/agent/mcp.json,项目级配置在 .pi/mcp.json。项目级配置需要项目信任后才会加载。
MCP 工具通常命名为:
mcp__<server>__<tool>例如:
mcp__filesystem__read_file5.3 为什么 MCP 工具默认可能不直接出现在模型工具列表里
跳转到“5.3 为什么 MCP 工具默认可能不直接出现在模型工具列表里”一个 MCP server 可能一次暴露几十个工具。如果全部直接声明,工具说明会占掉大量上下文。
所以 Pi 支持不同暴露方式:
codemode(默认):由 codemode 脚本调用,通常不直接声明给模型;deferred:先不声明,需要时由tool_search找出来;direct:像内置工具一样直接声明给模型,也能从 codemode 调用;hidden:注册但不可达。
这里列的是 MCP server 的 exposure。model-only 属于扩展工具的另一层概念,不要和 MCP server 的暴露方式混用。
默认使用 codemode 的意义是:工具仍然可用,但不把全部工具说明塞进每一轮请求。
5.4 MCP 不绕开权限
跳转到“5.4 MCP 不绕开权限”无论工具来自内置、扩展还是 MCP,只要走 Pi 的工具管线,就应该经过参数校验、tool_call 拦截、结果处理和会话记录。
MCP 只是“工具从哪里来”的差别,不是“安全规则可以跳过”的理由。
六、codemode:让脚本替模型做“多步杂活”
跳转到“六、codemode:让脚本替模型做“多步杂活””6.1 为什么需要 codemode
跳转到“6.1 为什么需要 codemode”假设模型要完成:
搜索 20 个文件 → 读取命中的 8 个 → 汇总每个文件的错误 → 只返回总结如果每一步都作为一次工具调用回给模型:
- 中间结果全部进入上下文;
- 循环要跑很多轮;
- 大量无关文本挤占 token;
- 费用和延迟都上升。
codemode 换了一种方式:
模型写一段 JavaScript → 脚本在沙箱里调用多个工具 → 脚本自己过滤、汇总 → 只有脚本输出回到模型这不是“让模型直接执行任意 Node 程序”。它运行在一个受限的 QuickJS 沙箱中,没有文件系统、网络和定时器,只能通过受控的 tools 和 models 接口触达外部。
6.2 它省的是什么
跳转到“6.2 它省的是什么”| 省的东西 | 原因 |
|---|---|
| 上下文 | 中间结果留在脚本里,不必每步都进模型 |
| 往返轮次 | 一个脚本可以并发调用多个工具 |
| 工具声明 | 可以配合搜索按需找到工具 |
| 手工整理 | 脚本可以过滤、排序、聚合后再输出 |
6.3 它没有省掉风险
跳转到“6.3 它没有省掉风险”脚本里的工具调用是真实调用:
- 写文件就真的写了;
- 发请求就真的发了;
- 前半段成功、后半段失败时,前面的副作用不会自动回滚。
所以 codemode 的沙箱限制的是“脚本自身能直接碰什么”,不是“工具调用不会造成后果”。权限策略仍然必须覆盖脚本触发的工具调用。
七、放在哪里:用户级、项目级和包
跳转到“七、放在哪里:用户级、项目级和包”7.1 用户级与项目级
跳转到“7.1 用户级与项目级”| 位置 | 作用范围 | 是否需要项目信任 |
|---|---|---|
~/.pi/agent/extensions/ |
所有项目 | 否 |
<项目>/.pi/extensions/ |
当前项目 | 是 |
~/.pi/agent/skills/ |
所有项目 | 否 |
<项目>/.pi/skills/ |
当前项目 | 是 |
~/.pi/agent/prompts/ |
所有项目 | 否 |
<项目>/.pi/prompts/ |
当前项目 | 是 |
命令行 --extension |
这次运行 | 否,但由你显式指定 |
项目资源之所以需要信任,是因为它们可能包含可执行扩展、脚本指令和模型配置。信任一个项目,等于同意让这些资源进入运行时。
7.2 Pi Package:分发单位
跳转到“7.2 Pi Package:分发单位”如果一套能力包含扩展、技能、提示模板和主题,一个个复制很麻烦。Pi Package 用 npm、git 或本地目录把它们打包分发。
典型结构:
my-pi-package/├── package.json├── extensions/├── skills/├── prompts/└── themes/package.json 可以显式列出 pi 资源:
{ "name": "my-pi-package", "pi": { "extensions": ["./src/extension.ts"], "skills": ["./resources/skills"], "prompts": ["./resources/prompts/*.md"] }}依赖纪律:
- Pi 宿主已经提供的包,放进
peerDependencies; - 扩展自己运行需要的第三方包,放进
dependencies; - 不要把宿主包复制进自己的依赖树,否则可能出现重复实例和初始化冲突。
八、核心还是扩展:判断练习
跳转到“八、核心还是扩展:判断练习”遇到一个需求,按顺序问:
- 它只是给模型看的流程说明吗? 是 → 技能。
- 它只是重复输入的提示词吗? 是 → 提示模板。
- 它需要真正执行代码、拦截事件或改变界面吗? 是 → 扩展。
- 外部服务已经提供 MCP 工具吗? 是 → 先接 MCP。
- 它需要多次工具调用并过滤中间结果吗? 是 → 考虑 codemode。
- 它对所有用户、所有场景都必不可少,而且必须改变核心不变量吗? 是 → 才讨论核心改动。
举几个例子:
| 需求 | 推荐 | 理由 |
|---|---|---|
| 每次评审都按同一份安全清单 | 技能 | 纯流程,按需加载 |
| 每次提交前生成固定格式的变更摘要 | 提示模板或技能 | 不需要新执行能力 |
拦截 rm -rf 并要求确认 |
扩展 | 需要监听工具事件 |
| 接入公司已有 Jira MCP server | MCP | 已有协议服务,不必重写 |
| 批量搜索 50 个文件并只返回错误摘要 | codemode | 多步调用,中间结果应留在脚本 |
| 给所有模型统一增加一种新的消息协议 | 核心讨论 | 可能改变全局约定,扩展未必合适 |
核心改动的门槛应该高,因为核心是所有人共享的稳定面;扩展适合局部、可选、可替换的能力。
九、安全与信任:扩展是最需要警惕的一层
跳转到“九、安全与信任:扩展是最需要警惕的一层”9.1 扩展能看见什么
跳转到“9.1 扩展能看见什么”扩展与 Pi 同进程运行,可能看到:
- 用户输入和模型输出;
- 工具调用参数;
- 文件内容;
- 凭据和请求头;
- 会话历史;
- 工作目录和进程环境。
所以“只安装可信扩展”不是客套话,而是安全边界。
9.2 技能和包也不是零风险
跳转到“9.2 技能和包也不是零风险”技能可能指示模型运行脚本;包可能包含扩展代码。项目信任只决定“是否加载项目资源”,并不能保证资源本身安全。
9.3 最小权限思路
跳转到“9.3 最小权限思路”- 只需要读文件,就不要给写工具;
- 只需要某一个 MCP server,就不要把所有 server 都设为 direct;
- 危险工具要有拦截和确认;
- 第三方扩展先看源码,再安装;
- 真正高风险任务放进容器或受限账户。
十、动手实验
跳转到“十、动手实验”实验 1:写一个最小命令扩展
跳转到“实验 1:写一个最小命令扩展”把前面的 hello.ts 放进用户扩展目录,加载并输入 /hello。确认:
- 它不需要模型调用;
- 它只是一个给人用的命令;
- 移除文件或重载后行为消失。
实验 2:写一个技能
跳转到“实验 2:写一个技能”创建:
~/.pi/agent/skills/review-checklist/SKILL.md写清 name 和 description,然后输入:
/skill:review-checklist 检查我刚改的代码观察模型有没有读取技能正文和附带资料。再故意把 description 写得很模糊,对比它是否还能被自动选中。
实验 3:接一个 MCP server
跳转到“实验 3:接一个 MCP server”cd $PI_SRC.\pi-test.ps1 mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem ..\pi-test.ps1 mcp list然后在会话里用 /mcp 查看工具、暴露方式和连接状态。
实验 4:比较直接调用与 codemode
跳转到“实验 4:比较直接调用与 codemode”让模型分别用“逐个工具调用”和“一个 codemode 脚本”完成同一组搜索和汇总。对比:
- 事件数量;
- 进入上下文的中间内容;
- token 使用;
- 失败时是否留下部分副作用。
十一、常见误区
跳转到“十一、常见误区”| 误区 | 正确理解 |
|---|---|
| 所有新需求都应该加进核心 | 核心保持最小;局部能力优先做成扩展/技能/MCP |
| 技能就是会自动执行的脚本 | 技能主要提供指令和资料,动作仍通过工具管线 |
| 扩展只是配置 | 扩展是可执行代码,拥有与 Pi 相近的进程权限 |
| MCP 工具绕开权限 | 只要进入工具管线,就应经过同样的校验和拦截 |
| codemode 是完整 Node 环境 | 它是受限 JS 沙箱,只能通过受控接口触达外部 |
| codemode 不会产生真实副作用 | 它调用的工具是真实执行,且失败不会自动回滚 |
| 项目受信任就万事大吉 | 信任只控制资源加载,不保证资源安全 |
| 包只是文件集合 | 包可以带扩展代码和脚本,安装前应审阅 |
| 工具越多越好 | 工具会占上下文、增加选择难度和风险面 |
十二、通关自检
跳转到“十二、通关自检”请回答:
- 为什么 Pi 坚持“核心最小、能力外置”?
- 技能、提示模板、扩展、MCP、codemode 各解决什么问题?
- 扩展工厂、
session_start、session_shutdown分别应该做什么? - 为什么扩展工厂里不适合启动长期进程?
- 技能为什么只先加载 description?这对上下文有什么好处?
- MCP 工具为什么仍然要走工具管线?
- codemode 省了哪些成本,又没有消除哪些风险?
- 什么情况下才应该考虑改核心?
建议产出:
- 一个能加载的扩展命令;
- 一个能被自动选中的技能;
- 一张“技能 / 扩展 / MCP / codemode”决策表;
- 一段 3 分钟讲解,说明为什么“能力外置”让系统更稳,也带来新的信任问题。
下一站是阶段 7:界面与集成。能力已经能长出来,接下来看同一套引擎怎样同时服务人、脚本和外部程序。
配套学习路线阶段 6 · 基于 v1.3.0(71023aec)