夜雨聆风学习资料网

ARTICLE · 1102049

Skill、工具还是插件?给 agent 加能力,pi 分成了三层

Skill、工具还是插件?给 agent 加能力,pi 分成了三层
\

这阵子,Skill 有点刷屏。

Claude Code 里装,Codex 里装,做 PPT 的装,写前端的也装。时间线上隔三差五就有人甩出一个 Skill 合集,配一句装上就起飞。

装着装着,一个问题就冒出来了。你想让 agent 多会一件事,比如查公司内部的工单系统,这件事到底该写成一篇 Skill,还是注册成一个工具,还是干脆写个插件改掉 agent 本身的行为?

说实话,挺多人是凭感觉选的。

这是「Pi系列」的第七篇。开源的 pi 在这件事上分得特别干净,今天就拆它怎么让产品长出新器官,以及同一颗内核,怎么住进终端、网页和 Slack 这几个完全不同的壳里。

核心罩在玻璃罩里,只管三件事

01

WAYPOINT

核心只管三件事

EXTENSIONS

一个编程 agent,要加新工具、新命令、新界面,还要接公司内部的模型。如果这些全写进核心,核心迟早会被撑爆。

按我拆下来的理解,pi 的核心大致只干三件事,调模型,跑循环,管状态。第一篇拆过的那个 78 行循环,就是其中的跑循环。除此之外的能力,全部外置,挂在核心外面。

外置的器官分两种,一硬一轻。

02

WAYPOINT

最硬的器官,extension

EXTENSIONS

extension 就是一段 TypeScript。放进 ~/.pi/agent/extensions/ 是全局的,放进项目里的 .pi/extensions/ 只对这个项目生效,改完输入 /reload 就能热加载。

它能干的事多到有点吓人。官方文档里的快速上手例子,一段代码就演示了三种

ts

// 摘自 pi v0.66.0 packages/coding-agent/docs/extensions.md,有删减,排版有压缩

export default function (pi: ExtensionAPI) {

  // 订阅事件,工具调用前先过一眼

  pi.on("tool_call", async (event, ctx) => {

    if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {

      const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");

      if (!ok) return { block: true, reason: "Blocked by user" };

    }

  });

  // 给模型注册一个新工具

  pi.registerTool({

    name: "greet",

    description: "Greet someone by name",

    parameters: Type.Object({ name: Type.String() }),

    async execute(toolCallId, params) {

      return { content: [{ type: "text", text: `Hello, ${params.name}!` }], details: {} };

    },

  });

  // 给人加一个斜杠命令

  pi.registerCommand("hello", {

    description: "Say hello",

    handler: async (args, ctx) => ctx.ui.notify(`Hello ${args || "world"}!`, "info"),

  });

}

第一段订阅了 tool_call 事件,模型想跑 rm -rf,先弹窗问你。第二段给模型注册了一个新工具,名字、说明、参数格式会一起写进提示词,模型就知道自己多了一只手。第三段给人加了一个 /hello 命令。

我数了一下 types.ts 里 pi.on 能订阅的事件,二十多种。会话开始、每轮开始结束、工具调用前后、压缩之前、用户输入进来的那一刻,都能插一脚。有的只是旁观,有的能直接改写结果,比如拦下一次工具调用,或者把用户输入改掉再往下传。除此之外,还能换页脚、加快捷键,甚至接入一家新的模型厂商。

各种器官插在核心周围

好家伙,这已经不是插件了,是半个产品。

03

WAYPOINT

先登记,再干活

EXTENSIONS

能力这么大,边界就得写死。

pi 加载扩展分两步。第一步登记,扩展的代码被执行,报上自己要订阅什么事件、注册什么工具和命令。第二步才是干活,核心准备好了,扩展才能真正发消息、写记录。

如果你在登记阶段就急着发消息,会怎样?看 loader.ts 里这一段

ts

// 摘自 pi v0.66.0 packages/coding-agent/src/core/extensions/loader.ts,有删减

