跳转到内容

阶段 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”,而是:

遇到一个新需求,应该把它放在哪一层?


二、六类资源,先分清用途

跳转到“二、六类资源,先分清用途”
你想要的东西 首选方式 原因
一段固定的评审/写作/排查流程 技能(Skill) 是指令和资料,不需要写程序,可以按需加载
一句重复输入的提示词 提示模板 只是一个可复用命令,最轻
新的可执行动作、事件拦截或界面 扩展(Extension) 需要真正运行代码
已经存在的外部工具服务 MCP 不必重写工具,只要接协议
一次脚本里调用多个工具、过滤中间结果 codemode 中间结果不必全塞回模型
把以上几样发给别人安装 Pi Package 统一打包、版本和分发

2.2 最重要的一条分界:指令 vs 代码

跳转到“2.2 最重要的一条分界:指令 vs 代码”
技能和提示模板:告诉模型“怎么做”
扩展和 MCP 工具:让系统“真的能做”

这个区别不是概念上的,而是安全上的:

  • 技能本身主要是文本,不会自动获得执行能力;但它可以指示模型去运行附带脚本。
  • 扩展是 TypeScript/JavaScript 代码,直接运行在 Pi 进程里,通常拥有和 Pi 相同的操作系统权限。
  • MCP 服务可能是独立进程或远程服务,但工具调用仍然进入 Pi 的工具管线。
  • codemode 的脚本运行在更窄的 JS 沙箱里,但它调用的工具本身会产生真实影响。

所以“装一个技能”和“装一个扩展”的风险级别完全不同。


三、扩展:在运行时接入新行为

跳转到“三、扩展:在运行时接入新行为”

扩展是一个模块,导出一个默认工厂函数。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。

注意:这里注册的是命令,不是给模型调用的工具。命令由人触发,工具由模型触发,两者不要混。

可以按“它想接入哪一层”来记:

贡献点 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: "用户拒绝执行" };
});
}

这里体现了一个重要原则:没有界面询问时,默认拦截,而不是默认放行。


四、技能:把“做事方法”按需带进来

跳转到“四、技能:把“做事方法”按需带进来”

一个技能通常是一个目录,里面有 SKILL.md:

review-checklist/
├── SKILL.md
├── references/
│ └── security.md
└── scripts/
└── scan.sh

SKILL.md 开头是元数据:

---
name: review-checklist
description: 审查代码改动时使用,重点检查安全性、错误处理、并发和回归风险。
---
# 代码评审清单
1. 先读改动涉及的调用方。
2. 检查失败路径,而不只看成功路径。
3. 搜索是否已有同类实现。
4. 给出可复现的验证步骤。

如果系统里有一百个技能,把每个技能全文都塞进上下文,会非常浪费。Pi 的做法是:

启动时:只把每个技能的 name + description + 路径放进系统提示
需要时:模型读取对应的 SKILL.md 全文
需要资料时:再读 references/ 或运行 scripts/

这叫按需加载。它让技能数量可以很多,而当前请求只支付相关技能的成本。

因此 description 写得准不准,直接决定技能能不能被正确选中。只写“帮助处理文档”通常太模糊;写清“什么场景、做什么、何时不要用”才有路由价值。

4.3 技能不算“自动执行”

跳转到“4.3 技能不算“自动执行””

技能正文是给模型的指令,不会因为文件存在就自动运行。技能可以指导模型去调用工具、读取附带文件或运行脚本,但真正的动作仍要经过工具管线。

这也意味着:审阅陌生技能时要看它的附属脚本和指令。它可能要求模型执行高风险操作。

4.4 技能、提示模板和扩展怎么选

跳转到“4.4 技能、提示模板和扩展怎么选”
问题 技能 提示模板 扩展
主要是文字指令吗? 是 是 不是
需要多个文件、脚本、参考资料吗? 很适合 不适合 可以
需要可执行的新工具吗? 不能直接提供 不能 可以
需要按任务自动选择吗? 可以(靠 description) 通常由人输入命令 可以监听事件
成本 低,按需加载 最低 中到高,需要代码信任

