libGDX 之父写了个 AI 编程工具,却故意不做“安全沙箱”——Pi 框架深度拆解
别人都在给 coding agent 加护栏,他偏偏把护栏拆了,然后把内核缩到极小,把一切都做成可插拔的插件。
如果你用过 Claude Code、Cursor、Codex 这类 AI 编程工具,你大概默认了一件事:一个"靠谱"的 coding agent,应该内置权限系统——改文件要确认、跑命令要拦一道、危险操作要弹窗。
然后 Mario Zechner(badlogic,libGDX 游戏框架的作者)做了一个叫 Pi 的框架,官方文档里白纸黑字写着一句话:
Pi does not include a built-in sandbox.(Pi 不包含内建沙箱。)
不是没来得及做,是故意不做。而且他给出的理由,比"做了"更值得琢磨。
这篇文章我们就来把 Pi 这个框架从里到外拆一遍——它到底是什么、内核怎么设计的、那个让人拍案的扩展系统长什么样,以及"不做沙箱"背后的工程哲学。
一、先搞清楚:Pi 到底是个什么东西
Pi 的官方定位只有一句话:
a minimal terminal coding harness.(一个极简的终端编程线束。)
注意那个词——harness(线束/挽具),不是 agent,不是 IDE,不是 assistant。这个用词很讲究。
"线束"是什么?是把发动机、变速箱、各种传感器连起来的那套线缆。它自己不产生动力,但没有它,所有部件都是散的。Pi 想做的,就是 coding agent 世界里的那套"线束":内核极小,只负责把模型、工具、会话、上下文这几件事可靠地连起来;真正的能力,靠外挂扩展无限生长。
几个硬信息先摆出来:
- • 作者:Mario Zechner(
badlogic),libGDX 之父,技术圈的老牌硬核工程师 - • 出品方:Earendil Works
- • 仓库:
earendil-works/pi-mono - • npm 包:
@earendil-works/pi-coding-agent,命令行就叫pi - • 协议:MIT,完全开源
- • 官网:pi.dev
一行命令就能装:
npm install -g --ignore-scripts @earendil-works/pi-coding-agent然后在任意项目目录敲 pi,就进入了一个终端里的 AI 编程会话。
但真正有意思的,不是它怎么用,而是它怎么"搭"出来的。
二、四层积木:一个 coding agent 的最小骨架

