第 09 章:进阶 —— 让 agent 长成你的样子

mini-pi 已经完成。这一章不写大功能,而是给你一套"继续生长"的地图:三个最常用的扩展手法(自定义工具、工具钩子、上下文变换)、pi 的扩展哲学,以及接下来该读什么。

手法一:自定义工具

在 pi 栈上,工具就是一个普通对象:名字、行为导向的描述、TypeBox 参数 schema、execute 函数。给第 08 章的 mini-pi 加一个工具只需几行:

加一个工具(基于 code/mini-pi/src/main.ts)
import { Type } from "@earendil-works/pi-ai";

const timeTool = {
  name: "get_time",
  label: "Get Time",
  description: "Get the current date and time",
  parameters: Type.Object({
    timezone: Type.Optional(Type.String({ description: "IANA timezone, e.g. Asia/Shanghai" })),
  }),
  execute: async (_toolCallId: string, args: { timezone?: string }) => {
    const text = new Date().toLocaleString("en-US", { timeZone: args.timezone ?? "Asia/Shanghai" });
    return {
      content: [{ type: "text" as const, text }], // 给 LLM 看的内容
      details: { timezone: args.timezone }, // 给 UI 看的结构化数据(可选)
    };
  },
};

agent.state.tools = [...agent.state.tools, timeTool];

回顾你在第 02–03 章学到的东西,这里没有任何新魔法:schema 负责让模型"会调",execute 负责执行,返回值分"给模型的 content"和"给 UI 的 details"两层——后者是 pi-ai 的独到设计,让你不用解析文本就能渲染界面(见设计思想:LLM 抽象层)。

工具的失败约定也和老朋友一致:失败就 throw,agent 会捕获并以 isError 回喂模型。

手法二:工具钩子 —— 权限门与结果改写

Agent 在每个工具调用的前后都留了钩子,这是不修改 loop 就能改变行为的关键扩展点:

一个 10 行的权限门
agent.beforeToolCall = async ({ toolCall, args }) => {
  if (toolCall.name === "bash" && /\brm\s+-rf\s+(\/|~|\$HOME)/.test(String(args.command))) {
    return { block: true, reason: "Blocked dangerous command by mini-pi policy" };
  }
  // 返回 undefined = 放行
};

block 的调用不会执行,原因文本会作为工具错误回给模型——它会看到"被策略拦截"并换方案。afterToolCall 则相反:在执行后改写结果、追加审计信息,或返回 terminate: true 让 loop 提前收尾。pi 把这类能力做到了扩展系统里,examples/extensions/permission-gate.tsprotected-paths.ts 就是完整的生产级例子。

关于权限的真实边界

pi 默认 YOLO,不是不知道风险,而是认为弹窗确认挡不住能写代码又能跑代码的 agent(论证见设计思想:"不做"清单)。钩子是"策略",不是"防线";真正的边界是容器/沙箱。两者按需组合。

手法三:上下文变换

每个 turn 调 LLM 之前,transformContext 允许你改写即将发送的消息列表——上下文工程的官方入口:

const agent = new Agent({
  // ……
  transformContext: async (messages, signal) => {
    // 例:超过 50 条时,把最旧的用户消息折叠成一条备注
    if (messages.length <= 50) return messages;
    return [
      { role: "user", content: "[earlier messages elided by custom policy]", timestamp: Date.now() },
      ...messages.slice(-50),
    ];
  },
});

第 07 章的压缩、pi 的自定义摘要(custom-compaction.ts 示例)、往上下文里注入动态信息(时间、git 状态),都发生在这个点上。

pi 的扩展哲学:为什么核心什么都不内置

至此你看完了 mini-pi 的全部,回头看 pi 的产品形态就顺理成章了。pi 核心刻意不内置 plan mode、sub-agent、todo 等功能,而是提供四个扩展面:

  • Extensions:TypeScript 模块,可注册工具/命令/快捷键/事件钩子/UI 组件,放在 ~/.pi/agent/extensions/ 或项目 .pi/extensions/。plan mode、sub-agent 在社区看来该是"核心功能"的东西,在 pi 这里是 examples/extensions/ 下的示例——plan-modesubagent 都值得一读,它们是你写复杂扩展的最好教材。
  • Skills:遵循 Agent Skills 标准 的按需能力包——一个 SKILL.md(可附带脚本、参考文档),模型需要时才加载,不占常驻上下文。这是 pi 对 MCP 的回答:渐进式披露。
  • Prompt templates:带参数的可复用提示词 markdown,/name 展开。
  • Themes:热重载的界面主题。

这四个面共享一个思想:能力以文件形式存在、按需加载、可以用 git/npm 分享(pi packages)。你自己的 agent 长到一定复杂度后,也会需要同样的一课:不要把所有能力焊死在核心里。

接下来的路

  1. 把 mini-pi 用烂:给它加会话恢复(裸写版已有)、加 steering 队列(agent.steer())、把 REPL 换成你喜欢的外壳。真正的理解来自使用中的磕碰。
  2. 源码导读读 pi 源码:agent-loop.ts → 工具 → system-prompt.tssession-manager.tsagent-session.ts。有了本教程的底子,每一行你都知道为什么在那里。
  3. 读作者的构建手记原文:设计决策的第一手论证,比任何转述都精彩。
  4. 给 pi 写一个扩展并分享:pi packages 可以通过 npm/git 分发,这是检验你是否真的掌握了这套哲学的最好方式。

结课语

回顾这十章:从一次 fetch 开始,我们亲手实现了 loop、四大工具、流式与中断、系统提示词、会话树、压缩,再用 pi 的官方积木重构——你已经拥有了一个 coding agent 的全部骨架,并且理解每一块的"为什么"。

pi 教给我们的最重要一课不是某个具体实现,而是一种判断力:agent 的威力来自模型本身,harness 的职责是管好上下文、提供恰好的工具、不挡路。剩下的,交给你和你的 agent 了。

源码导读:源码地图 →