跳到主要内容

AI 编程代理在遗留代码库上的表现:为什么在你最需要它们的地方,它们往往会失败

· 阅读需 11 分钟
Tian Pan
Software Engineer

最迫切需要 AI 编程帮助的团队,通常并不是那些正在构建全新服务(greenfield services)的团队。他们往往正在维护 2012 年产出的 50 万行 Rails 单体应用,或是处理过数十亿笔交易的 COBOL 支付系统,亦或是架构师早在三次收购前就已离职的微服务网格。在这些代码库中,一个位置不当的重构就可能引入隐蔽的数据损坏漏洞,而这些漏洞往往在三周后的生产环境中才会浮现。

而这恰恰是目前的 AI 编程助手(agents)失败得最惨烈的地方。

令人沮丧的是,这种失效模式在爆发前是隐形的。AI 助手生成的代码可以通过编译,通过现有测试,并在审查中看起来非常合理。问题往往出现在预发环境(staging)、深夜的批处理作业,或者是某个客户在月份特定日期才会触发的边缘情况中。

全新项目基准测试陷阱

现代 AI 编程助手在 HumanEval(根据文档字符串实现函数)和 SWE-bench(修复孤立的 GitHub 问题)等任务上进行基准测试。这些基准测试几乎已经饱和——前沿模型在 SWE-bench Verified 上的得分超过 80%,而 HumanEval 已被基本攻克。

但最近发布的一项名为 SWE-EVO 的基准测试衡量了不同的维度:在真实遗留系统上的长周期软件演进。目前最好的模型只能完成 21% 的任务,而相比之下,它们在 SWE-bench Verified 上的完成率为 65%。这 21% 与 65% 之间的差距,真实地反映了当 AI 助手离开受控、自洽的孤立错误报告世界时,其性能是如何退化的。

原因在于上下文(context)。全新代码在机械意义上是自描述的:约定是明确的,依赖图能装进上下文窗口,没有缺失的内部知识。遗留代码则恰恰相反。关于一个函数可以安全执行什么的实际约束,散落在事故后分析(postmortems)、Slack 讨论串、2017 年的注释以及一位在 2020 年离职的开发者的记忆中。

AI 助手是在“能运行的代码”上训练出来的,而不是在“能存活的代码”上。

实践中的“似是而非”是什么样子的

CodeRabbit 对 470 个开源拉取请求(PR)的分析发现,AI 编写的代码每 PR 产生的问题比人工编写的代码多 1.7 倍。详细分类揭示了真相:逻辑错误增加了 75%,安全漏洞出现的频率是 2.74 倍,I/O 性能问题频繁了 8 倍,并发/依赖错误大约翻了一番。这些类别的问题通常无法通过审查时的惊鸿一瞥发现。它们是那种会静静地潜伏在生产环境中数周的漏洞。

失效模式通常可以归纳为几种典型的模式:

未记录的不变量(Undocumented invariants)。 每个成熟的代码库都有必须在特定条件下调用的函数——例如在特定的初始化序列之后、绝不能从多个 goroutine 调用、或仅在某些配置下使用非空参数。这些不变量不在函数签名中。它们在编写者的脑海里。一个将该函数移动到新模块的 AI 助手看到了干净的代码和类型签名。它看不到那些不变量。

隐式依赖顺序(Implicit dependency ordering)。 服务 A 写入队列。服务 B 从中读取。服务 A 恰好在服务 B 检查之前刷新(flush)。没有人记录这种时间依赖性,因为它一直有效且从未崩溃。重构刷新逻辑的 AI 助手改变了时序。现在,服务 B 在生产环境的负载下,会断断续续地看到空队列。

看起来不像是测试缺口的测试缺口。 遗留代码通常有 60–80% 的行覆盖率,但这几乎毫无意义,因为测试的编写初衷是覆盖代码行而非行为。一个通过了测试套件的 AI 助手,对于它是否保留了系统的实际行为几乎没有验证。Meta 发现,在他们构建预计算系统来记录隐性知识之前,其 4,100 多个流水线模块中只有约 5% 具有 AI 助手可访问的上下文。

基于 DRY 错觉的过度抽象。 当两个函数看起来相似时,AI 助手会合并它们。通常它们相似只是偶然的——底层的领域概念不同,随着需求演变,相似性会发生分歧。合并后的版本充满了条件判断。在 AI 参与度高的项目中,重复代码率从 3.1% 跃升至 14.2%,平均 file size 几乎翻倍。代码看起来“更干净”了,但也更难修改了。

为什么标准的防御措施无法捕捉这些漏洞

最显而易见的反应是:代码审查(code review)。但在遗留代码库中,代码审查在面对 AI 生成的更改时往往会失效。审查者依赖于 AI 助手所缺乏的上下文——AI 助手输入中缺失的那些内部知识,同样也缺失在审查者检查代码时的心理模型中。

在 AI 编程助手普及后的 12 个月里,每个 PR 的事故增加了 23.5%,变更失败率上升了 30%。在 AI 密集的仓库中,静态分析警告增长了 18%,认知复杂度分值上升了 39%。这些指标并不像是一个如宣传般奏效的技术所应有的。

标准的 Lint 检查和类型检查门槛起不到作用,因为错误不是语法或类型层面的。它们是语义上的——代码的行为偏离了系统的需求,这种偏离只有在系统运行时才会显现。

真正有效的脚手架模式