一句话:

能写成流程说明的,先写技能;只有必须执行代码时,才写扩展。


五、MCP:把外部工具接进同一条管线

跳转到“五、MCP:把外部工具接进同一条管线”

MCP 可以理解成“工具服务的通用插头”。已经有人把 GitHub、数据库、文件系统、内部系统封装成 MCP server 时,你不需要再写一个 Pi 专用工具,只要配置连接。

Pi 的工具管线
│
├── 内置 read/bash/edit/write
├── 扩展注册的工具
└── MCP server 暴露的工具

对模型来说,它们最终都是工具调用;对权限、校验、截断和会话回填来说,也应该走同一条流水线。

终端窗口
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_file

5.3 为什么 MCP 工具默认可能不直接出现在模型工具列表里

跳转到“5.3 为什么 MCP 工具默认可能不直接出现在模型工具列表里”

一个 MCP server 可能一次暴露几十个工具。如果全部直接声明,工具说明会占掉大量上下文。

所以 Pi 支持不同暴露方式:

  • codemode(默认):由 codemode 脚本调用,通常不直接声明给模型;
  • deferred:先不声明,需要时由 tool_search 找出来;
  • direct:像内置工具一样直接声明给模型,也能从 codemode 调用;
  • hidden:注册但不可达。

这里列的是 MCP server 的 exposure。model-only 属于扩展工具的另一层概念,不要和 MCP server 的暴露方式混用。

默认使用 codemode 的意义是:工具仍然可用,但不把全部工具说明塞进每一轮请求。

无论工具来自内置、扩展还是 MCP,只要走 Pi 的工具管线,就应该经过参数校验、tool_call 拦截、结果处理和会话记录。

MCP 只是“工具从哪里来”的差别,不是“安全规则可以跳过”的理由。


六、codemode:让脚本替模型做“多步杂活”

跳转到“六、codemode:让脚本替模型做“多步杂活””

假设模型要完成:

搜索 20 个文件 → 读取命中的 8 个 → 汇总每个文件的错误 → 只返回总结

如果每一步都作为一次工具调用回给模型:

  • 中间结果全部进入上下文;
  • 循环要跑很多轮;
  • 大量无关文本挤占 token;
  • 费用和延迟都上升。

codemode 换了一种方式:

模型写一段 JavaScript
→ 脚本在沙箱里调用多个工具
→ 脚本自己过滤、汇总
→ 只有脚本输出回到模型

这不是“让模型直接执行任意 Node 程序”。它运行在一个受限的 QuickJS 沙箱中,没有文件系统、网络和定时器,只能通过受控的 tools 和 models 接口触达外部。

省的东西 原因
上下文 中间结果留在脚本里,不必每步都进模型
往返轮次 一个脚本可以并发调用多个工具
工具声明 可以配合搜索按需找到工具
手工整理 脚本可以过滤、排序、聚合后再输出

脚本里的工具调用是真实调用:

  • 写文件就真的写了;
  • 发请求就真的发了;
  • 前半段成功、后半段失败时,前面的副作用不会自动回滚。

所以 codemode 的沙箱限制的是“脚本自身能直接碰什么”,不是“工具调用不会造成后果”。权限策略仍然必须覆盖脚本触发的工具调用。


七、放在哪里:用户级、项目级和包

跳转到“七、放在哪里:用户级、项目级和包”
位置 作用范围 是否需要项目信任
~/.pi/agent/extensions/ 所有项目 否
<项目>/.pi/extensions/ 当前项目 是
~/.pi/agent/skills/ 所有项目 否
<项目>/.pi/skills/ 当前项目 是
~/.pi/agent/prompts/ 所有项目 否
<项目>/.pi/prompts/ 当前项目 是
命令行 --extension 这次运行 否,但由你显式指定

项目资源之所以需要信任,是因为它们可能包含可执行扩展、脚本指令和模型配置。信任一个项目,等于同意让这些资源进入运行时。

