☰
大型语言模型在自动化程序修复中的实证评估方法与流程
2026/10/21 21:18:00 网站建设 项目流程

前阵子我拿三个主流大语言模型做了一轮自动程序修复的横向实证对比。跑完这轮实验,我最大的感受是:如果你想演示“模型会修bug”,那太容易了,随便丢一个缺陷进去它就能生成补丁;但如果你想搞清楚“模型到底能修好多少真实缺陷、哪些缺陷类型它无能为力、哪些评测设计会骗人”,这就必须做一套严谨的实证评估。这篇内容不是论文复现,而是我把这套评估流程从零搭起来的过程中,整理出的方法、代码细节、指标设计和踩坑记录。文章的标题是“大型语言模型在自动化程序修复中的实证评估”,下面所有讨论都围绕实际操作展开。

1. 项目全景:为什么LLM自动修复需要实证评估

1.1 自动程序修复到底在解决什么问题

自动程序修复(Automated Program Repair,简称APR)的目标很简单:给定一个有bug的程序和一组测试用例,其中至少有一个失败测试能反映缺陷,自动生成一个补丁,使得修复后的程序能通过全部测试。这个领域并不是因为大语言模型才出现的。传统APR方法包括基于模板的修复,也就是把常见错误模式套用到缺陷位置;基于约束求解的修复,通过符号执行搜索满足条件的补丁;还有基于搜索的程序合成方法。

但这些传统方法有一个共同问题:修复能力受限于人工定义的模板和规则。软件缺陷千奇百怪,很多缺陷依赖上下文语义甚至外部行为,模板根本覆盖不了。实际跑过传统APR工具的人都知道,它们要么生成的补丁太少,要么大量补丁只是让测试通过但逻辑是错的——也就是大家常说的“测试过拟合”补丁。我在模拟项目X上试过一个经典模板工具,面对70个缺陷只修好了8个,而且其中3个是硬编码绕过,根本没有通用性。

大语言模型进入这个领域后,事情发生了本质变化。模型从海量代码中学到了程序语义和常见修复模式,不需要人工枚举模板。你只需要把缺陷代码、上下文、失败测试信息拼成一个提示词,模型就能直接生成修复后的代码。实验里我用模型A修一个需要理解状态机转移逻辑的缺陷,它给出的补丁不仅让失败的测试通过了,还把相邻的一个潜在越界问题一并修正——这种“理解语义后主动修复”的行为,传统模板工具完全做不到。

但问题也随之而来:不同模型的修复能力差异很大,同一个模型在不同缺陷上的表现极不稳定,而且评估方式稍有变化,结论就可能反转。比如模型B在一个函数级基准上修复率为45%,看起来不错,但换一个上下文组织方式后掉到28%。这种不稳定性直接催生了做严格实证评估的必要性。

1.2 实证评估:不是跑通demo,而是一套严格的对照实验

实证评估(Empirical Evaluation)在软件工程研究里是成熟的方法论,核心是把研究问题变成可测量的实验假设,通过可控实验、基线对比、统计检验得出结论。放到LLM自动修复这个主题上,实证评估至少回答三个层面的问题。

第一层是修复能力层面:模型在给定测试失败信息和代码上下文之后,能产出多少补丁?这些补丁有多少是真正正确的,不只是让当前测试通过,而是经得起额外验证或人工判断?我在实验中经常遇到补丁通过原测试但被额外测试打回的情况,这层评估远比想象中复杂。

第二层是鲁棒性层面:换一批缺陷、换一个数据集、换一种提示词模板,结论还成立吗?一开始我用同一套提示词在模型A和模型C上做对比,模型C的修复率只有模型A的一半。但后来我把提示词按模型各自的偏好调整后,差距缩小到不足10个百分点。这说明如果不做多配置对比,很容易得出“模型C很差”的错误结论。