正在这一领域取得进展的团队已经不再将 AI 代理视为自动化的工程师,而是将其视为需要结构化护栏的、速度极快的初级工程师。这种规范是架构层面的,而非提示词工程(prompt-engineering)。

为每个 PR 设置范围限制。 将代理生成的 PR 限制在 20 个文件和 500 行新增代码以内。这并不是为了限制代理的能力,而是为了控制影响范围(blast radius),并确保人工评审是可行的。大规模的 AI 提交会掩盖严重的问题。即使代理可以一次性完成整个重构,也要将其拆分为可评审的块。

在生成任何代码前进行只读分析。 在代理触碰代码之前,要求其生成依赖图谱,识别它计划修改的功能的所有调用点,并列举从注释、测试和提交历史中发现的不变性约束(invariants)。这一阶段可以在问题变成代码之前捕捉到“我以前不知道这一点”类的问题。它还能识别出代理缺乏足够上下文来安全操作的情况——在这些情况下,应该停止操作,而不是凭猜测继续。

在重构前进行基准测试。 AI 重构最大的风险不是代码混乱,而是隐性的行为变化——函数在边缘条件下返回的内容可能略有不同,而现有测试并未覆盖这些条件。在允许任何代理驱动的重构之前,先运行特性测试(characterization tests,记录当前行为而非正确行为的测试)。如果你因为代码太乱而无法编写特性测试,这本身就是一个信号:代理目前还不应该碰这段代码。

针对高风险更改进行影子运行(Shadow runs)。 对于影响共享基础设施的更改,将新版本作为“影子”部署——接收真实流量,产生真实输出,但不实际执行。在切换之前,将影子输出与生产输出进行对比。虽然这种设置成本很高,但它是验证缺乏足够测试覆盖的代码是否保持行为一致的唯一方法。

显式地预计算团队隐性知识。 Meta 的方法——构建一个由 50 多个专业代理组成的系统,来记录其流水线代码库中的隐性知识——将上下文覆盖率从 5% 提高到了 100% 的模块。结果是,由于代理不再需要在运行时重新发现上下文,每个任务的工具调用次数减少了 40%。这种投入在代理的准确性上得到了回报,而不仅仅是文档质量。

限制代理的写入权限。 对代理可以修改的文件路径使用允许列表。基础设施配置、数据库迁移文件和身份验证代码应要求显式的人工启动。一个无法意外修改迁移文件的代理,也就无法意外引入破坏数据的更改。

代码健康度这一前提条件

从实践者那里得到的一个发现是一致的:AI 代理在低质量代码上的表现更差,而不仅仅是在大型代码库上。过长的函数、职责过多的类以及圈复杂度过高的模块,对于代理来说更难进行正确推理——就像对人类来说一样难。

这创造了一个有用的前提条件检查。在允许代理修改模块之前,测量其代码健康分。如果低于阈值(某个团队使用的是 8.5/10),则要求先由人工进行清理。这种清理提高了代理在后续自动化工作中的准确性,并减少了代理“似是而非”的错误可能隐藏的区域。

CodeScene 的 Adam Tornhill 将其总结为:“速度会放大设计决策的好坏。” 在结构良好、有良好测试覆盖且有显式文档的代码上工作的代理,能快速生成高质量的更改。而在混乱、隐晦、缺乏文档的代码上工作的代理,则会更快地产生混乱、隐晦、缺乏文档的更改。

基准测试差距说明了什么

目前最顶尖模型在 SWE-bench Verified (65%) 与 SWE-EVO (21%) 表现之间存在的 44 点差距,反映了一个行业必须诚实面对的事实:我们还没有能够真正理解遗留系统的代理。我们有的只是能够正确处理孤立的、定义明确的、且代码大多自文档化的更改的代理。

这并不是毫无用处。在成熟的代码库中,孤立且定义明确的更改经常发生。一个能够可靠地实现遵循现有模式的新端点、或向模型添加新列并更新所有相关读取路径、或为定义明确的工具函数编写测试的代理,确实能节省大量时间。

错误在于范围。代理最擅长处理那些资深工程师会评价为“这基本上是机械性工作”的、边界清晰的任务。而在那些资深工程师会说“我需要仔细考虑下游影响”的任务中,它们是危险的。然而,由于业务压力,这些危险的任务往往最容易诱使人们去尝试 AI 辅助。

展望未来

2026 年技术现状的真相是:AI 编程代理只有在被足够的脚手架包围,且这些脚手架承担了大部分风险管理工作时,才对遗留系统有用。这些脚手架——范围限制、只读分析阶段、特性测试、影子部署、显式的团队知识文档——都是真正的工程工作。那些因为代理看起来很自信而跳过这些步骤的团队,正是那些在撰写事后分析报告的团队。

前进的道路并不是让代理更聪明地理解遗留代码,尽管这最终会实现。而是让遗留代码库对代理更加清晰易读:显式的不变性、记录在册的依赖关系、以及能够真实反映系统预期行为的行为测试覆盖。这些工作对人类工程师同样有益,这意味着无论代理是否能完全弥合差距,这些投入都是值得的。

在成熟系统中,最能从 AI 编程辅助中获益的团队,并不是那些最早采用代理的团队,而是那些投资于让代码库变得清晰易读的团队——无论是对代理,还是对下一位加入团队的人类工程师。

References:Let's stay in touch and Follow me for more thoughts and updates