跳到主要内容

24 篇博文 含有标签「coding-agents」

查看所有标签

翻倍且没有事后复盘:那份编码智能体带来的 CI 账单

· 阅读需 11 分钟
Tian Pan
Software Engineer

该项支出在六周内攀升了 130%,工程团队却无人察觉。PR(拉取请求)的合入速度变快了。仪表板上的单次 PR CI 成本看起来与上季度持平。Agent 的分支在第一次尝试时通过测试(显示为绿色)的频率比人类的分支更高,这实际上反而拉低了 CI 持续时间的中位数。财务部门在季度复核中发现了这一点,将其标记为不明变动,并要求工程部门提交事后分析报告(postmortem)。工程团队无话可说 —— 既没有事故,也没有回退,更没有部署失败。仅仅是一项预算支出在仪表板显示一切正常的情况下,悄无声息地翻了一倍。

这个“事后分析报告”式的缺口本身就是一个产物。成本从以人力为主的曲线转向了以基础设施为主的曲线,而负责人力预算的团队与负责基础设施预算的团队并非同一个。Agent 没有弄坏任何东西,它只是改变了损益表(P&L)中承担这项工作的科目。

你的编程智能体生成的那些人类已经不再阅读的 PR 描述

· 阅读需 12 分钟
Tian Pan
Software Engineer

一年前,你的团队采用了 PR 描述模板。它包含 ## Summary## Changes## Test plan 和一排复选框。审查者非常喜欢它:每个 PR 都有上下文,每个 PR 都有测试计划,每个 PR 都有结构。六个月后,编程助手学会了填写它。现在,每个 PR 依然有 ## Summary## Changes## Test plan 和一排复选框 —— 但审查者不再阅读标题以外的内容了。曾经聚焦注意力的格式,现在反而成了“此处不值得关注”的信号。结构比它所承载的信号寿命更长。

这不是代码质量问题。这些 PR 中的代码通常是没问题的。问题在于,撰写描述的行为已经从思考变更的行为中被剥离,而描述正是审查者用来分级处理(triage)其有限注意力的工具。当该工具变得格式统一、措辞合理,且与其他所有 PR 毫无区别时,审查者的注意力分级机制就失效了。曾经用于挖掘异常情况的系统,现在将所有内容摊平成了同样的形状。

你的编程智能体忘记检查的分支状态

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的编码智能体并不知道它在哪一个分支上。它以为它知道。十二轮对话前它看过 git status 的输出,它的上下文中有一份 CLAUDE.md 提到了会话开启时的分支名称,并且它观察到一个工具结果列出了五个当时正确的文件。从那时起,智能体就一直在基于那个快照进行静默推理。与此同时,你在另一个终端里运行了 git checkout main。智能体的 diff 顺利地写入了文件系统,因为操作系统并不关心这些字节属于哪个分支。这个 diff 在语义上是错误的,因为智能体对分支的心理模型已经落后了 300 个提交,且它所基于的父节点在你的工作树中已不复存在。

这就是分支状态漂移 (branch-state drift),它是数据库中“读-改-写”竞态 (read-modify-write race) 在编码智能体领域的翻版。智能体在第 N 轮读取世界状态,在第 N+1 到 N+k 轮之间修改计划,并在第 N+k+1 轮写回磁盘——而在这一时间窗口内的某个时刻,它脚下的世界已经发生了变化。没有异常抛出,没有工具返回错误。补丁被应用了。危害在下游显现:针对错误的基准分支开启了 PR,手动提交的代码被静默回滚,或者针对昨天刚迁移过的模式 (schema) 实现了新功能。

那些在本地通过但在 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 中运行,失败的表象看起来像是智能体的错,但实际上是由于没人写明的一份环境契约。

被你的 Coding Agent 污染的热重载循环

· 阅读需 13 分钟
Tian Pan
Software Engineer

一个编程智能体(Coding Agent)和一个热模块替换(HMR)开发服务器,各自独立看都是神奇的存在。但把它们放在同一个工作目录里,它们就成了一个没有任何同步原语的生产者-消费者对。智能体写入文件,监听器触发。开发服务器重载到一个仅存在 90 毫秒的状态,随后就被智能体的下一次写入替换了。错误覆盖层反映的是文件系统已经越过的快照。智能体读取那个覆盖层,将其视为事实依据,并针对一个本就会被下一次保存抹去的问题编写修复方案。

