编程 pi-mono 深度拆解:libGDX 作者的 4 万 Star AI Agent 全家桶,不到 1000 token 的 System Prompt 凭什么成为 OpenClaw 的引擎

2026-07-28 05:13:25 +0800 CST views 6

写在前面:当所有人都在造"全家桶",有人选择造"乐高"

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 拆开揉碎讲清楚:

  1. 它的 Monorepo 里 7 个包分别是什么、怎么协作
  2. 统一 LLM API 层(pi-ai)是怎么抹平 20+ Provider 差异的
  3. Pi 这个编码 Agent 为什么敢用不到 1000 token 的 System Prompt
  4. "学徒模式"这个设计哲学到底解决了什么问题
  5. 上手实战:从安装到自定义工具、接入自己的模型
  6. 它和 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 格式。这样做的问题是:

  1. 有损转换——OpenAI 格式是最小公倍数,Anthropic 的 cache control、Google 的 context caching 这些特性会在转换中丢失
  2. 多一跳网络——延迟、部署、维护成本
  3. 流式细节被抹平——不同 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

作为对比(社区逆向的数据,量级可信):

AgentSystem Prompt 规模
Claude Code~10,000+ token
Codex CLI数千 token
某些企业级 Agent20,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 CodeCodex CLI
开源程度全栈开源,7 个包可单独用客户端开源,Harness 逻辑深度绑定 Anthropic开源
模型支持20+ Provider + 自托管 vLLMAnthropic 系为主OpenAI 系为主
System Prompt< 1000 token~10k token数千 token
扩展方式进程内 TS 工具 + MCP + 命令文件MCP + Hooks + SkillsMCP + 配置
状态存储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 值得关注,不只因为它好用,更因为它代表了一种正在回潮的工程观:

  1. 做库,不做平台。 平台锁定你,库服务你。
  2. 每层可拆、可换、可单独用。 正交性是复用的前提。
  3. 状态明文化、可 grep。 调试性是第一等公民。
  4. Prompt 极简,知识进仓库。 能力应该版本化、可 review、属于用户。
  5. 相信模型,但校验一切。 参数校验、错误反馈回路,健壮性来自工程而不是祈祷。

这五条,去掉 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 项目文档。

推荐文章

7种Go语言生成唯一ID的实用方法
2024-11-19 05:22:50 +0800 CST
页面不存在404
2024-11-19 02:13:01 +0800 CST
55个常用的JavaScript代码段
2024-11-18 22:38:45 +0800 CST
38个实用的JavaScript技巧
2024-11-19 07:42:44 +0800 CST
Vue3中如何处理路由和导航?
2024-11-18 16:56:14 +0800 CST
2024年微信小程序开发价格概览
2024-11-19 06:40:52 +0800 CST
JavaScript中的常用浏览器API
2024-11-18 23:23:16 +0800 CST
js一键生成随机颜色:randomColor
2024-11-18 10:13:44 +0800 CST
JavaScript 异步编程入门
2024-11-19 07:07:43 +0800 CST
程序员茄子在线接单