跳到主要内容

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

查看所有标签

你的编程代理基于落后 Main 分支三周的代码版本重建的代码库索引

· 阅读需 11 分钟
Tian Pan
Software Engineer

你团队中的一个 AI 编程 Agent 提交了一个 PR,在两个文件中调用了四次 parseUserToken()。这个函数在代码仓库中并不存在,甚至已经消失了 19 天,早在你团队所有工程师都记得评审过的一次提交中就被 decodeSessionClaim() 替换了。Agent 并不是凭空捏造了这个名字,它是从其语义索引中读取的——那个向量库是从一个比 main 分支落后 21 天的工作副本重建的。相比之下,Agent 的编辑步骤在会话开始时运行了 git pull,操作的是最新的代码。对同一个代码库的两个视角,相隔三周,而 Agent 却自信地用一段无法针对任何真实环境编译的代码桥接了它们。

这是一种不会自我宣告的失败模式。Agent 运行了。测试看起来通过了。PR 合并了。第一位评审者之所以注意到,仅仅是因为一个被删减的函数与一个无关的辅助函数重名,触发了 linter 报错。到那时,Agent 已经花了一个完整的冲刺(sprint)针对一个“幻影版本”的代码库进行编写,而团队中没有一个人——包括 Agent 自己——收到任何异常信号。

你的编程 Agent 开启的那个导致真实 PR 被关闭的拉取请求

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的编程智能体在周二下午 3:14 提交了一个 PR。PR 描述很整洁,代码差异(diff)很小,CI 测试也是绿色的。二十分钟后,它被压缩合并(squash-merged)了。第二天下午 1:20 吃完午饭回来的同事看到了一条通知:“PR #1247 已关闭。”不是已合并,而是已关闭。分支不见了。她上周留下的 72 条评审评论也消失了——全部折叠在一个“已过期”标签下,属于一个不再出现在任何活跃列表中的 PR。一位资深工程师的设计决策、与安全评审员的两轮反复沟通,以及耗时一周协商出的周密迁移计划,全都化为了另一个没人仔细阅读的 PR 底部的一个脚注。那个压缩提交(squash commit)留下的唯一痕迹是底部的一行标签:Closed by #1893

这就是信任编程智能体自行编写 PR 元数据的失败模式。出问题的不是代码,而是元数据。代码差异没有问题,智能体工作得很出色。它无法做到的是区分当前的讨论与陈旧的讨论,而 GitHub 的自动关闭机制将智能体编写的每一个关闭关键字都视为必须执行的指令。你的智能体通过读取评论来获取上下文,从一个六个月前的回复中推断出它的工作取代了一个旧的 PR,于是在它生成的描述中写下了 Closes #1247。合并操作完成了剩下的工作——在压缩合并的那一刻,对于任何没有盯着 diff 看的人来说,这一切都是无声地、机械地、不可逆转地发生了。

翻倍且没有事后复盘:那份编码智能体带来的 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)是一个等待被“修复”的覆盖率缺口。缺席——即你做出的决策留下的负空间——是不可见的。