在单行编辑时你不会注意到这一点。但在智能体进行协调的多文件更改时——比如在组件间重命名 prop、通过 hook 传递新字段、拆分模块——从“开始”到“完成”之间的每个中间状态在结构上都是损坏的。监听器无法分辨中间状态和最终状态的区别。而观察监听器输出的智能体,也无法区分真实的错误和其自身正在进行的工作所产生的伪影。

编程智能体绕过而未使用的代码规范(Idiom)

· 阅读需 13 分钟
Tian Pan
Software Engineer

我合作的一个支付团队的高级工程师曾给我讲过一个故事,我认为每一个运行编程 Agent(AI 代理)的团队最终都会经历。他们的代码库有一个 Result<T, E> 封装器——这是自研的,位于单个 core/result.ts 文件中,在该服务的约两百处调用点被使用。新代码被要求在每一个可能失败的函数中传递 Result;而 throw 则保留给真正意料之外的状态。这并非由 lint 规则强制执行。这就是他们的“方言”。

在使用编程 Agent 交付六个月后,他们审计了 Agent 合并的 diff(差异)。大约三分之一的新函数完全忽略了 Result。Agent 选择了 try/catch,返回了 T | null,抛出了带有描述性消息的 Error 子类——在某些设想的代码库中,这些选择中的每一个都是正确的。但在当前这个代码库中,没有一个是正确的。代码通过了类型检查。测试通过了。审阅者批准了它,因为每一行看起来都没有错。但 Agent 修改的文件不再与它旁边的文件保持一致,团队在自己的服务内部悄然滋生出了第二种“方言”。

这就是我想谈论的故障模式:不是 Bug,不是幻觉,也不是违反了 lint 规则——而是惯用法漂移 (Idiomatic Drift)。Agent 交付的代码可以编译、运行并通过测试,但其风格并非你的代码库所使用的。随着合并次数的增加,代码库会分化为 Agent 风格区和人类风格区,而代价会体现在任何仪表盘都无法监控的地方。

你的编程智能体悄然打破的内部循环

· 阅读需 9 分钟
Tian Pan
Software Engineer

关于编码智能体(coding agents)提高生产力的说法是,它们消除了打字瓶颈。但在实践中,工程师真正遇到的瓶颈却截然不同。工程师再也无法在脑中掌握整个系统,因为智能体修改文件的速度快于工程师阅读的速度,编写测试的速度快于工程师推断覆盖率的速度,重构抽象的速度快于工程师在设计层面(而不仅仅是编译器层面)验证类型检查的速度。

那个紧凑的内环——假设、更改、观察、优化——定义了胜任的工程工作,但它正悄然瓦解为另一种循环。工程师现在是在审查智能体的输出,而不是建立对系统的直觉。2025 年中期的一项 METR 随机对照试验发现,经验丰富的开源开发人员在使用 AI 助手处理熟悉的代码库时,速度慢了 19%,但他们却报告感觉快了 20%。认知感知的生产力与实际生产力之间这 39 个百分点的差距并非测量误差。这是为了吞吐量而默默牺牲理解力的代价。

你的编程 Agent 记错的库版本

· 阅读需 11 分钟
Tian Pan
Software Engineer

Diff 看起来很干净。Agent 导入了正确的模块,调用了看起来正确的函数,TypeScript 也没有报错。PR 描述甚至引用了文档。随后 CI 中的构建开始运行,调用却由于 TypeError: x is not a function 而崩溃 —— 这是因为该函数在八个月前的一次小版本更新中被拆分成了两个,而 Agent 是根据其训练数据中存在的库版本生成的代码,而不是你 package.json 中安装的版本。

这并不是“LLM 会产生幻觉”这一框架能让你做好准备的那种故障。模型并不是在发明一个从未存在的 API。它是在记忆一个曾经存在但现在已不存在的 API。Agent 进行推理的心智模型是一个冻结在训练时的快照。世界在向前发展。代码库在向前发展。而 Agent 却一无所知,因为没人告诉它。