export function createExtensionRuntime(): ExtensionRuntime {

  const notInitialized = () => {

    throw new Error("Extension runtime not initialized. Action methods cannot be called during extension loading.");

  };

  const runtime: ExtensionRuntime = {

    sendMessage: notInitialized,

    sendUserMessage: notInitialized,

    appendEntry: notInitialized,

    // registerTool() is valid during extension load

    ...

  };

}

登记阶段拿到的是一个假的运行时,所有会产生副作用的方法都是占位,一调就抛错。等核心绑定完成,这些占位才被换成真的实现。

先登记,再干活,顺序反了当场报错

当场报错,而不是悄悄失败。我挺喜欢这个选择的。写插件最怕的就是那种没报错但也没生效的情况,你盯着日志找半天,最后发现是时机不对。

另外几条边界也很硬。扩展碰不到第一篇讲的那个循环本身,只能从它留好的钩子插手,想往会话里留点东西,只能通过 appendEntry 追加一条自己的记录,不能回头改历史。能长新器官,但不能换心脏。

04

WAYPOINT

最轻的器官,Skill

EXTENSIONS

另一头是 Skill,轻到一行代码都没有。

一个 Skill 就是一篇带开头信息的 markdown。目录里有 SKILL.md,这个目录就是一个 Skill。开头信息里最重要的是两个字段,name 和 description,一个叫什么,一个什么时候该用。pi 按照 Agent Skills 这个公开规范来实现,所以格式和 Claude Code 那边是通的。

关键在于它怎么进提示词。看 skills.ts 里拼提示词的函数

ts

// 摘自 pi v0.66.0 packages/coding-agent/src/core/skills.ts,有删减

export function formatSkillsForPrompt(skills: Skill[]): string {

    const visibleSkills = skills.filter((s) => !s.disableModelInvocation);

    const lines = [

        "\n\nThe following skills provide specialized instructions for specific tasks.",

        "Use the read tool to load a skill's file when the task matches its description.",

        "",

        "<available_skills>",

    ];

    for (const skill of visibleSkills) {

        lines.push("  <skill>");

        lines.push(`    <name>${escapeXml(skill.name)}</name>`);

        lines.push(`    <description>${escapeXml(skill.description)}</description>`);

        lines.push(`    <location>${escapeXml(skill.filePath)}</location>`);

        lines.push("  </skill>");

    }

    lines.push("</available_skills>");

    return lines.join("\n");

}

提示词里只放名字、用途和文件位置,全文一个字都不放。模型觉得这个任务用得上,再自己用读文件工具把全文读进来。

书架上只贴目录,要用再抽出来

这招叫渐进式披露。你装五十个 Skill,每次对话常驻的也只是五十条目录,不会一上来就把上下文吃掉一大块。

几个细节也挺实用。开头信息里标了 disable-model-invocation: true 的,提示词里根本看不到它,只能你用 /skill:名字 点名调用,适合那种你不想让模型自作主张触发的流程。两个地方有同名的 Skill,先找到的赢,冲突会报一条警告,不会悄悄覆盖。

还有一条我读到时笑了一下。你在 Claude Code 或 Codex 里攒的 Skill,pi 可以直接拿来用,settings 里加两行就行

json

{

  "skills": ["~/.claude/skills", "~/.codex/skills"]

}

当然也得说句公道话。文档里自己承认了,模型不一定每次都会主动去读 Skill 全文,关键流程最好还是用斜杠命令点名。目录再漂亮,也架不住模型偷懒。

05

WAYPOINT

三样东西,到底怎么选

EXTENSIONS

回到开头那个问题。想给 agent 加一个新本事,写成什么?

我自己的判断方法是问一句,这件事改变的是谁。

要是想教模型怎么做一件事,比如你们团队的发版流程、代码评审的规矩、调某个命令行工具的姿势,写成 Skill。它不改 agent,只是给模型一份按需翻阅的说明书,成本最低,Claude Code 和 pi 还能共用。

