PiAgent 完全指南:从零到写扩展
写作日期:2026-08-07 版本锚定:pi 0.81.1(
@earendil-works/pi-coding-agent)。pi 迭代很快,读到本文时若版本已变,请以pi --version和本机docs/为准。 实测环境:macOS(Darwin 25.5)。Windows 未测,理论上走 WSL,本文不覆盖。 取代:PiAgent/260724-PiAgent-小白教程-从入门到精通.md(入门部分)与PiAgent/260805-PiAgent-扩展实战-视频笔记全流程教程.md(扩展部分)。那两份保留作存档,其中若干结论已被本文推翻。
本文的标注约定
全文每一条结论都带来源标记,请按标记决定信任程度:
| 标记 | 含义 |
|---|---|
| ✅ | 本机实测过,命令、输出、报错都是真跑出来的 |
| 📖 | 官方文档有明确出处,本文给出文件名和行号,但没亲自跑 |
| ⚠️ | 未验证或有前提条件,照做前请自己确认 |
入门篇(第 0–5 章)的每一条操作步骤都是 ✅——小白照着敲,错一步就卡死,这部分不允许出现「据说」。
速查表
真正天天要翻的就这一页。
五条命令
pi # 启动交互界面(TUI)
pi -p "把这个目录里的图片按日期改名" </dev/null # 跑一次就退出,适合脚本
pi -c # 接着上次的会话继续
pi --list-models # 看当前能用哪些模型
pi list # 看装了哪些扩展包
✅
-p后面那个</dev/null不是可有可无的。 在脚本、CI、或任何非交互环境里,pi -p会一直等 stdin 的 EOF 而挂住。本文写作过程中我自己就先踩了一次,一条命令跑了两分钟没退出。手动在终端里敲则不受影响。
四个目录
| 放什么 | 放哪 |
|---|---|
| 全局 skill | ~/.pi/agent/skills/<名字>/SKILL.md |
| 全局 extension | ~/.pi/agent/extensions/*.ts |
| 模型/网关配置 | ~/.pi/agent/models.json |
| 包登记、主题 | ~/.pi/agent/settings.json |
✅ 换配置目录用环境变量
PI_CODING_AGENT_DIR(📖docs/usage.md:296)。想干净试验又不弄脏现有配置,就用它——本文全部入门实测都跑在PI_CODING_AGENT_DIR=/tmp/pi-fresh里。
🚨 一条能省钱的规矩
--provider 和 --model 要显式写全,写错一个字母 pi 不会报错,只会悄悄给你换模型。
✅ 实测三组对照(同一台机器、同一份配置):
① --provider anthropic --model claude-haiku-4.5 → 实际用 anthropic / claude-haiku-4.5
② --provider anthropik (少打一个 c) → 实际用 anthropic / claude-opus-4-8
③ 两个参数都不给 → 实际用 anthropic / claude-opus-4-8
②③ 用的是最贵的 Opus。拼错 provider 名等价于没写,pi 不报错、不警告,直接回退到默认。
常见报错对照
| 现象 | 真实文案 | 退出码 | 怎么办 |
|---|---|---|---|
| 没配 key | No API key found for the selected model. + 指向 /login |
✅ 1 | 见第 2 章配 provider |
| 模型名写错 | Warning: Model "xxx" not found for provider "anthropic". Using custom model id. 然后由服务端报错 |
✅ 0 | pi 不校验模型名,把错名当自定义 id 直接发出去 |
| provider 名写错 | 没有任何报错 | ✅ 0 | 见上面那条省钱规矩 |
| 装完 skill 用不上 | 无报错 | — | 改完 skill 必须退出重进,/reload 不管用(第 5 章) |
| 卸载 skill 卸不掉 | No skills found to remove. 但文件还在 |
✅ 0 | 手动 rm -rf 并核对(第 4 章) |
怎么读这份文档
这份文档分两半,中间有一条明确的停止线:
- 入门篇(第 0–5 章,约 400 行):读完你就能真用起来,全程不需要会写代码。
- 🛑 停止线:有一份 5 题自测清单。做得出来,说明入门篇的目的已经达到,可以合上文档去用了。
- 进阶篇(第 6–13 章):写 TypeScript 扩展、拦截工具调用、多 agent 协作、打包分发、迁移到真实团队项目。等你真的撞上问题再回来看,不必一口气读完。
入门篇
第 0 章:给谁看,需要准备什么
这份文档假设你:
- 能打开终端,会跟着敲命令
- 听说过 Claude Code / Codex 之类的东西,但不一定用过
- 不需要会写代码(入门篇结束前不出现一行 TypeScript)
你需要准备:
- macOS 或 Linux(✅ 本文实测在 macOS)
- Node.js 18+(
node -v检查) - 一个 LLM 的 API key——Anthropic / OpenAI / Google / xAI / OpenRouter 任选其一即可
你不需要:
- 订阅 Claude Pro 或 ChatGPT Plus(用 API 更便宜,也更适合命令行)
- 任何网页版工具
第 1 章:pi 到底是什么
一句话
An autonomous agent is just an LLM + tools + a loop.
pi 是一个跑在终端里的编程助手:它能读文件、执行命令、改代码、写新文件,然后根据结果决定下一步——循环,直到任务完成。
和 Claude Code / Codex 的区别
pi 的取舍是核心保持小,其余推给扩展。官方把话说得很直白:
📖
docs/usage.md:309:It intentionally does not include built-in MCP, sub-agents, permission popups, plan mode, to-dos, or background bash.
这六项是故意不做的,不是还没做。别指望以后官方内置。想要,就自己装扩展或者自己写——这也正是本文进阶篇在讲的事。
⚠️ 一个必须先说清的混淆:oh-my-pi ≠ pi
网上(尤其是视频教程里)常出现 oh-my-pi,它是 pi 的一个 fork,自带大量插件。它的代码结构和原生 pi 不一样——比如 oh-my-pi 自带 MCP 支持,而原生 pi 没有。
✅ 实测:原生 pi 的 dist/ 里 grep 不到 mcpServers。
看教程时先确认对方跑的是哪一个,否则会照着 fork 的结构去找原生 pi 里根本不存在的文件。
第 2 章:装好,并且真的跑起来
2.1 装
npm i -g @earendil-works/pi-coding-agent
pi --version # 应该输出 0.81.1 或更高
📖 安装命令来自官方 README。⚠️ 本文实测的机器上 pi 是随另一套工具链装的,这一条我没有从零重装验证过。
2.2 第一次运行会看到什么
✅ 实测——一个 key 都没配的情况下跑 pi,会得到:
No API key found for the selected model.
Use /login to log into a provider via OAuth or API key. See:
<pi安装路径>/docs/providers.md
<pi安装路径>/docs/models.md
退出码 1。
✅ 同时它会在配置目录下建两样东西,仅此而已:
~/.pi/agent/
├── auth.json 2 字节,内容是 {}
└── sessions/
注意没有 settings.json——那个文件是你第一次装扩展包时才会被创建的。很多人对着教程找 settings.json 找不到,就是这个原因。
2.3 配 provider:两条路
路线 A:官方 API(推荐新手)
最简单的方式是环境变量。📖 docs/providers.md:63-86 给了完整对照表,常用的几个:
| 服务商 | 环境变量 | --provider 的值 |
|---|---|---|
| Anthropic | ANTHROPIC_API_KEY |
anthropic |
| OpenAI | OPENAI_API_KEY |
openai |
| Google Gemini | GEMINI_API_KEY |
google |
| xAI | XAI_API_KEY |
xai |
| OpenRouter | OPENROUTER_API_KEY |
openrouter |
| DeepSeek | DEEPSEEK_API_KEY |
deepseek |
export ANTHROPIC_API_KEY=sk-ant-...
pi --provider anthropic --model claude-haiku-4.5
也可以在 TUI 里用 /login 走 OAuth 或粘贴 key(📖 docs/providers.md)。
路线 B:走中转网关(自建/公司网关)
如果你的 key 不是直连官方,而是走一台中转网关,需要覆盖 baseUrl。在 ~/.pi/agent/models.json 里写:
{
"providers": {
"anthropic": {
"baseUrl": "https://<你的网关地址>/claude",
"models": [
{
"id": "claude-haiku-4.5",
"name": "Claude Haiku 4.5 (网关)",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 200000,
"maxTokens": 16000
}
]
}
}
}
然后照常用环境变量给 key:
ANTHROPIC_API_KEY="<网关的key>" pi --provider anthropic --model claude-haiku-4.5
✅ 本文所有实测都走的这条路线,确认可用。
几个要点:
- 📖
docs/models.md:145:非内置 provider 的配置必须有baseUrl和api;apiKey不必写进文件,可以由/login、auth.json、--api-key或环境变量提供。没有 auth 时模型仍会加载,但在/model里显示为不可用——这个状态很容易被误读成「配置没生效」。 - ⚠️ 模型 ID 有三层,别混:
/model面板里显示的是 pi 内建目录的写法(claude-opus-4-8,横线);models.json里写的是发给网关的名字;网关自己认什么名字取决于它的实现。有的网关会做横线↔点的映射,有的不会。
2.4 🚨 这两个参数必须显式写全
这是本文最想让你记住的一条,因为它直接关系到账单。
✅ 实测,同一台机器、同一份配置,三组对照:
① --provider anthropic --model claude-haiku-4.5
→ 实际用了 provider=anthropic model=claude-haiku-4.5 ✔ 符合预期
② --provider anthropik (少打一个字母 c)
→ 实际用了 provider=anthropic model=claude-opus-4-8 ✘ 静默换成 Opus
③ 两个参数都不给
→ 实际用了 provider=anthropic model=claude-opus-4-8 ✘ 静默用 Opus
拼错 provider 名 pi 不报错、不警告,效果等同于没写,直接回退到默认——而默认是当前有 auth 可用的 provider 加它的旗舰模型。你以为在用便宜的 haiku,实际每一句话都在烧 Opus。
⚠️ 另外,pi --help 里写着 --provider <name> Provider name (default: google),但 ✅ 实测什么都不给时用的是 anthropic 而不是 google——帮助文本和实际行为不一致,它实际是挑了个有可用 auth 的 provider。以实测为准。
怎么确认自己到底在用哪个模型:TUI 里敲 /model 看当前值;脚本里用 --mode json,返回的 assistant 消息里带 provider 和 model 字段,那是最硬的证据。
2.5 模型名写错会怎样
✅ 实测:pi 不校验模型名。
$ pi --provider anthropic --model claude-opus-9 -p "hi" </dev/null
Warning: Model "claude-opus-9" not found for provider "anthropic". Using custom model id.
503 {"type":"..._error", ... "当前分组 xx 下对于模型 claude-opus-9 无可用渠道 ..."}
它只给一句 Warning,然后把这个不存在的名字当成“自定义模型 id”直接发给服务端,最后由服务端报错。退出码是 0——脚本里靠退出码判断成败会漏掉这种失败。
2.6 第一个任务
pi --provider anthropic --model claude-haiku-4.5
进 TUI 后直接说人话:
看看当前目录里有什么文件,按大小排前五个
把这个目录下所有 .jpeg 改成 .jpg
pi 会自己决定调 bash 还是 read,执行前你能看到它要跑什么。
⚠️ pi 默认放行所有工具调用,读和写都不弹权限确认。它没有 Claude Code 那种
acceptEdits/bypassPermissions模式。想限制,用--tools <白名单>或--exclude-tools <黑名单>。这一条推翻了 260724 旧版教程里「pi 有安全模式,每次要批准」的说法——那是错的。
2.7 会话续接
pi -c # 接着上一次继续
pi -r # 列出历史会话,选一个恢复
会话存在 ~/.pi/agent/sessions/,按项目目录分文件夹。不用每次从头交代背景。
第 3 章:日常怎么用
两种运行模式(入门只需要这两种)
| 模式 | 命令 | 什么时候用 |
|---|---|---|
| 交互 | pi |
日常干活,能看到它每一步做什么 |
| 一次性 | pi -p "..." </dev/null |
写进脚本、批处理、定时任务 |
还有
--mode json和 RPC 模式,那是给程序集成用的,进阶篇再说。
常用 slash 命令
在 TUI 里敲:
| 命令 | 作用 |
|---|---|
/model |
看/换当前模型 |
/login |
配置 provider 认证 |
/reload |
重载 extension(⚠️ 对 skill 无效,见第 5 章) |
/help |
全部命令 |
让它闭嘴少花钱的几个习惯
- 简单活儿显式指定
--model claude-haiku-4.5,别让它默认跑 Opus(见 2.4) - 一次只交代一件事,别在一个会话里堆五个不相干的任务——上下文越长越贵
- 长任务用
-c续接,而不是每次重新描述背景
第 4 章:装别人写好的 skill
skill 就是一个 SKILL.md 文件(可以带脚本),告诉模型「遇到某类任务时按这个套路做」。装别人的 skill 是投入产出比最高的一步。
4.1 用 skills CLI 装(主流入口)
npx skills add <仓库> # 装整个仓库的 skill
npx skills add <仓库> -l # 只列出有什么,不装
npx skills add <仓库> --skill <名字> --agent pi -g -y # 只装指定的一个
npx skills list # 看装了什么
npx skills find <关键词> # 搜索
✅ 实测三个来源:
| 仓库 | 内容 |
|---|---|
badlogic/pi-skills |
pi 官方 8 个:brave-search、browser-tools、gccli、gdcli、gmcli、transcribe、vscode、youtube-transcript |
anthropics/skills |
Anthropic 官方,含 skill-creator(帮你写 skill 的 skill) |
jimliu/baoyu-skills |
社区热门,中文场景友好 |
同一份 skill 可以被 Claude Code / pi / Codex 共用——它们读同一批目录。
4.2 装的时候会看到什么
✅ 实测装 badlogic/pi-skills 的 vscode:
● claude-code_2-1-220_agent Agent detected — installing non-interactively
◇ Source: https://github.com/badlogic/pi-skills.git
◇ Found 8 skills
● Selected 1 skill: vscode
◇ Installation Summary ────╮
│ ~/.agents/skills/vscode │
│ copy → Pi │
├───────────────────────────╯
◇ Security Risk Assessments ────────────────────────────╮
│ Gen Socket Snyk │
│ vscode Safe 0 alerts Low Risk │
├────────────────────────────────────────────────────────╯
两个值得注意的地方:
- 它会自动检测你在什么 agent 里跑,检测到就转非交互模式,不再弹确认
- 装之前给三家的安全评估(Gen / Socket / Snyk)。装陌生仓库前先看这一行
4.3 🚨 两个实测踩到的坑
坑一:安装摘要显示的路径是错的。
✅ 摘要写 ~/.agents/skills/vscode,但装完实际检查:
~/.pi/agent/skills/vscode ← 真的在这(真目录,copy 进来的,不是软链)
~/.agents/skills/vscode ← 不存在
装完别信摘要,自己 ls 一下确认落点。
坑二:卸载会静默失败。
✅ 实测:
$ npx skills remove vscode
◇ Found 0 unique installed skill(s)
└ No skills found to remove.
$ ls ~/.pi/agent/skills/vscode
~/.pi/agent/skills/vscode ← 还在!
它说没找到可卸载的,但文件好端端待在那儿。add 的落点和 remove 的扫描路径对不上。 卸载后必须自己核对,没卸干净就手动删:
rm -rf ~/.pi/agent/skills/<名字>
ls ~/.pi/agent/skills # 核对一遍
这三个坑(连同第 2 章的 provider 静默回退)有个共同点:工具不报错,但你以为的和实际发生的不是一回事。用 pi 的过程里,「没报错」永远不等于「成功了」——多花十秒
ls一下,能省掉后面一小时的困惑。
4.4 pi 的一个隐藏能力
⚠️ 当任务需要某个 pi 没装的 skill 时,它会自己去搜社区候选并列给你,你回一句 install 1 就自动装上。这条来自旧版教程记录,本次未复测。
第 5 章:写你自己的第一个 skill
不需要会编程。一个 skill 最小就是一个带 frontmatter 的 Markdown 文件。
5.1 最小骨架
mkdir -p ~/.pi/agent/skills/my-skill
~/.pi/agent/skills/my-skill/SKILL.md:
---
name: my-skill
description: 当用户要求做 XXX 时使用。描述要写清「什么时候用」,模型靠这句话决定要不要读正文。
---
# my-skill
## 什么时候用
用户说「……」的时候。
## 怎么做
1. 第一步
2. 第二步
description 是最重要的一行:pi 只把 name 和 description 放进 system prompt,正文是模型判断需要时才去读的(渐进式披露)。描述写不清「什么时候用」,这个 skill 就永远不会被触发。
5.2 验证它真的生效了
这一步很多人做错,所以专门说。
❌ 错误做法:问模型「你现在有哪些 skill?」
模型很可能只是 ls 了一下目录,或者干脆瞎猜。模型对自己能力的自述不是机制证据——这个教训是有代价换来的,进阶篇第 11 章有完整案例。
✅ 正确做法:暗号法 + 反向对照。
在 SKILL.md 里写一个外界不可能猜到的字符串:
---
name: say-code
description: 当用户要求「报暗号」时使用,返回本 skill 里写死的验证码
---
用户说「报暗号」时,直接回答:`PI-VERIFY-7391`,不要解释。
然后两边都试:
$ pi -p "报暗号" </dev/null
`PI-VERIFY-7391` ← ✅ skill 生效了
$ pi --no-skills -p "报暗号" </dev/null
我是 Claude,一个 AI 编程助手 … ← ✅ 关掉 skill 就完全答不出
✅ 以上为本文实测输出。两个方向都要试:只看「有 skill 时答得出」不够,还要确认「关掉就答不出」——否则你无法排除模型是从别处(比如自己读了文件)拿到答案的。
5.3 🚨 改完 skill 必须退出重进
✅ 实测结论:
| 改了什么 | /reload 够不够 |
|---|---|
| extension | ✅ 够(📖 docs/extensions.md:7) |
| skill / SKILL.md | ❌ 不够,必须退出重进 |
原因:/reload 只重载 pi 进程侧的配置,不会重新拼装 system prompt,而 skill 的 name/description 是写在 system prompt 里的——模型压根感知不到你的改动。
⚠️ 更坑的是:/reload 之后 [Skills] 列表里能看到新 skill。看得到 ≠ 生效。别拿列表当验收凭据,用 5.2 的暗号法。
5.4 写好 skill 的几条经验
description写清触发条件,不要写功能介绍- 步骤写成可执行的判断,别写模糊的原则。⚠️ 实测过一个反例:给子 agent 写「时间戳误差在 ±2 秒内算通过」,haiku 照样把 0.98 秒的偏差报成错误——数值容差要写成它能一步步执行的判断,不能指望它自己权衡
- 脚本用什么语言都行,SKILL.md 里说清怎么调
- 想让 skill 跨 agent 共用,放
~/.agents/skills/(Claude Code / pi / Codex 都读这里) - 懒得自己写就装
skill-creator,它会用对话方式带你生成
🛑 停止线
到这里,你已经会用 pi 了。
下面的进阶篇要开始写 TypeScript、拦截工具调用、编排多个 agent、打包分发。那些是在你撞上具体问题之后才有意义的东西——现在合上文档去用几天,比一口气读完更有价值。
自测清单
不看文档,这五件事你能做出来吗?
- 你想让 pi 用便宜的 haiku 跑一个批处理脚本。完整命令怎么写?(提示:有两个参数必须显式写,还有一个重定向不能少)
- 你写了个 skill,改了
description,怎么确认模型真的感知到了改动? npx skills remove xxx说卸载成功了,你下一步该做什么?- 怎么在完全不动现有配置的前提下,起一个干净的 pi 环境做试验?
- 你的账单突然暴涨,怀疑模型跑错了。用什么命令拿到最硬的证据?
答案分别在:2.4 + 速查表 / 5.2 / 4.3 / 速查表「四个目录」 / 2.4 结尾。
五题里有三题的答案是「别信它说成功了,自己去核对」——这不是巧合,是 pi 这类工具的通用相处方式。
进阶篇
从这里开始需要读得懂一点 TypeScript。不需要精通——本章的例子都在 30 行以内。
第 6 章:pi 的扩展面到底有几个
官方 README 宣传「六类扩展」,✅ 实际核对下来它们不在同一层,真实结构是 4 + 1 + 1:
TypeScript extension(唯一的代码扩展面)
├── pi.registerTool() 注册工具,模型自行决定调不调
├── pi.registerCommand() 注册 /命令,用户主动触发
├── pi.on(事件, handler) 挂生命周期钩子(session_start / tool_call …)
└── pi.registerProvider() ← README 单列的「custom provider」其实就是这一条
skill / prompt template / theme 纯文件配置,不写代码
pi package 分发层,把上面这些打包
README 自己也承认「model provider 本质上就是一个 extension 文件」。所以**「六类」是营销口径,实际你只需要学会一个 .ts 文件里的四个方法**。
⚠️ 视频教程里作者口播的「五种扩展」是 extension 内部的细分,和 README 的「六类」不是同一套数,两边对不上很正常。
加载位置
📖 docs/extensions.md:109、skills.md:24、prompt-templates.md:9:
| 类型 | 全局 | 项目级 |
|---|---|---|
| extension | ~/.pi/agent/extensions/*.ts(或 */index.ts) |
.pi/extensions/(需 trust) |
| skill | ~/.pi/agent/skills/*/SKILL.md |
.pi/skills/、.agents/skills/(需 trust) |
| prompt template | ~/.pi/agent/prompts/*.md |
.pi/prompts/ |
| subagent 定义 | ~/.pi/agent/agents/*.md |
.pi/agents/ |
✅ 全局目录不需要任何登记,放进去就会被自动发现——本文第 7 章的实测可以证明这一点。
第 7 章:第一个 extension
7.1 完整代码
~/.pi/agent/extensions/hello.ts:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("hello 扩展已加载", "info");
});
// 最小可用的自定义工具:返回本机当前时间。
// 选它是因为结果能跟系统时间对照,能验证「模型真的调用了工具」而不是自己编的。
pi.registerTool({
name: "local_time",
label: "本机时间",
description: "返回运行 pi 的这台机器的当前时间,格式 YYYY-MM-DD HH:MM:SS",
parameters: Type.Object({}),
async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
const d = new Date();
const p = (n: number) => String(n).padStart(2, "0");
const text = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
return { content: [{ type: "text", text }], details: {} };
},
});
pi.registerCommand("hello", {
description: "打个招呼",
handler: async (args, ctx) => {
ctx.ui.notify(`你好 ${args || "世界"}`, "info");
},
});
}
三个要点:
- 默认导出一个函数,参数是
pi(ExtensionAPI) - 参数 schema 用
typebox的Type,pi 自带这个依赖,不用自己npm install execute的返回值形状固定:{ content: [{type, text}], details: {} }
7.2 实测:它真的被调用了
放进 extensions/ 目录,不需要任何登记,直接问:
$ date '+%Y-%m-%d %H:%M:%S'
2026-08-07 20:35:04 ← 系统时间基准
$ pi -p "调用 local_time 工具,把它返回的原样告诉我" </dev/null
2026-08-07 20:35:09 ← 模型返回
✅ 相差 5 秒(模型往返耗时),证明这是工具真实执行的结果,不是模型编的。
这个「用可对照的外部事实做验证」的思路,比问模型「你调用工具了吗」可靠得多。全文反复用它。
7.3 快速试验的两种方式
pi -e ./my-extension.ts # 临时加载,适合调试(⚠️ 这种方式不支持 /reload)
📖 docs/extensions.md:7:放在自动发现目录里的 extension 可以用 /reload 热重载;pi -e 临时加载的不行。
| 改了什么 | /reload 够不够 |
|---|---|
| 自动发现目录里的 extension | ✅ 够 |
pi -e 临时加载的 |
❌ 不适用 |
启动后动态 registerTool() |
✅ 连 reload 都不用(📖 docs/extensions.md:1339) |
| skill | ❌ 必须退出重进(第 5.3 章) |
第 8 章:另外三种 API
8.1 registerCommand:注册 /命令
上面代码里的 hello 就是。用户敲 /hello 张三 触发。
✅ 实测坑:
registerCommand注册的命令在-p模式下完全没有输出。因为提示走的是ctx.ui,而-p模式没有 TUI。写自动化脚本时别指望它。
8.2 pi.on:挂生命周期钩子
常用事件:
| 事件 | 时机 | 典型用途 |
|---|---|---|
session_start |
会话开始 | 注入项目上下文、打招呼 |
tool_call |
模型要调工具前 | 拦截、改写、阻断(第 9 章) |
tool_call 的 handler 返回 { block: true, reason: "..." } 就能拦下这次调用。
8.3 registerProvider:接入自定义模型服务
⚠️ 本文没有实测这一条(需要再准备一个模型服务)。但有一条从踩坑记录里来的重要提醒:
注册 provider ≠ 请求真的走过去。
~/.pi/agent/settings.json里的defaultProvider/defaultModel(📖docs/settings.md:30-31)记着首次配置的值,注册一个新 provider 不会自动改它。有人遇到过这种情况:CLI 输出一切正常,实际请求还在走旧 provider,最后靠翻网关后台日志的时间戳才发现。排查一律从下游验证(网关日志 / 账单 / 请求记录),别信 CLI 的正常输出。
这跟第 2.4 章 provider 静默回退是同一类问题的两个面:pi 在「你以为配了」和「真的生效了」之间,不会主动提醒你有落差。
第 9 章:拦截工具调用,以及它的天花板
9.1 怎么拦
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("危险操作", "确定要执行 rm -rf 吗?");
if (!ok) return { block: true, reason: "用户拒绝" };
}
});
✅ 实测有效:write / edit / bash 里带 rm 的三类调用都能拦住。
9.2 🚨 天花板:它是启发式,不是沙箱
✅ 实测:上面这种基于关键词的拦截,一句话就能绕过:
python3 -c 'open("被保护的文件","w").write("")'
这条命令不含 rm、不含 mv、不含 >,关键词匹配全部落空,文件照样被清空。
更值得记的是当时的第二个发现:模型在这次操作后说「应该先创建目录」,暗示自己失败了——但文件实际已经被覆盖。
🚨 模型对自己动作后果的自述不可信,要看文件系统。
9.3 两条硬规矩
规矩一:真正的防线在 OS 层。
chflags uchg <文件> # macOS:锁定,python 写入和 rm 都挡得住
chattr +i <文件> # Linux 对应命令
✅ 实测确认锁定后 python 写入和 rm 都失败。extension 层的拦截只能挡住「模型不小心」,挡不住「命令绕道」。
规矩二:ctx.hasUI 为假时必须 fail-safe 阻断。
拦截逻辑里如果要弹确认框,一定要先判断有没有 UI:
if (!ctx.hasUI) return { block: true, reason: "无人值守环境,一律阻断" };
否则在 -p 模式、CI、定时任务里,你的保护恰好等于不存在——最需要保护的自动化场景反而裸奔。
第 10 章:多 agent 分工
pi 官方不内置 sub-agents(见第 1 章那句 intentionally),要装扩展。
10.1 两个方向相反的方案
pi-subagents |
pi-dynamic-workflows |
|
|---|---|---|
| 抽象方式 | 预定义角色(内置 9 个) | 主 agent 现场生成 JS 脚本,在 Node VM 沙箱里编排 |
| 上下文 | fresh(干净)/ fork(继承主会话)可选 |
完全不继承主会话 |
| 短板 | 角色要事先定义好 | 无持久化、无断点恢复 |
✅ 本文实测用的是 pi-subagents 0.40.0(内置 agent 9 个)。
⚠️
dynamic-workflows完全不继承主会话,背景信息必须重复写进提示词,否则子 agent 基于空白上下文瞎猜。⚠️ 选型提醒:
pi-dynamic-workflows的仓库 About 为空、无文档、写作时已两个月未更新。
10.2 🚨 用工具白名单做权限约束,别用提示词
这是最值得搬到任何多 agent 系统的一条:
---
name: security-reviewer
tools: [read, grep, find, ls, bash] # ← 没有 write,它想改也改不了
---
比在 prompt 里写「请不要修改文件」可靠一个数量级。 提示词是请求,白名单是物理限制。
同理,审查类 agent 必须用 fresh 而不是 fork——继承了主会话就等于让它自己批准自己。(内置的 oracle / planner / worker 是 fork,其余 fresh。)
10.3 实测得到的三条经验
- ✅ 子 agent 很慢:单条核对要几分钟;连续两次 10 分钟超时是常态。
--mode json还会整体缓冲,超时的话一个字节都拿不到 - ✅ 便宜模型不守数值规则:给 haiku 写「误差 ±2 秒内算通过」,0.98 秒的偏差照样被报成错误。容差要写成它能一步步执行的判断步骤
- ✅ 给 LLM 用的工具,报错信息要具体到字段和类型:报
each meta phase must have a title string,模型一次就自己改对了;只报invalid workflow它就只能瞎猜。这是「让模型现场写脚本」这种范式能跑通的前提
第 11 章:打包分发
把 extension + skill + prompt + agent 定义打成一个包,换台机器一条命令装好。
11.1 包长什么样
~/pi/my-package/
├── package.json
├── extensions/my-tool.ts
├── skills/my-skill/SKILL.md
├── prompts/my-prompt.md
└── agents/my-agent.md
package.json:
{
"name": "my-package",
"version": "0.1.0",
"private": true,
"pi": {
"extensions": ["./extensions/my-tool.ts"],
"skills": ["./skills"],
"prompts": ["./prompts"]
},
"pi-subagents": {
"agents": ["./agents"]
}
}
安装:
pi install /绝对路径/my-package # 本地路径登记,改包即生效,不用重装
pi list # 确认登记上了
装完 ~/.pi/agent/settings.json 会出现:
{ "packages": ["...", "../../pi/my-package"] }
11.2 ✅ 四类资源全部随包走
唯一的开关是 settings.json 里的 packages 登记,不需要任何软链接。
✅ 零软链状态下的三组对照:
| 配置 | 结果 |
|---|---|
| 有包登记 | agent 可发现,source=package,路径直指包内 |
| 无包登记 | 直接消失 |
| 有软链 + 有登记 | 12 个,但三个 agent 报成 source=user(软链目录赢) |
优先级是 builtin < package < user < project。⚠️ 注意第三行:同名只保留优先级最高的那份,所以有软链时 [package] 一栏根本不出现——这会让你完全看不出包分发到底通没通。
11.3 🚨 一个价值很高的翻车案例
这一节讲的是怎么验证,比结论本身更重要。
当初的错误结论:「包里的 agents 不生效,稳妥做法是软链到 ~/.pi/agent/agents/」。
错在判据——当时拿「问模型:你有哪些自定义 subagent?」当探针:
- 软链之前,模型答「没找到」→ 判定包机制不生效
- 软链之后,模型报得出名字 → 判定软链才是关键
真相是:pi-subagents 的工具描述根本不列举任何 agent 名字(连内置的 9 个也不列),模型只能靠 {action:"list"} 主动查。它没查,多半只是 ls 了一下那个空目录。以为在测包机制,实际在测那个目录里有没有文件。
🚨 最该记住的一条:当时已经写下「不能排除它自己读了那个 md」的警惕,却只用在「软链后模型报得出」这一侧,没用在「软链前答不出」那一侧。
同一个探针,证实和证伪两个方向必须用同一把尺子量。单向的怀疑等于没怀疑。
正确做法:别拿模型的自然语言回答当机制探针,直接调发现函数。
// 直接调 pi-subagents 的 discoverAgents,绕开模型看 agent 到底从哪加载
const agents = await jiti.import(path.join(SUBAGENTS, "src", "agents", "agents.ts"));
const found = agents.discoverAgents(cwd, "both").agents;
// 按 source 分组打印:builtin / package / user / project
⚠️ 技术细节:不能用
node --experimental-strip-types直接跑包里的.ts(Node 拒绝为node_modules下的文件剥类型,报ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING),借pi-subagents自带的jiti加载即可。
✅ 用这个探针做的最终验证(删掉全部软链前后各跑一次):
删除前:三个 agent source=user 路径 ~/.pi/agent/agents/*.md
删除后:三个 agent source=package 路径 ~/pi/my-package/agents/*.md
总数都是 12 个,只有来源变了。
11.4 验收自检
装完跑这几条才算真装好:
pi list # 包在登记里
pi -p "调用 <你的工具名>" </dev/null # 工具真被调用(用可对照的外部事实验证)
pi -p "报暗号" </dev/null # skill 生效(配合 --no-skills 反向对照)
node tools/probe-agents.mjs # agent 的 source 是 package 而不是 user
第 12 章:迁移到真实团队项目
前面都在自己的一亩三分地。搬进一个多人协作的大仓库,关注点完全不同。
本章实测目标:某内网 Unity 游戏仓库,191 GB,git 实际跟踪约 10 万个文件,团队协作,已有 27 个自建 skill 和一份大号
AGENTS.md。测于 2026-08-05。(具体标识已脱敏,实测数字原样保留。)
12.1 ✅ 性能不是问题
10 万文件的仓库,pi 启动只要 5.8 秒(含一次模型往返)。
「大仓库会拖慢 agent」这个担心可以划掉。
12.2 ✅ trust 前后的差别(实测了一半)
不加 --approve 时:
$ pi -p '(1) 你能看到几个 skill;(2) 上下文里有没有 AGENTS.md'
(1) 看到 15 个 skill,项目里那 27 个一个都没有
(2) 有 AGENTS.md 的内容
两条都印证了官方文档:
- 项目级 skill 需要 trust 才加载——那 15 个全是全局的
- ⚠️
AGENTS.md/CLAUDE.md不管 trust 与否都加载(📖docs/security.md:27)。每次启动固定进上下文,这部分省不掉- ✅ 而且它会长大:同一个文件 08-05 实测 26 KB,08-07 复查已经 29.2 KB(29,905 字节)。团队仓库的
AGENTS.md只增不减是常态,这是一笔每次启动都要付、且在悄悄变贵的固定成本
- ✅ 而且它会长大:同一个文件 08-05 实测 26 KB,08-07 复查已经 29.2 KB(29,905 字节)。团队仓库的
✅ 跑完确认副作用:trust.json 不存在、项目里没多出 .pi/ 目录。没 trust 就不写任何东西。
🚧 带 --approve 的那一半没测:这个动作会加载并执行该项目 27 个未经审核的本地 skill,超出「只读探查」的授权范围,因此停下了。
这次停下本身就说明了要点:--approve 的粒度很粗——它一次性信任项目里所有 agent 可执行内容(.pi/extensions、.pi/skills、.agents/skills、.pi/settings.json),不是逐个批准。
12.3 🚨 真正的风险是这两件事
风险一:.pi/extensions 是团队仓库里的可执行代码。
📖 docs/security.md:31:pi 没有内置沙箱,extension 以你的完整系统权限运行。
任何人往仓库提交一个 .ts,只要你 trust 过这个项目,它就在你的机器上跑。 内网协作仓库尤其要当心——你信任的是「这个项目」,不是「今天提交这个文件的人」。
风险二:编辑器会让工作树一直在动。
✅ 实测:Unity 编辑器开着的时候,工作树持续出现改动(99 个改动来自编辑器的自动导入和 shader 编译)。
所以给 agent 的第一条项目规矩应该是:不碰 git 写操作。 否则它会把编辑器生成的一堆临时改动一起提交上去。
12.4 迁移清单
# 1. 只读探查(先别加 --approve)
cd <项目>
pi -p "这个项目的结构是什么" </dev/null
# 2. 决定要不要 trust —— trust 前先看清楚要执行什么
ls .pi/extensions .pi/skills .agents/skills 2>/dev/null
# 3. 全局资源天然可用,不需要 trust
# 你打包好的工具/skill/agent 都是全局的,进任何项目都能用
# 4. 尽量什么都别往项目里加
# trust 是项目级开关:你不加 .pi/extensions,拦不住队友加
第 13 章:贯穿案例——把一条真实流程装进 pi
前面 12 章的能力,合起来能干什么?这是一个真实跑通的例子:视频 → 带可核验时间戳的学习笔记。
一个包(~/pi/pi-video-note/)里装了四类资源:
| 资源 | 做什么 | 用到的章节 |
|---|---|---|
extension video-context.ts |
注册 video_context 工具,按时间查转录原文和帧 |
第 7 章 |
skill video-note/ |
固化笔记规矩(时间戳必须有来源、数字要核对、覆盖率按最坏情况声明) | 第 5 章 |
prompt video-note.md |
一句 /video-note 启动整条流程 |
第 6 章 |
| 3 个 agent | 时间戳核对 / 数字核对 / 读帧,全用便宜模型、无 write 权限、fresh | 第 10 章 |
tool_call 拦截 |
保护转录产物不被误删 | 第 9 章 |
从这个案例里得到的通用教训
🚨 「格式完好、数值漂亮的索引」也可能是假权威值。
当初用 ffmpeg 的 fps=1/N,showinfo 抽帧,打印出来的 pts 数值非常漂亮(0、N、2N…,帧数对得上,偏移为 0),于是直接当权威值写进了文档和索引文件。
当天就被推翻:那个 pts 是滤镜输出轴的时刻,不是源视频里的时刻。用另一种方法抽帧,pts 完全相同,但 53 张图里有 50 张内容不一样。
凡是「元数据自称的时刻」,都要用「按这个时刻去取一次」反过来验证内容。
当时验了帧数、间隔、偏移三项全中,唯独没验第四项——图到底是不是来自那个时刻。
这条教训的通用形式,和第 11 章的探针翻车、第 4 章的卸载静默失败是同一件事:验证要打在「结果」上,不能打在「过程看起来对不对」上。
附录 A:这份文档推翻了什么
合并两份旧文档时,以下结论被实测推翻,请勿再参考旧版:
| 旧说法 | 出处 | 真实情况 |
|---|---|---|
| 「pi 有安全模式,每次调用要批准」 | 260724 | ❌ pi 默认放行所有工具调用。要限制用 --tools / --exclude-tools |
「--approve 是权限开关」 |
260724 | ❌ 它是信任项目本地文件的开关,粒度很粗 |
| 「本机自带 31 篇官方文档」 | 260724 | ❌ 复点是 29 篇 |
「改完 skill /reload 就行」 |
通行说法 | ❌ 对 skill 无效,必须退出重进 |
| 「包里的 agents 不生效,要软链」 | 260805 初版 | ❌ 四类资源全部随包走,开关是 settings.packages |
| 「10 万文件的仓库会拖慢启动」 | 260804 归档 | ❌ 实测 5.8 秒(含模型往返) |
「用 grep -F 查引文定位时间戳」 |
260804 笔记 | ❌ 只能单行匹配,跨字幕条的引文一律漏 |
「--provider 默认是 google」 |
pi --help |
❌ 实测默认走 anthropic + 旗舰模型(挑有 auth 的) |
这张表本身就是一条方法论:这些错误没有一条是「查文档不仔细」造成的,全都是**「看起来成功了」和「真的成功了」之间的落差**。
附录 B:覆盖率声明
本文实测覆盖的部分(✅):
- 零配置首次启动的完整行为(报错文案、退出码、生成的文件)
- provider 配置(走中转网关路线)、provider/模型名写错的三组对照
- extension 自动发现、
registerTool真实调用验证 - skill 编写、生效验证(暗号法 +
--no-skills反向对照) - skills CLI 装 / 列 / 卸的完整流程,含两个静默失败的坑
- 包机制的三组对照与
discoverAgents探针 PI_CODING_AGENT_DIR隔离环境
没有实测、按标记信任的部分:
- 📖
npm i -g从零安装(本机 pi 是随其他工具链装的) - 📖 官方 API 直连(本文实测走的是中转网关,机制相同但未直接验证)
- 📖
/login的 OAuth 流程 - ⚠️
registerProvider自定义 provider(需另备一个模型服务) - ⚠️
--approve之后的完整行为(涉及执行未审核的第三方 skill,主动停下了) - ⚠️ pi 的「自动推荐 skill」能力(旧记录,本次未复测)
本文没有覆盖的:Windows / WSL、RPC 与 SDK 集成模式、theme 定制、MCP 接入(pi 原生不支持,需装 pi-mcp-adapter)。
附录 C:延伸阅读
- 本机自带全套官方文档(查 pi 的问题优先看这里,别搜网):
<pi安装路径>/docs/,29 篇,与本机版本完全对齐。 常用:extensions.md(2953 行,API 全集)、usage.md、skills.md、settings.md、providers.md、models.md、security.md、packages.md - 存档文档(本文取代了它们,但保留了更细的实测记录):
PiAgent/260724-PiAgent-小白教程-从入门到精通.mdPiAgent/260805-PiAgent-扩展实战-视频笔记全流程教程.md(含完整的证据链和逐步实测记录)
- 视频笔记:
PiAgent/260804-{1,2,3,4}-…_学习笔记.md(P1–P4 四期,各自独立成篇)