概览
代码生成的难题与普通的自然语言处理不同 —— 它们涉及严格遵循目标编程语言的语法规则、识别正常和边界情况、关注问题规范中的众多细节,并应对代码特有的其他问题和需求。因此,自然语言生成领域的许多常用优化技巧对代码生成任务来说可能并不适用。
在这项研究中,我们提出了一种全新的代码生成方法,名为 AlphaCodium —— 一种基于测试、分阶段、专注于代码的迭代处理流程。这种方法显著提升了大语言模型 (LLM) 在处理代码问题上的能力。
我们在一个具有挑战性的代码生成数据集 CodeContests 上进行了 AlphaCodium 的测试,这个数据集包含了来自 Codeforces 等平台的竞赛编程题目。我们的方法在这些测试中始终保持着显著的性能提升。
例如,在验证数据集上,使用 AlphaCodium 流程后,GPT-4 的准确率(pass@5)从单一精心设计的直接提示的 19% 提升到了 44%。AlphaCodium 在性能上不仅超越了之前的研究成果,如 AlphaCode,而且所需的计算资源也大大减少。
我们认为,在这项工作中形成的许多原则和最佳实践普遍适用于代码生成的各种任务。我们在最新开源的项目 AlphaCodium 中分享了我们针对 CodeContests 的 AlphaCodium 解决方案,并提供了完整的数据集评估和基准测试脚本,以便社区进一步研究和探索。
CodeContests 数据集解析
CodeContests 是由谷歌 Deepmind 推出的一项挑战性编程数据集。它从如 Codeforces 这样的竞赛编程平台精选了约 1 万个编程题目,旨在训练和评估大语言模型 (大语言模型,如 GPT 或 DeepSeek) 解决复杂编程问题的能力。
本研究并未专注于开发一个全新的模型,而是着眼于创建一个适用于各种已经能够处理编码任务的大语言模型的编程流程。因此,我们主要关注 CodeContests 的验证集和测试集,其中包括 107 和 165 个编程问题。图 1 展示了数据集中的一个典型问题实例:
每个问题都包括了一个问题描述和一些公开的测试数据,可以直接用于模型输入。挑战在于编写出一个程序,能够针对任何合法输入都给出正确答案。此外,还有一个不对外公开的测试集,用于评估提交的程序是否正确。
为什么说 CodeContests 是测试大语言模型编程能力的理想数据集呢? 首先,与其他编程竞赛数据集不同,CodeContests 包含大量私有测试数据(每个问题约 200 个测试用例),以确保评估的准确性。其次,大语言模型通常不擅长注意到问题描述中的细节,而这些细节往往对于找到正确的解决方案至关重要。CodeContests 的问题描述通常既复杂又详细,充满了影响解决方案的细微差别(图 1 中展示了一个典型例子)。这种设计模拟了现实中的复杂问题,迫使模型考虑多种因素,这与一些较为简单直接的数据集(如 HumanEval)形成了鲜明对比。附录 1 中展示了一个典型的 HumanEval 编程问题。
图 2 展示了模型如何深入分析图 1 中的问题。通过深入的分析,问题变得更加清晰和条理化,这强调了在编程过程中对问题深入理解的重要性。
提出的方法
在处理代码生成的复杂挑战时,我们发现,无论是单一提示的优化,还是连续思考式的提示,都未能显著提升大语言模型 (LLM) 在 CodeContest 上的问题解决效率。这是因为模型往往难以完全理解问题,从而反复产生错误或无法应对新的测试用例的代码。对于代码生成任务来说,适用于一般自然语言处理的方法可能并不理想。这类任务隐藏着巨大的潜力,比如反复执行所生成的代码,并对其进行已知示例的验证。与常规自然语言处理中的提示优化技术不同,我们发现,在解决 CodeContest 的问题时,采用专门针对代码生成和测试的流程更为有效。这个流程围绕着迭代过程展开,即我们不断地运行和调整生成的代码,使其通过输入 – 输出测试。这种针对代码的流程的两个关键环节是:(a) 在预处理阶段生成额外数据,例如自我反思和公开测试用例的推理,以支持迭代过程,以及 (b) 用 AI 生成的额外测试用例来增强公开测试用例。在图 3 中,我们展示了我们为解决竞赛编程问题设计的流程:
图 3 中的流程分为两个主要阶段:
- 预处理 阶段,我们使用自然语言对问题进行推理,这是一个线性流程。
- 代码迭代 阶段,包括多个迭代环节,我们在这些环节中生成、运行并修正代码,以应对各种测试。
在表 1 中,我们详细回顾了这些不同阶段:
阶段名称 | 任务说明 |
问题反思 | 用简洁的要点形式概述问题,涉及问题的目标、输入、输出、规则、约束及其他重要细节。 |
公开测试用例的逻辑分析 | 阐述每个测试用例的输入是如何导致特定输出的。 |
构思可能的解决方案 | 提出 2-3 种可能的解决方案,并用通俗易懂的语言进行描述。 |
解决方案评估 | 对各种可能的解决方案进行评估,挑选出最佳方案,考虑到其正确性、简洁性和稳健性。(不必局限于效率最高的方案)。 |
补充 AI 测试 | 为问题补充 6-8 个不同类型的输入输出测试,试图覆盖原始公开测试用例未涉及的情况和方面。 |
初步代码方案 | 本阶段的目标是形成对问题的初步代码解决方案。重要的是,这个代码应尽可能贴近正确答案,以便在接下来的修正过程中更有可能成功。 操作流程如下: – 选定一个可能的方案,为其编写相应代码,并在选定的公开测试用例和 AI 测试中进行试运行。 – 重复此过程,直至通过测试或达到尝试次数上限。 – 第一个通过测试的代码,或者输出结果最接近正确答案的代码,将作为后续步骤的基础代码。 |
公开测试用例的迭代优化 | 以基础代码为起点,逐一在公开测试用例中运行并优化。若代码在某一测试中出现问题,根据错误信息尝试修正。 |
AI 测试的迭代优化 | 继续在 AI 生成的测试中进行迭代优化。运用“测试锚点”(这是一种在测试中固定某些特定元素的技术,以便更精确地调试和改进代码)。 |
表 1. 梳理 AlphaCodium 各阶段的特点。
在探索所提出的流程时,我们获得了一些深刻的直觉和洞察。
首先是知识积累的阶段:我们会从简单的任务开始,逐渐挑战更复杂的问题。例如,在流程的首步——自我反思,我们学习到的知识可用于后续更难的步骤,比如生成可能的解决方案。流程中的预处理阶段产生的成果,将助力最具挑战性且关键的部分:代码迭代,即我们实际上尝试编写出能够正确解决问题的代码。
接下来,生成额外的 AI 测试比生成一整套解决方案代码要简单 —— 这个过程主要依赖于对问题的理解以及基础的暴力破解或逻辑推理,而不必完全解决问题就能生成有用的输入输出测试对。这与编写一个完整的、正确的解决方案代码不同,后者要求我们提出一个能够正确应对任何输入输出测试对的完整算法方案。因此,我们可以创造更多的 AI 测试,利用它们来优化代码创建阶段,正如图 4 所示。我们还通过要求模型专注于原始公开测试用例未涵盖的内容,如处理大型输入、边缘情况等,来进一步增强这些额外测试的效果。
最后,多个步骤可以合并成一次大语言模型 (LLM) 调用 —— 图 3 中展示的流程是理念上的展示,强调了流程的主要步骤。在实际操作中,通过结构化输出(见下一节),我们可以将多个阶段合并成一次大语言模型调用,以节约资源或提升模型在同时处理特定任务时的表现。
图 4. 展示应用 AlphaCodium 流程时的改进。 当模型只根据直接提示来解决代码问题时,它往往会遇到困难。在公开测试用例基础上迭代可以稳定并改进解决方案,但由于公开测试用例不够全面,仍会留有“盲点”。而当我们采用完整的 AlphaCodium 流程,包括预处理阶段和在公开及 AI 生成的测试上进行迭代时,可以进一步提升解决方案的质量,从而显著提高解决问题的成功率。
针对代码的设计概念
在这一节中,我们会介绍一些在解决代码生成问题时发现的有益设计理念、技巧和最佳实践。我们在图 3 中提出的 AlphaCodium 流程,就广泛应用了这些设计理念:
YAML 结构化输出: 我们提出的流程中一个关键部分是使用结构化输出 —— 要求模型生成一个等效于特定 Pydantic 类的 YAML 格式输出。举个例子:
...
你的目标是提出可能的解决方案。确保每个方案都全面考虑到了问题的目标、规则和限制。输出应是一个 YAML 对象,与 $PossibleSolutions 类型相对应,根据以下 Pydantic 定义:
class Solution(BaseModel):
name: str = Field(description="解决方案的名称")
content: str = Field(description=解决方案的描述")
why_it_works: str = Field(description="为什么这个解决方案有效。需针对问题的规则和目标进行具体详细说明")
complexity: str = Field(description="解决方案的复杂性")
class PossibleSolutions(BaseModel):
possible_solutions: List[Solution] = Field(max_items=3, description="针对问题的可能解决方案列表。确保每个解决方案都全面考虑到了问题的规则和目标,并且在现代计算机上的运行时间合理 — 对于大量输入的问题约束,运行时间不超过三秒。")
表 2. 结构化输出提示的示例(生成可能解决方案阶段)。
结构化输出减少了“提示工程”的复杂性以及对黑科技的要求,转而以一种直接、类似代码的方式呈现复杂任务。它还使得获得包含多个阶段、反映逻辑和有条理思考过程的复杂答案成为可能。
虽然新版 GPT 支持 JSON 风格 输出,但我们认为,尤其是在代码生成任务中,YAML 输出更为适合,详见附录。
要点列表(Bullet points)分析 – 当让大语言模型 (LLM) 分析问题时,通常以要点列表(Bullet points)格式要求输出会获得更好的结果。要点促进了对问题的深入理解,并迫使模型将输出划分为逻辑上的语义区域,从而提高了结果的质量。例如,以要点自我反思问题(见图 2),每个要点代表了对问题不同部分的语义理解——一般描述、目标与规则、输入结构、输出结构。
大语言模型在生成模块化代码方面更加出色 – 当我们让大语言模型(LLM)去编写一个长篇的单个函数时,常常会遇到问题:代码中经常出现错误或逻辑漏洞。更严重的是,这种庞大而单一的代码块会影响到后续的迭代修复工作。即便提供了错误信息,模型也很难准确地定位和解决问题。但如果我们明确指导模型:“把生成的代码分割成多个小的子功能模块,并给它们起上有意义的名称”,结果会好得多,生成的代码错误更少,且在迭代修复的阶段有更高的成功率。
灵活决策和双重验证的重要性 – 大语言模型在处理那些需要深思熟虑、合理推断和做出严肃、非常规决策的代码任务时,往往会遇到困难。例如,在生成问题的附加测试时,模型生成的测试常常存在错误。为了解决这个问题,我们引入了双重验证的过程。在这个过程中,模型在生成了初始输出之后,会被要求再次生成相同的输出,并在必要时进行修正。比如,模型在接收到它自己生成的 AI 测试作为输入后,需要重新生成这些测试,并及时纠正其中的错误(如果有的话)。我们发现,这种双重验证的步骤,不仅促使模型进行批判性思考和推理,而且比直接提出“这个测试正确吗?”这样的是/否问题更为有效。
延迟做决策,避免直接提问,给予探索空间 – 当我们直接向模型询问复杂问题时,经常会得到错误或不切实际的答案。因此,我们采取了类似 Karpathy 在下面的推文中所述的方法,逐步积累数据,从简单任务逐渐过渡到复杂任务:
- 首先从最简单的任务开始,即对问题进行自我反思和关于公开测试用例的推理。
- 然后转向生成附加的 AI 测试和可能的问题解决方案。
- 只有在得到模型对上述任务的回答后,我们才进入实际的代码生成和运行修复的迭代过程。
再比如,我们不是选择一个单一的算法解决方案,而是评估并排序多个可能的解决方案,优先考虑排名靠前的方案进行初始代码编写。由于模型可能会出错,我们更倾向于避免做出不可逆的决定,而是留出空间进行探索,以及尝试不同可能解决方案的代码迭代。
测试锚点技术 – 尽管经过两次验证,某些 AI 生成的测试仍可能出错。这就带来了一个问题:当测试失败时,我们如何判断是代码的问题还是测试本身的错误?直接向模型查询“错误在哪里”时,我们常常得到不切实际的回答,有时甚至导致错误地修改了代码。为应对这一挑战,我们引入了一种名为“测试锚点”的方法:
- 首先对公开的、已知正确的测试进行迭代。完成这一步骤后,把所有通过的测试定为基准测试(锚点测试)。
- 然后开始逐个检查 AI 生成的测试。
- 测试通过的,就加入到锚点测试列表中。
- 测试未通过的,则默认为代码有误,进而尝试修正代码。重要的是,修正后的代码必须同时通过所有已有的锚点测试。
通过这种方法,锚点测试在我们修正代码的过程中起到了保护作用,避免我们错误地修正代码。此外,对于锚点测试的另一项改进是,将 AI 生成的测试按难易程度排序。这样一来,在迭代过程的初期更容易获得锚点测试,当处理更复杂的 AI 测试时,这些锚点测试就能提供额外的保障,尤其是在处理那些更可能出现错误输出的 AI 测试时。这种策略有效地增强了测试过程的稳定性和可靠性,特别是在面对复杂和高难度的 AI 生成测试时。
结果
直接提示与 AlphaCodium 的比较
在图 5 中,我们对 AlphaCodium 与单一经过精心设计的直接提示方法的结果进行了对比。评价标准是 pass@k(解决问题的成功率),即对每个问题,通过使用 k 个生成解决方案的比例。
图 5. 在不同模型上,AlphaCodium 方法与直接提示方法的比较。
可以看出,在解决 CodeContests 的编程问题上,AlphaCodium 方法能够显著并持续地提升大语言模型(LLM)的表现。这一结论适用于开源模型(如 DeepSeek)和闭源模型(如 GPT),无论是在验证集还是测试集上。
与其他研究的比较:
在表 3 中,我们展示了 AlphaCodium 与文献中其他方法的对比结果。
模型 | 数据集 | 方法 | 得分 |
GPT-3.5 | 验证集 | AlphaCodium (pass@5) | 25% |
GPT-3.5 | 验证集 | CodeChain (pass@5) | 17% |
GPT-3.5 | 测试集 | AlphaCodium (pass@5) | 17% |
GPT-3.5 | 测试集 | CodeChain (pass@5) | 14% |
GPT-4 | 验证集 | AlphaCodium (pass@5) | 44% |
DeepMind 微调 | 验证集 | AlphaCode (pass@10@1K) | 17% |
DeepMind 微调 | AlphaCode (pass@10@100K) | 24% | |
GPT-4 | 测试集 | AlphaCodium (pass@5) | 29% |
DeepMind 微调 | 测试集 | AlphaCode (pass@10@1K) | 16% |
DeepMind 微调 | 测试集 | AlphaCode (pass@10@100K) | 28% |
Gemini-pro | AlphaCode2: 在现有的 CodeContests 版本中,AlphaCode2 的比较结果未报告。根据 AlphaCode2 的技术报告,研究人员将 AlphaCode 与 AlphaCode2 在一个未发布的数据集上的结果进行了比较,发现在大幅减少大语言模型(LLM)调用次数(@100)的情况下,AlphaCode2 的表现与 AlphaCode 相当,均为 29%, pass@10。 |
表 3. AlphaCodium 与文献中其他研究作品的对比
图中显示,在各种模型和评价标准下,AlphaCodium 方法都展示了优异的性能,特别是在使用大语言模型解决编程挑战时。这些比较结果不仅体现了 AlphaCodium 在技术上的创新,也突显了其在实际应用中的有效性和适用性。
总体而言,AlphaCodium 在智能编程领域展示了其卓越的潜力,特别是在提升大语言模型处理复杂编程问题的能力上。这些发现对于未来的研究和开发具有重要的启示作用,为进一步开发和优化大语言模型提供了宝贵的参考。
图 6: 效率比较。 这是 AlphaCodium 与其他方案在准确度与大语言模型(LLM)调用次数方面的对比。相较于 AlphaCode,AlphaCodium 在达到相似的准确率时,所需的 LLM 调用次数少了数千倍。
当我们将 AlphaCodium 与使用相同的 GPT-3.5 模型和“5 次尝试通过率”标准的 CodeChain 相比较时,可以明显看到 AlphaCodium 表现更佳。在与 AlphaCode 的方法比较时,需要指出 AlphaCode 采用了不同的代码生成策略:它是对特定的模型进行针对性优化,以解决编码问题,产生大量的编码方案,然后进行分类,最终从主要分类中选出若干方案提交。比如,“10 次尝试通过率在 10 万个方案中”意味着它产生了 10 万个方案,经过分类后,选出 10 个方案提交。AlphaCode 采用了经过特别优化的模型,使用了更多的 LLM 调用次数,类似于采取了一种穷举策略。尽管如此,AlphaCodium 在顶尖成果方面还是表现更加出色。
另外值得一提的是,AlphaCode 和 CodeChain 都没有提供可复制的解决方案,包括完整的端到端评估脚本。在评估结果时,需要考虑许多细节,比如处理多解题目、容错机制、超时问题等。我们的比较基于他们论文中报告的数据,但为了未来比较的可靠性和可复制性,我们提供了一套完整的可复制代码和评估脚本。
计算力度的比较:AlphaCode 与 AlphaCode2
在 AlphaCodium 的流程中,解决每个问题大约需要调用 15-20 次大语言模型(LLM),这意味着在进行五次尝试的过程中,大约需要调用 100 次大语言模型。
而 AlphaCode 并没有明确报告每个问题需要调用多少次大语言模型。如果我们假设它每次尝试调用一次(这还是未知数,可能实际更多),那么在 10 次尝试中,从 100,000 个解决方案中筛选出来的每个尝试,就需要调用 100 万次大语言模型,这比 AlphaCodium 多出四个数量级。然而,从我们所看到的结果来看,AlphaCodium 的表现却更为出色,如图 3 所示,它清晰地展示了这一点。
最近发布的一项名为 AlphaCode2 的研究(技术报告)中,研究者对一个名为 Gemini-Pro 的模型进行了针对编程问题的微调,并对其进行了评估。这项研究还对 CodeContests 的基准测试进行了探讨,但使用的是未公开的更新版。根据 AlphaCode2 的报告,只需大约 100 个样本,AlphaCode2 就能达到 AlphaCode 使用百万样本才能达到的性能水平,这使得它在样本效率上比 AlphaCode 高出超过 10000 倍。因此,从大语言模型调用的数量来看,AlphaCode2 和 AlphaCodium 都比 AlphaCode 高效得多。
然而,AlphaCode2 采用了一种专门为 CodeContests 竞赛而精心微调的现代基础模型,而 AlphaCodium 则使用了未经修改的通用模型。即便如此,AlphaCodium 在没有额外数据和昂贵训练阶段的情况下,依然提升了模型的性能。
附录
1) 一个人工评估代码问题的例子:
/*在一组数字中,检查是否存在两个数字之间的距离小于特定的数值阈值。 >>>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 格式中,LLM 很难正确放置这些字符,因为 JSON 输出需要用双引号包围。而 YAML 输出则采用块标量风格,只需遵循缩进规则,任何正确缩进的文本或代码都是合法的。此外,YAML 输出的 token 更少,这意味着更低的成本和更快的推理时间,同时因为模型需要关注的非关键 token 更少,所以质量也得到提升。以下是一个 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}"
# 创建一个字典,键为变量名,值为字符串
data ={'s1': s1,'s2': s2,'s3': s3,'s4': s4}
# 将字典转换成 JSON 格式字符串
json_data = json.dumps(data, indent=2)
print(json_data)
# 将字典转换成 YAML 格式字符串,采用块标量风格
yaml_data = yaml.dump(data, indent=2, default_style='|')
print(yaml_data)
Output:
表 5.
JSON 输出:
图 7. 使用 JSON 输出进行 Token 计数的示例。
YAML 输出示例如下图所示:
图 8. 使用 YAML 输出进行 Token 计数的示例。
显然,生成代码时仅保持适当的缩进,这样做不仅更简洁明了,而且还能有效减少错误。