第三层是工程层面:在实际修复流水线里,定位缺陷、生成补丁、验证补丁这三个环节,瓶颈到底在哪里?评估时如果把定位环节省略掉,直接给模型精确到行号的缺陷位置,结果会非常好看,但对落地没有参考意义。真实项目里,定位一个缺陷往往比修复它更耗时。

在设计评估流程时,我始终提醒自己一件事:不要让实验设计偏向让模型“看起来更强”。这一轮实验里,我选择了一个经典Java缺陷基准库、一个Python缺陷基准库、一个工单级现代修复基准,三个模型分别是某商用闭源大模型、某开源大模型、某中等规模的代码专用模型。下面所有讨论都基于这三组实验的实际记录。

2. 评估方法论:基准、指标与实验设计

2.1 基准选择:数据泄漏比我想象的更严重

做LLM评估,第一个要面对的问题是基准选择。过去研究APR常用的几个缺陷基准库,现在需要重新审视,因为它们很多已经被纳入了大模型的训练数据。这里说的“纳入”不一定是完整代码,可能是某段函数的变体、某个缺陷修复的讨论、或者Stack Overflow上的问答片段。模型见过这些内容之后,修复变成了一种记忆回忆,而不是推理。

我一开始用的经典Java缺陷基准库,在LLM时代之前是APR领域的黄金标准。结果实验跑完,我遇到一个非常诡异的现象:模型C在其中的修复率高达62%。这个数字高得离谱,因为模型C的参数量只有同组模型A的三分之一。后来排查发现,这个基准里的很多缺陷版本,模型早就在训练语料里见过了,或者是缺陷和补丁的配对信息已经以代码审查讨论的形式出现过。严格说,这个数字衡量的是记忆能力,不是修复能力。

为了规避这个问题,我把基准分成三类:

  • 训练覆盖可能较高的经典基准,存在泄漏风险,结果仅作参考
  • 较新的低泄漏风险基准,包括近两年发布的缺陷库和工单级数据集
  • 私有验证集,通过修改小型开源项目构造,这部分完全是干净的

后两类才是判断模型真实修复能力的主要依据。私有验证集构建起来很繁琐,我从一个约5000行的内部工具项目里挑出20个历史缺陷,每个缺陷都有一个对应的失败测试和确认修复补丁。这20个缺陷覆盖了空指针、逻辑条件错误、资源未释放、并发竞争等类型。因为项目代码从未公开发布,所以不存在泄漏问题。

处理经典基准时,我做了另外一个工作:代码混淆脱敏。把变量名替换为无意义符号、删除注释、调整空白后,模型的修复率从62%降到了39%。这个实验说明一个问题:如果模型只是记住了某个函数的“样子”,一旦外观变化,记忆就没用了。但真实项目里缺陷代码不会长这样,所以这种偏差直接来自评估数据本身。

2.2 评估指标:修复率不能只看一个数

指标设计是实证评估里最容易出错、也最容易被忽视的一环。初版实验里我只统计了补丁与参考补丁完全相同的比例(通常叫完全匹配率),后来发现这个指标严重低估了LLM的真实修复能力,因为模型经常生成语义等价但文本不同的补丁。比如参考补丁是修改循环边界,模型给出的补丁是提前判断输入合法性,两者都能让测试通过且逻辑等价,但完全匹配率会把这个修复判为失败。

最终我采用的指标体系如下:

指标含义为什么重要
编译通过率修复后代码能否重新编译反映LLM生成损坏代码的比例,是基本代码生成能力的门槛
测试通过率修复后代码能否通过全部测试最基本的有效性门槛,但存在测试过拟合风险
正确修复率通过额外验证或人工确认的修复比例排除“假通过”骗局后的真实修复能力
补丁类型分布按修改粒度和模式分类统计揭示模型擅长与不擅长的缺陷类型