要是想给模型一只新手,让它能干一件以前干不了的事,比如查内部工单、调公司的接口,而且需要结构化的参数和返回值,注册成工具。代价是工具说明会常驻提示词,所以要少而精。

要是想改 agent 自己的规矩,比如危险命令先弹窗、每轮结束自动打个 git 快照、换一种压缩方式,写成 extension。这些事模型根本不需要知道,它发生在模型看不见的地方。

说明书、新接口、改规矩,各管一层

顺带一提,pi 的 README 里明确写了不内置 MCP。它给的替代方案正好落在这三层里,要么把能力做成带 README 的命令行工具,也就是 Skill 的路子,要么写个 extension 自己把 MCP 接进来。

这些 extension、Skill,还有提示词模板和主题,最后都从全局、项目、npm 包三个来源走同一个加载入口。哪个坏了,只记一条诊断信息,其他的照常加载。测试的时候还能直接把加载结果换掉,比如一键关掉所有 extension,不用去伪造整个文件系统。

06

WAYPOINT

同一颗内核,四个壳

EXTENSIONS

器官长在核心外面,壳也是。

同一个 pi,可以在终端里敲,可以被编辑器插件驱动,可以嵌进网页,还能住进 Slack。这些都不是另写了一个 agent。

终端这个壳,pi 没用现成框架,自己写了一个叫 pi-tui 的包。每个组件只需要实现一个 render(width),给它一个宽度,它交回几行字,刷新时只重画变了的部分。

第二个壳没有界面,叫 RPC 模式。运行 pi --mode rpc,别的程序往它的标准输入里一行写一条 JSON 命令,它在标准输出里一行回一个事件。不用端口,父进程一退出,会话自然结束,编辑器插件和自动化脚本都能这样驱动它。如果你本来就在写 Node 程序,还可以直接把 AgentSession 当库导入,连子进程都省了。

第三个壳在浏览器里。pi-web-ui 是一组网页组件,其中的 AgentInterface 接收的就是 agent 核心里那个 Agent 对象,界面自己画,模型接入和 agent 用的是同一套。

第四个壳叫 mom,住在 Slack 里。它直接依赖终端版的编程 agent 包,会话存档、压缩都是现成的,换掉的是界面和工具。工具执行的细节发在消息的回复线程里,主消息保持干净,官方还推荐把它整个关进 Docker 容器里跑。

四个壳,里面是同一颗内核

07

WAYPOINT

代价

EXTENSIONS

代价也写得很清楚。

extension 和核心跑在同一个进程里,没有沙箱。文档在安装位置那一节就写了,扩展拥有你的全部系统权限,只装信得过的。一个写坏的扩展,能直接拖垮整个程序。Skill 也一样,它能指挥模型做任何事,装之前最好读一遍。

四个壳也各得写一套界面和交互,壳越多,要维护的界面就越多。

换来的,是一个小而稳的核心,和一个能不断长出新器官、住进新地方的产品。

回到开头那股 Skill 热。装 Skill 很爽,但它只是三层里最轻的那一层。你下次想给 agent 加一个本事,不妨先问一句,这件事改变的是模型的做法,是模型的手,还是 agent 自己的规矩。

下一篇是这个系列的最后一篇。pi 的 README 里有一长串它故意不做的东西,没有子 agent,没有计划模式,没有权限弹窗。不做什么,往往比做什么更难。

你现在装的 Skill 里,有没有哪个其实更适合写成工具或者插件?

这个系列的内容,我还做成了一套彩铅动画,13 集,每集两三分钟,一集讲透一个设计决定。今天这篇对应第 10 集和第 12 集,器官怎么长、壳怎么换,动起来看会更直观。

公众号后台回复「pi」,获取课程地址,预告和前两集可以免费看。

本文基于 pi v0.66.0 源码,新版本如有变化,以源码为准。讲解脉络参考了开源书《pi 的设计艺术》,想系统深入的推荐直接读原书。

相关学习资料