- Published on
【译】使用 AlphaCodium 的最先进代码生成 - 从提示工程到流程工程
TL;DR
代码生成问题与常见的自然语言问题不同——它们需要匹配目标语言的确切语法,识别快乐路径和边缘案例,关注问题规范中的众多小细节,并解决其他特定于代码的问题和要求。因此,许多在自然语言生成中成功的优化和技巧可能对代码任务无效。
在这项工作中,我们提出了一种新的代码生成方法,称为 AlphaCodium——一种基于测试的多阶段代码导向迭代流程,旨在提高 LLM 在代码问题上的表现。
我们在一个名为 CodeContests 的具有挑战性的代码生成数据集上测试了 AlphaCodium,该数据集包括来自 Codeforces 等平台的竞争编程问题。所提出的流程始终如一地显著改善结果。
例如,在验证集上,GPT-4 的准确率(pass@5)从使用单个设计良好的直接提示的 19% 提高到使用 AlphaCodium 流的 44%。AlphaCodium 还超越了之前的工作,如 AlphaCode,同时计算预算显著更小。
我们相信,在这项工作中获得的许多原则和最佳实践广泛适用于一般代码生成任务。
在我们非常新的开源项目 AlphaCodium 3.4K 中,我们分享了我们的 AlphaCodium 解决方案,以应对 CodeContests,以及完整的可重复数据集评估和基准测试脚本,以鼓励该领域的进一步研究。
CodeContests 数据集
CodeContests 是一个由谷歌 Deepmind 提出的具有挑战性的代码生成数据集,涉及从竞争编程平台如 Codeforces 精心策划的问题。该数据集包含约 10K 个问题,可用于训练 LLM,并且有一个验证集和测试集用于评估 LLM 解决具有挑战性的代码生成问题的能力。
在这项工作中,我们没有训练一个专门的模型,而是专注于开发一个面向代码的流程,可以应用于任何经过预训练以支持编码任务的 LLM,例如 GPT 或 DeepSeek。因此,我们选择忽略训练集,专注于 CodeContests 的验证集和测试集,分别包含 107 和 165 个问题。图 1 描绘了 CodeContests 数据集中一个典型问题的示例:
图 1. 一个典型的 CodeContests 问题。
每个问题由描述和公共测试组成,作为模型的输入。目标是生成一个代码解决方案,该解决方案对任何(合法)输入产生正确的输出。一个私有测试集,不对模型或参赛者开放,用于评估提交的代码解决方案。
是什么使 CodeContests 成为评估 LLM 在代码生成任务上表现良好的数据集?
- CodeContests 与许多其他竞争编程数据集不同,利用了一个全面的私有测试集以避免假阳性——每个问题包含约 200 个私有输入输出测试,生成的代码解决方案必须通过这些测试。
- LLM 通常不擅长关注小细节,因为它们通常将问题描述转化为某种“平均”描述,类似于它们训练时的常见案例。另一方面,现实世界的问题通常包含对其正确解决至关重要的小细节。CodeContests 数据集的一个关键特征是,问题描述在设计上复杂且冗长,包含小细节和细微差别(见图 1 中的典型问题描述)。我们认为,增加这种问题理解的自由度是有益的,因为它模拟了现实生活中的问题,这些问题通常复杂并涉及多个因素和考虑。这与更常见的代码数据集如 HumanEval 相对,在这些数据集中,问题较简单且以简洁的方式呈现。一个典型的 HumanEval 代码问题的示例出现在附录 1 中。
图 2 描述了模型对图 1 中提出的问题的自我反思。注意,适当的自我反思使问题变得更加清晰和连贯。这说明了理解问题的重要性,这是一个可以高概率导致正确代码解决方案的流程的一部分。
图 2. 对图 1 中提出的问题的 AI 生成的自我反思。
提出的流程
由于代码生成问题的复杂性,我们观察到单一提示优化,甚至是思维链提示,并没有显著提高 LLM 在 CodeContest 上的解决比例。模型难以理解和“消化”问题,并不断产生错误代码,或者生成无法推广到未见私有测试的代码。适合自然语言任务的常见流程,可能并不适合代码生成任务,这其中包含了一个未开发的潜力——反复运行生成的代码,并将其与已知示例进行验证。
与 NLP 中常用的提示工程技术不同,我们发现解决 CodeContest 问题时,采用一个专门针对代码生成和测试的 流程 更为有益,该流程围绕一个 迭代 过程展开,我们反复运行并修复生成的代码以应对输入输出测试。这个以代码为导向的流程的两个关键要素是 (a) 在预处理阶段生成额外数据,例如自我反思和公共测试推理,以帮助迭代过程,以及 (b) 用额外的 AI 生成测试丰富公共测试。在图 3 中,我们展示了我们提出的解决竞争编程问题的流程:
图 3. 提出的 AlphaCodium 流程.
图 3 中的流程分为两个主要阶段:
- 预处理 阶段表示一个线性流程,我们用自然语言推理问题。
- 代码迭代 阶段包括迭代阶段,我们生成、运行并修复代码以应对特定测试。在表 1 中,我们回顾不同的阶段:
阶段名称 | 任务 |
问题反思 | 用要点描述问题,同时阐明问题目标、输入、输出、规则、约束和其他相关细节。 |
公共测试推理 | 解释每个测试输入如何导致输出。 |
生成可能的解决方案 | 生成2-3个可能的解决方案列表,用自然语言描述。 |
排名解决方案 | 对可能的解决方案进行排名,并选择“最佳解决方案”,考虑正确性、简单性和稳健性。(不一定选择“最高效”的解决方案)。 |
生成额外的AI测试 | 为问题生成额外的6-8个多样化的输入输出测试。尽量覆盖原始公共测试未覆盖的情况和方面。 |
初始代码解决方案 | 本阶段的目标是生成问题的初始代码解决方案。确保这段代码尽可能接近正确代码,以便在接下来的阶段中运行修复迭代有更好的成功机会。阶段流程:– 选择一个潜在解决方案。生成相应的代码,并在选定的公共和AI测试上运行。– 重复此过程,直到测试通过,或达到尝试限制。– 第一个通过测试的代码,或输出最接近的代码,将用作下一步的基础代码。 |
在公共测试上迭代 | 从基础代码开始。迭代地在公共测试上运行。如果代码在特定测试中失败,尝试根据错误信息进行修复。 |
在AI生成的测试上迭代 | 在AI生成的测试上继续进行运行修复迭代。使用“测试锚点”(见下一节) |
对提议流程的额外直觉和见解:
知识积累 – 我们尝试从简单到困难逐步推进,在此过程中获得知识和见解,以帮助应对更困难的阶段。例如,第一步,自我反思,可以作为更困难步骤如 生成可能的解决方案 的输入。预处理阶段的输出用于帮助最具挑战性和关键性的阶段,即代码迭代,在这一阶段我们实际尝试生成正确解决问题的代码。
生成额外的AI测试比生成完整的解决方案代码更容易 – 生成额外的测试主要需要理解问题和基本的暴力破解或逻辑推理。生成额外有用的输入-输出测试对并不需要完全“解决”问题。这与生成正确的解决方案代码形成对比,后者需要完整的算法解决方案,相当于正确解决任何可能的输入-输出测试对。因此,我们可以生成更多的AI测试,然后利用它们来改善代码创建阶段,如图4所示。我们通过要求模型关注原始公共测试未涉及的方面(例如大输入、边缘案例等)进一步增强这些额外测试的贡献。
步骤可以合并为单个LLM调用 – 图3中描述的流程是一个概念流程,强调过程的高层步骤。在实践中,结构化输出(见下一节)使得可以将多个阶段合并为单个LLM调用,以节省资源,或者因为模型在同时执行特定任务时表现更好。
图4. 说明应用AlphaCodium流程时的改进. 通过直接提示,模型在解决代码问题时遇到困难。对公共测试的迭代稳定并改善了解决方案,但留下了“盲点”,因为公共测试并不全面。完整的AlphaCodium流程,包括预处理阶段以及对公共和AI生成测试的迭代,允许进一步改善解决方案,从而提高解决比例。
面向代码的设计概念
在本节中,我们将介绍一些设计概念、技巧和最佳实践,这些在解决代码生成问题时被我们认为是有益的。图3中提出的AlphaCodium流程广泛使用了这些设计概念:
YAML结构化输出: 使用结构化输出——要求模型生成与给定Pydantic类等效的YAML格式输出——是我们提出的流程中的一个关键组成部分。以下是此类指令的示例:
...
Your goal is to present possible solutions to the problem.
Make sure that each solution fully addresses the problem goals, rules, and constraints.
The output must be a YAML object equivalent to type $PossibleSolutions, according to the following Pydantic definitions:
class Solution(BaseModel):
name: str = Field(description="The name of the solution")
content: str = Field(description="A description of the solution")
why_it_works: str = Field(description="Why this solution is correct. Be specific and detailed regarding the problem rules and goals")
complexity: str = Field(description="The complexity of the solution")
class PossibleSolutions(BaseModel):
possible_solutions: List[Solution] = Field(max_items=3, description="A list of possible solutions to the problem. Make sure each solution fully addresses the problem rules and goals, and has a reasonable runtime - less than three seconds on a modern computer, given the problem constraints for large inputs.")
表 2. 带有结构化输出的提示示例(生成可能解决方案阶段)。
结构化输出消除了“提示工程”所需的大部分麻烦和隐性知识,而是允许复杂任务以简单的、类似代码的方式呈现。它还使得获得涉及多个阶段的复杂答案成为可能,代表了一种逻辑和系统的思维过程。
虽然较新版本的 GPT 内置支持 JSON 风格 输出,但我们认为 YAML 输出对于代码生成任务更为合适,见下文附录。
要点分析 – 当请求 LLM 对一个问题进行推理时,通常在要求输出为要点格式时会获得更好的结果。要点鼓励对问题的深入理解,并迫使模型将输出分成逻辑语义部分,从而提高结果。例如,通过对问题进行要点自我反思(见图 2),每个要点代表对问题不同部分的语义理解——一般描述、目标和规则、输入结构和输出结构。
LLM 在生成模块化代码时表现更好 – 当要求 LLM 生成一个单一的冗长函数时,我们观察到结果较差——代码通常包含错误或逻辑错误。更糟糕的是,单一的整体代码会影响迭代修复的能力——即使在给出错误信息时,模型也很难定位和修复问题。当明确要求模型:“将生成的代码划分为小的子函数,具有有意义的名称和功能”时,我们观察到生成的代码更好,错误更少,迭代修复阶段的成功率更高。
双重验证的软决策 – LLM 在需要思考、推理和做出严格、非平凡决策的代码任务中往往会遇到困难。以生成问题的额外测试为例,模型生成的一些测试往往是错误的。通过 双重验证 过程,我们增加了一步,在给定生成的输出时,要求模型重新生成相同的输出,但在必要时进行修正。例如,给定生成的 AI 测试作为输入,要求模型重新生成相同的测试,同时修正错误的输出(如果存在)。我们发现,这一步 双重验证 在鼓励模型进行批判性思考和推理方面,比直接问“这个测试正确吗?”更有效。
推迟决策,尽量避免直接提问,并留出探索的空间 – 当我们向模型提出关于复杂问题的直接问题时,我们始终会看到幻觉和错误答案。
因此,类似于Karpathy在下面推文中的信息,我们采用逐步数据积累的流程,从简单任务到困难任务:
- 从最简单的任务开始 – 对问题进行自我反思,并推理公共测试。
- 转向生成额外的AI测试,以及可能的解决方案。
- 只有在我们获得模型对上述任务的回答后,才进行实际的代码生成和运行修复迭代。
作为另一个例子,我们更倾向于对多个可能的解决方案进行排名,而不是选择单一的算法解决方案,并在生成初始代码时优先考虑排名最高的解决方案,但不排他。由于模型可能是错误的,最好避免不可逆的决策,并留出探索和不同可能解决方案的代码迭代空间。
测试锚点 – 即使经过双重验证,一些AI生成的测试也会是错误的。这使得对它们的迭代变得具有挑战性 – 当测试失败时,我们如何知道是代码错误,还是测试错误?当我们直接问模型“谁错了”时,我们经常会看到幻觉,并可能最终得到错误修复的代码。为了解决这个问题,我们利用了一种“测试锚点”的技术:
- 首先对我们知道是正确的公共测试进行迭代。当完成后,将所有通过的测试设置为锚点测试。
- 现在逐个对AI生成的测试进行迭代。如果测试通过,将其添加到测试锚点列表中。
- 如果测试失败,假设是因为代码不正确,并尝试修复代码。然而,要求修复后的代码也通过所有已获得的测试锚点。结果,测试锚点将保护我们免受错误修复代码的影响。
另一个针对测试锚点的优化是将AI生成的测试从简单到困难进行排序。这样,迭代过程在开始时获得锚点的机会更多,这些锚点可以在后续迭代更复杂的AI测试时作为保护,因为在这些情况下错误测试输出的可能性更高。
结果
直接提示与AlphaCodium
在图5中,我们将AlphaCodium的结果与使用单个设计良好的直接提示获得的结果进行比较。使用的指标是pass@k,定义为每个问题通过使用k个生成解决方案解决的问题的百分比。
图5. AlphaCodium流程结果与各种模型的直接提示比较。
如图所示,AlphaCodium流程持续且显著地提高了LLMs在CodeContests问题上的表现。这对于开源(DeepSeek)和闭源(GPT)模型,以及验证集和测试集都是如此。
与其他工作的比较:
在表3中,我们比较了文献中其他方法的AlphaCodium结果。
模型 | 数据集 | 方法 | 得分 |
GPT-3.5 | 验证集 | AlphaCodium (pass@5) | 25% |
CodeChain (pass@5) | 17% | ||
测试集 | AlphaCodium (pass@5) | 17% | |
CodeChain (pass@5) | 14% | ||
GPT-4 | 验证集 | AlphaCodium (pass@5) | 44% |
DeepMind微调 | 验证集 | AlphaCode (pass@10@1K) | 17% |
AlphaCode (pass@10@100K) | 24% | ||
GPT-4 | 测试集 | AlphaCodium (pass@5) | 29% |
DeepMind微调 | 测试集 | AlphaCode (pass@10@1K) | 16% |
AlphaCode (pass@10@100K) | 28% | ||
Gemini-pro | AlphaCode2:在可用版本的CodeContests上没有可比较的结果。根据AlphaCode2技术报告,作者在一个未发布的数据集上比较AlphaCode与AlphaCode2的结果,AlphaCode在使用4个数量级更少的LLM调用__(@100)时,取得了类似的结果(29%,pass@10)__。 |
图6. 效率比较. AlphaCodium与其他解决方案的准确性与LLM调用比较。虽然AlphaCodium在与AlphaCode的比较中达到了相似的准确性,但它使用的LLM调用数量少了四个数量级。
如图所示,在将AlphaCodium与CodeChain进行比较时,使用相同的模型(GPT-3.5)和相同的指标(pass@5),AlphaCodium的表现始终更好。在将AlphaCodium与AlphaCode的工作进行比较时,我们需要考虑到AlphaCode使用了不同的生成方法——对一个(未知的)模型进行微调,专门针对代码问题,生成大量的代码解决方案,对其进行聚类,并从顶级聚类中提交K个解决方案。例如,pass@10@100K意味着生成和聚类了100K (!) 个解决方案,最终选择并提交了10个解决方案。AlphaCode使用了一个微调的模型,并采用了类似暴力搜索的方法,LLM调用的数量显著更高。然而,AlphaCodium获得的最佳结果更好。
还需注意的是,AlphaCode和CodeChain都没有发布可重复的解决方案,包括端到端评估脚本。在评估结果时存在一些细微差别。例如——如何处理具有多个解决方案的问题,如何解决容忍度问题、超时等。我们比较了论文中报告的数字,但发布了完整的可重复代码和评估脚本,以便未来的比较更加可靠和可重复。
计算努力与AlphaCode和AlphaCode2的比较:
AlphaCodium流程每个解决方案执行约15-20次LLM调用,因此pass@5提交涉及约100次LLM调用。
AlphaCode没有报告每次运行进行了多少次LLM调用。假设每次运行进行了一个调用(未知,可能更多),那么pass@10@100K(即十次提交,从100,000个生成的解决方案中筛选)涉及100万次LLM调用,比AlphaCodium多出四个数量级。然而,AlphaCodium获得的最佳结果更好。请参见上面的图3,它可视化了这些结果。
最近,发布了一项名为AlphaCode2的新工作(技术报告),其中对Gemini-Pro模型进行了微调并在代码编程问题上进行了评估。该论文还报告了在CodeContests基准上的结果,但在一个未向公众发布的更新变体上。根据AlphaCode2报告:“AlphaCode2需要大约100个样本才能达到AlphaCode在一百万个样本下的性能水平,使其样本效率超过10000倍。”因此,AlphaCode2和AlphaCodium在LLM调用方面比AlphaCode高出四个数量级。
但是,AlphaCode2 利用了一个现代的基础模型,该模型经过了专门针对 CodeContests 竞赛的 微调,而 AlphaCodium 则直接使用通用模型,并在没有额外数据和昂贵训练阶段的情况下提高其性能。
附录
1) HumanEval 代码问题的示例:
/*
Check if in given vector of numbers, are any two numbers closer to each other than given threshold. >>>
has_close_elements({1.0, 2.0, 3.0}, 0.5) false >>>
has_close_elements({1.0, 2.8, 3.0, 4.0, 5.0, 2.0}, 0.3) true
*/
#include<stdio.h>
#include<vector>
#include<math.h>
using namespace std;
bool has_close_elements(vector<float> numbers, float threshold){
表 4.
我们可以看到这个问题相当简单,没有很多模型需要推理的小细节和细微差别。
2) 为什么 YAML 输出比 JSON 输出更适合代码生成任务
虽然较新的 GPT 版本对 JSON 风格 输出有固有支持,但我们认为 YAML 输出对于代码生成要好得多。
原因是生成的代码通常包含单引号、双引号、特殊字符等。大型语言模型在有效地将这些字符放入 JSON 格式时会遇到困难,因为 JSON 输出需要用初始的双引号包围。相比之下,YAML 输出 使用块标量 只需遵循缩进。任何具有适当缩进的文本或代码都是合法的。此外,YAML 输出的标记更少,从而降低了成本和推理时间,并且由于模型需要关注的非必要标记更少,质量也得到了提高。以下是 JSON 与 YAML 比较的示例(使用 https://platform.openai.com/tokenizer 生成):
import json
import yaml
s1 = 'print("double quote string")'
s2 = "print('single quote string')"
s3 = 'print("""triple quote string""")'
s4 = f"{s1}\n{s2}\n{s3}"
# Create a dictionary with keys as variable names and values as the strings
data = {'s1': s1, 's2': s2, 's3': s3, 's4': s4}
# Convert the dictionary to a JSON-formatted string
json_data = json.dumps(data, indent=2)
print(json_data)
# Convert the dictionary to a YAML-formatted string, with block scalar style
yaml_data = yaml.dump(data, indent=2, default_style='|')
print(yaml_data)
Output:
表 5.
JSON 输出:
图 7. 使用 JSON 输出的令牌计数.
YAML 输出:
图 8. 使用 YAML 输出的令牌计数.
显然,在仅保持缩进的情况下生成代码更简单、更易读且更不容易出错。
Source: https://www.codium.ai/blog/alphacodium-state-of-the-art-code-generation-for-code-contests