这里最核心的坑是“正确修复率”怎么算。如果只用原测试集验证通过就认为修复正确,可能会被一些靠删除断言或绕过逻辑的补丁欺骗。我在实验里采用两层验证:第一层运行原测试集,第二层对每个候选补丁做人工审查,或者用额外构造的测试用例再跑一遍。人工审查成本高但可信度也高,最终我对约40%的样本做了人工标注,其余用启发式方法过滤。

补丁类型分布这个指标一开始我没有重视,后来发现它是解释结果的关键。模型A修好的30个缺陷里,有18个是单行修改(常量替换、条件反转、空值判断),只有4个涉及跨函数调用调整;模型C修好的20个缺陷里反而有7个是跨函数修改。如果只报修复总数,模型A看起来更强,但按缺陷类型拆分后会发现模型C在复杂场景下的表现其实不弱。这些信息对团队选型非常重要。

另外一个容易忽略的细节是“修复基数”。有的基准里存在本来就“空转”的缺陷,缺陷版本本身就能通过所有测试,这类样本应该从统计中剔除。第一轮统计时我没做这个清洗,导致所有模型的修复率都被高估了2到3个百分点。清洗方法是:对每个缺陷的原始版本跑一遍完整测试套件,如果原始版本全部通过,则该缺陷标记为无效样本。

2.3 统计显著性与实验重复性

小样本实验最常犯的错误是只看平均值就下结论。第一轮实验里,模型A的平均修复数比模型C高18%,但我用配对检验一看,p值高达0.23,根本不显著。原因就是缺陷样本之间的难度差异太大,个别超难样本直接拉低了平均值。

正确的姿势是把每个缺陷看作一次配对观测:同一个缺陷,分别在模型A和模型C下跑,记录是否修复成功,然后使用配对统计检验。样本不多的时候,宁可多做几次重复实验、换不同的提示词变体,也绝不能靠一次实验的平均值下结论。这里还涉及一个关键概念:效应量。修复数量差异5个和15个,对应的实践意义完全不同。我后来在报告里同时报告了修复率差异和置信区间,而不是只写一个百分比。

重复性还涉及一个很多人忽略的问题:温度参数。如果评估时把温度设为1.0,同一个模型对同一个缺陷的修复结果每次都可能不同。为了让实验可复现,我在所有核心评估中把温度设为0(贪心解码),只在对比稳定性实验里才调高温度做多次采样。这看起来是个小细节,实际上对结论的可信度影响很大。我用温度0跑三轮完全相同的实验,修复结果完全相同;把温度调到0.8后,同一批缺陷跑三轮的修复数波动达到8%。

3. 实操过程:跑通一套LLM修复评估流水线

3.1 整体流水线设计

我的评估流水线分为四个阶段:缺陷准备、补丁生成、补丁验证、结果统计。每个阶段都有独立的脚本和输出格式,方便出错时回溯。

缺陷准备阶段要做的事情是:把每个缺陷的代码版本、失败测试、缺陷上下文整理成统一的JSON格式。对于需要定位的工单级基准,还要保存缺陷定位信息;对于已经给定缺陷位置的函数级基准,直接把相关函数切出来作为上下文。这个JSON结构我设计成下面这样:

{ "id": "defect-0017", "language": "java", "project": "internal-tool", "buggy_code": { "file": "src/main/java/Processor.java", "start_line": 112, "end_line": 145, "content": "完整函数内容..." }, "test_failures": [ { "test_name": "testProcessNullInput", "failure_message": "expected null but got NPE", "relevant_snippet": "测试中的断言片段" } ], "metadata": { "defect_type": "null_pointer", "patch_size": 2, "source": "private_validation_set" } }

补丁生成阶段是核心:把整理好的信息组装成提示词,调用模型接口,获取补丁文本。为了防止模型输出格式漂移(有时给整段代码,有时给diff,有时给解释),我在提示词里要求模型输出统一的diff格式,并配合解析器做校验。解析失败的情况单独记录,不计入修复失败。

