← 返回博客

论文阅读:Towards Understanding the Characteristics of Code Generation Errors Made by Large Language Models

LLM 代码错误画像、修复代价与复杂度分析笔记。

也是在中大草草阅读的一篇文章,来自 ICSE 25.

I. Introduction #

作者关注的核心问题是:大模型到底会犯什么类型的代码错误、这些错误难修到什么程度、与任务复杂度和测试通过率之间又有何关联? 为此,他们挑选 CodeGen-16B、InCoder-1.3B、GPT-3.5、GPT-4、SantaCoder、StarCoder 六个代表性模型,在 HumanEval 164 个 Python 题目上汇总出了 557 份失败代码,并进行了细粒度的人工标注与分析。

贡献主要有四点:

  • 构建了覆盖语义(根因)与句法(位置)两个维度的错误分类法,并在 GitHub 开源了标签及标注结果;
  • 比较了各模型语义/句法错误分布的异同,探索训练数据与错误模式的关系;
  • 定量评估了修复这些错误所需的工作量,并研究任务复杂度与测试通过率对错误分布的影响;
  • 基于标签体系搭建了交互式分析网站,为后续研究者提供可检索的错误案例库。

II. Methodology #

A. 研究设计与数据采集 #

  • 数据集:HumanEval,共 164 道函数级 Python 编程题,平均 7.7 个单元测试。
  • 提示策略:沿用官方 prompt,温度设为 0 保证可复现性。
  • 错误收集:先跑单元测试过滤失败样本,再人工复核通过测试但语义不等价的情况,额外补充了 19 份隐性错误。

B. 开放式编码与标签体系构建 #

两位作者先从 557 份错误代码中随机抽取 160 份进行开放式编码,定位错误片段、记录根因;之后引入另外两位作者共同迭代 codebook。经过三轮 Fleiss’ κ 校验,最终收敛到 13 类语义特征 × 14 类句法特征,全量标注的一致度达到 0.91/0.91。

C. 修复代价的度量方式 #

作者分别计算:

  • Levenshtein 距离:字符级最小编辑次数;
  • Jaccard 相似度:基于标记集合的重合度;
  • CodeBERTScore:利用 CodeBERT 提取语义向量的余弦相似度。

所有指标在计算前都会去掉注释,避免非功能性文本干扰。

III. Results #

A. RQ1:LLM 常犯哪些错误? #

语义维度:最常见的是条件错误(缺失条件、条件写错)和逻辑方向错误。小模型更容易生成“垃圾代码”或漏掉关键步骤,大模型则倾向于在细节处翻车(常量取值错误、算术操作符写错)。大部分错误跨越多行,意味着简单的点修补往往救不了。

句法维度:错误高发于整块代码块、if/else、循环和函数调用。不同模型有自己的“弱项”:CodeGen-16B、InCoder-1.3B 更易把函数名写错;GPT-3.5、SantaCoder、StarCoder 更常犯参数错误。整体来看,定位错误位置只是第一步,同一个位置的根因可能截然不同。

即使错误出现在相似的位置,其语义根因也可能千差万别,因此需要采用不同的修复方式。——定位只是第一步,真正修好还得看根因。

一种语义特征,如常量值错误,可能出现在不同的位置。精确地定位此类错误可能具有挑战性。

在语义层面,GPT-3.5 和 GPT-4 没有产生任何“无意义代码”片段,而另外四个模型都会出现。这很可能源于前两者训练数据规模巨大,使它们更有能力避开完全跑题或毫无作用的输出。

在句法层面,作者发现 CodeGen-16B 生成“错误代码块”的比例反而更小。这可能与它只用 Python 代码进行专门化训练有关:单一语言的深度学习,有助于减少大段语法结构性错误。而其他模型是在多语言代码语料上训练的,可能导致在具体语言(如 Python)上生成整段结构时更容易失手。

B. RQ2:修复这些错误要多大代价? #

  • 84.21% 的错误需要超过 50 次编辑,52.63% 超过 200 次;多数错误属于“多行/多块”级别。
  • CodeBERTScore 中位数约 0.2,语义偏差大。尤其中位数只有 0.05 的 GPT-3.5,一旦出错往往偏得更远:要么一次就写对,要么直接写成大片错误代码。
  • 参考自动程序修复的粒度划分:InCoder-1.3B 的“多块错误”占比最高(41%),修复成本最吓人;SantaCoder 的单行错误最多(35%)。

总体来看,文本相似度低 + 语义相似度低,与 RQ1 的发现一致:LLM 常犯的不是“小错”,而是“缺失多步”“逻辑方向错误”这类非平凡问题,修起来需要大量编辑。例如,“缺失多步”这类错误的中位编辑次数就达 108 次。

有趣的是,GPT-3.5 和 GPT-4 虽然整体性能最好,但一旦出错,偏差反而更大:它们的 Levenshtein 距离中位数比其他模型更高。这意味着它们通常要么对,要么一错就错成“大段代码块错误”,修复所需工作量反而更多。

作者借鉴自动程序修复(APR)领域的做法,把所有错误按修复粒度分成三类。

GPT-3.5 与 GPT-4 的三类错误分布更均衡;SantaCoder 的单行错误最多(35%);StarCoder 的单块错误最多(57%);InCoder-1.3B(准确率最低的模型)多块错误最多(41%),意味着它的错误往往散落在多处,修起来最麻烦。

C. RQ3:任务复杂度如何影响表现? #

作者用提示词长度、标准答案的 LOC、AST 节点数量作为复杂度代理。统计显示:

  • 对每个模型,“成功任务 vs 失败任务”在三项指标上都存在显著差异(p < 0.05)。
  • 提示超过 150 个词的失败案例中,64.0% 属于垃圾代码;标准答案 LOC 超过 12 行的失败案例中,55.9% 同样是垃圾代码。
  • 复杂任务下,模型更容易在理解题意阶段就跑偏,连基本骨架都没搭出来。

