pi:把克制当设计的 AI Coding Agent

2026-08-25 8 min read 墨然

最近看 pi(badlogic 的 pi-mono,作者就是经典游戏框架 libGDX 的作者),我的第一反应不是“又多了一个 AI Coding Agent”,而是想起了那句话——功能越多,工具离工具越远。

过去两年,AI 编程工具的主旋律是“加”。子 Agent、计划模式、权限系统、待办列表、后台任务、MCP……每家的路线图都在比谁更像一个操作系统。Claude Code 有 Plan Mode 和 TodoWrite,Codex 有 sandbox 和 worktree,大家都在往“全能”的路上狂奔。

pi 走的是反方向。这个项目最显眼的设计决策,是一张写进 README 的“不做”清单。

让 pi 适应你的工作流,而不是反过来。

这句话背后是一个少见的立场:工具不该替你决定工作流长什么样,它只该把最核心的那层做扎实,把其余的部分留成口子。

它到底是什么

pi 是一个极简的开源 AI Coding Agent,整个项目是一个 monorepo,拆成六个 npm 包:

  • pi-ai — 统一封装 20+ 家 LLM 供应商(OpenAI、Anthropic、Google 等)
  • pi-agent-core — Agent 运行时,负责工具调用和状态管理
  • pi-coding-agent — 面向开发者的 CLI 界面
  • pi-tui — 终端 UI 库
  • pi-web-ui — Web 组件
  • pi-pods — GPU 节点上的 vLLM 部署管理

其中 pi-agent-core 和 pi-coding-agent 是理解整个项目的钥匙:一个管 Agent 怎么运转,一个管你怎么和它打交道。下面主要围绕这两个包展开。

骨架只有两个循环和五个方法

pi 的 Agent 核心,是一个双层 while 循环。

内层管“这一轮干什么”:拿到模型回复,看有没有工具调用,有就执行,把结果送回模型,直到这一轮没有工具可跑了。外层管“还要不要继续”:检查有没有新消息进来,有就再开一轮,没有就停。

这样拆不是因为强迫症,而是“工具跑完了”和“对话结束了”本来就是两件事。一次工具返回可能接着触发新的工具调用——这还在内层;也可能用户插了句新指令——这得回到外层。硬塞进一个循环里,状态判断会越写越拧。

更值得注意的是,这个循环本身是无状态的。对话历史、消息队列、中断信号全部挂在 Agent 对象上,循环函数不碰任何状态。好处是同一套循环逻辑,新对话能跑,断点续传也能跑,不用写两套。

Agent 对外只暴露五个方法:

prompt()    — 发起新对话
continue()  — 继续当前对话
steer()     — 当前轮次结束后注入消息
followUp()  — Agent 即将停下时注入消息
abort()     — 中断

五个方法,把和 Agent 交互的所有场景都覆盖了。尤其 steer 和 followUp 这对组合很有意思:前者是模型正在跑工具时你想改方向,消息会在当前轮次结束后注入;后者是模型说“干完了”但你还有事,消息在它即将停下时注入。两个队列都支持一次性全给,也支持一条一条投递——逐条投递时每条消息都有机会影响后续行为,批量投递更像一次性交代完毕。

克制具体长什么样:四个工具和一张“不做”清单

pi 默认只给模型四个工具:读文件、执行命令、编辑、写入。

没有搜索,没有列目录,没有任何多余的东西——除非你主动开。想加,用 --tools 打开内置的另外三个(搜索、查找、列目录),或者通过扩展自己注册。

工具少不是省事,是防错。模型可选的工具越多,选错的概率越大。四个工具覆盖了改代码的完整闭环,剩下的都是“锦上添花”,而 pi 把“要不要这朵花”的决定权交给你。

比四个工具更狠的,是 README 里那张“不做”清单:

  • 不内置 MCP——用扩展或技能
  • 不内置子 Agent——用 tmux 跑多个实例,或扩展自建
  • 不内置权限弹窗——跑在容器里,或扩展自建确认流程
  • 不内置计划模式——计划写文件里,或扩展实现
  • 不内置待办列表——会干扰模型,用 TODO.md
  • 不内置后台任务——用 tmux,可观测性完整

对习惯了 Claude Code 的人来说,这张清单读起来像挑衅。MCP、subagent、权限确认、Plan Mode、TodoWrite——这些全被 pi 视为“不该锁死在核心里”的东西。

pi 的逻辑是:这些功能本身没问题,有问题的是它们住进了核心。住进核心,意味着每个人都得为它们付复杂度——不管你的工作流需不需要。pi 的选择是核心一概不收,谁需要,谁通过扩展自己接。

功能不消失,只是被推迟到扩展里

“不做”不等于“没有”。pi 把那些被拒绝的功能,全部交给了一套贯穿 Agent 全程的钩子系统。

扩展可以在六个阶段插进来:

  • 会话阶段:启动、关闭、切换、分叉前
  • 输入阶段:用户输入可以被拦截(不让模型看到)或改写
  • Agent 启动前:注入消息,或改系统提示词
  • 模型调用前:改发送给模型的上下文和请求参数
  • 工具执行前后:阻止某次执行,或改执行结果
  • 资源发现:声明自己提供哪些技能、提示词模板和主题