补丁验证阶段对模型生成的补丁做两步验证:先编译,再运行测试。这里要注意的是,很多修复基准的测试套件非常消耗资源,必须做增量验证:只运行与被修改代码相关的测试用例,而不是全部测试。我最初用全量测试跑,两个模型跑完整个基准花了两天,后来改成增量验证,耗时压缩到4小时以内。

结果统计阶段把所有记录汇总成表格,输出每个基准、每个模型的对应指标。同时生成一个明细CSV,每行是一个缺陷的修复状态、验证结果、消耗token数,方便做深度分析。

3.2 提示词设计的几个关键选择

提示词设计在LLM评估里是个充满主观性的环节。同一个模型,提示词写得稍微不同,修复率能差10个百分点以上。我在实验中对提示词做了三种变体:

  • 纯代码上下文模式:只给代码和失败测试输出,不带任何额外说明
  • 功能描述模式:加入对修复目标的功能描述,说明这个代码片段应该做什么
  • 迭代反馈模式:如果第一轮生成的补丁测试失败,把失败信息加回去让模型重新生成

测试下来,迭代反馈模式在函数级基准上效果最好,但在仓库级基准上提升有限且成本翻倍,因为每次迭代都要重新调用模型。功能描述模式在两类基线上表现稳定,而且不容易受到上下文长度限制的影响。纯代码上下文模式的优点是最接近真实API调用场景,缺点是模型经常误解测试意图。

下面是我实际用过的提示词模板之一,针对diff输出做了强约束:

你是资深软件工程师。以下是某项目中的一段代码和失败测试信息。请根据测试失败原因修复代码中的缺陷。 代码文件:{file_path} 行号范围:{start_line}-{end_line} 代码内容: {buggy_code} 失败测试:{test_name} 失败信息:{failure_message} 要求: 1. 只输出统一的diff格式补丁,不要输出任何解释性文字 2. diff格式为:@@ -行号,列号 +行号,列号 @@ 3. 每一处修改都要对应原代码行,不要重写整个文件 4. 不要修改测试文件

这个提示词里我特意加了“不要重写整个文件”,因为早期实验里模型经常把整个函数重写一遍,虽然也能通过测试,但补丁规模过大,在实际代码审查中不会被接受。加入这条约束后,补丁平均行数从20行降到4行。

还有一个容易被忽视的细节:对输出格式的约束方式。我之前用“请直接输出修复后的完整函数”,结果模型偶尔输出带解释的markdown代码块,解析器直接崩了。后来改为明确要求“只输出diff格式,不要输出任何其他文本”,解析成功率从约70%提升到98%。这个教训让我在评估时格外重视提示词与解析器的对齐。

3.3 推理配置与成本控制

模型推理配置直接影响评估结果可比性。我在所有模型上统一采用温度0、最大输出token 1024、无额外采样。这样虽然损失了一部分修复上限(有些缺陷需要多次采样才能修好),但保证了三组评估结果之间的公平比较。如果评估目的是找修复上限,做法完全不同:会采用温度0.2到0.8、每个缺陷采样20到50次、然后聚类去重找最优补丁。这两种实验回答的问题不一样,不能混着来。

成本控制方面,我统计过一轮完整实验的token消耗:

基准类型缺陷数平均每缺陷消耗token总消耗约
函数级基准180300054万
仓库级基准501500075万
私有验证集20500010万

如果三个模型都跑全量,总消耗超过250万token。这还没算迭代反馈模式带来的额外消耗。我给的建议是:先用50到80个缺陷的小样本集做配置验证,调通提示词和流水线之后,再上全量基准。第一轮我没经验,直接全量开跑,最后发现提示词拼接处有个bug,浪费了大量预算。

另外建议给每个模型单独做一次成本预估:先跑10个缺陷,用实际token数乘以模型单价,再外推到全量。商用闭源模型和开源模型的成本差异可以到5倍以上,这个估算在选型时很有参考价值。

