跳到主要内容

那些在本地通过但在 CI 中失败的编程智能体

· 阅读需 12 分钟
Tian Pan
Software Engineer

智能体(agent)生成的 diff 在你的电脑上显示为绿色。测试通过了,lint 通过了,开发服务器也干净地完成了热重载。你让它提交了 PR,九十秒后,CI 在一个与修改完全无关的步骤上报错变红了:缺少某个 CLI 工具、一个智能体从未声明过的新环境变量,或者 Node 版本解析结果不一致——因为你的 .nvmrc 是通过 runner 并不具备的全局 shim 进行解析的。智能体并没有写出有问题的 diff。它写出的是一个依赖于你机器环境的 diff,而你的机器和 runner 并不是同一台电脑。

“在我的机器上能运行”曾是一个人为 Bug。解决办法是保持纪律性——锁定版本、编写 Dockerfile、阅读 CI 日志。而编程智能体大规模地继承了这个 Bug,却丢弃了曾经用来弥补它的纪律性。因为智能体不知道它所依赖的东西哪些来自代码库,哪些来自你 shell 历史记录中的“温热沉淀物”。每个开发者的笔记本电脑都是一个配置独特的环境,智能体在不知不觉中吸收了这些环境。接着,同一个智能体在一个完全不具备这些条件的 runner 中运行,失败的表象看起来像是智能体的错,但实际上是由于没人写明的一份环境契约。

没人列出的“温热环境税”

你的笔记本电脑是一个“温热环境”。你的 shell 会加载一个导出 AWS_PROFILE 的点文件(dotfile),有一个包含私有仓库令牌的 ~/.npmrc,有一个通过 Homebrew 安装的全局 Node——当代码库的 .nvmrc 缺少次版本号时,你的 nvm shim 会解析到这个版本;还有一个六个月前就在本地仓库缓存好的 Docker 镜像,已经认证过的 gh,指向某个集群的 kubectl,以及在 localhost 上运行的 Postgres——那是你在 2024 年的某个周四用 brew 安装的。

智能体没有安装其中的任何东西,它只是继承了它们。当它运行 npm install 时,私有仓库能解析是因为令牌已经存在于你的 shell 中。当它运行 make seed 时,容器能启动是因为镜像早已拉取。当它运行测试套件时,数据库已经在那里了。所有这些机制都不在代码库中,而 runner 也不具备其中的任何一项。

这种“税”会产生复利,因为智能体看不到它正在使用的东西。当人类开发者运行 npm install 并看到 npm warn deprecated using cached token 时,至少还有机会注意到它。而智能体将同样的输出作为工具执行结果读取时,会将成功视为契约:命令运行了,测试通过了,任务完成了。接着智能体写下一条 commit 信息,断言修改已完成。CI runner 读取同一个代码库,运行同样的命令,却发现这一切都不是真的。

Runner 是你 Shell 的陌生人

CI runner 被刻意设计得非常“简陋”。它们拥有标准的 shell、最精简的工具集、没有缓存的凭据、被清理过的环境变量、全新的文件系统,以及一个不会静默允许你访问私有仓库的网络策略。这是正确的设计——runner 的存在是为了证明 git 中的产物是自洽的。任何 runner 能做到但 git 产物无法描述的事情,都是认证过程中的漏洞。

对于人类开发者来说,runner 的简陋虽然烦人但清晰易懂。你阅读失败日志,找到缺失的部分,然后提交它。对于编程智能体来说,runner 的简陋是不可见的,因为智能体从未见过 runner。智能体接触的是你的 shell,运行的是你的命令,观察的是你的输出,并据此得出任务已完成的结论。对智能体而言,runner 仅作为一种失败日志存在,它必须在事后解释这些日志——通常是在一两轮对话之后,人类将 CI 输出“粘贴炸弹”般丢回对话框的时候。

这种偏差是不对称的。仅本地成功的代码可能被发布,而仅 CI 成功的代码则不会。智能体吸收的每项开发环境便利,都变成了 CI runner 以构建失败(红标)形式支付的“税款”,而智能体并没有原生渠道去询问:“这在决定能否发布的那个环境中运行得通吗?”

为什么智能体闻不出它继承了什么

一位高级工程师运行 npm install 时,在某种程度上会知道这次安装使用了私有仓库,知道凭据来自某处,知道 lockfile 解析到了特定的 Node 版本,并且知道其中一些事实并不在代码库中。高级工程师脑中有一个不成文的模型,即“一个陌生人需要什么才能复现这个过程”。正是这个模型促使工程师提交 .tool-versions 文件,或者编写 make bootstrap 目标,亦或是将凭据设置写进脚本。

除非有人把它写下来,否则智能体没有这种模型。智能体读取仓库,看到 package.json,运行 npm install,获得成功,然后继续。它内部没有信号会提示:该命令成功的原因并不包含在你刚刚阅读的代码库中。智能体的训练目标是“完成任务”。找出任务未明确声明的依赖项并不是任务本身。

这就是失败模式上升到架构层面的时候。智能体并不是在偷懒。它的运行完全符合设计——将目标转化为行动,观察输出,当输出为绿色时宣告完成。没有人设计的部分是这样一个层级,它会问:“如果你不在这个开发者的机器上,这还会是绿色的吗?”

缩小差距的模式

修复方法不是“让 Agent 变得更聪明”。而是给 Agent 一份关于环境必须包含什么的、可读的合同(contract),并在编辑代码之前验证该合同。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates