67 of 584

为何在AI时代,代码验证比以往更重要

B
ByteByteGo

能够顺利执行的代码,与真正安全、值得信任的代码之间,差距正在迅速扩大。多年来,编写代码一直是缓慢而昂贵的环节,审查则是最后一项相对小的任务。随着AI辅助编程兴起,这种平衡正在改变。

如今,AI工具可以在几秒内生成一个能运行的函数,在几分钟内完成整个功能。团队每个月都能写出更多机器生成的代码。换句话说,生成代码如今既快又相对容易,代码验证(code verification)才是更难的部分。审查者仍然需要阅读变更、理解变更,并判断它是否应该进入生产环境。事实上,写出的代码越多,需要验证的代码就越多。

我们最近有机会与Andrea Malagodi交谈。他是Sonar的CTO;这家公司开发了一些使用最广泛的代码验证软件。他深入解释了代码验证,尤其是在AI背景下,Sonar如何适应最近发生的变化。

本文将探讨代码验证如何运作,为何AI生成代码的兴起会给它带来更大压力,并介绍Andrea对未来的有益见解。

工作重心的倒转:AI出现前,编写代码占用较大部分;使用AI后,编写所占部分缩小,验证、审查和集成所占部分扩大。从想法到生产,二者之间的分界向左移动。

转变

观察数据,代码生成与验证之间的转变十分明显。最清晰的信号之一来自Google的DORA研究。这个长期研究项目考察数千个团队如何构建和交付软件。最近的研究发现,随着团队采用更多AI,交付稳定性有所下降。人们对AI生成代码的信任仍然较低,远超三分之一的开发者表示,他们对这些工具生成的内容缺乏信心[2]。换句话说,编写代码更快,也给后续环节带来了更多压力。

研究机构METR的一项对照试验给出了类似信号。参与者是经验丰富的开源开发者,工作对象是他们自己成熟的项目;每项任务都被随机分配为允许或不允许使用AI工具。开发者预计,AI能让他们快大约四分之一。

但结果呈现了完全不同的图景:AI辅助任务的耗时反而增加了约19%[3]。而且,即便如此,开发者仍认为AI提高了他们的生产效率。实际情况是,大量额外时间花在了编写提示、等待、阅读输出和纠正输出上。公平地说,同一个团队后来报告了一个更难解释的后续信号,部分原因在于开发者更愿意继续使用自己的AI工具[4]。

不过,把这些结果放在一起看,显而易见的是,AI确实增加了代码产量,同时也带来了更多后续验证工作。

因此,我们先来理解代码验证究竟意味着什么。

赢得信任

代码验证是一个总称,涵盖了确保一段代码足够正确、安全、可维护,可以交付到生产环境的所有检查。换句话说,它是在变更面对真实用户之前,赢得足够信任的工作。这里需要注意的关键词是“赢得”。因为信任有不同程度,它通过一次次检查逐渐建立,无法一举授予。

想一想起草合同这项任务。写下文字只是其中一部分。审阅、法律检查和签署,才让这些文字成为人们真正可以依赖的东西。写代码也是如此。代码离开编辑器并提交到代码仓库的那一刻,就带着对其功能的隐含主张。代码验证会检验这一主张,直到团队认为在真实生产环境中使用这段代码足够安全。

某些领域通过严格的形式化验证(formal verification),将这一环节推到极致。在这些领域,工程师必须用数学证明,即将部署的代码符合精确的规格说明。对于飞行控制系统和内核等关键系统,这样的过程是标准做法,因为一个缺陷就可能危及生命。但对大多数软件而言,采用类似方法的成本可能远高于收益。因此,团队会选择更轻量的检查,并将它们分层安排。

检查层次

进一步沿用分层的比喻,我们可以把代码验证想象成一叠过滤器。如下图所示,每一层都会捕获某一类问题。

检查层次及成本:类型检查器捕获类型使用错误;代码检查器捕获危险模式和风格疏漏;测试捕获返回错误答案的行为;人工审查捕获错误方法和遗漏的意图;生产监控捕获穿过此前各层的问题。成本向下递增。

最上层是最便宜的检查。例如:

  • 类型检查器(type checker):确认代码中流动的值,其类型是否恰好符合每项操作的预期。这样,它能在代码运行之前捕获一整类错误。
  • 代码检查器(linter):扫描可疑模式和代码风格问题。

这些检查几乎瞬间就能完成,成本也接近于零。再往下是测试。

单元测试(unit test)会用已知输入运行一小段代码,并确认它是否返回预期输出。测试能捕获类型检查器无法发现的行为错误,因为一段代码的类型可以完全有效,计算结果却仍然错误。例如,一个简单函数本来应该将两个数相加,却将它们相乘。在这种情况下,类型检查器和代码检查器都不会指出任何问题。只有将操作结果与已知答案比较的单元测试,才能揭示这个错误。