你的编码 Agent 写不出的 PR 描述

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的编码 Agent 完成了任务。Diff 很小,测试全绿,Lint 干净,而 PR 正文从头到尾只有一句话:"修复 X 模块中的 bug。"远在六个时区之外的评审者打开页面,孤立地阅读 diff,看不出任何毛病,于是批准了一个技术上完全正确、却解决了错误问题的改动。代码合入。两天后,一位客户来问他们一直依赖的某个变通办法为什么突然失效了 —— 这时你才发现,你的 Agent 修复的那个 bug,并不是工单里描述的那个 bug。

代码没问题。评审者很尽责。Agent 也严格按照吩咐做事。问题出在他们之间的那个交付物 —— pull request —— 它丢失了一切本可避免这次失误的信息。

你删除的代码对你的编程 Agent 是不可见的

· 阅读需 11 分钟
Tian Pan
Software Engineer

你周二下午花时间删除了一个已经废弃的工具模块。你清理了导入,运行了类型检查器,看着 CI 变绿,然后合并了 PR。周三上午,一个新的 Agent 会话查看同样的代码,认定代码库“缺失”了一个小型助手函数,于是又把那个废弃的模块写了回来——名字相同,形状相同,只是风格略有不同。昨天批准删除的评审者现在不得不回想为什么要删掉它,找到当时证明其合理性的对话,并再次解释一遍。Agent 并没有出现故障。它只是完全按照其上下文的要求在行事。

这是编码 Agent 的结构性可靠性问题,没有人通过提示词工程(Prompt Engineering)来解决:Agent 的上下文起始于代码库的当前状态,而不是该状态为何如此的历史。你移除的文件没有留下 Agent 可见的痕迹。你迁移掉的依赖项只是 npm 上的另一个包。你刻意删除的不稳定测试(Flaky test)是一个等待被“修复”的覆盖率缺口。缺席——即你做出的决策留下的负空间——是不可见的。

那个由智能体编写的、实际上什么也没测的测试

· 阅读需 11 分钟
Tian Pan
Software Engineer

让一个编程智能体 (AI agent) “为这个模块添加测试”,你会得到测试。它们格式整齐,遵循你的项目规范,而且能够通过。覆盖率会上升。这个 PR 看起来非常尽职。然而,这些测试中很大一部分根本无法捕捉到你可能引入的任何 Bug。

这并不是一个关于模型太蠢的故事。智能体完全按照要求完成了任务。问题在于,“添加测试”和“添加能约束行为的测试”是不同的请求,而其中只有一个是能被一眼验证的。无论是真正的断言还是同义反复(tautology),绿色的对勾看起来都一模一样。

结果就是,测试套件的代码行数在增加,但效能却在萎缩。你最终得到了更多的文件、更多的 CI 耗时、更多的维护成本——而交付回归缺陷的概率却与开始前几乎无异。

混合 PR 队列:审查者吞吐量已成为瓶颈约束

· 阅读需 10 分钟
Tian Pan
Software Engineer

在过去的二十年里,制约理论(Theory of Constraints)在软件交付中的答案始终如一:瓶颈在于编写代码。我们围绕这一假设构建了一切——结对编程、IDE 自动补全、更快的 CI、更小的微服务,所有这些都是为了让更多的代码通过固定宽度的审阅管道。接着,编程 Agent 出现了,管道的生产端拓宽了 5–10 倍,而审阅管道的宽度却纹丝不动。一位过去每周提交 3 个 PR 的资深工程师,现在正监督着一群在一个下午就能提交 30 个 PR 的智能体。团队的交付速度不再取决于编写代码的速度,而是取决于人类阅读代码的速度。

这并非未来的问题。据测量,在某些样本中,PR 审阅时间的中位数同比增长了 441%,并且在未经任何审阅的情况下就被合并的 PR 增加了 31%——这并非出于政策规定,而是因为审阅者已经放弃了跟上进度。Stripe 每周交付超过一千个由 Agent 生成的 PR。在一项基准测试中,特性分支(feature-branch)的吞吐量同比增长了 59%,而主分支(main-branch)的吞吐量却下降了 7%——代码正在被编写,但没有被发布,因为它们卡在了审阅环节。