3.4 验证闭环的实现细节

补丁验证环节有个常见的工程陷阱:如何判定“测试通过”是真实通过。有的基准测试套件本身存在不稳定,比如依赖网络或时序,跑出来一个补丁通过了测试,重跑一次又失败。这说明测试环境不稳定,不能作为有效验证。我把所有测试运行限定在离线沙箱环境,并且对每个补丁的验证结果做至少两次重复运行,两次结果一致才记录。

沙箱环境我用的是轻量级容器方案,为每个基准单独准备一个镜像。镜像里只装必要的编译器、依赖库和测试框架。这样既保证了评审之间的隔离性,也防止测试程序访问外部网络。实测下来,容器重启开销在2到3秒左右,对批量验证没有明显影响。

另一个实现细节是“最小补丁验证范围”:仓库级基准里,测试用例动辄几百个,每个补丁都跑全量测试成本太高。我的做法是先编译看是否通过,然后用测试影响分析工具计算修改代码可能影响的测试用例集合,只跑影响范围内的测试。跑完再补做一小部分随机抽样的全量测试以控制遗漏风险。在模拟项目X的验证中,这种策略能覆盖到95%以上的真实失败,同时节省了大约70%的测试执行时间。

4. 常见问题与排查技巧实录

4.1 数据泄漏与基准污染排查

评估中发现单基准修复率异常偏高时,第一反应应该检查数据泄漏。我是这么排查的:先拿一个与修复无关的随机问题问模型,看它是否能“背出”基准库的信息;再用提示工程的方式,直接询问模型是否见过某个特定的缺陷函数片段。如果模型能准确背出基准库里的代码,说明这个基准对模型已经不新鲜了。

还有一种更隐蔽的泄漏形式:模型没有记住完整代码,但“见过类似的修复模式”。比如某个基准里大量缺陷都是数组越界,模型可能在训练语料里见过数千个类似修复,这种模式记忆也会推高修复率,但它衡量的其实是泛化能力中的记忆成分。很难完全区分,我在报告中用“基准泄漏风险等级”来区分可信度。

处理方式有两种:一是换新基准,二是在评估报告里单独标注“该基准存在泄漏风险,结果偏高”。我选择了两者结合:新基准作为主结论依据,旧基准作为对照参考,绝不混在一起计算平均值。最终报告里,主结论基于低泄漏风险基准和私有验证集,经典基准的结果单独列成一张参考表。

4.2 补丁“假通过”问题的识别

我在人工审查时遇到一种“绕圈补丁”:不修改缺陷逻辑,而是在失败测试处加一个try-catch吞掉异常,或者直接注释掉一段断言。用原始测试集验证时,这些补丁全部通过,看起来像修复成功,实际上一无是处。最典型的一个例子是:缺陷代码是get()返回null导致空指针,模型生成的补丁是在调用处加if (x != null)判断,但后续仍然使用x.field,逻辑上没有解决问题,只是让当前测试路径恰好没触发空指针。

识别假通过需要做额外验证:检查补丁是否改变了测试文件本身(修复补丁只能改源码,不能改测试)、检查补丁是否引入了未使用的异常捕获、以及在补丁前后对比测试执行的代码路径。我在统计“正确修复率”时,把这类补丁单独归为“无效修复”,避免影响结论。

启发式过滤规则我整理成了四类:

  • 补丁修改了测试文件或断言逻辑,直接判无效
  • 补丁新增了空的catch块或直接return,需要人工确认
  • 补丁引入了与缺陷无关的日志输出,通常视为噪音
  • 补丁删除了原有功能分支,需人工检查是否误删功能

4.3 上下文截断导致的假失败