测试下面是人工审查这一层。另一位开发者会查看变更,判断它是否适合系统、是否解决了正确的问题,以及是否易于阅读。这一层能捕获机器可能遗漏的东西,例如某个解决方案虽然能运行,却采用了团队标准不推荐的方法。

所有这些层次之下,是生产监控系统。它的任务是在真实流量下观察代码,并标记可能穿过此前所有层次的问题。

现实中的检查层次还可以更多,例如安全扫描器和依赖检查。但重点在于,每一层都会弥补上面一层的某项弱点。因此,认真对待软件的团队会按照特定顺序运行多层检查,然后才将代码发布到生产环境。

静态分析与动态分析

这些过滤器分为两类:

  • 静态分析(static analysis):不执行代码,直接检查源代码,因此快速且范围广。静态分析可以一次扫描整个代码库,类型检查器和代码检查器都属于这一类。代价是,它无法完全看到真实的运行时行为。因此,静态分析有时会对一个在实际运行条件下可能并不存在的问题发出警报。
  • 动态分析(dynamic analysis):用真实输入运行代码,并观察结果。测试属于这一类。动态分析依靠对实际行为的检查,但只能覆盖真正执行过的路径。如果测试套件只运行正常路径(happy path),就无法发现空输入可能引发的崩溃。

静态分析与动态分析:前者不运行代码,快速识别结构与模式,但会遗漏真实运行时行为;后者输入数据、运行代码并观察结果,能看到执行路径上的真实行为,但会遗漏未执行的路径。

即使覆盖范围广,扫描结果干净、测试全部通过,两者加起来仍可能留下空缺。因此,代码验证需要多个过滤器协同工作,才能发挥作用。

误报

我们很容易得出一个诱人的结论:检查越多总是越好。但代码验证的核心存在取舍。每个过滤器都可能犯两种错误:

  • 假阳性(false positive):代码实际上没有问题,却被标记为有问题,也就是误报。
  • 假阴性(false negative):真正的缺陷漏过去了,工具却没有发出警报,也就是漏报。

如果把工具调到试图捕获所有潜在问题,它可能让开发者淹没在误报中。但如果把工具调到只有确定时才报警,它又可能开始漏掉真实缺陷。这两个方向互相牵制。

误报的代价很高。工具频繁误报,开发者就会开始忽略它。但这种习惯可能造成灾难:即便偶尔出现真正的警告,也可能和其他警告一起被一笔带过。有关静态分析工具的研究描述了这种模式:高假阳性率会侵蚀信任,直到团队关闭工具,或不再理会它的警告。这样做,却会重新给那些工具原本要阻止的缺陷打开大门[7]。

混淆矩阵:有缺陷且工具沉默是假阴性,缺陷漏到生产环境,是最坏结果;有缺陷且工具报警是真阳性,及时捕获问题;无缺陷且工具沉默是真阴性,没有问题也没有警报,信号干净;无缺陷却工具报警是假阳性,误报逐渐侵蚀信任。

因此,良好的代码验证体系既关心覆盖范围,也关心信号质量。Sonar的Andrea把这种平衡比作代码验证中的CAP定理。这个比喻来自一个经典想法:某些性质的提升,只能以牺牲另一些性质为代价。代码验证中相互竞争的三项优先目标是速度、准确性和覆盖范围,没有工具能在三项上都完全胜出。Andrea的团队尝试始终把人放在中心,遵循一条简单规则:如果某个发现能让开发者采取行动,它就值得被报告。

过滤器放在哪里,也会改变一次错误所带来的成本。这就涉及代码验证流水线的整体安排。

流水线

过滤器会在变更请求生命周期的不同时间运行,时机决定了成本。将这些时刻按顺序展开,就得到一条流水线。变更从开发者的编辑器开始,经过代码提交时触发的一组自动检查,再到审查、合并,最后进入部署和线上监控。

同一项检查运行得越晚,成本就越高,因为随着开发推进,错误之上会叠加更多工作。例如,在编辑器中发现缺陷,可能只需要作者花片刻注意力。但同一个缺陷到达生产环境后才被发现,可能引发事故或回滚,也可能影响用户。这才是“左移(shift left)”的真正含义:把检查移到流水线更早的位置,在问题修复成本仍然较低时,让它们暴露出来。

验证流水线:编辑器、提交、CI检查(代码检查、测试、静态分析)、审查、合并、部署、监控。左移意味着更早发现问题;示意曲线展示修复成本从早期较低,向后期较高变化。

供应商常常会用精确倍数来宣传这种方法。你可能遇到这样的说法:缺陷每往后推进一个阶段,修复成本就会增加十倍。具体数字值得怀疑,但重要的是,从成本角度看,把检查提前这一总体方向是有益的。