Pi 是一个 monorepo(多包仓库),核心就四块积木,职责切得干干净净:
| 包 | 中文理解 | 干什么 |
|---|---|---|
pi-ai | 模型层 | 把 30+ 家大模型 provider 统一成一套 API |
pi-agent-core | 引擎层 | Agent 循环、工具调用、会话树、上下文压缩 |
pi-coding-agent | 应用层 | 面向用户的 CLI,内置 read/bash/edit/write 工具,加载扩展 |
pi-tui | 界面层 | 终端 UI 库,差分渲染 |
这个分层本身就是一堂课。很多人以为 coding agent 是个庞然大物,其实剥开看,核心就这四件事:怎么调模型、怎么转圈干活、怎么记住干过什么、怎么显示给你看。
我们一层层往下钻。
模型层:一套 API 通吃 30 多家
pi-ai 这个包最能体现工程功力。它支持的 provider 列出来能吓你一跳:
OpenAI、Anthropic、Google、AWS Bedrock、Mistral、xAI(Grok)、DeepSeek、Kimi(月之暗面)、Qwen(通义千问)、Groq、智谱、MiniMax、小米、Cerebras、Fireworks、OpenRouter、Cloudflare、GitHub Copilot、NVIDIA、Together、HuggingFace……
这么多家,每家的 API 协议、鉴权方式、流式格式、思考(reasoning)参数全都不一样。Pi 的做法是:在底层把它们统一成同一套 stream() 事件流。
不管你用的是 OpenAI 的 Responses API、Anthropic 的 Messages API,还是 Google 的 GenerativeAI,上层拿到的都是同一种事件:text_delta(文本增量)、thinking_delta(思考增量)、toolcall_delta(工具调用增量)……
这意味着上层的 Agent 引擎完全不需要知道你在用哪家模型。换模型?改一行配置的事。这就是"统一抽象"的价值——把变化关在最底层的笼子里,让上面的世界保持干净。
引擎层:Agent Loop 才是心脏
如果说模型层是"嘴",那 pi-agent-core 里的 Agent Loop 就是"心脏"。这是整个框架最值得精读的部分。
大多数人对 coding agent 循环的理解是这样的:
用户提问 → 模型回答 → 模型想用工具 → 执行工具 → 结果喂回模型 → 再回答 → 结束
这没错,但太粗了。Pi 的 agent-loop.ts 里,真实的循环是双层嵌套的,而且藏着几个精妙的设计。
外层循环管"续接"(follow-up)。 当 Agent 觉得自己该停了(不再调用工具、给出了最终回答),它不会立刻退出,而是先探一眼:有没有排队的新消息? 有的话,接着干;没有,才真正结束。
内层循环管"插话"(steering)。 这是我觉得最人性化的设计。当 Agent 正在埋头干活时,你临时想补一句"等等,顺便把测试也跑一下"——这条消息不会被丢掉,也不会打断当前动作,而是在下一次模型响应之前被注入进去。
用大白话说:Pi 的 Agent 是可打断、可插话、可续接的。它不是一条道跑到黑的直线,而是一个随时能接收你新指令的活物。
再看工具执行的细节,有两个设计特别见功力:
第一,并行 vs 串行。 模型一次可能提出好几个工具调用。Pi 默认让它们并行执行(读三个文件同时读,快),但如果某个工具标记为 sequential(比如写文件、跑命令这种有副作用的),就自动切成串行,避免互相踩踏。
第二,被截断的工具调用,全部作废。 这是个魔鬼细节。当模型的输出因为达到 token 上限被"切断"时(stopReason === "length"),这一批工具调用的参数很可能是残缺的——但残缺的 JSON 经过"尽力解析"后,可能看起来能用、甚至能通过校验,实则是错的。Pi 的处理简单粗暴:只要是被截断的消息,里面所有工具调用一律判失败,让模型重新发一遍完整的。
源码里这段注释写得很直白:
截断的消息可能产生"参数能解析、能校验,但内容悄悄不完整"的工具调用。它们没一个是安全的,全部报错让模型重发。
这种"宁可重来,不冒险执行半截操作"的克制,正是一个成熟框架和一个 demo 的区别。
三、会话树 + 上下文压缩:AI 也需要"记忆管理"

Agent 干活久了,对话历史会越来越长,迟早撑爆模型的上下文窗口。怎么办?Pi 的答案有两层。
第一层:会话是一棵"树",不是一条"线"。
Pi 把会话存成 JSONL 格式的 append-only(只追加)树。每条消息带一个指向父节点的指针。这意味着你可以:
- • 从任意一个历史节点**分叉(fork)**出一条新对话
- • 在会话树里自由导航,回到过去某个状态重新出发
- • 历史文件只追加、不修改,天然抗损坏、可恢复
这比"一条线性对话,清空就没了"高级太多。它更像 Git——你的每一步探索都留痕,随时能 checkout 回去换条路走。
第二层:上下文压缩是"投影",不是"删除"。
当上下文快满时,Pi 会做 compaction(压缩)。但关键在于——它压缩的是"喂给模型的那份视图",而不是真实历史。 真实的会话历史文件一个字都不动。
具体做法:按完整的"工具交互"为单位切分历史,在 token 预算内保留最近的后缀,再用结构化摘要把早期的关键事实补回来。
源码作者特意强调:compaction 是 context 的投影(projection),绝不能被理解成"修改或删除会话历史"。这个区分很重要——你的历史永远是完整的,模型看到的只是一个"按预算裁剪的镜像"。
四、灵魂所在:一个 TypeScript 文件,能改造整个 Agent
前面都是铺垫。Pi 真正让我拍案的,是它的扩展系统(Extensions)。