仓库级基准的代码上下文经常超过模型的上下文窗口。早期实验有一次大规模修复失败,排查后发现上下文中部被截断了,模型根本没看到失败测试所在的那一行。修复方式是把上下文按“缺陷位置优先”的原则重组:把最相关的代码块放在提示词最前,而不是按代码文件顺序排列。这个改动很小,但对修复率的正面影响非常明显,在工单级基准上修复率提升了约15%。

具体实现上,我维护了一个上下文裁剪函数:先根据测试失败堆栈定位到最相关的函数,把该函数的完整代码放在最前面;然后是调用链上游的两层函数;最后才是类定义和其他无关代码。如果总长度仍然超出上限,就逐级裁剪最不相关的部分,而不是从头部截断。

4.4 对比不公平的经验

做模型间对比时,公平性比绝对效果更难保证。我先后遇到两种不公平情况:

  • 模型A支持更长的上下文,可以把整个文件都塞进去,而模型B只能看到精简后的上下文,这种对比本质上变成了上下文长度对比
  • 提示词是为模型A调优的,在模型B上效果自然差

第一种情况我在实验记录里明确标注了每个模型的有效上下文长度,并额外做了一组“上下文长度拉齐”实验,把模型A也限制在精简上下文中,这样才算公平对比了模型本身的能力。

第二种情况的解决方案是:每个模型各维护一套独立调优过的提示词,而不是所有模型用同一套提示词。这样虽然牺牲了严格一致性,但对比的是“在各自合理使用方式下的修复能力”,更贴近实际应用。同时在报告里明确每个模型的提示词差异,让读者能判断这种差异的影响。

5. 做这套评估的一点体会与后续可扩展方向

5.1 小规模复现的建议清单

如果你是第一次做类似的LLM修复评估,我建议最小化实验集合可以这样搭:一个函数级基准(100到200个缺陷)、一个私有小项目(20个缺陷)、两个模型(一个商用闭源、一个开源)、三个提示词变体。总token消耗控制在几十万以内,耗时一天内可以完成初步结论。不要一上来就追求全量基准和全模型矩阵,先把流水线逻辑跑对,再逐步扩展规模。

具体到流程,我的建议是分四步走:先跑通10个缺陷的端到端验证,确认编译、测试、统计环节没有逻辑错误;再扩展到一个函数级基准的小样本子集,调优提示词;然后加入仓库级基准,重点验证上下文裁剪和增量测试策略;最后才全量跑。每步之间一定要保存中间结果,方便回溯。

5.2 我总结的几个实操心得

用模拟项目X跑完这轮评估,我有几个体会。第一,评估的“实验设计”往往比“模型选择”更影响最终结论。同样的模型,不同的基准、不同的提示词、不同的验证方法,得出的排名可能完全相反。第二,修复率只有配上了缺陷类型分布才有解释意义。一个模型修复率高,可能是因为测试集里大量简单缺陷,而不是它真的更聪明。第三,不同模型在“擅长修复的缺陷类型”上是互补的,团队实际选型要看自己的bug分布,而不是看总分排名。比如模型A擅长修逻辑条件错误,模型C擅长修空指针和资源管理问题,如果项目里空指针居多,选模型C反而更合适。

还有一个经验:人工审查的比例不能太低。纯靠自动验证指标容易迷糊,但每轮抽20%的样本人工过一遍,就能发现很多指标对不上号的边缘案例。这些案例往往能反哺评估流程本身的改进。

5.3 后续可以折腾的几个方向

这个项目后续还可以扩展很多方向。一是把缺陷自动定位和修复结合成一条完整链路,评估端到端表现,而不只是修复环节。二是引入更多多语言基准,看模型的语言迁移能力,这会涉及跨语言上下文裁剪的新问题。三是把模型生成补丁的人类审查成本纳入度量,评估“人工确认一个正确补丁”的期望成本。四是加入更多基线方法,包括传统模板工具和基于信息检索的修复方法,这样能更清楚地看到LLM的增量价值在哪。这些方向都能在现有评估流水线上低成本扩展,不需要推倒重来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询