AI带来的压力

这套检查层次建立在开发者编写大部分代码的环境中。正如我们所见,随着AI编程工具兴起,这个假设在过去几年中已经弱化。变化会以两种不同方式影响每一层。

第一种压力来自数量。

当智能体(agent)用过去一个人写一百行代码的时间写出一千行,审查负担就会急剧增加。它还会对批次大小产生一种微妙影响,也就是一次审查捆绑了多少变更。AI编程工具倾向于生成更大的变更,而大变更更难审查,因为面对庞大的差异内容(diff),注意力会被分散,小错误更容易漏过去。Andrea提到这种失败模式时,用了许多开发者都熟悉的情景:面对一个5,000行的拉取请求(pull request),审查者写下“看起来不错”,心想反正有问题也会在生产环境暴露。

第二种压力与AI可能犯的错误类型有关。一项覆盖一百多个模型的研究测试了AI生成代码的安全性,发现约45%的案例引入了已知安全缺陷[5]。

在同一时期,这些模型生成能够顺利运行的代码的能力大幅提高,安全检查表现却基本没有变化。换句话说,AI让代码能用的能力大幅改善,让代码真正安全的能力却只改善了一点。两者之间的差距甚至扩大了。另一项分析了数百万次代码变更的研究还发现,代码重复增加、复用减少[6]。

审查AI代码

当代码数量超过开发者能够仔细阅读和分析的范围,将部分代码验证交给机器就成了自然的做法。因此,AI驱动的代码审查在过去几年中获得了明显的发展动力。

AI代码审查者有三个主要优势:

  • 速度:变更一出现就能扫描,早于人工查看。
  • 覆盖范围:及早捕获相当一部分缺陷和安全问题。
  • 一致性:对每次变更和每位团队成员应用相同标准。由于AI代码审查具有概率性,维持一致性可能很困难。因此,需要多层审查,同时包括AI驱动工具和其他确定性的算法工具。

这种审查也可以在智能体自身的循环中运行。智能体写出初稿,审查者标记问题,智能体再进行纠正,全部发生在开发者看到代码之前。这样可以缩短反馈循环,从人工审查者手中移走常规工作,最终帮助团队处理更多生成代码。

见下图:

AI审查循环:智能体编写,AI审查,智能体修复,构成自我纠正循环;当代码看起来没有问题时,再交给人工审查者。

但这种方法也存在风险。如果审查模型与生成代码的模型属于同类,它们往往采用相同假设,因此也有相同盲点。当编写和审查都依赖类似训练和类似模式时,审查者可能会确认代码看起来是对的,而代码是否实现了实际意图这个关键问题仍然未被关注。换句话说,两个相似模型可能更像同一个意见被说了两遍,而不像两次完全独立的检查。

设想一个智能体将任务单转换成函数。AI审查者扫描后报告代码没有问题。代码能编译、能运行,也符合常见模式。但它是否实现了任务单的真实含义,无法仅靠模式匹配回答。对此存在不同观点:

  • 有人认为,模型的能力已经足以减少、甚至取消人工审查。
  • 另一些人认为,人类在架构、上下文和责任方面的判断仍然不可或缺。

两派都有合理理由。今天比较合理的回答是:这取决于你正在交付什么,以及潜在错误的代价。

现代验证体系

接下来看看,这些想法如何组成真实工作流。

一切从上下文开始。大多数工程工作发生在既有代码(brownfield code)中,也就是具有历史和种种特殊情况的大型现存代码库,而非从零开始的新项目(greenfield projects)。智能体如果没有指导就查看这些代码,会自行摸清布局,但每次采用的方法都不一样。Andrea把这种不一致称为“一盒巧克力”:每次运行、每位开发者得到的结果都可能不同。

成熟的体系会在一开始就向智能体提供共享且一致的整体认识,包括真实架构、对应语言的编码指南,以及试图表达预期设计的规则。三个循环负责验证工作,概括如下:

  • 智能体循环(agentic loop)——智能体迭代构建的地方:优化智能体沙箱内生成的代码,提高智能体效能;同时降低token成本、提升输出质量并减少风险。
  • CI验证循环(CI verification loop)——面向所有代码的验证流水线:负责代码审查、零信任、多层验证,以及沙箱出口的质量门禁;同时让团队有信心地高速、大批量合并修复。
  • 代码维护循环(code maintenance loop)——在后台修复技术债:持续巡查,以智能体方式在后台处理遗留问题。更干净的代码能让编程智能体更高效地工作。