如果一套能力包含扩展、技能、提示模板和主题,一个个复制很麻烦。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;
  • 不要把宿主包复制进自己的依赖树,否则可能出现重复实例和初始化冲突。

八、核心还是扩展:判断练习

跳转到“八、核心还是扩展:判断练习”

遇到一个需求,按顺序问:

  1. 它只是给模型看的流程说明吗? 是 → 技能。
  2. 它只是重复输入的提示词吗? 是 → 提示模板。
  3. 它需要真正执行代码、拦截事件或改变界面吗? 是 → 扩展。
  4. 外部服务已经提供 MCP 工具吗? 是 → 先接 MCP。
  5. 它需要多次工具调用并过滤中间结果吗? 是 → 考虑 codemode。
  6. 它对所有用户、所有场景都必不可少,而且必须改变核心不变量吗? 是 → 才讨论核心改动。

举几个例子:

需求 推荐 理由
每次评审都按同一份安全清单 技能 纯流程,按需加载
每次提交前生成固定格式的变更摘要 提示模板或技能 不需要新执行能力
拦截 rm -rf 并要求确认 扩展 需要监听工具事件
接入公司已有 Jira MCP server MCP 已有协议服务,不必重写
批量搜索 50 个文件并只返回错误摘要 codemode 多步调用,中间结果应留在脚本
给所有模型统一增加一种新的消息协议 核心讨论 可能改变全局约定,扩展未必合适

核心改动的门槛应该高,因为核心是所有人共享的稳定面;扩展适合局部、可选、可替换的能力。


九、安全与信任:扩展是最需要警惕的一层

跳转到“九、安全与信任:扩展是最需要警惕的一层”

扩展与 Pi 同进程运行,可能看到:

  • 用户输入和模型输出;
  • 工具调用参数;
  • 文件内容;
  • 凭据和请求头;
  • 会话历史;
  • 工作目录和进程环境。

所以“只安装可信扩展”不是客套话,而是安全边界。

9.2 技能和包也不是零风险

跳转到“9.2 技能和包也不是零风险”

技能可能指示模型运行脚本;包可能包含扩展代码。项目信任只决定“是否加载项目资源”,并不能保证资源本身安全。

  • 只需要读文件,就不要给写工具;
  • 只需要某一个 MCP server,就不要把所有 server 都设为 direct;
  • 危险工具要有拦截和确认;
  • 第三方扩展先看源码,再安装;
  • 真正高风险任务放进容器或受限账户。

实验 1:写一个最小命令扩展

跳转到“实验 1:写一个最小命令扩展”

把前面的 hello.ts 放进用户扩展目录,加载并输入 /hello。确认:

  • 它不需要模型调用;
  • 它只是一个给人用的命令;
  • 移除文件或重载后行为消失。

创建:

~/.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 不会产生真实副作用 它调用的工具是真实执行,且失败不会自动回滚
项目受信任就万事大吉 信任只控制资源加载,不保证资源安全
包只是文件集合 包可以带扩展代码和脚本,安装前应审阅
工具越多越好 工具会占上下文、增加选择难度和风险面

请回答:

  1. 为什么 Pi 坚持“核心最小、能力外置”?
  2. 技能、提示模板、扩展、MCP、codemode 各解决什么问题?
  3. 扩展工厂、session_start、session_shutdown 分别应该做什么?
  4. 为什么扩展工厂里不适合启动长期进程?
  5. 技能为什么只先加载 description?这对上下文有什么好处?
  6. MCP 工具为什么仍然要走工具管线?
  7. codemode 省了哪些成本,又没有消除哪些风险?
  8. 什么情况下才应该考虑改核心?

建议产出:

  • 一个能加载的扩展命令;
  • 一个能被自动选中的技能;
  • 一张“技能 / 扩展 / MCP / codemode”决策表;
  • 一段 3 分钟讲解,说明为什么“能力外置”让系统更稳,也带来新的信任问题。

下一站是阶段 7:界面与集成。能力已经能长出来,接下来看同一套引擎怎样同时服务人、脚本和外部程序。


配套学习路线阶段 6 · 基于 v1.3.0(71023aec)