用 Pi 学习 Agent 怎么运行
pi 是学习 harness 的好帮手
想搞懂一个 agent 到底怎么跑起来,最好的办法不是读别人的文章,而是拿一个真正可观察的 agent 自己跑一遍。
agent = model + harness。
- model 做的事很单纯——收到一个
messages列表,返回一段文本。它不知道文件系统在哪、不知道你项目里有什么、不知道上一次对话发生了什么。 - harness(中文是缰绳还是线束什么的,随便把,反正是把模型和外部环境连起来的那一层)是让模型"变聪明"的关键:它把模型包进一个循环,负责拼装上下文、调用工具、把结果塞回下一轮对话。
harness 的核心是一个循环:
- 拼装
system prompt+ 历史消息 → HTTP 请求体 - 发给模型 API,拿到响应
- 解析响应——是普通文本还是 tool call?是 tool call 就执行
- 把执行结果塞进下一轮的
messages,回到第 1 步 - 模型不再调工具,结束
想理解 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
展开后能看到:
- 请求信息:
POST https://api.deepseek.com/chat/completions - 延迟、request id、stop reason、模型实际返回的名字
- 值得注意的响应头(rate limit、retry-after、request-id 之类)
- 完整的 REQUEST JSON(默认隐藏
tools字段避免被几百行 tool schema 撑爆,长字符串超过 50 字符截断) - 完整的 RESPONSE JSON(模型返回的消息原文,含 reasoning / thinking)
→ 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"
}
这里面能看到:
system prompt的完整内容(角色定义、工具列表、技能、项目规则拼接后的最终形态)- 消息历史怎么一轮一轮累积变大(这次只发了
system+ 一条user: hello,是最小形态) - tool 的 schema 是怎么传给模型的(
tools默认折叠,这里是 9 个) - 缓存命中了多少 token、费用是多少(这次
cacheRead: 6400、⚡99%,命中率极高) - 模型什么时候调用工具、什么时候给文字回复
- 支持推理的模型,思考过程会作为独立的
thinkingcontent 返回(reasoning_effort: high)
默认只显示当前 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 来用,是最快的路。
这就是可扩展性的差异。