Andrea还帮助描绘了2026年成熟体系可能具备的样子。以Sonar为例,现代验证体系包括以下组成部分:

  • 验证引擎(verification engine):包含数千条规则,对超过40种编程语言、框架和基础设施即代码(IaC)技术的可靠性、可维护性和安全性进行自动代码分析。Sonar的产品SonarQube(它最为人熟知的产品)和Sonar Vortex(一项新解决方案)在智能体循环中提供这项能力。
  • AI代码审查(AI code review):一项基于AI的新能力,来自Sonar对Gitar的收购。它使用精心编写的指令,而非固定规则。真正价值在于,让每项发现都能向审查者解释清楚,使一个5,000行的变更成为开发者能够推理分析的内容。它位于CI验证循环中。
  • 修复智能体(remediation agent):面向现有积压问题,逐步处理旧问题、清理历史,而非只守住新代码。它覆盖代码维护循环。

其他公司也或多或少趋向类似想法。

保持代码干净还带来一个很大优势。Sonar团队测量了AI生成代码在半年或更长时间内,经过多次开发会话持续演变时会发生什么。这类代码常常纠缠、密集。他们发现,混乱代码最终会消耗更多token,因为每次变更时,AI模型都需要投入更多精力来理解它。

这场讨论最前端还存在另一项问题:机密信息(secrets),也就是API密钥、密码等凭证。机密信息的危险很少来自蓄意破坏。大多数时候,是有人把代码粘贴到AI会话中,或加载了配置文件。机密信息随之进入变更,通常被发现得太晚,那时它已成为提交或日志文件的一部分。

解决办法是在开发者粘贴代码之前,就在终端运行扫描器。换句话说,目标是在流程允许的最早时刻阻止它发生。Sonar称这种方法为“从左边开始(starting left)”,比我们前面讨论的“左移”又提前了一步。Andrea建议每位开发者都运行这样的防护,确保机密信息安全。

信任与风险

所有这些观点都引向一个重要的实践问题。

某项变更究竟需要多少代码验证?

答案取决于一次特定失败的代价。判断这个代价,才是真正需要洞察力的技能。例如,营销页面上的错别字,与支付系统中的缺陷,值得接受截然不同的审查。开发者或许花一分钟、没费多少力就能改好错别字。但支付系统的缺陷可能把钱转到错误地方、破坏信任,还引发一条谁也不愿看到的新闻。

成熟团队会把验证深度当成一个随风险因素调节的旋钮。低风险变更经过轻量自动检查即可通过,高风险变更则经常交给人工,并接受更严格审查。

信任与风险:横向从低风险到高风险,纵向为验证深度。低风险区采用轻量自动检查并自动合并,高风险区交给人工,以更严格的审查和判断处理;验证深度随风险增加。

这条界线究竟画在哪里,需要团队自己作出判断。例如,一个逗号就可能决定操作系统能否运行,还是直接崩溃,因此必须有意识地选择风险承受程度。没有一条固定规则能够适用于所有情境。

智能体在护栏范围内,独立将变更尽可能推进到干净、经过验证的状态。它们可以自动合并低风险工作,把风险更高的内容交给开发者。但恰当地设置这些层级,让变更被送到适当强度的检查中,是一项正在形成的实践,随着时间推移只会越来越重要。

结论

软件开发的重心正在移动。写代码如今已成为更快的活动,而验证同一段代码、确认它正确、安全、值得交给真实用户,似乎正成为需要投入更多工作的地方。

正如我们所见,代码验证由多层过滤器组成,每一层都用一些成本换取一些信心,并弥补上一层的某项弱点。这些过滤器分为静态和动态两类。每个过滤器都在误报与漏报之间取舍,将它们提前运行,则能让错误的代价保持较低。

AI生成代码的洪流影响了这一切,同时增加数量和风险。让AI审查AI这一诱人的捷径,也有实实在在的陷阱:机器检查机器,可能一致认为代码看起来没问题,却忽略了更重要的检查。

这场转变对人类工作的影响是,随着写代码变得便宜,开发者的工作反而更重要。开发者需要花更多时间通过提供指令来协调智能体,也可能需要更多关注那个更古老、更困难的问题:究竟应该构建什么。更便宜的代码生成,为这种判断提供了更多时间空间,并没有消除判断的必要性。

参考资料

  1. CrowdStrike故障:我们终于知道了原因和代价
  2. 2024年DORA报告发布
  3. 衡量2025年初AI对资深开源开发者生产效率的影响
  4. 我们正在调整开发者生产效率实验设计
  5. 2025年生成式AI代码安全报告
  6. AI Copilot代码质量:2025年数据表明,代码克隆增长至原来的四倍
  7. FP-Predictor:静态分析报告中的假阳性预测
  8. 人人都说生产环境修复缺陷贵100倍,但那项研究可能根本不存在

此文章由AI翻译。