这六个钩子点拼起来,基本能拼出任何被“不做”掉的特性。权限确认?挂工具执行前的钩子。计划模式?挂 Agent 启动前的钩子。MCP?用资源发现加自定义工具。换句话说,pi 不是反对这些能力,它反对的是把它们焊死在核心循环里。

资源加载也是同样的思路。扩展、技能、提示词模板、主题、上下文文件、系统提示词,这六类资源来自四个地方:全局配置、项目配置、命令行参数、扩展自身声明。包管理器把路径收齐,过滤掉禁用的,按优先级合并,再分发给对应的加载器。规则统一,谁覆盖谁清楚。

其中上下文文件(AGENTS.md / CLAUDE.md)的加载方式值得单说:从当前目录一路向上找到根目录,每一层的指引都收进来,全局的最轻,离项目最近的最重。配置合并用深度合并,写入时只写变更过的字段——你改了一个开关,其他字段一个都不碰。

上下文和记忆:有损压缩与 JSONL 会话树

任何长会话都会撞上上下文窗口,pi 的答案是自动压缩:令牌数逼近阈值时,把旧的对话历史喂给模型生成摘要,再用摘要替换原始消息。

三个细节让这个“有损”的过程不那么危险:

  1. 按轮次切,不按令牌数切。截断点找的是最近一个完整对话轮次的边界,不会在工具调用的中间把历史劈开。
  2. 文件操作记录提前抽出。压缩前,把历史里所有文件的增删改记录单独提取,防止模型压缩完忘了自己动过什么。
  3. 原始数据永远不丢。完整历史躺在 JSONL 里,/tree 随时能翻出压缩前的全貌。

和压缩配套的还有重试策略:指数退避只处理三类错误——429、5xx、网络问题,其他错误直接抛。上下文溢出?不重试,走压缩。这两条路径被分得清清楚楚:重试解决的是“对方暂时不行”,压缩解决的是“装不下了”,混在一起只会互相添乱。

会话本身也值得一提。每条记录带 ID 和父 ID,一个会话文件就是一棵树。想回到某个节点分叉?直接在原地长出新枝,不需要复制文件,也不会碰旧路径。分叉时 pi 会往新分支开头注入一段摘要,告诉模型“你从哪里来、之前干了什么”——比复制整个上下文省,也足够让新分支接着干活。

工程细节里的小心思

pi 里还有几处小设计,单独看不起眼,放在一起就是质感。

写文件的串行化。多个工具同时写文件是 AI Coding Agent 的常见事故源。pi 的实现极简:一个 Map,每个文件路径对应一条 Promise 链,新写操作追加到链尾,串行自然形成;不同文件的链互不相干。十几行代码,对调用方完全透明。

!! 前缀。用 !! 执行的命令,结果不会发给模型,只存本地。有时候你只是在终端里确认个东西,不需要模型也知道——这条边界尊重的是用户自己的判断空间。

消息转换层。pi 内部的消息格式支持一堆自定义类型:命令执行结果、扩展注入、压缩摘要、分支摘要……但模型只认三种角色:用户、助手、工具结果。中间这层转换按角色分发,自定义类型统一折算成模型能懂的形式。再配合 TypeScript 的 declaration merging,扩展想加新消息类型也不用动核心代码。

模型管理。20+ 供应商,一套接口。匹配模型可以精确到“供应商/模型名”,可以模糊搜,可以用别名。密钥解析有明确优先级:环境变量 > 本地授权存储 > 浏览器认证 > 命令行——运维配置最重,临时覆盖最轻,本地开发、CI、生产环境不用写三套逻辑。同一个会话里还能随时换模型,上下文自动序列化:琐碎任务用便宜模型,难题切贵的,不用重开对话。

这笔账划不划得来

pi 这套设计不是免费的。

代价明摆着:开箱即用性差。别的工具装完就能干活,pi 装完你还得自己选扩展、自己搭工作流、自己决定什么该有。它的默认体验就是“什么都没有”。

习惯了 Claude Code 那种“装上就能打”的人,第一次打开 pi 大概率会觉得简陋。但换个角度看,那些“开箱即用”的功能,对不需要它们的人来说本身就是噪音:弹窗打断思路,子 Agent 分走注意力,计划模式推着你走它的流程。

所以 pi 真正适合的,是对控制权有执念的那类人:想精确知道模型每一步能干什么、不能干什么;想把自己的工作流原样搬进工具,而不是反过来被工具的工作流收编;宁可自己搭,也不要别人替。

总结

把 pi 的各个模块摊开看,其实只有一条主线:

核心只做不可避免的部分,其余的留出口,但不替你决定。

大多数 AI 工具在回答“还能加什么”,pi 在回答“什么可以不加”。前者让工具越来越像操作系统,后者让工具重新变回工具。

“让 pi 适应你的工作流,而不是反过来”——这句话可以是口号,也可以是真的。pi 的特别之处在于,它把这句话写进了代码结构里:写不进扩展的,才配进核心。

对一个正在被功能膨胀吞没的赛道来说,这个反方向的样本,本身就值得一读。

相关链接