用 Pi 学习 Agent 怎么运行

pi 是学习 harness 的好帮手

想搞懂一个 agent 到底怎么跑起来,最好的办法不是读别人的文章,而是拿一个真正可观察的 agent 自己跑一遍。

agent = model + harness。

harness 的核心是一个循环:

  1. 拼装 system prompt + 历史消息 → HTTP 请求体
  2. 发给模型 API,拿到响应
  3. 解析响应——是普通文本还是 tool call?是 tool call 就执行
  4. 把执行结果塞进下一轮的 messages,回到第 1 步
  5. 模型不再调工具,结束

想理解 agent,就得理解这个循环。而 pi 是学这个最好的工具。

例如可以让它帮你理解 harness 过程,因为它极度透明

大多数 agent 工具把和模型的对话过程藏起来,你只能看到最终输出的文本。pi 不一样:它把整个生命周期都暴露成事件——session 启动、消息拼装、API 请求发出去之前、响应回来之后、工具执行……每一个节点你都能插手。

换句话说,harness 在 pi 里不是黑盒,而是一组你能监听的事件。理解 agent 怎么运行,本质上就是理解 harness 在那个循环里做了什么——而 pi 让你直接看见它,甚至改写它。

比如我们可以让它展示在请求大模型时发送的 HTTP 内容

一个直接的例子:让 pi 展示它发给大模型 API 的完整请求。我写了一个 extension model-api-inspector 做这件事——监听 provider 的请求 / 响应事件,把每次调用的 request body 和 response body 都渲染到终端里:

// 请求发出前,记录完整的 payload
pi.on("before_provider_request", (event, ctx) => {
  // event.payload 就是发送给 API 的完整请求体
  // ctx.model 里有 provider / id / baseUrl / api
});

// 响应首包到达,记录 status 和 headers
pi.on("after_provider_response", (event) => {
  // event.status, event.headers
});

// LLM 一整段消息结束,记录 usage + 完整响应
pi.on("message_end", (event) => {
  // event.message 是模型返回的完整消息
  // usage 里包含 input / output / cacheRead / cost
});

// 新一轮对话开始,清理上一轮记录
pi.on("before_agent_start", () => { /* 新建一个 inspector 面板 */ });

这样你就能看到模型实际收到的东西:system 消息是怎么拼出来的、历史消息怎么累积、tools 数组里每个工具的 schema 长什么样、缓存命中率是多少、每次调用花了多少 token 和钱。

这个插件的效果

折叠时,每次调用是一行密集摘要:

API deepseek-cmic/deepseek-v4-pro 200 1.62s ↑6.5k ↓35 ⚡99% $0 10:30:48 PM

展开后能看到:

→ REQUEST ─────────────────────────────────────
{
  "model": "deepseek-v4-pro",
  "messages": [
    { "role": "system", "content": "You are an expert coding assistant operating insid…" },
    { "role": "user",   "content": [{ "type": "text", "text": "hello" }] }
  ],
  "stream": true,
  "stream_options": { "include_usage": true },
  "max_completion_tokens": 384000,
  "tools": "[9 items hidden]",
  "thinking": { "type": "enabled" },
  "reasoning_effort": "high"
}

← RESPONSE ────────────────────────────────────
{
  "role": "assistant",
  "content": [
    { "type": "thinking", "thinking": "The user is just saying hello. I should greet them…", "thinkingSignature": "reasoning_content" },
    { "type": "text",     "text": "Hi! How can I help you today?" }
  ],
  "api": "openai-completions",
  "provider": "deepseek",
  "model": "deepseek-v4-pro",
  "usage": {
    "input": 82, "output": 35, "cacheRead": 6400, "cacheWrite": 0,
    "reasoning": 25, "totalTokens": 6517, "cost": { "total": 0 }
  },
  "stopReason": "stop"
}

这里面能看到:

默认只显示当前 turn 最近 5 次调用,避免跑长任务时刷屏。开着它跑几个任务,上下文怎么膨胀的、缓存命中率怎么掉的、模型到底"看"到了什么,都一目了然。

有别的想了解的,也可以用同样的方式学

这种"写个 extension 把内部的某个环节暴露出来"的思路,不止能用在 API 调用上。比如我还写了一个展示 system prompt 的 show-system-prompt——监听 before_agent_start 拿到 event.systemPrompt(这是所有 extension 修改之后、真正发送前的最终版本),然后用一个全屏 TUI 把它打印出来。

pi.on("before_agent_start", (event) => {
  lastPrompt = event.systemPrompt;  // 真正发给模型的 system prompt
});

pi.registerCommand("system-prompt", {
  description: "查看当前 session 的完整 system prompt",
  handler: async (_args, ctx) => {
    // 用 ctx.ui.custom() 全屏展示 lastPrompt,按 Esc/q 退出
  },
});

输入 /system-prompt 就把 pi 当前拼装好的完整 system prompt 全屏展示出来——能直接看到 AGENTS.md、技能列表、项目上下文是怎么一层层叠上去的最终结果。

同样的套路可以用在任意你想观察的环节:工具调用前后的参数(tool_call)、每次 turn 的 token 用量、被压缩(compaction)之前和之后的历史差异(before_compaction)、上下文过滤掉了什么(context)、分支系统怎么工作(session_before_fork)…… pi 给每个环节都留了事件口,hook 进去就能看到内部数据。本质上是把 pi 当成一个"agent 运行过程的可视化平台"。

已经发布到 npm,可以直接装

我把 model-api-inspector 和 show-system-prompt 整理后发到了 npm,包名叫 pi-dev-inspector:

pi install npm:pi-dev-inspector
# 装完 /reload 即可用

装好之后每个 turn 都会在终端里渲染 API 调用的明细,并多出 /system-prompt 命令。不用自己写代码,纯本地运行。

这些插件可以让 pi 自己写

这个 extension 是我让 pi 自己写出的。你可以按照上面的命令直接装,也可以让你自己的 pi 写一个

这本身也印证了前面说的:pi 的扩展体系足够透明,能够自举,它能理解自己的内部事件,并写出观察自己的工具。这也是 pi 适合学习 agent 内部的原因,你能把自己的工具需求描述给 pi,让它生成 extension 代码,装上立刻看效果,不满意继续改。学的过程中你既是开发者又是观察者。

这种透明度和可扩展性是 claude code 没有的

claude code 也是 agent,但它的内部太多的不透明。你只能看到输出——模型回了什么、执行了什么命令。claude code 的扩展方式是你写 .claude/commands 和 MCP server,本质是在 agent 外面挂工具。pi 的 extension 是直接插进 agent 循环里的,你能改的不是"能用什么工具",而是"agent 怎么运行"。想搞懂 agent 怎么运行,先拿一个能让你看见自己怎么运行的 agent 来用,是最快的路。

这就是可扩展性的差异。