背景:
最近在做代码大语言模型生成项目代码的课题。代码生成现在大部分的工作是在做即时代码生成,这个有点类似代码智能提示,只不过生成的可能是一段片段代码;然而对于整个项目代码的生成做的团队并不多,原因大致如下:
1.项目代码比较复杂,关联的代码文件较多,文件关系、类关系、方法关系理清楚比较难
2.项目代码涉及的代码量较大,也就是上下文较多;要让模型理解整个项目抓住重点很难
3.项目代码量大,所以要求模型允许输入的token长度较长
4.项目代码生成如何抽象问题、设计任务、构建语料训练模型诗歌难题
也正是因为上面的几个问题点,导致虽然项目代码的生成是一个很有商业价值的方向,但却找不到合适的解决方案。今天介绍的这篇文章《CodePlan: Repository-level Coding using LLMs and Planning》并未提出项目代码生成方案,但是提出了项目代码增删改后如何自动的把应用到发生改变的代码片段自动修改的方案。
这个方案的解决思路如下:
1.对项目代码做依赖关系的分析,构建代码方法依赖关系数
2.定义了代码片段可能的几种变化行为,并针对行为定义了几种actiom
3.当代码片段发生变化,把信息结构化成prompt输入LLM,LLM根据prompt触发对应项目修改action
4.循环3直到所有的代码依赖全部更新完为止
依赖图的示例,带有关系作为边缘标签的注释
代码片段可能的几种变化行为和对应actiom
prompt模版
论文翻译:
软件工程活动,如包迁移、修复来自静态分析或测试的错误报告以及向代码库添加类型注释或其他规范,涉及广泛编辑整个代码存储库。我们将这些活动形式化为存储库级别的编码任务。
最近的工具,如GitHub Copilot,由大型语言模型(LLMs)驱动,已成功提供了高质量的解决方案来解决局部编码问题。存储库级别的编码任务更加复杂,无法直接使用LLMs解决,因为存储库内的代码相互依赖,整个存储库可能太大,无法适应提示。我们将存储库级别的编码视为一个规划问题,并提出了一个任务不可知的框架,称为CodePlan来解决它。CodePlan合成了一系列多步编辑(计划),其中每一步都会调用存储库中的代码位置上的LLM,上下文来自整个存储库、先前的代码更改和任务特定的说明。CodePlan基于一种新颖的增量依赖分析、变更可能影响分析和自适应规划算法的组合。
我们在两个存储库级别的任务上评估了CodePlan的有效性:包迁移(C#)和临时代码编辑(Python)。每个任务在多个代码存储库上进行评估,每个存储库都需要对许多文件进行相互依赖的更改(2至97个文件之间)。以LLMs自动化处理这种复杂程度的编码任务以前尚未实现。我们的结果显示,与基线相比,CodePlan与实际情况更匹配。CodePlan能够使5/6个存储库通过有效性检查(例如,无错误地构建和进行正确的代码编辑),而基线(没有规划但具有与CodePlan相同类型的上下文信息)无法使任何存储库通过这些检查。我们将在https://aka.ms/CodePlan发布我们的数据和评估脚本。
CCS概念:• 计算方法学→不确定性下的规划;• 软件及其工程→软件维护工具;软件演进;自动编程。
附加关键词和短语:自动编码、存储库、LLMs、静态分析、计划、一系列编辑。
**1 引言**
大型语言模型(LLMs)的卓越生成能力[24, 28, 30, 35, 57, 73]开辟了自动化编码任务的新途径。基于LLMs构建的工具,如Amazon Code Whisperer [14]、GitHub Copilot [38]和Replit [66],现在广泛用于根据自然语言意图和周围代码的上下文完成代码,并根据自然语言指令执行代码编辑[78]。这些编辑通常针对代码的小区域,例如完成或编辑当前行,或整个方法的主体。
虽然这些工具有助于软件工程的“内循环”,其中开发人员在编辑器中编码并编辑小代码区域,但在软件工程的“外循环”中有一些涉及整个代码存储库的任务。例如,如果我们的代码存储库使用了一个名为𝐿的库,而库𝐿的API从版本𝑣𝑛更改为版本𝑣𝑛+1,我们需要迁移我们的代码存储库以正确调用修改后的版本。这样的迁移任务不仅涉及对调用库𝐿相关API的存储库的所有区域进行编辑,还涉及对存储库的区域(跨文件边界)进行编辑,这些区域在更新后的代码上具有传递性的语法和语义依赖关系。
这在图1中进行了说明,图中显示了复杂数库的API变化。我们的任务是根据这个变化迁移我们的代码存储库。图3的左侧显示了我们的代码存储库的相关部分,其中使用了复杂数库。具体来说,文件Create.cs具有方法func,该方法调用库中的create_complex方法,而Process.cs具有方法process,该方法调用func。
我们可以将图1中的任务描述和func的主体传递给LLM,以生成func的修改后代码,如图3的右侧所示。如图所示,LLM已正确编辑了对create_complex API的调用,以使其返回Complex类型的对象,而不是两个浮点数值的元组。请注意,此编辑导致方法func的签名发生了变化-它现在返回Complex类型的对象。这需要对调用方法func的调用者进行更改,例如图3左下角显示的文件Process.cs中的process方法。如果没有对process方法的合适更改,我们的代码将无法构建!图3右下角显示了将存储库置于一致状态以使其构建无错误的process方法的适当更改。
问题阐述。上述迁移任务代表了一类任务,涉及为各种目的编辑整个代码存储库,例如修复静态分析或测试中的错误报告,修复有缺陷的编码模式,重构或添加类型注释或其他规范。每个这类任务都涉及一组种子规范,例如图1中所示的规范,这些规范是代码编辑任务的起点。这些种子规范通常会触发代码的其他编辑要求,而这些要求需要在代码存储库中的依赖关系中传播,以执行存储库中的其他编辑以完成编码任务。通常,这种跨依赖传播的编辑工作是手动完成的。
我们的目标是构建一个存储库级别的编码系统,该系统可以自动为像图3中的process方法所需的派生规范生成规范,以将存储库置于有效状态。这里,有效性是相对于一个“oracle”来定义的,可以实例化为多种强制存储库级别正确条件的方式,例如无错误地构建、通过静态分析、通过类型系统或一组测试,或通过验证工具。我们定义一个由LLM驱动的存储库级别编码任务如下:
提出的解决方案。在本文中,我们提出了一种通过将(LLM驱动的)存储库级别编码视为一个规划问题来计算派生规范的方法。自动规划[37, 67]旨在解决多步问题,其中每一步执行多个替代操作之一,以达到目标状态。它在许多领域广泛应用,如运动规划[47]、自动驾驶[39]、机器人学[44]和定理证明[26]。
我们提出了一个称为CodePlan的任务不可知框架,它合成一个多步计划来解决存储库级别编码任务。如图2所示,CodePlan的输入包括一个存储库、一个通过自然语言指令或一组初始代码编辑表达的任务种子规范、一个正确性oracle和一个LLM。CodePlan构建一个计划图,图中的每个节点标识LLM需要履行的代码编辑义务,并且一条边表示目标节点需要在源节点之后履行。CodePlan监视代码编辑并自适应扩展计划图。编辑Δ来自任务描述𝑠𝑒𝑒𝑑𝑠,而编辑Δ则根据一种新颖的增量依赖分析、变更可能影响分析和自适应规划算法的组合来确定和上下文化。合并块将LLM生成的代码合并到存储库中。一旦计划中的所有步骤都完成,存储库将由oracle进行分析。如果oracle验证存储库,则任务完成。如果发现错误,则将错误报告用作下一轮计划生成和执行的种子规范。
再次考虑图1中指定的示例API迁移任务,在图3的代码上。CodePlan使用图1中的指令作为种子规范执行方法func的编辑。通过分析图3(a)到(b)之间的代码更改,它将更改分类为逃逸更改,因为它影响到方法func的签名。变更可能影响分析确定func的调用者可能会受到影响,因此自适应规划算法使用调用者-被调用者依赖关系推断出一个派生规范,以编辑调用func的方法process。种子和派生更改都通过为LLM创建适当的提示来执行,生成的代码存储库通过oracle,即构建无错误。请注意,这只是一个简单的示例,只有一次更改传播。在实践中,派生更改本身可能需要传递其他更改,而CodePlan处理这种情况。
提出的解决方案。 与我们的规划方法相比,一个更简单的替代方案是使用oracle来推断派生规范。例如,在图3中进行种子更改后,构建系统可以找到process方法中的错误。这有重要的限制。首先,即使它们导致行为更改,也不是所有更改都会引发构建错误,例如,将返回值从True更改为False而不更改返回类型。其次,构建系统在代码中断时对因果关系毫不关心。例如,如果根据种子规范更改了覆盖方法的签名,那么相应的虚拟方法需要进行类似的更改。然而,构建系统(在运行在存储库的中间、不一致快照时)指责覆盖方法不符合虚拟方法。试图天真地修复构建错误会导致回滚种子更改。CodePlan的静态分析和规划组件克服了这些限制。我们在实验中将CodePlan与使用构建系统迭代识别破坏性更改并使用LLM修复它们的基线进行了比较。我们的定量和定性结果表明,CodePlan优于这种以oracle为导向的修复技术。
贡献。 就我们所知,迄今为止还没有明确识别和解决LLM对存储库进行的代码编辑的影响以及系统地规划一系列相互依赖的编辑的问题,这是自动化存储库级别编码任务的问题。
在存储库级别编码任务的领域中,已经发现两种上下文对于提示LLMs非常有用:(1)空间上下文,通过静态分析[9, 34, 51, 59, 61, 70, 71, 77]或检索[81, 85]提供跨文件信息给模型,和(2)时间上下文,以历史编辑为条件对仓库中的预测进行调整[23, 40, 64, 76]。由于CodePlan监视代码更改并维护存储库范围的依赖关系图,因此我们在一个统一的框架中提供了这两种形式的上下文。现有技术假定开发人员提供下一个编辑位置,并且不考虑编辑对依赖代码的影响。相反,通过推断每个更改的影响,CodePlan将更改传播到依赖代码,从而自动化存储库级别编码任务通过一系列编辑的方式。
总结一下,我们在本文中做出了以下贡献:
- 我们首次正式化了使用LLMs自动化存储库级别编码任务的问题,这需要分析代码更改的影响并在整个存储库中传播它们。目前没有系统且可扩展的解决方案来解决这个问题。
- 我们将存储库级别编码视为一个规划问题,并设计了一个称为CodePlan的任务不可知框架,该框架基于增量依赖分析、变更可能影响分析和自适应规划算法的新颖组合。CodePlan合成了由LLM执行的多步编辑链(计划)。
- 我们使用gpt-4-32k模型在两个存储库级别编码任务上进行了实验:C#存储库的包迁移和Python存储库的时间代码编辑。我们将其与使用oracle(C#的构建系统和Python的静态类型检查器)来识别派生编辑规范(与CodePlan中使用的规划相反)的基线进行比较。我们在基线中使用了与CodePlan相同的上下文化方法。
- 我们的结果表明,与基线相比,CodePlan与实际情况更匹配。CodePlan能够使5/6个存储库通过有效性检查,而基线无法使任何存储库通过。除了2个专有存储库外,我们将在https://aka.ms/CodePlan发布我们的数据和评估脚本。
**2 设计**
在本节中,我们首先概述用于自动化存储库级别编码任务的CodePlan算法(第2.1节)。然后,我们介绍了CodePlan的静态分析(第2.2节)和自适应规划以及计划执行(第2.3节)组件。
**算法 1:CodePlan算法,用于自动化存储库级别编码任务。Cyan和Orchid中的数据结构和函数在第2.2和第2.3节中有解释。**
CodePlan算法(算法1)接受四个输入:
- 存储库的源代码 𝑅。
- 任务的一组种子编辑规范 Δ。
- 一个oracle Θ。
- 一个LLM 𝐿。
该算法的核心数据结构是一个计划图 𝐺,它是一个具有多个根节点的有向无环图(第4行)。计划图中的每个节点都是一个元组 〈𝐵, 𝐼, 𝑆𝑡𝑎𝑡𝑢𝑠〉,其中 𝐵 是存储库 𝑅 中的一块代码(即代码位置的序列),𝐼 是编辑指令(类似于图1中所示的示例),𝑆𝑡𝑎𝑡𝑢𝑠 要么是 𝑝𝑒𝑛𝑑𝑖𝑛𝑔 要么是 𝑐𝑜𝑚𝑝𝑙𝑒𝑡𝑒𝑑。
CodePlan算法还维护一个依赖图 𝐷(第5行)。图4说明了依赖图的结构。我们将在第2.2.1节中详细讨论它。目前,只需知道依赖图 𝐷 表示存储库 𝑅 中代码块之间的语法和语义依赖关系。
在第6-9行的循环中,直到 Δ 不为空为止执行。第7行调用 InitializePlanGraph 𝑠𝑒𝑒𝑑𝑠 函数(第11-13行),该函数将 Δ 中的所有更改添加为计划图的根节点。每个编辑 𝑠𝑒𝑒𝑑𝑠 规范包括一个代码块 𝐵 和一个编辑指令 𝐼。对于根节点,状态设置为 pending(第13行)。然后,在第8行调用 AdaptivePlanAndExecute 函数,该函数执行计划,更新依赖图以反映每个代码更改,并根据需要扩展计划。一旦计划图完全执行,就会在存储库上运行 oracle Θ。它返回错误位置和诊断消息,这些将形成下一轮的 Δ。如果存储库通过了oracle的检查,则返回一个空集合,并且CodePlan算法终止。现在让我们讨论 AdaptivePlanAndExecute,这是主要的工作流程。它迭代地选择每个待处理节点并处理它。处理带有编辑规范的待处理节点,其中包含块 𝐵 和编辑指令 𝐼,涉及以下五个步骤:
- 第一步(第19行)是提取要编辑的代码片段。简单地提取块 𝐵 的代码会丢失与周围代码的关系信息。另一方面,保留整个文件占用了提示空间,并且通常是不必要的。我们发现,当一个块属于一个类时,周围的上下文最有帮助。对于这样的块,我们绘制包含类。也就是说,除了块 𝐵 的代码之外,我们还保留了包围类及其成员的声明。正如我们稍后讨论的那样,这种草图表示还有助于更容易地将LLM的输出合并到源代码文件中。
- 第二步(第21行)是收集编辑的上下文。编辑的上下文(第38-41行)包括(a)空间上下文,其中包含与块 𝐵 相关的代码,例如从块 𝐵 中调用的方法,以及(b)时间上下文,其中包含导致需要编辑块 𝐵 的先前编辑。时间上下文由从计划图的根节点到 𝐵 的路径上的编辑形成。
- 第三步(第23-24行)使用第一步中提取的片段、编辑规范中的指令 𝐼 和第二步中提取的上下文构建编辑的提示,并使用提示调用LLM以获取编辑后的代码片段。
- 第四步(第26-28行)将编辑后的代码合并回存储库。由于代码已更新,许多依赖关系,如调用者-被调用者、类层次结构等,可能需要更改,因此这一步还会更新依赖图 𝐷。
- 第五和最后一步(第30-35行)进行自适应规划,以传播当前编辑对依赖代码块的影响。这涉及对编辑块中的更改进行分类,并根据更改类型选择在依赖图中遍历和定位受影响块的正确依赖关系。例如,如果当前块 𝐵 中方法 𝑚 的编辑涉及到方法签名的更新,那么所有调用者 𝑚 都会受到影响(图3的情况)。对于每个受影响的块 𝐵′ 和依赖关系 rel,该关系将块 𝐵 与 𝐵′ 连接到依赖图中,我们得到一对 〈𝐵′, rel〉。如果计划图中存在 𝐵′ 的节点并且它处于待处理状态,则我们将从 𝐵 到 𝐵′ 的边添加到计划图中,标记为 rel。否则,该边将添加到新创建的 𝐵′ 节点中(第34行)。块 𝐵 被标记为已完成(第31行)。
- **2.2 静态分析组件**
现在我们关注CodePlan中使用的静态分析组件。我们将涵盖算法1中带有青色背景的所有数据结构和函数。
**2.2.1 增量依赖分析**
LLM可以提供一个代码片段和编辑指令来进行编辑。虽然LLM可以准确执行所需的编辑,但分析编辑对存储库其余部分的影响超出了LLM调用的范围。我们认为静态分析非常适合完成这项工作,并提出了相应的增量依赖分析。
**DependencyGraph.** 依赖分析[12]用于跟踪代码元素之间的语法和语义关系。在我们的情况下,我们对导入语句、方法、类、字段声明和语句之间的关系感兴趣(不包括仅在封闭方法内部定义的局部变量)。正式地说,依赖图 𝐷 = (𝑁,𝐸),其中 𝑁 是一组节点,代表上述的代码块,𝐸 是一组带有标签的边,边的标签表示边的源节点和目标节点之间的关系。图4说明了我们跟踪的所有关系,这些关系作为标记的边。这些关系包括(1)语法关系(ParentOf 和 ChildOf、Construct 和 ConstructedBy)表示代码块 𝑐 与在语法上包围 𝑐 的块 𝑝 之间的关系;特殊情况是构造函数和其封闭类之间的关系,由 Construct 和 ConstructedBy 表示,(2)导入关系(Imports 和 ImportedBy)表示导入语句与使用导入模块的语句之间的关系,(3)继承关系(BaseClassOf 和 DerivedClassOf)表示类与其超类之间的关系,(4)方法重写关系(Overrides 和 OverriddenBy)表示重写方法与被重写方法之间的关系,(5)方法调用关系(Calls 和 CalledBy)表示语句与其调用的方法之间的关系,(6)对象实例化关系(Instantiates 和 InstantiatedBy)表示语句与创建该对象的构造函数之间的关系,(7)字段使用关系(Uses 和 UsedBy)表示语句与使用的字段声明之间的关系。
**ConstructDependencyGraph.** 依赖关系是通过静态分析跨存储库中的源代码生成的。我们将存储库的源代码表示为抽象语法树(AST)的森林,并在AST子树之间添加依赖关系边缘。文件级别的分析用于派生语法和导入关系。所有其他关系需要跨类、跨过程的分析,可以跨越文件边界。特别是,我们使用类层次分析[32]来派生语义关系。
**ClassifyChanges.** 正如在第2.1节中讨论的,CodePlan在第四步中将LLM生成的代码合并到存储库中。通过对比前后的代码,我们对代码更改进行分类。表1(第一列和第二列)给出了原子更改的类型及其标签。广义上,更改分为修改、添加和删除更改,进一步根据更改的构造进行分类。我们区分方法体和方法签名的更改。同样,我们区分类声明的更改、其构造函数的更改或其字段的更改。还会识别导入语句或使用导入的语句的更改。这些都是原子更改。LLM可以在给定的代码片段中进行多个同时编辑,导致多个原子更改,所有这些更改都由ClassifyChanges函数识别。
**UpdateDependencyGraph.** 当LLM生成的代码合并时,与更改站点的代码相关的依赖关系会重新分析。表1(第三列)根据ClassifyChanges推断的标签提供了更新依赖图 D 到 D' 的规则。对于修改更改,我们重新计算已更改代码的关系,但不包括构造函数。构造函数与其封闭类之间有一个语法关系,不需要重新计算。对于添加更改,为添加的代码创建新的节点和边缘。与语法关系对应的边缘以直接方式创建。如果更改同时添加了一个元素(导入、方法、字段或类)及其使用,我们将在分析使用它的语句之前为添加的元素创建一个节点。方法的添加需要特殊处理,如表中所示:如果添加了一个重写方法 C.M,那么如果调用是在类型为 C 的接收对象上发出的,则将与匹配的被重写方法 B.M 的Calls/CalledBy边缘重定向到C.M。删除重写方法需要与表1中所述的类似处理。所有其他删除更改需要按照表中所述删除节点和边缘。
**2.2.2 变更可能影响分析**
在第五步中,CodePlan通过LLM进行的代码更改来识别可能受到影响的代码块。让 Rel(D, B, rel) 表示通过关系 rel 在依赖图 D 中与块 B 相连接的代码块的集合。让 D 和 D' 分别表示在表1中的更新之前和之后的依赖图。
**获取受影响的块。** 表1的最后一列告诉我们如何为每种更改类型识别受影响的代码块。当编辑方法 M 的主体时,我们执行逃逸分析 [22, 29] 来确定是否已受到调用 M 的调用者中可访问的任何对象(逃逸对象)的更改影响。如果是的话,M 的调用者(通过 Rel(D, M, CalledBy) 识别)被标识为受影响的块。否则,更改局限于该方法,没有受影响的块。如果编辑了方法的签名,则通过继承层次结构中的方法重写关系与之相关的调用者和方法受到影响。签名更改本身可能会影响 Overrides 和 OverridenBy 关系,例如,@Override 访问修饰符的添加或删除。因此,在更新后的依赖图 D' 中通过这些关系相关的块也被视为受影响,如表1所示(带有 MMS 标签的行)。当修改类 C 的字段 F 时,使用 F 的语句、C 的构造函数以及 C 的子类/超类都会受到影响。当修改类时,按照 D 和 D',实例化它以及其子类/超类的方法都会受到影响。对构造函数的修改具有类似的规则,只是这种更改不会更改继承关系,因此只需要 D。当修改导入语句 I 时,使用导入模块的语句会受到影响。
添加和删除更改比修改更改要简单,它们的规则设计沿用了上面讨论的原则。出于篇幅考虑,我们不会逐步解释每个更改。我们假设代码中不会使用新添加的类或导入。因此,添加它们不会导致任何受影响的块。在我们的实验中,我们发现表1中的规则已经足够了。但是,如果需要,CodePlan可以轻松配置以适应表1中规则的变化。
**2.3 自适应规划和计划执行**
现在我们讨论来自算法1的Orchid背景中的数据结构和函数。
**2.3.1 自适应规划**。在使用GetAffectedBlocks识别受影响块之后,CodePlan创建需要使用LLM解决的更改义务,以使依赖代码与更改保持一致。如第2.1节所讨论的,这是一个迭代过程。
**PlanGraph。** 计划图P = (𝑂, 𝐶)是一个带有一组义务𝑂的有向无环图,每个义务都是三元组 〈𝐵, 𝐼, 𝑠𝑡𝑎𝑡𝑢𝑠〉,其中B是一个块,I是一条指令,状态要么是待定的,要么是已完成的。𝐶中的边记录了原因,源义务和目标义务之间的块之间的依赖关系。换句话说,边的标签标识了在表1中的更改可能影响规则中的哪个Rel子句导致创建目标义务。
**ExtractCodeFragment。** 如第2.1节的第一步所讨论的,仅提取块B的代码是次优的,因为它会丢失上下文。ExtractCodeFragment函数获取代码块所属的整个类,保留B的完整代码,并仅保留类和其他类成员的声明。我们发现这很有用,因为类和其他成员的名称和类型为LLM提供了额外的上下文。通常情况下,LLM需要同时进行多个更改。例如,在我们的一些案例研究中,LLM必须添加字段声明,将参数传递给构造函数并在构造函数中使用它来初始化字段。通过将周围代码的草图作为代码片段提供给LLM,LLM可以在正确的位置进行这些更改。代码片段提取逻辑通过遍历AST并“折叠”掉已经草绘的子树(例如,方法主体)来实现。如第1节所述,即使存在多个同时更改,这种草绘表示也允许我们将LLM生成的代码放回AST而不会产生歧义。
**GetSpatialContext。** CodePlan中的空间上下文是指代码块在代码库中的排列和关系,有助于理解类、函数、变量和模块的结构和相互作用。它对于进行准确的代码更改非常关键。CodePlan利用依赖图来提取空间上下文,将代码表示为节点以及它们之间关系的边。这个图使CodePlan能够遍历代码库,识别相关的代码块,并保持对它们的空间上下文的意识。因此,在生成代码编辑时,依赖图使CodePlan能够进行上下文感知的代码修改,使其与代码的空间组织保持一致,从而增强了其代码编辑能力的准确性和可靠性。
**GetTemporalContext。** 计划图记录了所有更改义务及其相互依赖关系。通过将计划图的根节点到目标节点的所有路径线性化,可以提取时间上下文。每个更改都是在更改之前和之后的代码片段的一对。时间上下文还说明了“原因”(记录为边标签),将目标节点与其前驱节点连接起来。例如,如果节点A通过CalledBy边连接到B,那么B的时间上下文是A的前后片段,以及一条说“B调用A”的语句,这有助于LLM理解最新时间更改(对A进行更改)与当前义务(对B进行更改)之间的因果关系。
**2.3.2 计划执行。** CodePlan迭代选择计划图中的待定节点,并调用LLM来履行更改义务。
**MakePrompt。** 在提取了要编辑的代码片段以及相关的空间和时间上下文之后,我们构建了一个要传递给LLM的提示,其结构如下。我们首先使用特定于任务的说明p1,然后列出到目前为止在存储库中进行的与要编辑的片段相关的编辑p2。接下来的部分p3说明了p2中出现的每个片段与要编辑的片段的关系。然后是空间上下文p4和要编辑的片段p5。
**Oracle和计划迭代。** 一旦计划图中的所有节点都标记为已完成,并且没有添加新节点,就完成了一次存储库级别的代码编辑迭代。如图2所示,对存储库调用了Oracle。如果Oracle标记了任何错误(例如,构建错误),则将错误位置和诊断消息添加为下一次迭代的种子更改,然后自适应规划再次开始。如果Oracle没有标记任何错误,CodePlan终止。
**3 实施**
在本节中,我们提供了构成我们方法核心的实施组件的详细概述。
**依赖图构建。** CodePlan方法的核心是依赖图,它有助于表示代码块之间复杂的关系。为了从代码存储库构建这个依赖图,我们采用了系统性的方法。最初,我们解析存储库中的所有代码文件,利用tree-sitter库[25]生成类似AST的结构。这种结构化表示简化了代码库中各种基本代码块的识别。例如,图5示例了tree-sitter生成的C#代码片段的AST结构。代码块在不同的级别进行识别,包括类、方法、导入语句和非类表达式。例如,在图5中,以class_declaration节点为根的子树对应于SyncSubscriberTest类。
**C#中的关系识别。** 在C#存储库的背景下,在依赖图中建立边涉及到在AST内部仔细跟踪关系。我们为图4中列出的每种关系类型制定了自定义逻辑,包括重要的连接,如调用者-被调用者、覆盖-被覆盖、基类-派生类等。为了说明,对于调用者/被调用者关系,我们在AST中查找invocation_expression节点。随后,我们处理这些节点下的子树,以解析关键细节,如目标类和调用方法的名称。有了这些信息,我们在启动方法调用的代码块和目标类中的相应方法块之间创建Calls/CalledBy关系链接。虽然我们为这些关系实施了自定义逻辑,但值得注意的是,由于其固有的灵活性,也可以将用于C#的其他依赖关系分析工具(如C#的语言服务器(LSP)[5]、CodeQL [2]或类似解决方案)集成到我们的系统中。
**Python中的关系识别。** 对于Python存储库,我们使用Jedi [4] - 一种静态分析工具,用于在整个代码库中查找符号的引用和声明。这些功能被用来识别依赖图中的关系,如调用者-被调用者、覆盖-被覆盖和基类-派生类。
**集成GPT-4进行代码编辑。** CodePlan充分利用了GPT-4 [57]的卓越能力来有效地执行代码编辑。在为编辑模型构建输入数据时,我们仔细提供了时间上下文、空间上下文和实际要编辑的代码,以代码片段的形式提供。这些代码片段表示包含编辑位置的类或方法,并以草图表示,如第2.1节所述。这种草图表示确保了模型为每个编辑位置提供了丰富的上下文,从而显著提高了生成的编辑的质量和准确性。
**语言可扩展性。** 尽管我们当前的实现能够有效支持C#和Python存储库,但扩展到其他编程语言的存储库是一项简单的工作。它主要涉及创建具有图4中识别的关系的依赖图,并将其整合到CodePlan框架中,从而使其能够无缝适应各种编程语言。
**4 实验设计**
在本节中,我们将解释我们如何进行实验来测试CodePlan。我们将首先讨论我们使用的不同数据集。然后,我们将讨论我们比较CodePlan的方法,这些方法类似于我们的参考点。最后,我们将解释我们如何测量结果,以查看CodePlan与其他方法相比的性能如何。
**4.1 数据集**
在我们的实验中,我们利用了多样化的数据集,代表了各种复杂性和规模的代码存储库。这些数据集使我们能够在不同的现实世界情境中全面评估CodePlan的性能。
**内部存储库(Int-1和Int-2)。** 这些存储库是专有的,属于一家大型产品公司。它们的特点是规模庞大、模式复杂,并且代表了生产级别的代码库。我们主要关注的任务是将这些存储库从传统的日志框架迁移到现代的日志框架。这个迁移涉及到非平凡的更改,包括使用日志工厂创建特定于服务的日志记录器、通过调用链传递日志记录器、管理类层次结构、在不同范围(类、方法等)存储日志记录器引用,以及在静态和非静态类/方法中处理日志记录器。这两个生产存储库Int-1和Int-2之所以被选择,是因为它们具有不同的编码风格和设计模式,为我们的评估提供了全面的内部数据集。
**外部存储库(公共GitHub)。** 我们还考虑了来自GitHub的外部存储库,以丰富我们的数据集。选择这些存储库是为了代表两种不同的编码任务:迁移和时间编辑(后面会讨论)。
**迁移任务。** 这个任务涉及迁移API或解决代码库中的不兼容性变化。示例包括更新依赖项、适应外部库的更改或与新的编码标准对齐代码。它的复杂性在于通常需要在许多代码文件和依赖项之间进行一致的更新。为了选择这些存储库,我们搜索包含与各种迁移(API、框架等)相关的提交和拉取请求的存储库。我们筛选了至少包含50个文件的存储库。
**时间编辑任务。** 这个任务涉及在给定一些初始代码更改的情况下编排一系列代码更改。许多代码更改都可以被描述为时间编辑,包括重构或添加/删除功能。一个时间编辑任务由一组初始编辑(通常由用户进行)以及由种子编辑引发的派生编辑组成。工具的任务是从初始编辑集中推断出派生编辑。一个示例的时间编辑任务可以是,初始编辑是向方法添加一个参数,派生编辑是更改调用此方法的所有地方。我们从GitHub上的公共存储库中的提交中识别时间编辑任务。我们考虑具有宽松许可证的Python存储库,按星级排序,过滤掉与文档/教程相关的存储库,并从候选存储库中选择至少有10,000个星星的存储库,在2021年11月1日之后进行多个相关更改的提交。
**源/目标/预测存储库。** 为了收集迁移和时间编辑任务的代码更改,我们获取了GitHub上提交之前和之后的文件。我们将这些称为源存储库(提交前)和目标存储库(提交后)。分析这些更改允许我们通过手动检查识别种子更改。例如,在从NUnit迁移到XUnit时,种子编辑之一涉及将Console.WriteLine替换为写入ITestOutputHelper对象。有了源存储库和种子更改说明,CodePlan的任务是对源存储库进行必要的更改,从而产生我们称之为预测存储库的结果。如果CodePlan成功执行了所有更改,那么预测存储库应该与目标存储库匹配,为我们对其在这些不同代码库中的性能与实际情况进行了充分的评估提供了坚实的基础。
**预处理源和目标存储库。** 在处理大型存储库时,常常会有多个开发人员贡献代码,导致个人偏好驱动的各种编码风格。例如,一个开发人员可能会始终使用this限定符来引用类成员,而另一个则可能不会。当CodePlan通过LLM提示执行更改时,它往往会在整个代码库中建立统一的风格,这可能包括强制使用this限定符的一致性,等等。虽然这些更改确保了功能等效性,但在将预测存储库与目标存储库进行比较时,它们可能会影响评估指标。为了解决这些问题,我们在源和目标存储库上进行了手动预处理步骤。这个预处理旨在在存储
表2 总结了迁移(C#)和时间编辑(Python)存储库的数据集统计信息,包括存储库的名称(内部和外部),以及各种关键统计信息,包括它们的大小、代码更改和其他相关指标:
- 文件数目:每个存储库中的文件总数。
- 代码行数:所有文件中的代码行数之和。
- 已更改文件数:在源存储库和目标存储库之间发生更改的文件数。
- 种子更改数:初始编辑的数量,通常被认为是代码更改的起点。
- 派生更改数:跟随初始种子更改的后续编辑的数量。
- 差异大小:源存储库和目标存储库之间不同的行数。
- 种子编辑的大小:当种子编辑直接在代码上进行时,表示初始编辑的行数。当种子编辑通过LLM指令进行时,它表示指令文本的大小。
- 提示模板大小:这个数字表示CodePlan使用的LLM提示模板的大小。相同的模板适用于所有迁移存储库任务,另一个类似的模板用于所有时间编辑存储库任务。
这些指标不仅提供了数据集特征的全面概述,而且突出了使用CodePlan相对于手动过程的显著优势,特别是对于大型存储库。在手动情景中,需要人工费力地识别依赖更改并实现每个修改。值得注意的是,“差异大小”和“种子编辑的大小”等指标提供了有关所需开发工作的见解。另外,值得注意的是,为CodePlan制定LLM指令所需的工作量明显少于手动进行所有代码更改所需的大量工作。这些指标共同展示了CodePlan在不同代码库上的高效性和有效性,强调了它在简化开发工作流程并节省宝贵开发人员时间方面的潜力。
4.2 神谕和基线
神谕。回想一下,我们对存储库级编码任务的定义是围绕满足可以确定解决方案有效性的神谕。在我们的实验中,我们考虑了两个特定的神谕实例。对于C#迁移任务,我们将神谕定义为通过C#构建工具而没有任何错误。对于时间编辑方案,我们使用Pyright [6],这是Python的静态检查器,作为神谕。
神谕引导修复。这两个神谕都以代码库作为输入,并可以输出代码库中的错误列表。这自然地导致了对我们任务的基线方法的制定,我们将其称为神谕引导修复。这些都是简单的反应性方法,在每个步骤中,我们尝试纠正神谕标记的错误。对于C#迁移场景,基线是构建修复,对于时间编辑,基线是Pyright修复,根据使用的神谕而定。
神谕引导修复包括以下步骤:
- 初始编辑:该过程从应用初始种子编辑到代码库开始。
- 构建和错误检测:在种子编辑之后,我们调用神谕,以检测由于种子编辑而在代码库中产生的错误。
- 错误消息分析:然后,解析神谕生成的错误消息,以精确定位错误在代码中的位置。
- LLM修补:随后,将错误消息以及标记位置的代码片段传递给LLM。LLM利用其代码生成能力为确定的错误生成修补程序或修复。为了与CodePlan进行公平比较,我们在神谕引导修复中使用了我们的实现,用于空间和时间上下文的提取。也就是说,CodePlan和神谕引导修复之间的主要区别在于,CodePlan使用自适应规划,而神谕引导修复使用神谕生成的诊断信息。需要注意的是,神谕引导修复方法是一种反应性方法,缺乏全面的“更改可能影响分析”。这意味着它们可能不会彻底评估所提议的代码更改对代码库的其他部分可能产生的影响。因此,在处理复杂的编码任务时,此类方法生成的修复可能不完整或不正确。
替代编辑模型:Coeditor [76]。CodePlan默认利用LLM的文本和代码处理能力,以在提供适当上下文的情况下对代码片段进行本地编辑。然而,从理论上讲,CodePlan的增量依赖分析、更
改可能影响分析和自适应规划组件可以与任何能够根据提供的意图进行局部编辑的工具或模型结合使用。Coeditor [76]是一个经过微调的基于变压器的模型,可以编辑代码片段,同时考虑到在存储库中之前进行的编辑。这样的模型非常适合时间编辑任务,其中我们需要从一组种子编辑中进行一系列编辑,其中每个编辑依赖于前面一组编辑的某个子集。实际上,Coeditor在[76]中对时间编辑任务进行了评估。为了展示我们的分析和规划的通用性,我们评估了我们的方法在时间编辑情境中的性能,将gpt-4-32k替换为Coeditor作为编辑模型。4.3 评估指标
我们研究中采用的评估指标旨在评估CodePlan(或基线)如何有效地在整个代码存储库中传播更改以及每个更改的正确性。为了实现这一目标,我们依赖于两个关键指标:块指标和编辑指标。
块指标。块指标帮助我们了解CodePlan准确识别需要修改的代码块的能力。这些指标包括:
- 匹配块:这些是存在于源存储库中、已在目标存储库中进行了编辑并且还在预测存储库中进行了编辑的代码块。基本上,这些是CodePlan成功识别需要更改的块。
- 未命中块:未命中块是指存在于源存储库中、已在目标存储库中进行了编辑但在预测存储库中未进行编辑的代码块。换句话说,这些是CodePlan在应该进行修改的情况下未能修改的块。
- 虚假块:虚假块是指在源存储库中找到的代码块,在目标存储库中未进行编辑,但在预测存储库中被CodePlan错误地进行了编辑。这代表了CodePlan不必要地进行的编辑。
理想情况下,匹配块数量高,未命中块和虚假块数量低。
编辑指标。虽然块指标评估代码块的识别,但编辑指标深入探讨了CodePlan所做修改的正确性。这些指标包括:
- Levenshtein距离:Levenshtein距离度量了预测存储库和目标存储库之间文件级别的编辑距离。它计算了将一个文件转换为另一个文件所需的更改次数。较高的Levenshtein距离表示CodePlan未正确地对存储库进行了更改。
- Diff BLEU:通常,我们使用BLEU [58],这是自然语言处理中常见的指标,来衡量文本相似性。然而,在应用于我们的任务时,BLEU可能会产生过高的相似性分数,因为特定任务的代码编辑通常只涉及文件的一小部分。为解决这个问题,我们计算Diff BLEU:一种修改后的BLEU分数,表示为BLUE(DIFF(源存储库文件,目标存储库文件),DIFF(源存储库文件,预测存储库文件))。在这里,DIFF计算两个文件之间的差异(diff hunks)。Diff BLEU的独特之处在于它专注于比较预测和目标存储库之间代码的修改部分,同时忽略常见的代码。当预测和目标存储库中的修改精确匹配时,Diff BLEU得分为1.0,表示在处理代码修改方面具有高度的正确性和一致性。
总之,这些评估指标全面评估了CodePlan的性能,无论是在识别需要修改的代码块方面,还是在确保所做的修改是正确的方面。
5 结果与分析
在本节中,我们提供实证结果以回答以下研究问题:
RQ1:CodePlan能够多么有效地定位并进行所需更改以自动化存储库级别的编码任务?
RQ2:时间和空间上下文对于CodePlan的性能有多重要?
RQ3:使CodePlan在解决复杂的编码任务中胜过基线的关键区别是什么?
5.1 RQ1:CodePlan能够多么有效地定位并进行所需更改以自动化存储库级别的编码任务?
动机。在现代软件工程背景下,研究问题涉及到CodePlan框架在自动化存储库级别编码任务中的有效性,这是至关重要的。有几个关键动机驱使这一问题的重要性:
- 存储库级别任务的复杂性:软件工程活动,如包迁移和时间代码编辑,通常超越了局部代码更改的范围。存储库级别编码任务涉及对整个代码库的广泛修改。这种复杂性需要新的方法来确保效率和正确性。
- 现实世界的相关性:在实践中,软件存储库经常需要进行大规模的更改。例如,包迁移涉及跨多个文件和依赖项更新依赖项,而时间代码编辑需要跟踪和管理不断演化的代码库。这些任务不仅耗时,而且在手动操作时容易出错。
- 与基线的比较:评估CodePlan与基线方法的比较至关重要。基线方法,如“Oracle-Guided Repair”,在软件开发中很常见,但在处理存储库级别任务时可能缺乏效率。将CodePlan与基线进行评估提供了衡量其有效性的基准,并突出了其表现出色的领域。我们还研究了在执行不同编辑模型时我们的系统的行为,通过在时间编辑任务上评估Coeditor和CodePlan的组合。
- 大规模存储库:研究不仅涉及孤立的编码问题,还评估了CodePlan在大型内部和外部存储库上的性能。这个广泛的范围确保了框架在各种复杂的实际场景下的有效性得到了测试。
实验设置。
为了研究CodePlan能够多么有效地定位并进行所需更改以自动化存储库级别任务,我们在4.1中描述的任务的上下文中对其进行评估。对于C#迁移和时间编辑任务,我们从没有进行任何编辑的源状态的存储库开始。我们在源状态的基础上应用种子编辑,此时CodePlan(或正在评估的基线)接管整个存储库的编辑。在C#迁移的情况下,使用合适的提示自行执行种子编辑,而对于时间编辑,我们为每个存储库任务存储种子编辑并将其应用为补丁。CodePlan在种子编辑后对存储库进行增量依赖分析,识别可能受其影响的代码,并计划下一步要进行的编辑,使用合适的提示查询LLM并将结果合并到存储库中。CodePlan会不断进行迭代,直到发现没有更多的站点需要进行编辑。
有时,由于大型语言模型(LLM)响应的固有变异性,可能需要多次迭代。我们启动第一次迭代,称为“Iter 1”,以使用LLM启动代码编辑过程。然而,LLM响应的偶尔不准确可能会引入错误的代码更改和后
续的构建错误。为了解决这些挑战,第二次迭代,称为“Iter 2”,变得重要。在此阶段,CodePlan积极识别并确认前一次迭代中的任何构建错误,并重新与LLM交互,以获取更精确的响应以纠正初始错误。
与CodePlan一起,我们还在相同的存储库上评估了一系列基线。对于C#迁移,我们评估了Build-Repair,对于时间编辑,我们评估了Pyright-Repair,其设置如4.2中所述。Pyright-Strict-Repair是一个变种,其中我们使用启用了严格模式的Pyright工具。在所有修复基线中,我们提供与CodePlan中相同的上下文(时间和空间),唯一的区别是本地化的编辑位置是使用oracle进行的。我们还评估了在时间编辑任务中使用Coeditor而不是gpt-4-32k作为编辑模型的情况,如4.2中所述。在所有Coeditor基线中,上下文化是根据[76]中的进行的,下一个编辑站点的本地化是通过CodePlan或使用oracle进行的。
我们评估所有这些方法在多大程度上能够通过匹配、未命中和虚假块指标定位要编辑的站点以及整体修改的正确性,如4.3中所述。我们还确定了方法执行结束后存储库的状态是否通过了有效性检查,即是否满足了oracle并根据基本事实进行了正确的编辑。
结果讨论。
表3中的实验结果展示了CodePlan框架在自动化存储库级别编码任务方面的有效性。
在内部(专有)存储库的C#迁移任务的背景下,结果表提供了两种方法的性能全面视图:CodePlan和Build-Repair。值得注意的是,CodePlan在几个关键方面表现出色。它在“匹配块”方面表现出色,为“Int-1(日志)”和“Int-2(日志)”数据集均实现了151个匹配块和438个匹配块的完美结果。这表明CodePlan在准确识别和处理预期代码更改方面具有卓越的精度。此外,CodePlan令人印象深刻地展现了零“未命中块”,确保没有忽略任何关键代码修改,从而最小化了功能问题的风险。同样值得注意的是“虚假块”的缺失。
**CodePlan vs. Build-Repair**
比较分析显示了为什么Build-Repair落后于CodePlan。导致其性能差距的一个关键因素是它依赖于“构建错误位置”作为代码更正的指示器。构建错误通常会标出错误检测到的位置,但它们不一定与实际所需的修复位置相符。例如,错误可能表现为派生类的重写函数签名不匹配,但修复是在基类的虚函数签名中需要的,这会导致Build-Repair错误地解释为校正位置。此外,在编译器优化的上下文中,构建过程可能会遮盖后续的错误,仅在特定时间显示选定的错误。这可能导致不正确的校正位置的识别,阻碍正确更改的传播,进一步加剧了CodePlan和Build-Repair之间性能差距。这些限制突显了Build-Repair在准确定位和处理复杂迁移任务中的代码修改时所面临的挑战。
**多次迭代**
正如在CodePlan的“Iter 1”后的“Int-1”数据集中所示,处理大型语言模型(LLM)响应固有变异性的需求,从而需要多次迭代来处理。为了纠正这一点,“Iter 2”发挥着重要作用。在此阶段,CodePlan识别并承认了前一次迭代中的构建错误,并重新与LLM合作,以获取更准确的响应以纠正初始错误。值得注意的是,在两次迭代之间的块度量没有发生变化,因为CodePlan正确地识别了需要纠正的块并与LLM进行了修改。然而,“Iter 1”中的LLM校正是错误的,导致Levenshtein距离度量较低。在代码编辑阶段,这种迭代的改进过程显著有助于减轻LLM输出偶尔不准确的影响。
**在Ext-1上的性能**
在“Ext-1”数据集上,CodePlan与基线方法Build-Repair的比较中,我们观察到它们性能上的显著差异。CodePlan成功地识别并更新了58个代码块,实现了1.00的完美DiffBLEU分数,表明它进行了与目标存储库相同的更改。相比之下,基线方法Build-Repair则未能识别六个块并生成了八个构建错误。这种差异突显了Build-Repair的一个关键局限性——缺乏全面的“更改可能影响分析”功能。具体来说,Build-Repair未能更新用于初始化新添加的ITestOutputHelper _output类成员的构造函数块。这个遗漏没有被注意到,因为缺少初始化没有触发构建错误,从而对调用者产生了级联影响。相比之下,CodePlan成功处理了新添加的ITestOutputHelper _output类成员的初始化。这一成就要归功于其强大的更改可能影响分析,它准确识别了在添加新字段时对构造函数块的必要修改。因此,CodePlan无缝地更新了构造函数和所有其调用者,避免了任何遗漏的块或构建错误。这一发现突显了CodePlan的高级规划能力的重要性,这确保了对存储库级别的代码编辑采用更全面和准确的方法。
**CodePlan vs Pyright-Repair**
在时间编辑任务的背景下,我们可以看到CodePlan能够成功地识别所有的派生编辑位置,其中两个存储库(T-2,T-3)中的效果几乎完美,第三个存储库(T-1)中也几乎成功。与此形成对比的是基线Pyright-Repair方法,它未能识别两个存储库(T-2,T-3)中的任何派生编辑。我们发现,在严格模式下使用Pyright可以提供更好的结果,但仅在一个存储库(T-1)中表现得与CodePlan一样好。总体而言,我们观察到Pyright-Repair基线在识别编辑位置方面不足。这也反映在DiffBLEU得分一直较高和Levenshtein距离(L.D.)一直较低的情况下。请注意,由于LLM不一定执行与基本事实相同的确切编辑,因此DiffBLEU和L.D.可能不会具有完美的1.0和0值。
对于T-2和T-3,我们看到Pyright-Repair基线无法识别任何派生编辑。在这两个存储库中,一旦进行了种子编辑,Pyright就不会标记代码库中的任何错误。在T-2的情况下,种子编辑涉及向方法添加参数,如图6所示。然而,此新参数还被分配了默认值。由于此参数存在默认值,Pyright不会标记此方法的所有调用站点为错误。但是在基本事实中,开发人员会跟进并编辑调用站点。CodePlan的更改可能影响分析将这些调用站点标识为“可能需要”编辑,因此我们将它们传递给LLM以进行编辑。
在T-3的情况下,种子编辑涉及修改函数的主体,如图6所示。在种子编辑之后,该方法现在期望传递给它
的字典包含一个新的键“api_endpoint”。Pyright不会在此阶段标记代码库中的任何错误,但很明显,这可能需要对send_request的调用者进行修改。CodePlan确实检测到了这一点,并能够在基本事实中的10个派生站点中识别和进行编辑。
在这两种情况下,我们事先不知道是否需要编辑调用站点。因此,CodePlan将此决策留给LLM根据上下文来决定。相比之下,Oracle-Guided基线甚至没有检测到可能需要更改。这可以归因于像Pyright或Build之类的Oracle的事实,它们旨在检测错误,因此只会标记违反某些规则的代码。这与在整个存储库中传播更改的任务不一致,因为在许多情况下,可能需要传播更改,但具有更改的存储库不会违反Oracle的任何规则。
**Coeditor评估**
在比较CodePlan和Coeditor-CodePlan时,我们可以看到它在T-1上表现得相当好,但在T-2和T-3上略逊一筹,每个存储库中都错过了一个编辑站点。尽管这两种方法都使用相同的分析和规划,但CodePlan中的本地编辑与基本事实更一致,与Coeditor-CodePlan在T-2和T-3中展示的更低的DiffBLEU分数和更高的L.D.反映出这一点。这可能是由于gpt-4-32k和Coeditor之间的上下文理解能力差异所致。例如,选择将方法的参数实例化而不是向调用者添加参数可能意味着错过了由于调用者签名更改而产生的编辑。作为一个更强大的模型,gpt-4-32k更擅长理解时间编辑的上下文,因此它所做的编辑与基本事实更加一致,与Coeditor相比。这也在T-2上比较Pyright-Strict-Repair和Coeditor-Pyright-Strict-Repair时观察到,Coeditor的错误本地编辑导致了编辑站点的丢失以及更差的DiffBLEU和L.D。
总之,实验结果证明了CodePlan的基于规划的方法在自动化存储库级别编码任务方面非常有效,相比传统的基线方法,它具有更好的匹配度、完整性和精度。其处理大规模存储库内复杂编码任务的能力,标志着自动化软件工程活动的重大进展。
**RQ2: 时态和空间背景对于CodePlan的性能有多重要?**
**动机:** RQ2的动机在于认识到大型语言模型(LLMs)需要时态和空间背景来提供准确和与上下文相关的代码更新建议。时态上下文对于理解代码更改的顺序和时机至关重要,使LLMs能够提出与代码的演变相一致并保持一致性的建议。没有这种时态意识,LLMs可能会提供与早期或后续修改相冲突的解决方案,导致代码错误。另一方面,空间上下文使LLMs能够理解代码库不同部分之间的复杂关系和依赖关系。这种理解对于确定需要更新的位置以及确保以保持代码功能的方式应用它们至关重要。因此,调查这些上下文在CodePlan的规划中的重要性对于有效地利用LLMs来自动执行存储库级别的编码任务至关重要。
**实验设置:** 为了评估CodePlan的规划中时态和空间上下文的重要性(RQ2),采用了特定的实验设置。回顾一下,CodePlan通过维护与先前相关的更改列表来跟踪时态上下文,并将这些上下文随后纳入到大型语言模型(LLM)提示中。
为了衡量这些上下文的重要性,进行了一项受控实验。在这个实验中,故意停止跟踪时态变化,不提供时态或空间上下文给LLM在代码修改任务期间。这使得可以详细研究当这些基本上下文被省略时,LLM响应受到了怎样的影响。该实验还包括了对各种指标的全面评估,包括代码一致性(块度量)和代码正确性(Diff BLEU和Levenshtein距离)。通过比较带有时态和空间上下文的CodePlan设置与没有这些上下文的实验设置之间的结果,研究旨在准确量化这些上下文在CodePlan的规划过程中的重要性以及它们对自动化代码修改质量的影响。
**结果讨论:** 关于时态和空间上下文对于CodePlan的规划的重要性的结果(RQ2)揭示了关键的见解。如表2所示,当不考虑时态上下文时,在代码修改过程中错过的块明显增加。这增加归因于大型语言模型(LLM)由于缺乏时态上下文而未对某些代码块进行必要的更改,因为它无法理解在没有时态上下文的情况下需要这些修改。
图8中的一个示例说明了这个问题。在这种情况下,根据派生类的方法的签名更改,需要在基类的虚方法中进行校正。然而,由于LLM缺乏时态上下文,它没有关于派生类的方法的信息,导致它认为不需要对基类方法进行任何更改。这突显了时态上下文在理解代码依赖关系和确保准确更新方面的关键作用。
此外,图9提供了另一个示例,说明了缺乏时态上下文如何影响代码修改过程。在这种情况下,需要在“Startup()”方法内的“Create-Service()”调用中添加“Context”参数。然而,由于LLM缺乏时态上下文,它不知道“CreateService()”的签名更改,因此无法识别需要更新所有调用者的需求。这个遗漏导致了整个代码库中的许多未能更新的内容。
**关于时态和空间上下文的重要性的观察:** 还需要强调另一个重要的观察结果,即在缺乏足够的空间上下文时,虚假块的数量增加。这种现象发生是因为在缺乏足够的空间上下文的情况下,大型语言模型(LLM)可能会错误地感知缺失的代码元素并尝试创建它们,导致虚假代码块的生成。
图10中的一个示例说明了这个问题。在这种情况下,任务是通过将日志调用从旧的日志框架迁移到新的框架来修改“AuthorizeUser()”方法。然而,由于缺乏空间上下文,无法指定“GetUserSubscription()”方法和“CurrentUser”属性的存在,LLM试图创建这些元素。因此,不仅解决了日志迁移问题,LLM还引入了不必要的代码块,比如创建“GetUserSubscription()”方法和将“CurrentUser”添加为类级对象。
这一观察结果强调了空间上下文在引导LLM理解代码结构和关系方面的关键作用。提供全面的空间上下文有助于防止生成多余的代码块,并确保代码修改精确且与预期的更改一致。
总之,实验结果强调了时态和空间上下文在CodePlan的规划中的基本性质。由于缺乏时态和空间上下文而导致的错过和虚假更新的增加强调了通过这些上下文向LLM提供对代码演化和依赖关系的全面理解对于确保准确和有效的代码修改的重要性。
**5.3 RQ3:CodePlan在解决复杂编码任务方面的关键差异因素是什么?**
**动机:** RQ3旨在揭示CodePlan相对于基线方法在处理复杂编码任务方面表现出色的关键不同之处。这个研究问题的动机在于需要识别和理解有助于CodePlan表现出众的具体因素。考虑到编码任务日益复杂,尤其是存储库级别的任务,因此有必要明确指出将CodePlan与传统方法区分开的具体方面。通过识别这些不同之处,研究旨在揭示CodePlan的优势和优点,为解决复杂编码任务所带来的挑战提供有价值的见解。
**实验设置:** 这里的主要关注点是定性分析。在使用CodePlan和基线方法进行代码修改后,通过手工检查结果进行仔细分析。这包括详细检查每种方法所做的更改,目的是深入了解决策过程和代码修改的微妙差异。通过手动检查和比较这些更改,研究旨在发现微妙但关键的差异,以阐明CodePlan在处理复杂编码任务时的优势和基础机制。这种定性方法提供了全面了解CodePlan在这一背景下表现出色以及它与传统方法的不同之处的理解。
**结果讨论:**
**CodePlan的战略规划和上下文感知:**
CodePlan在处理复杂编码任务方面的卓越表现可以归因于其强大的功能,尤其是其增量分析和更改可能影响分析。这些功能使其与像Build-Repair这样的基线方法区分开来,后者主要侧重于保持语法正确性,而忽略了关键的上下文细节。为了说明这一点,让我们深入研究图11所示的存储库Ext-1中的一个示例,在这个示例中,CodePlan的任务是将Console.WriteLine方法迁移到ITestOutputHelper.WriteLine。这个迁移涉及到一系列的更改1到4,如图11所描述。这些级联更改始于引入ITestOutputHelper _output作为类级成员,这是通过LLM更新完成的。
在这种情况下,CodePlan的更改可能影响分析在发挥作用方面无价。它认识到添加新字段需要修改构造函数以确保正确初始化。因此,CodePlan安排了必要的构造函数修改。因此,构造函数Subscriber(...)被正确更新为接受ITestOutputHelper作为参数并初始化类成员_output。这反过来导致了存储库中的一系列更改,如图11中的步骤1到4所解释的那样。
这个例子说明了CodePlan如何根据上下文和上下文感知的能力,有序地对存储库进行更改,得益于它的更改影响分析和引入时态上下文的能力。相比之下,仅依赖于语法正确性的Build-Repair无法检测到Subscriber构造函数中的修改需要修改。由于遵守了所有语法规则,它不会触发构建错误,因此无法在图4中所示的步骤2到4中执行更改,而只会执行步骤1中概述的修改,导致代码更新不完整。
CodePlan的独特优势在于它对代码关系的全面理解和精心规划,确保了代码库的完整性和功能在复杂编码任务中得以保持。这种定性分析突显了CodePlan在处理复杂编码任务时的微妙方法如何胜过基线方法。
**增量分析:与依赖图的关系维护:**
CodePlan在应对复杂编码任务时的卓越表现归功于其增量分析,该分析有效地将更改与底层的依赖图关联起来。与静态代码快照不同,静态代码快照可能导致依赖关系的不完整表示,我们的增量分析方法确保了依赖图内部的关系保持到受影响的代码块被修改之前。
考虑一个调用者函数进行重命名的情况。传统的静态快照可能难以保持调用者-被调用者关系,因为在它们的视图中,调用者已经被重命名。然而,CodePlan的增量分析介入,保持调用者
**7 相关工作**
**用于编码任务的大型语言模型 (LLMs):** 已经训练了大量的LLMs [10, 19, 21, 24, 28, 30, 35, 57, 73–75, 80],这些模型是基于大规模的源代码和自然语言文本语料库的。它们已被用于完成各种编码任务。一些示例包括程序合成 [50, 56]、程序修复 [11, 43, 79]、漏洞修补 [60]、推断程序不变式 [62]、测试生成 [69] 和多任务评估 [72]。然而,这些研究是在从其存储库中提取的策划示例上进行的,并且旨在通过独立调用LLM来完成。我们考虑的是一类不同的任务,这些任务是在代码存储库的规模上提出的,LLM在不同的示例上被多次调用,这些示例是相互依赖的。我们在存储库范围内监视每个LLM调用的结果,以确定未来的代码更改义务,以使存储库达到一致状态,例如,存储库不包含构建或运行时错误。
**自动化规划:** 自动化规划 [37, 67] 是人工智能领域中一个广泛研究的主题。在线规划 [67] 用于在不知道动作效果并且无法预先列举状态空间的情况下。它需要监视动作和计划扩展。在我们的情况下,编辑动作由LLM执行,其结果不能在预先知道之前预测,并且状态空间是无界的。因此,我们的自适应规划是一种在线算法,我们通过静态分析监视动作并扩展计划。在另一方面,[42] 使用LLM从自然语言意图中推导出一个计划,然后生成代码以解决复杂的编码问题,而[86]执行前瞻规划(树搜索)来引导代码LLM的标记级解码。我们的工作中的规划是基于分析依赖关系和LLM对代码存储库进行更改的影响而进行的。
**代码更改分析:** 静态分析用于确保软件质量。每当代码发生更改时,重新计算分析结果都是昂贵的。增量程序分析领域提供了一些技术,可以仅重新计算受更改影响的分析结果。针对数据流分析 [18, 68]、指针分析 [84]、符号执行 [63]、错误检测 [52] 和类型分析 [27] 已经开发了专门的算法。程序差异分析 [16, 46, 48] 和更改影响分析 [17, 41] 确定了两个程序版本之间的差异以及更改对程序的其余部分的影响。已经研究了更改对回归测试 [65]、分析重构 [33] 和协助代码审查 [13, 36] 的影响。我们分析由LLM生成的代码,逐步更新了语法(例如,父子关系)和依赖关系(例如,调用者-被调用者关系)。我们进一步分析了这些更改对相关代码块的可能影响,并创建了由LLM执行的更改义务。
**空间和时间上下文化:** 如介绍中所讨论的,LLMs受益于从存储库中的其他文件和过去的编辑中提取的相关上下文。我们通过跟踪代码更改和依赖关系提供了这两种信息给LLM。
**学习编辑模式:** 已经开发了许多方法,用于从过去的编辑或提交中学习编辑模式,包括重写规则 [31]、错误修复 [15, 20]、类型更改 [45]、API迁移 [49, 82] 和编辑的神经表示 [83]。诸如 [53] 和 [54] 的方法从用户提供的示例中合成上下文感知的编辑脚本,并将它们应用于新的上下文中。其他方法观察IDE中的用户操作,以自动化重复的编辑 [55] 和与时间相关的编辑序列 [87]。我们不打算学习编辑模式,也不假设编辑之间存在相似性。我们的重点是识别LLM进行的代码更改的影响,并引导LLM进行必要的其他更改。
**8 结论和未来工作**
在本文中,我们介绍了CodePlan,这是一个新颖的框架,旨在解决存储库级别编码任务的挑战,这些任务涉及到跨大规模和相互依赖的代码库的广泛更改。CodePlan利用增量依赖分析、更改可能影响分析和自适应规划,通过Large Language Models引导多步编辑。我们在不同复杂性和大小的各种代码存储库上评估了CodePlan,包括内部专有存储库和C#和Python的公共GitHub存储库,用于迁移和时间编辑任务。我们的结果表明,CodePlan优于基线方法,与实际情况更加一致。总之,CodePlan提供了一种有望自动化复杂的存储库级别编码任务的方法,既提高了生产率又提高了准确性。它在解决这些挑战方面的成功为高效和可靠的软件工程实践开辟了新的可能性。
虽然CodePlan表现出了巨大的潜力,但还有许多未来研究和改进的方向。首先,我们的目标是将其适用于更广泛的编程语言和代码工件范围,包括配置文件、元数据和外部依赖项,以提供更全面的存储库级别编辑解决方案。此外,我们计划进一步定制CodePlan的更改可能影响分析。这可以通过通过基于规则的方法或更高级的机器学习技术将任务特定的影响分析规则纳入其中,以微调其特定的编码任务的编辑决策。此外,我们将解决处理动态依赖关系的挑战,例如数据流依赖关系、复杂的动态调度(通过虚拟函数和动态转换)、算法依赖关系(例如,输入列表预计要排序)和各种执行依赖关系(例如,多线程和分布式处理),以使CodePlan在更广泛的软件工程任务中更加灵活。