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 章:给谁看,需要准备什么

这份文档假设你

你需要准备

  1. macOS 或 Linux(✅ 本文实测在 macOS)
  2. Node.js 18+(node -v 检查)
  3. 一个 LLM 的 API key——Anthropic / OpenAI / Google / xAI / OpenRouter 任选其一即可

你不需要


第 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

✅ 本文所有实测都走的这条路线,确认可用。

几个要点:

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 消息里带 providermodel 字段,那是最硬的证据。

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 全部命令

让它闭嘴少花钱的几个习惯


第 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-skillsvscode

●   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  │
├────────────────────────────────────────────────────────╯

两个值得注意的地方:

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 只把 namedescription 放进 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 的几条经验


🛑 停止线

到这里,你已经会用 pi 了。

下面的进阶篇要开始写 TypeScript、拦截工具调用、编排多个 agent、打包分发。那些是在你撞上具体问题之后才有意义的东西——现在合上文档去用几天,比一口气读完更有价值。

自测清单

不看文档,这五件事你能做出来吗?

  1. 你想让 pi 用便宜的 haiku 跑一个批处理脚本。完整命令怎么写?(提示:有两个参数必须显式写,还有一个重定向不能少)
  2. 你写了个 skill,改了 description,怎么确认模型真的感知到了改动?
  3. npx skills remove xxx 说卸载成功了,你下一步该做什么
  4. 怎么在完全不动现有配置的前提下,起一个干净的 pi 环境做试验?
  5. 你的账单突然暴涨,怀疑模型跑错了。用什么命令拿到最硬的证据

答案分别在: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:109skills.md:24prompt-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");
		},
	});
}

三个要点:

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 实测得到的三条经验


第 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 的内容

两条都印证了官方文档:

✅ 跑完确认副作用: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:31pi 没有内置沙箱,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:覆盖率声明

本文实测覆盖的部分(✅):

没有实测、按标记信任的部分

本文没有覆盖的:Windows / WSL、RPC 与 SDK 集成模式、theme 定制、MCP 接入(pi 原生不支持,需装 pi-mcp-adapter)。


附录 C:延伸阅读


← 返回 AI 编程