PI系列之Extensions: 用 TypeScript 改工具本身

前面讲的全是"喂给模型的文本",这一篇不一样------它改的是工具本身。

前面三层分工是这样的:

层 你改的是 谁执行
AGENTS.md 模型遵守的规矩 模型
SKILL.md 模型照着走的流程 模型
Prompt Template 模型收到的输入 你自己敲

这三样本质都是提示词。模型心情好就照做, 心情不好就漏一步; 你只能事后纠正。

扩展(extension)不是提示词。它是跑在 pi 进程里的 TypeScript 代码 ------你给它加一个新工具、一条新命令、一个钩子, 从那以后这是代码行为, 不是"请遵守"。

一句话: 前面是说服模型, 这一篇是给它装器官。

一个文件就是一个扩展

typescript 复制代码
// ~/.pi/agent/extensions/doge-tool.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

export default function (pi: ExtensionAPI) {
  // 在这里注册工具 / 命令 / 事件钩子
}

放在这两个地方就会被自动加载:

  • ~/.pi/agent/extensions/*.ts ------ 全局
  • .pi/extensions/*.ts ------ 项目级(项目被信任后)

临时试一个不熟的扩展, 用 -e 显式加载, 不落盘:

bash 复制代码
pi -e ./my-extension.ts

三个入口, 覆盖三种需求

入口 干什么 典型用途
pi.registerTool() 给模型加一个新工具 把你自己的一段脚本变成它能调的动作
pi.registerCommand() 给你加一个新斜杠命令 一条要跑十步的操作压成一个词
pi.on(event, fn) 挂在生命周期上 拦截危险命令、改系统提示、加状态栏

第一个和第二个的区别很关键: 工具是给模型的, 命令是给你的。 工具是"它能自己做", 命令是"你按一下它做"。

我写的那个: 43 行, 两个能力

上一篇里我写了个 DOGE 规则的 Python 脚本(scripts/doge_sun_overlay.py)。它一直得我自己 cd 过去、敲命令、再读输出。

现在我把它包成一个扩展(全文 43 行):

typescript 复制代码
const REPO = "/home/shenjian/code/ai/risk_guard";

export default function (pi: ExtensionAPI) {
  // 1) 自定义工具: 把脚本包成 agent 能直接调用的动作
  pi.registerTool({
    name: "doge_check",
    label: "DOGE 规则检查",
    description: "按孙割规则跑 DOGE 买卖检查,返回四栏结论(需求/注意力/买卖点/出局条件)。只读行情与纸面账本。",
    promptGuidelines: ["Use doge_check when the user asks whether to buy, hold, trim or exit DOGE."],
    parameters: Type.Object({
      mode: Type.Optional(Type.String({ description: "now=当前建议, backtest=近90天回放, hold=按均价回放(需 avg)" })),
      avg: Type.Optional(Type.Number({ description: "hold 模式下的持仓均价,如 0.0855" })),
    }),
    async execute(_id, params, signal) {
      const args = ["scripts/doge_sun_overlay.py"];
      if (params.mode === "backtest") args.push("--backtest", "90");
      if (params.mode === "hold") args.push("--hold-test", String(params.avg ?? 0.0855), "--days", "43");
      const r = await pi.exec("python3", args, { cwd: REPO, timeout: 90_000, signal });
      return { content: [{ type: "text", text: (r.stdout || "").trim() || "(无输出)" }], details: { code: r.code } };
    },
  });

  // 2) 护栏: 拦截危险命令
  const DANGER = [/rm\s+-rf\s+\/(?!tmp)/, /git\s+push\s+.*--force/, /drop\s+table/i, /truncate\s+table/i];
  pi.on("tool_call", async (event) => {
    if (event.toolName !== "bash") return;
    const cmd = String((event.input as any)?.command ?? "");
    if (DANGER.some((re) => re.test(cmd))) {
      return { block: true, reason: `doge-tool 护栏拦截:${cmd.slice(0, 80)}` };
    }
  });
}

两个能力的性质完全不同:

  • 第一个是"加能力": 模型多了一个它原来没有的动作;
  • 第二个是"加约束" : 模型想干危险的事, 被代码拦住了------不是"请别做", 是做不了。

实测一: 它自己会去调那个工具

复制代码
$ pi -e ~/.pi/agent/extensions/doge-tool.ts -p "用 doge_check 工具查一下现在 DOGE 该怎么操作"

它没有让我贴命令, 直接调用, 然后给出:

复制代码
**DOGE 现在:HOLD,空仓不动(0% 仓位)**

| 项        | 读数                  | 判断              |
| 现价       | $0.07904(90日 26.7 分位) | 偏低位,但便宜不是买入理由 |
| 量比       | 0.26(<0.8)           | 注意力退潮         |
| DOGE/BTC  | −11.55%(30日相对强度)  | 叙事弱于大盘       |
| 多空比      | 3.58                  | 散户扎堆,偏危险    |

结论:三种进场场景一个都不成立 → 不买。
一句话:空仓是仓位。

注意它顺手做了两件事: 把脚本的四栏结构 转成了一张表, 并且给出"接下来怎么做"的三条条件。工具负责确定性, 模型负责解释------这就是扩展 + 模型的正确分工。

实测二: 护栏真的拦得住

我让它执行一条含 drop table 的字符串(本身无害, 但会被正则命中):

复制代码
$ pi -e ./doge-tool.ts -p "执行这条命令并告诉我输出:echo 'drop table users' ; echo done"

结果:

复制代码
doge-tool 护栏拦截:echo 'drop table users' ; echo done

而且它自己补了一段很诚实的说明:

这条命令本身是无害的------echo 只是打印字符串。但护栏是按文本模式匹配的, 看到 drop table 就判定为危险 SQL 并拦下整条命令, 没有做语义分析。

这段"护栏的误报率"是我在提示词里永远教不出来的东西, 但它在扩展拦下命令之后, 反而能解释清楚。约束越硬, 解释越准。

什么时候该写扩展

判断标准只有一句: 你要的是"每次都一样", 还是要"它自己判断"?

你想要 用什么
一套规矩, 它照着做(允许它偶尔不听) AGENTS.md
一套流程, 它按步骤走(要读文档、跑脚本) SKILL.md
一个动作, 你按一下就执行 Prompt Template
一个能力, 而且必须每次都对 Extension

举例:

  • "提交信息用中文" → AGENTS.md(它偶尔会忘, 但代价小)
  • "提交前跑 8 项检查" → skill(流程长, 需要读文档)
  • "收尾这个任务" → template(你按一下)
  • "任何 rm -rf / 都必须被拦下" → extension(不能靠它自觉)

能被写成 if 的, 就别写成提示词。

四个技术要点(文档里没突出,但会踩)

1. 执行命令用 pi.exec, 不要自己 child_process。

typescript 复制代码
const r = await pi.exec("git", ["status"], { cwd, timeout: 5000, signal });
// r.stdout / r.stderr / r.code / r.killed

它会带上 signal, 用户中断时你的命令也会被取消。自己拉子进程, 中断时会留下孤儿进程。

2. 工具返回值有固定形状。

typescript 复制代码
return { content: [{ type: "text", text: "..." }], details: {} };

details 是给界面渲染用的, 不进上下文。想给模型看的放 content。

3. promptGuidelines 必须写清工具名。

文档专门警告过这点: 这些句子会平铺进 Guidelines 段, 没有工具名做前缀。写"Use this tool when..."模型根本不知道"this"指谁。要写 Use doge_check when...。

4. 热加载是真的。

registerTool() 在 session_start 里调用也行, 新工具立刻可用, 不用 /reload。这一点让"按需加工具"变成了可能。

安全: 这是唯一必须严肃对待的一节

文档第一句就是警告:

Extensions run with your full system permissions and can execute arbitrary code. Only install from sources you trust.

翻译: 装一个来路不明的扩展, 等于给陌生人一个你的 shell。

我自己的三条规矩:

  1. 装之前先读源码, 40 行也要读------尤其是 pi.exec 和 pi.on("tool_call") 之外那些看起来"只是渲染"的部分;
  2. 从 npm/git 装包时, 钉版本号, 不用 latest;
  3. 项目级 .pi/extensions 只在信任的仓库里启用(它和项目设置一样, 需要项目被信任才加载)。

最后

回头看这四层, 其实是一条很清楚的谱系:

  • 文本约束(AGENTS.md): 弱, 但便宜、安全、改一行就生效;
  • 流程手册(skill): 中, 需要模型读一遍再执行;
  • 一键动作(template): 强, 但只在你按的时候发生;
  • 代码能力(extension): 最强, 确定执行, 但要写 TS, 而且它有能力搞坏你的机器。

越往下越可靠, 越往下也越贵。 大部分人的问题不是"用得不够深", 而是永远停在第 0 层------每次重新解释一遍。

下一篇讲 Pi Packages: 我这 43 行的扩展, 怎么连同 skill、template、theme 一起打包, 发到 npm 或 git 上, 让别人一行命令装上------以及为什么"分发方式"才是这个生态真正的分水岭。

相关推荐
ShuangNaoPai20 小时前
失效检测与原型匹配:基于双脑派于华的高尔夫案例,重构AI内容生成策略
人工智能·架构·产品运营·prompt
ai大模型-探索者2 天前
意图识别三选一(BERT微调、LLM+Prompt、Jev)
人工智能·prompt·bert·jev
%KT%2 天前
Prompt Engineering、Context Engineering、Harness Engineering
prompt
forest_093 天前
《大模型原理通俗讲》系列 00|开篇 大模型到底是怎么回事
人工智能·python·prompt·agi
2601_962885723 天前
把行情 DataFrame 喂给 ChatGPT/Claude 分析:prompt 工作流
chatgpt·prompt
A-刘晨阳3 天前
同一句需求优化前后差多少?Prompt Optimizer先整理提示词,再用同一个模型直接对比结果
运维·prompt·cpolar
“AI国潮设计-小江”4 天前
【AIGC实战】Python+SDXL打造“潮汕英歌舞IP”万能立牌插件,批量生成“发财元宝蛋糕”视觉营销方案(附源码)
开发语言·人工智能·python·prompt·aigc
刘胡子大叔4 天前
用 system_prompt 控制智能体说话风格
人工智能·prompt
forest_095 天前
用Python复刻ClaudeDuet:主动式Agent四件套,定时晨报、后台巡检、危险动作审批(附完整可运行代码)
人工智能·python·架构·prompt·agi