回到那个"线束"的比喻——Pi 的内核故意做得很小,那能力从哪来?答案是:几乎所有东西都能通过一个 TypeScript 扩展来插拔、拦截、改写、增强。
我读了它的扩展类型定义文件,将近 1000 行。一个扩展能干的事,多到离谱:
1. 订阅全生命周期事件(30 多个)
从会话启动、Agent 开始、每一轮 turn、每条消息、每次工具调用、每次模型切换……全链路的事件你都能监听。而且很多事件是可拦截、可改写的:
- •
tool_call:工具执行前触发,你可以block(拦截)它,或者原地改掉参数 - •
context:每次调模型前触发,你可以改写喂进去的消息 - •
before_provider_headers:请求发出前,你能往 HTTP 头里塞东西 - •
session_before_compact:压缩前触发,你能用自己的方式做摘要
2. 给模型注册全新工具
内置就 read/bash/edit/write 几个基础工具。想要别的?自己注册。几行代码:
pi.registerTool({
name: "greet",
description: "Greet someone by name",
parameters: Type.Object({ name: Type.String() }),
async execute(id, params) {
return { content: [{ type: "text", text: `Hello, ${params.name}!` }] };
},
});这个工具立刻就成了模型能调用的能力。
3. 运行时动态注册模型 provider(含 OAuth 登录流)
想接一个公司内部的模型代理?想加一个带 SSO 登录的 provider?扩展里 pi.registerProvider() 一下就行,不用重启,热生效。
4. 自定义 TUI 界面
自定义 footer、header、编辑器(例子里真有人写了个 Vim 模式编辑器)、主题、快捷键、slash 命令……界面层也是全开放的。
官方例子夸张到什么程度?
Pi 的 examples/extensions/ 目录里,除了正经的 confirm-destructive(危险命令确认)、git-checkpoint(每轮自动 git 存档)、dirty-repo-guard(脏仓库保护)这些实用扩展,居然还有一个 doom-overlay——在终端里跑 DOOM(毁灭战士)游戏,让你等 Agent 干活的时候顺便打两局。
这就是"harness"哲学的极致体现:内核只管把线接好,至于你要在上面接个游戏机还是接个火箭发射器,随你。
而且官方文档里有一句话特别有意思:
pi can create extensions. Ask it to build one for your use case.(Pi 能自己写扩展。让它给你的场景造一个就行。)
——让这个 AI 编程工具,给自己写插件。 这个自举(self-extensible)的设计,才是它敢叫"self-extensible coding agent"的底气。
五、回到那个"故意不做沙箱"的决定
现在我们有资格来聊开头那个反直觉的决定了。
Pi 的 README 和安全文档里,反复强调三件事:
- Pi 没有内建权限系统。 它以启动它的那个用户的权限运行——你能读写的文件,它就能读写;你能跑的命令,它就能跑。
- Project Trust(项目信任)不是沙箱。 它只是一道"输入加载"的门——防止一个仓库在你还没批准前,就偷偷改掉 Pi 的配置或塞进恶意扩展。它不阻止模型启动后让工具干任何事。
- 要真隔离,去容器化。 官方给了三种方案:整个 Pi 塞进 Docker、用 Gondolin 微型虚拟机只把工具执行路由进去、或者用 OpenShell 策略沙箱。