也就是说,对所有 6 个模型而言,“通过”的任务与“失败”的任务在两项复杂度代理指标上都存在显著统计差异(作者单独做了p 值检验) 失败的任务通常提示更长(描述文字更多、更复杂),标准解答行数更多,语法结构也更复杂(AST 节点更多)。

进一步深入查看每个模型中“提示长度超过 150 个词”的失败任务。按之前的语义分类,这些失败里有 64.0% 属于“垃圾代码”——即与任务目标无关的无效代码。举例任务 129 的提示长达 249 词,要求在给定网格中找到长度为 k、路径和最小的路径。InCoder-1.3B 没理解复杂需求,只生成了一串毫无意义的 append 操作。

在 LOC 维度上也出现类似模式:当标准答案超过 12 行代码时,失败的任务中有 55.9% 属于垃圾代码。比如任务 105 需要三步:排序(1–9)、反转、把每个数字换成对应的英文字符串。正确答案有 24 行代码,但 CodeGen-16B 没弄懂要求,只返回了一个空数组。

D. RQ4:测试通过率与错误模式 #

把没通过所有测试的代码细分为完全失败(所有用例都挂)和部分失败(只挂一部分用例),并排除了那 19 个“全通过但其实不等价”的特殊案例。

首先看语义特征。纯注释(only comments)和无意义代码(meaningless code)是最容易导致完全失败的两类根因。看似小问题的“未定义名称”(undefined name)也常常导致完全失败,因为一运行就异常崩溃。反而像“缺失多步”这种听起来很严重的错误,经常只是让模型漏掉部分需求,因此还能侥幸通过一些测试——说明测试用例不够强,或者模型并非完全误解题意。

举例:Task 125 需要在“字符串没有空格”的情况下按逗号分割;若也没有逗号,还要继续做别的处理。CodeGen-16B 只实现了按空格切分,后面步骤全漏,于是它只能通过包含空格的测试,用例里没空格时就挂了,属于“部分失败”。

还有一个意外发现是“逻辑方向错误”的代码也可能偶然通过几例测试。比如 Task 75,InCoder-1.3B 没按题意实现,但却能过 5、10、30 这些测试。合理推测是:提示里自带的测试样例给了模型“表层关联”,它记住了某些输入-输出组合,从而用不相关的逻辑碰巧命中部分用例。

再看句法特征。如果错误发生在 if 语句(if error),82% 是部分失败,18% 才是完全失败。原因是 if 错误往往只是误解了其中某个条件,其余代码可能仍然正确,所以能过掉一些用例。

比如 Task 0,SantaCoder 只考虑了列表中相邻元素,忽略了非相邻的情况,因此错过了那条特定测试,形成部分失败。

IV. Discussion & Future Work #

  1. 如何修复 LLM 生成的错误代码:

作者指出:不同模型犯错的语义与句法特征差异巨大(Finding 1、2)。像 if 条件错误、函数参数错误这种单行或单点逻辑问题,传统自动程序修复(APR)技术已经比较擅长修复。但论文的发现也显示,大量错误其实是多行、多块的结构性问题。

为突破这一局限,近来有人用 LLM 本身来做“程序修复代理”(Program Repair Agents),依靠错误信息等粗粒度信号引导修复。本文贡献的细粒度分类(语义+句法)可以更精确地指导修复:先训练一个模型预测错误类型,再把预测结果嵌入提示词里引导修复代理。 例如,不再是笼统地说“修 Line 6 的 bug”,而是明确提示“修正第 6 行的函数参数错误”。

这里文章用 GPT-4 做了一个小实验验证:在 HumanEval 的 18 个错误里,如果加入他们的标签信息,GPT-4 能修好 7 个(不用标签时只修好了 4 个);在更难的 BigCodeBench-Hard 上,带标签能修好 12 个,而不用标签只能修好 3 个。说明这套分类越在困难任务上越能发挥作用。

  1. 如何更精准地定位错误:

找准“错在哪儿”是修复前的第一步,但 Finding 2 显示错误可能出现在各种结构里,定位并不容易。传统定位方法依赖高质量、覆盖全面的测试用例(谱系法等),构造这些用例很费力;深度学习式定位通常靠源代码特征和错误信息。但 LLM 生成代码还有更多可利用的信号:如每步解码的 logits、token 概率分布、自注意力分数等。未来可以探索把这些“生成过程信号”与错误消息、代码特征一起用来预测模型从哪一步开始跑偏。

  1. 如何估计代码正确性与任务上限:

Finding 4 表明,当前 LLM 对复杂任务仍很吃力。这里有两类问题:

1)能否给定一个模型,估计它能处理的任务复杂度上限?论文里只用了提示长度、LOC 这类简单指标,后续可以考虑更精细的度量,以便准确评估模型能力边界,提高生成可靠性。

2)能否仅凭提示特征(如长度)来预测任务难度和最终代码是否正确?已有工作尝试让 LLM 直接当“评审员”,不依赖测试用例就判定代码是否正确(如 CodeJudge 在 HumanEval 上做到 73.13%,但在 BigCodeBench 上只有 54.56%)。这类技术若更成熟,可帮助开发者根据“正确性估计”分配审查与测试资源。

V. Threats to Validity #

  • 内部效度:人工标注可能引入主观性;作者通过多轮共识与 κ 检验把不一致压到了 0.09 以内。
  • 外部效度:只覆盖 Python 函数级任务、单一数据集与温度设置;尚未验证到其它语言、任务类型或提示策略上的可推广性。
  • 模型范围:仅分析六个模型,且大模型样本受限。随着更强模型出现,错误分布可能会继续演化。