写在前面:当所有人都在造"全家桶",有人选择造"乐高"
2026 年的 AI Agent 赛道,卷得已经没法看了。每周都有新的 Agent 框架发布,每个都号称"生产就绪"、"企业级"、"下一代"。打开这些项目的仓库,扑面而来的是几百个配置项、十几层抽象、一份读不完的文档,以及一个动辄几千 token 的 System Prompt。
然后你会看到 badlogic/pi-mono——一个 TypeScript Monorepo,7 个包,核心编码 Agent 的 System Prompt 不到 1000 token,累计拿下 4 万+ Star,并且成为爆火的 OpenClaw(对,就是那个小龙虾)背后真正干活的 Coding Agent 引擎。
它的作者是 badlogic——如果你写过游戏,大概率认识这个 ID:libGDX 游戏框架的作者。一个在 Java 游戏引擎领域深耕十几年的老兵,转身进入 AI Agent 领域,做出来的东西和市面上的主流产品气质完全不同。
这篇文章我会把 pi-mono 拆开揉碎讲清楚:
- 它的 Monorepo 里 7 个包分别是什么、怎么协作
- 统一 LLM API 层(pi-ai)是怎么抹平 20+ Provider 差异的
- Pi 这个编码 Agent 为什么敢用不到 1000 token 的 System Prompt
- "学徒模式"这个设计哲学到底解决了什么问题
- 上手实战:从安装到自定义工具、接入自己的模型
- 它和 Claude Code、Codex CLI 的架构差异,以及你该选哪个
字数不少,建议先收藏。但我保证每一节都有干货,不灌水。
一、背景:为什么 Coding Agent 需要一个"底座"
1.1 Harness 比模型更重要
先说一个很多人没意识到的事实:决定 Coding Agent 体验的,不只是模型,更是模型外面那层"挽具"(Harness)。
同样是 Claude 或 GPT 系列模型,为什么不同的 Coding Agent 用起来差别巨大?因为:
- 工具怎么给(给多少个工具?工具描述写多细?)
- 权限怎么管(哪些命令自动执行?哪些要确认?)
- 上下文怎么压缩(长对话怎么办?超出窗口怎么办?)
- 失败怎么恢复(命令报错后 Agent 怎么自我修正?)
- 输出怎么展示(diff 怎么渲染?流式输出怎么处理?)
这些都不是模型的事,是 Harness 的事。Harness 这个词很形象——模型是野马,Harness 是挽具,挽具的设计决定了这匹马能不能拉好车。
问题在于:市面上每个 Coding Agent 都自己造一套 Harness,而且都是封闭的。Claude Code 的 Harness 你拿不出来单独用,Codex CLI 的工具层你没法嫁接到自己的 Agent 上。想自己造一个 Agent?对不起,从 LLM API 适配开始,全部重写一遍。
pi-mono 做的事情,就是把 Harness 的每一层都拆成独立的包,全部开源,让你像搭乐高一样组装自己的 Agent。
1.2 作者的"游戏引擎思维"
badlogic 做 libGDX 的年代,游戏引擎领域也有类似的问题:Unity 什么都给你,但你被锁死在它的工作流里;自己从 OpenGL 写起,又要处理无数跨平台的脏活。libGDX 的答案是:提供一组正交的、可单独使用的模块(图形、音频、输入、文件 IO),你可以全用,也可以只用其中一块。
pi-mono 是同一个思路在 AI Agent 领域的复刻。这不是巧合,是工程品味的延续。
二、架构总览:7 个包的 Monorepo
pi-mono 是一个 TypeScript Monorepo(仓库地址 github.com/badlogic/pi-mono,官网 pi.dev),核心包结构如下:
pi-mono/
├── packages/
│ ├── ai/ # pi-ai: 统一 LLM API 层
│ ├── agent/ # pi-agent: Agent 运行时(状态机 + 工具循环)
│ ├── coding-agent/ # pi: 终端编码 Agent(旗舰产品)
│ ├── tui/ # 终端 UI 组件库
│ ├── web-ui/ # Web 聊天组件(可嵌入任何前端)
│ ├── slack-bot/ # Slack 机器人接入层
│ └── pods/ # vLLM GPU Pod 管理(自托管模型部署)
依赖关系是严格单向的:
pods(部署层)
↑
ai(模型访问层)
↑
agent(运行时层)
↑
coding-agent / slack-bot(应用层)
↑
tui / web-ui(展示层)
这个分层值得细品:每一层只依赖下一层,且每一层都可以被单独替换或单独使用。你可以只用 pi-ai 做多模型调用,不碰 Agent;也可以用 pi-agent 造自己的 Agent,UI 完全自己写。
下面逐层拆解。
三、pi-ai:一层薄薄的统一 LLM API
3.1 它解决什么问题
写过多模型应用的人都知道这个痛点:OpenAI、Anthropic、Google、xAI、Groq……每家的 API 格式都不一样。消息结构不同、工具调用格式不同、流式协议不同、思维链(reasoning)字段不同、Token 计费字段更是五花八门。
社区里已有的方案(比如各种 "gateway" 和 "router" 项目)大多走"代理服务"路线:起一个中间服务,把所有请求翻译成 OpenAI 格式。这样做的问题是:
- 有损转换——OpenAI 格式是最小公倍数,Anthropic 的 cache control、Google 的 context caching 这些特性会在转换中丢失
- 多一跳网络——延迟、部署、维护成本
- 流式细节被抹平——不同 Provider 的 SSE 事件粒度不同,代理层往往只能取交集
pi-ai 走的是"库"路线:不起服务,直接在你的进程里做适配,而且不做有损转换——它定义了一套超集类型,每个 Provider 的独有特性都保留。
3.2 API 设计
核心 API 就一个函数族,签名大致长这样:
import { complete, stream } from "@mariozechner/pi-ai";
// 非流式
const result = await complete({
model: "anthropic:claude-sonnet-4-5",
messages: [
{ role: "user", content: "解释一下 TCP 慢启动" }
],
tools: [/* ... */],
});
// 流式
for await (const event of stream({
model: "openai:gpt-5.2",
messages,
})) {
switch (event.type) {
case "text_delta":
process.stdout.write(event.delta);
break;
case "tool_call":
// 完整的工具调用对象
break;
case "reasoning_delta":
// 思维链增量(支持的模型才有)
break;
case "usage":
// 统一的 token 用量,含 cache 命中细节
break;
}
}
切换模型只改一个字符串:"anthropic:claude-sonnet-4-5" 换成 "google:gemini-3-pro",业务逻辑零改动。支持 20+ Provider,包括 OpenRouter 这种聚合商——而且值得一提的是,它对 OpenRouter 的 cached token 语义做了专门保留(这是 2026 年 5 月一个社区 PR 修的,连这种细节都有人盯,可见社区活跃度)。
3.3 类型系统的巧思
pi-ai 用 TypeScript 的条件类型做了一件很漂亮的事:模型字符串决定返回类型。
// reasoning 字段只在支持思维链的模型上存在
const r1 = await complete({ model: "openai:gpt-5.2", ... });
r1.reasoning; // ✅ 类型存在
const r2 = await complete({ model: "some-basic-model", ... });
r2.reasoning; // ❌ 编译期报错
这意味着很多"运行时才会发现的坑"被挪到了编译期。相比之下,大多数网关类方案返回的是 any 味儿很重的宽松对象,你永远不知道哪个字段在哪个模型下是 undefined。
3.4 为什么"库"比"代理"更适合 Agent 场景
这里有个我自己的观点:Agent 场景下,LLM 调用层必须是库,不能是代理服务。
原因是 Agent 的工具循环对流式事件的粒度极其敏感。一个编码 Agent 需要在"模型开始调用工具"的瞬间就开始渲染 UI(哪怕参数还没生成完),需要精确知道 cache 命中率来优化上下文排布,需要在流中断时精确恢复。这些细节一旦经过代理层的"翻译",损失的信息很难补回来。
pi-ai 的存在,让 pi-mono 上层的 Agent 运行时能拿到每个 Provider 的原始事件粒度。这是后面 Pi 体验流畅的地基。
四、pi-agent:一个刻意"无聊"的运行时
4.1 Agent 运行时的本质是什么
剥掉所有营销话术,一个 Agent 运行时的本质就是这个循环:
while (true) {
response = await llm(messages, tools)
if (response.没有工具调用) break
for (call of response.工具调用列表) {
result = await 执行工具(call)
messages.push(工具结果(result))
}
}
就这么多。所有 Agent 框架,剥到最后都是这个 while 循环。区别在于各家往这个循环上叠了多少东西:记忆系统、规划模块、多 Agent 编排、DAG 工作流、向量检索……
pi-agent 的选择是:几乎什么都不叠。它提供:
- 一个类型安全的工具定义接口
- 上面那个循环的健壮实现(含错误重试、流中断恢复、并发工具调用)
- 会话状态的序列化/反序列化(JSONL 格式,每行一个事件)
- 事件订阅机制(UI 层靠这个渲染)
没有内置记忆、没有内置 RAG、没有多 Agent 编排。这些它认为都是应用层的事。
4.2 工具定义:TypeBox 而不是 JSON Schema 手写
import { defineTool } from "@mariozechner/pi-agent";
import { Type } from "@sinclair/typebox";
const readFileTool = defineTool({
name: "read_file",
description: "Read a file from the local filesystem",
parameters: Type.Object({
path: Type.String({ description: "Absolute file path" }),
offset: Type.Optional(Type.Number()),
limit: Type.Optional(Type.Number()),
}),
async execute({ path, offset, limit }) {
// 参数已经过运行时校验,类型完全推导
const content = await fs.readFile(path, "utf-8");
return { output: sliceLines(content, offset, limit) };
},
});
TypeBox 的好处是一份定义同时产出三样东西:TS 类型(编译期)、JSON Schema(发给 LLM)、运行时校验器(防止模型幻觉出非法参数)。模型给的参数不合法时,运行时会把校验错误作为工具结果返回给模型,让它自己修正——这是 Agent 健壮性的关键一环,很多自己手搓 Agent 的人会漏掉这步,导致一次参数幻觉就整个循环崩掉。
4.3 会话即 JSONL:简单到令人发指的状态管理
pi-agent 的会话存储就是一个 JSONL 文件,每行一个事件:
{"type":"user_message","content":"帮我修一下这个测试","ts":1753650000}
{"type":"assistant_message","content":"我先看看测试文件","ts":1753650003}
{"type":"tool_call","name":"read_file","args":{"path":"/src/foo.test.ts"},"ts":1753650004}
{"type":"tool_result","output":"...","ts":1753650004}
没有数据库、没有向量存储、没有专有格式。想恢复会话?逐行读回来重建 messages 数组。想分析 Agent 行为?grep + jq 直接上。想迁移?拷文件。
这种设计我称之为"可 grep 性"(grepability)——一个系统的内部状态能不能用最普通的 Unix 工具检查,是衡量它工程品味的重要指标。对比某些把会话存进加密 SQLite + 自定义二进制格式的竞品,出问题时的可调试性天差地别。
五、Pi:不到 1000 token 的 System Prompt,凭什么能打
5.1 旗舰产品登场
@mariozechner/pi-coding-agent,命令行里就叫 pi,是 pi-mono 的旗舰应用:一个跑在终端里的编码 Agent。读文件、写文件、改代码、跑命令,Coding Agent 该有的它都有。
但它最出名的特征是:System Prompt 不到 1000 token。
作为对比(社区逆向的数据,量级可信):
| Agent | System Prompt 规模 |
|---|---|
| Claude Code | ~10,000+ token |
| Codex CLI | 数千 token |
| 某些企业级 Agent | 20,000+ token |
| Pi | < 1,000 token |
5.2 短 Prompt 背后的三个赌注
这不是偷懒,是三个深思熟虑的赌注:
赌注一:模型已经知道怎么编程了。
2026 年的前沿模型,训练数据里塞满了代码和终端操作。你不需要在 System Prompt 里教它"修改文件前请先阅读文件"、"请遵循项目现有的代码风格"——它知道。长 Prompt 里的大部分指令,是在对抗两年前模型的缺陷,而这些缺陷已经不存在了。
赌注二:上下文窗口是最贵的资源。
每一个 System Prompt token,都在你会话的每一轮重复计费,并且挤占真正有用的上下文(你的代码、你的报错信息)。10,000 token 的 System Prompt 意味着:100 轮对话下来,你为"说明书"支付了一百万 token 的开销,而这些 token 一个字都没变。
赌注三:项目特定的知识应该来自项目,而不是厂商。
Pi 会读取项目目录下的 AGENTS.md(社区标准),你的项目约定写在你的仓库里。厂商的 System Prompt 不该假设你的工作流。
5.3 "学徒"而不是"万事通"
这是 Pi 设计哲学里我最欣赏的部分。主流 Coding Agent 的隐含定位是"万事通":厂商预设好一切,你只管提需求。Pi 的定位是"学徒"(apprentice):
- 它出厂时几乎是"素"的,只有最基本的工具和极简的指令
- 你通过项目里的
AGENTS.md、自定义工具、自定义命令逐步教会它你的工作方式 - 你教它的东西以文件形式存在你的仓库里,可以 git commit、可以 code review、可以在团队内共享
换句话说:万事通模式下,能力属于厂商;学徒模式下,能力属于你,而且可以版本化。
有意思的是,这个思路和最近爆火的 mattpocock/skills(把方法论写成 Markdown 技能文件)殊途同归——2026 年 AI 工程的一条主线愈发清晰:把知识从 Prompt 黑盒里解放出来,放进 git 仓库。
5.4 扩展机制实战
Pi 的扩展是真·代码级的。举个例子,给 Pi 加一个查询公司内部服务状态的工具:
// .pi/tools/service-status.ts
import { defineTool } from "@mariozechner/pi-agent";
import { Type } from "@sinclair/typebox";
export default defineTool({
name: "service_status",
description: "查询内部服务的健康状态和最近部署记录",
parameters: Type.Object({
service: Type.String({ description: "服务名,如 payment-gateway" }),
}),
async execute({ service }) {
const res = await fetch(
`https://ops.internal/api/v1/services/${service}/status`,
{ headers: { Authorization: `Bearer ${process.env.OPS_TOKEN}` } }
);
if (!res.ok) return { output: `查询失败: HTTP ${res.status}`, isError: true };
const data = await res.json();
return {
output: [
`状态: ${data.health}`,
`最近部署: ${data.lastDeploy.version} @ ${data.lastDeploy.time}`,
`错误率(5m): ${data.errorRate5m}%`,
].join("\n"),
};
},
});
放进项目的 .pi/tools/ 目录,Pi 启动时自动加载。之后你在终端里问"payment-gateway 现在正常吗,是不是刚发过版本",它就会调这个工具,把结果和它看到的代码、日志综合起来回答。
注意这里没有 MCP Server、没有单独的进程、没有 JSON-RPC 握手——就是一个 TypeScript 文件。对于"工具和 Agent 跑在同一台机器上"的场景(编码 Agent 的绝大多数场景),进程内工具比 MCP 简单一个数量级。当然 Pi 也支持 MCP,用于接第三方服务,两条路不冲突。
5.5 自定义命令与会话工作流
除了工具,Pi 还支持自定义斜杠命令。比如团队常用的"提交前检查"流程:
<!-- .pi/commands/precommit.md -->
执行提交前检查流程:
1. 运行 `pnpm lint` 和 `pnpm test`,如有失败先修复
2. 检查是否有遗留的 console.log 和 TODO
3. 用 conventional commits 格式生成提交信息,列出变更摘要
4. 不要自动 push,把 commit 命令展示给我确认
之后 /precommit 一下,整套流程自动跑。这个文件进 git,全组共享,新人入职第一天就能用上老司机的工作流。
六、TUI 与 Web UI:被严重低估的两个包
6.1 pi-tui:终端 UI 的脏活有人替你干了
做过终端应用的都知道,终端 UI 是天坑:ANSI 转义序列、宽字符(CJK!)测量、括号粘贴模式、终端尺寸变化、Windows Terminal 和 iTerm2 的行为差异……
pi-tui 把这些封装成了组件库:可编辑的多行输入框(带语法高亮)、流式 Markdown 渲染器、diff 视图、可折叠面板。Pi 自己的界面就是用它搭的,但它是独立包,你自己的 CLI 工具也能用。
中文用户尤其要感谢它对 CJK 宽字符的处理——多少终端工具在中文输入时光标乱飞,pi-tui 在这块做对了。
6.2 pi-web-ui:把 Agent 嵌进任何网页
pi-web-ui 提供一组 Web Components(框架无关,React/Vue/原生都能用),包含聊天界面、工具调用的可视化渲染、Artifact 预览。配合 pi-agent 的事件流,几十行代码就能把一个完整的 Agent 聊天界面嵌进你的内部系统:
import { AgentInterface } from "@mariozechner/pi-web-ui";
import { Agent } from "@mariozechner/pi-agent";
const agent = new Agent({
model: "anthropic:claude-sonnet-4-5",
tools: [/* 你的业务工具 */],
});
const ui = new AgentInterface({ agent });
document.querySelector("#chat")!.appendChild(ui);
对做企业内部工具的人来说,这个包省掉的可能是几周的前端工作量。
6.3 pods:自托管模型的最后一公里
pi-mono 里最"偏门"但很有意思的包:vLLM GPU Pod 管理。一行命令在你的 GPU 服务器(或租用的 GPU 云)上拉起 vLLM 实例,跑开源模型,然后通过 pi-ai 的统一接口调用——和调 OpenAI 一模一样的代码。
# 在 GPU 机器上拉起一个 Qwen 编码模型
pi pods start --model Qwen/Qwen3-Coder-480B-A35B --gpus 8
# 本地 Pi 直接用它
pi --model pods:qwen3-coder "重构这个模块"
从商业模型到自托管开源模型的迁移路径被打通了。对在意数据不出内网的团队,这是从"能用 Agent"到"敢用 Agent"的关键一步。
七、横向对比:Pi vs Claude Code vs Codex CLI
| 维度 | Pi (pi-mono) | Claude Code | Codex CLI |
|---|---|---|---|
| 开源程度 | 全栈开源,7 个包可单独用 | 客户端开源,Harness 逻辑深度绑定 Anthropic | 开源 |
| 模型支持 | 20+ Provider + 自托管 vLLM | Anthropic 系为主 | OpenAI 系为主 |
| System Prompt | < 1000 token | ~10k token | 数千 token |
| 扩展方式 | 进程内 TS 工具 + MCP + 命令文件 | MCP + Hooks + Skills | MCP + 配置 |
| 状态存储 | JSONL 明文,可 grep | 专有格式 | 会话文件 |
| 可嵌入性 | agent/tui/web-ui 均为独立库 | 不可拆 | 部分可拆 |
| 定位 | 学徒/底座 | 万事通/产品 | 万事通/产品 |
我的选择建议,直说:
- 只想要开箱即用、公司买了 Claude 订阅:Claude Code,产品打磨确实好
- 要构建自己的 Agent 产品/内部工具:pi-mono,没有第二个选项能给你这么全的乐高块
- 多模型混用、成本敏感、或需要自托管:Pi,pi-ai 的多 Provider 支持和 pods 是刚需级优势
- 想深入理解 Coding Agent 原理:读 pi-mono 源码。它的代码量是几个主流 Agent 里最小的,抽象层次最少,是最好的教材。OpenClaw 选它做引擎,很大程度上就是因为这份可读性和可控性
八、生产使用的三个坑(踩过才知道)
坑一:极简 Prompt 依赖强模型。 Pi 的短 Prompt 哲学建立在"模型足够聪明"的前提上。如果你接的是较弱的开源模型(尤其 30B 以下),会明显感觉它"不够主动"——不会自发地先读文件再改、不会主动跑测试验证。解法:针对弱模型,在 AGENTS.md 里把工作流写细,等于把厂商 Prompt 的活儿自己干了,但至少你有地方干。
坑二:进程内工具的安全边界。 .pi/tools/ 下的 TypeScript 工具跑在 Agent 同一进程,权限等同于你的 shell。克隆了别人的仓库就直接跑 Pi,等于执行了仓库里的任意代码。解法:对不信任的仓库,先检查 .pi/ 目录,或在容器/沙箱里跑。这个问题 MCP 同样有,只是 pi 的加载路径更隐蔽一些。
坑三:JSONL 会话文件会膨胀。 长会话 + 大文件读取,JSONL 文件轻松上百 MB。Pi 有上下文压缩机制,但磁盘上的原始事件流不会自动清理。解法:定期归档 ~/.pi/sessions/,或者写个 cron 清理 30 天前的会话——顺便,这种清理脚本让 Pi 自己写就行,一分钟的事。
九、总结:AI 工程正在回归 Unix 哲学
pi-mono 值得关注,不只因为它好用,更因为它代表了一种正在回潮的工程观:
- 做库,不做平台。 平台锁定你,库服务你。
- 每层可拆、可换、可单独用。 正交性是复用的前提。
- 状态明文化、可 grep。 调试性是第一等公民。
- Prompt 极简,知识进仓库。 能力应该版本化、可 review、属于用户。
- 相信模型,但校验一切。 参数校验、错误反馈回路,健壮性来自工程而不是祈祷。
这五条,去掉 AI 语境,就是 Unix 哲学和优秀开源库的通用准则。AI Agent 领域狂奔了三年之后,最能打的项目反而是把三十年前的老规矩认真执行了一遍的那个——这件事本身,比任何新概念都更值得玩味。
4 万 Star 不是给"又一个 Agent 框架"的,是给"终于有人把地基打对了"的。
如果你只有十分钟:npm install -g @mariozechner/pi-coding-agent,在一个你熟悉的项目里跑一次 pi,感受一下不到 1000 token 的 System Prompt 能干什么。然后你大概率会回来把这篇文章再读一遍。
参考资料:badlogic/pi-mono 仓库与官网 pi.dev、AI Engineer London 相关分享、社区对各 Coding Agent System Prompt 的逆向分析、OpenClaw 项目文档。