自动化开发中摩擦的正面作用;或者说,Pi 很糟糕
引言
我最近试用了 AI 开发工具 Pi。此前我一直在用 Goose——当初这类工具还算新鲜的时候,我最早接触的就是它。我能理解 Pi 为什么这么火,但实际用下来,我的体验有点“更好反而更糟”。目前我打算继续用 Goose。我得出结论:Goose 之所以更好,恰恰是因为它给整个流程引入了摩擦。在这篇博客的剩余部分,我会描述自己用 Open Router 的 deepseek-v4-flash 模型分别跑 Pi 和 Goose 的体验差异。我使用的 Goose 配置可以在这里找到,Pi 的全局配置在这里,Goose 的项目级封装在这里,Pi 的项目级配置在这里。
实验设计
我在 Goose 里敲一段三四句话的简短提示,它就能直接跑通。有时我需要给点引导,但一旦跑起来,我经常能离开工位,过一会儿再回来。我常听人说 AI 会“脱缰”——比如在单元测试里作弊、实现错误的东西、偏偏做你明确禁止它做的事,诸如此类。我之前一直不太理解这些抱怨。我就是写点东西,偶尔跟 AI 说“别那样,这样”,它就跑得还行。然而,我用同样简短的提示方式去试 Pi,它立刻就开始脱缰了。我测试 Pi 用的项目,是我正在从 Clojure 移植到 Common Lisp 的一个项目。我写了一个技能文件,告诉它我希望 Common Lisp 代码怎么写。在里面我说要写一堆 bead。我最初给 Pi 配置了通过 bead MCP 服务器来写 bead。然后我在自己写的技能文件里让 AI 使用 beads MCP,后来 AGENTS.md 文件里也写了同样的内容。我启动 Pi,让它读 AGENTS.md,同时给了它一个初始提示,让它使用该技能并开始移植代码。
启动 Pi 后,它立刻无视了我在 skill 中给它的指令。它没有编写任何 beads,几乎马上就动手写代码。我不得不冲它吼了两三次,它才反应过来“哦对,我应该先停下来写 beads”。我停掉 Pi,做了一些研究,想找出这个 AI 如此公然无视指令的根本原因。Pi 的 MCP 插件默认采用懒加载方式把 MCP 服务器载入上下文。Mario Zechner 写过一篇有趣的博客文章,说 MCP 可能不太好,因为不管你是否使用 MCP 工具,它都会把关于这些工具的信息塞进上下文窗口,占用 token。Pi 的 MCP 适配器就遵循这个理念,只把 MCP 工具的定义加载到上下文中。正因如此,我提到的 beads 很可能是 AI 第一次看到这个词。它不知道该怎么处理,可能就直接忽略了。我再次启动时改用 bead 插件,而不是 bead MCP 服务器。但这个方法在 Common Lisp 的 cl-mcp MCP 服务器上行不通,因为没有人写过功能相当的插件。Pi 总是偏离我让它使用 MCP 服务器的指令,这是个很大的问题。
从命令行运行 Common Lisp 代码时,经常会掉进 Lisp 交互式调试器。Common Lisp 不喜欢被“vibecode”,交互式调试器就是原因之一,但远不止这一个。长时间运行的 REPL 开发、REPL 重载、在 REPL 环境里做一次性 Lisp 求值、以 Lisp 形式为单位编辑(而不是按行编辑)、括号配对,这些都是 AI 经常搞不定的问题。cl-mcp 服务器解决了其中好几个问题,还不止这些。
Goose 通过命令行运行 Lisp 代码时,对 Lisp 调试器的处理非常糟糕。在 Goose 里,调试器会彻底卡住 Goose 会话,必须重启 Goose。重启后,我会告诉 Goose:“你又用了 Lisp 命令行工具,别这样,用 MCP 服务器。”我用 Goose 的 Top Of Mind 功能来提醒 Goose 改用 MCP 服务器。启用 Top Of Mind 之后,Goose 基本上不再出现这个问题了。
Pi 处理调试器、甚至括号匹配都比 Goose 更从容,但这反而在会话里引发了问题。Pi 似乎会把 Lisp 进程放进独立的 shell 里,一旦进入调试器提示符就把它杀掉,从而优雅地处理交互式提示符。不过,和用 Goose 时一样,我仍想强制它使用 cl-mcp。我做这项测试时,Pi 的包系统里还没有类似「top of mind」的上下文注入机制,于是我给它写了一个 top of mind 插件,用来提醒 Pi 有个 MCP 服务器。可即便我每隔大约 10 次工具调用就把它塞进上下文,Pi 还是不停出错,最后干脆不用 MCP 服务器了。它通过 CLI 与 Common Lisp 临时交互,拿到的奖励已经足够大,于是就不再使用那个专为帮它取得更好结果而设计的正经工具了。
问题 1:Pi 忽视 MCP 服务器
问题 2:Pi 太快了
同一个模型、同一个 API 后端,Pi 生成文本的速度比 Goose 快得多。默认情况下,它还不让用户中途停止生成。我想跟它说话时,它根本不停,只顾着一路朝着毁灭狂奔。这种速度让我紧张得攥紧拳头,心里想:“这回它又要搞砸什么?”最后我装了 pi-stop 插件。这帮助很大,因为我终于能叫停 Pi 了。但它生成文本仍然太快,我根本来不及消化它在生成什么。常常是我刚意识到需要引导它,它已经生成了一大堆垃圾,堆在那里污染上下文,更别提它写出的烂代码或错误内容了。最终,每次 Pi 会话结束后,我都得切回 Goose,因为 AI 已经搞不清下一步该做什么了。速度带来的问题不只是上下文污染:它在极短时间内制造大量需要清理的混乱,结果在 OpenRouter 上花的钱更多,效果却更差。它就是跑得太快,太容易出错,根本不给人留出引导它的时间。
问题 3:Pi 的插件打地鼠
Goose 自带几个关键功能:随时打断并完全停止生成、最大轮次限制,以及一套细粒度权限系统(虽然我不用),可以精确控制哪些工具暴露给 AI、哪些不暴露。最后,它还有最大上下文 token 设置(我在 Pi 里为了这个又装了一个插件 pi-context-cap)。这些都是 Goose 开箱即用的。而在 Pi 里,我得四处寻找这些功能,有一次甚至得自己写扩展。更糟的是,这些插件往往只有几百次下载,让我觉得它们都是业余的一次性项目(就像我刚为 Pi 写的那个随时打断插件一样!),维护不善,也不够可靠,没法用在正经工作流里。
结论
Pi 很惊艳,速度与灵活性也确实过硬。但在我看来,正是缺少内置的摩擦机制,才让模型更容易跑偏:它会无视 MCP 服务器,错过可以介入引导的时机,制造出一堆无用改动,污染上下文,清理起来更费劲。Goose 的摩擦——生成更慢、时刻提醒、停止控制、回合上限和上下文上限——更像是给代理套上了更短的缰绳。这种摩擦不只是烦人,它是一层控制面。对于自动化开发来说,能通过框架把正确行为变成默认选项,比单纯跑得更快更有价值。
真正由创造者维护。感觉像在打地鼠。“哎呀!Pi 没有 x,只好去市面上找。”我可不会为我的车到处找不同的安全带,只希望制造商出厂就装好了。插件很棒,活跃的包生态也很棒,但我在 Jenkins 战壕里摸爬滚打的经验告诉我:插件不该到处都用。
草未必更绿
Goose 大量使用 MCP 让它工作得更好。Goose 有钩子和插件,但大多数扩展只是通过 MCP 实现的。据我所知,它会提前把定义加载到上下文窗口里。它用顺序思考 MCP,用待办 MCP,用 beads MCP。很多时候我并没有明确提示它这么做,但因为我加载了这些东西,就已经算是暗示了。
因为所有 MCP 工具定义都一次性加载进去,AI 就觉得自己必须用它们。这种加载本身就是一种暗示:这些 MCP 工具是拿来用的。
在 Pi 里,我装了不少插件,比如任务插件(task plugin),但除非我明确告诉它,否则它根本不会用。后来我终于搞明白怎么把 MCP 服务器预加载(eagerly load)到 Pi 的上下文里,但一开始这确实是个麻烦。Goose 的理念是把 MCP 服务器限定在你实际会用到的那些。我之前用 cl-mcp 时,也被“求求你用一下我的 MCP——真的,拜托了”这个问题困扰过,所以很认同这个思路。如果你确实想让 AI 用某个工具,那你就应该把 MCP 定义、技能(skills)以及其他杂七杂八想让它用的工具,提前都加载好。
Goose 生成代码的节奏要可控得多。Goose 是用 Go 写的。Go 的异步工具很出色,但作者好像没怎么用。整个流程感觉差不多就是单线程的。这客观上是更差,但客观上也更好。它能轻松拦住 AI 干错事。它会让 AI 稍微慢一点,但速度从来不是 AI 的问题。真正的问题是我认知负荷太大,跟不上 AI。就像汽车和道路一样,我的 AI 工具也需要限速,好给“驾驶员”留出反应时间。
用 Goose 的时候,我先搭好会话,然后花大约 20 分钟盯着它。我经常得给它发一句话的纠偏消息(steering message),不过随着会话推进,频率会越来越低。一开始很频繁,但过一阵子它就像“进入状态”了一样,开始稳定输出质量很高的好代码。之后我隔一段时间看一眼,有时把它停下来写一句纠偏消息,然后再回去干自己的事。如果我忘了这个会话,过段时间回来,它通常已经撞上了配置的 token 上限或者最大轮次限制,但写出来的东西依然是好的。
这一切让我觉得,Goose 是一群真心实意想把 AI 收拾得服服帖帖、又不折腾人的人写出来的。Goose 就是能跑起来。它让人感觉有点无聊。无聊是好事。
结论
现在可以看出来,我的经历印证了“更差即更好”这个效应。消耗更多上下文反而更好,因为这样能告诉 AI 该怎么思考。更慢反而更好,因为我有时间去思考 AI 正在生成的内容。Lisp 调试器能跑 Goose 但跑不了 Pi,这也是好事,因为它逼着我去要求 AI 使用 MCP 服务器。
这让我想到 Go 和 JavaScript 的对比,因为 Goose 和 Pi 分别是用这两种语言写的。类型化语言出了名的取舍是开发速度更慢,但它们引入的摩擦有助于写出更精确的代码。写前端的话这很糟糕,因为前端的变化节奏需要更快;但在更底层,东西需要变化更少、更正确,因为当你身处浏览器之外时,很难检查代码的效果。用 Goose 时,我能感受到这种更慢、更重的 AI 工具带来的正面效果。
我用 NeoVim 作为编辑器,从 Goose 换到 Pi 的感觉就像从 Vim 换到 VS Code。我发现的东西和我预期的一样:更好的插件系统、丰富的扩展生态、更精致的用户体验,就像 VS Code 那样。但我在编码上的结果也更差了。Goose 需要更强的那一点,它确实更强,就像 Vim 一样。Pi 也有一些非常棒的地方让我喜欢,就像 VS Code 也有一些非常棒的地方。但 Vim 一直更适合我的大脑。
我想我会再用 Goose 一段时间。形式固然好,但在这个行当里,功能是没得商量的。