为什么这么设计?作者的原话逻辑是这样的:
一个"半吊子"的进程内沙箱,很容易被误当成一道安全边界——但它实际上仍然依赖宿主的 shell、文件系统、包管理器、凭证和扩展代码。真正的隔离,必须来自操作系统或虚拟化/容器层面。
翻译成人话:假的安全感,比没有安全感更危险。
如果 Pi 做一个看起来能拦一拦的进程内权限系统,用户会误以为"哦它帮我兜底了",从而放松警惕。但这个系统底下全是漏洞(因为它终究跑在你的 shell 里、用你的凭证)。与其给你一个纸糊的盾牌让你自我安慰,不如明确告诉你:这里没有盾牌,你要么盯着看,要么去操作系统层面拿真盾牌。
这是一种非常"工程师"的诚实。它把安全的责任和边界讲得清清楚楚,而不是用一堆弹窗营造"我很安全"的幻觉。
对比一下就明白:大部分工具选择"给你护栏,让你安心";Pi 选择"告诉你没护栏,让你清醒"。哪种更好没有标准答案,但 Pi 的选择背后是一套自洽的哲学——内核只做能做对的事,做不对的事就不做,而不是做个半成品糊弄你。
六、藏在细节里的工程品味
一个框架靠不靠谱,往往看那些"用户根本不会注意到"的地方。Pi 有几处让我印象深刻:
供应链加固到偏执。 Pi 把所有第三方依赖的版本钉死(不用版本范围),.npmrc 里设置 min-release-age=2——拒绝安装当天刚发布的依赖(防止投毒攻击的时间窗)。lockfile 被当成代码来审查,CI 全程 --ignore-scripts(不跑依赖的生命周期脚本)。在 npm 供应链攻击频发的今天,这种偏执是加分项。
锁步版本,没有大版本。 所有包共享同一个版本号,一起发布。而且永远没有 major 版本——patch 管修复和新增,minor 管破坏性改动。这是一种"我们不搞版本地狱"的克制承诺。
鼓励开放真实会话数据。 作者主动把自己用 Pi 干活的真实会话(成功的、失败的、修 bug 的)公开发到 HuggingFace,并鼓励社区一起做。他的理由是:用真实世界的任务、工具调用、失败和修复来改进 coding agent,比用玩具 benchmark 强得多。 这个开放姿态,本身就是对整个赛道的贡献。
多种运行模式。 除了交互式 TUI,Pi 还有 print(一次性输出)、json(结构化事件流)、rpc(stdin/stdout JSONL 双向通信)三种模式。这意味着 Pi 的内核可以被当成一个后端引擎,前面套任何 UI——网页、移动端、CI 流水线都行。这个设计对想做"内核 + 多端"产品的人来说,是绝佳参考。
七、Pi 到底适合谁?
聊了这么多,落到实处——Pi 值不值得你关注?
如果你是想"开箱即用"的普通用户: Pi 可能不是最舒服的选择。它没有花哨的 GUI,没有内置的安全护栏,一切能力都要你自己(或让它自己)通过扩展搭。它更像一套"专业工具",而不是"消费品"。
如果你是想深入理解 coding agent 内核的开发者: Pi 是教科书级别的存在。它把一个 coding agent 拆成了最干净的四层积木,源码可读性极高,连配套的中文教材《动手学 Pi》都有(那是社区复刻它的教学项目)。想搞明白"AI 编程工具内部到底怎么转的",读 Pi 的源码是捷径。
如果你想做类似的产品(尤其是"内核 + 多端"架构): Pi 的分层设计、统一模型抽象、RPC/JSON 双向通信模式,几乎就是一份现成的架构蓝图。你想做一个移动端的 AI 编程客户端?后端跑 Pi 内核走 RPC,前端只管渲染事件流——这条路 Pi 已经帮你趟平了。
写在最后
Pi 最打动我的,不是它有多少功能,而是它敢做减法。
在这个人人都往 AI 工具里疯狂堆功能、加护栏、造壁垒的时代,一个 libGDX 之父级别的老工程师,选择把内核缩到极小,把安全边界讲得明明白白,然后把无限的可能性交给一个开放的扩展系统。
它不假装安全,不制造幻觉,不搞版本地狱,不用玩具 benchmark 自欺欺人。它就是诚实地告诉你:这是一套线束。我把最难做对的那几件事做对了,剩下的世界,交给你。
这种克制和诚实,在今天的 AI 工具圈,反而成了一种稀缺的品味。
如果你也在做 AI 编程相关的东西,真心建议去 pi.dev 逛逛,或者直接 clone 一份 earendil-works/pi-mono 读读源码。有时候,最好的教材不是文档,而是一份写得足够干净的代码本身。
本文基于 Pi(earendil-works/pi-mono)0.80.x 版本源码分析,所有技术细节均来自对仓库 README、AGENTS.md、安全文档、agent-loop.ts、扩展系统类型定义等真实源码的精读。
夜雨聆风