GPT 代码编辑基准测试

基准测试结果

Aider 是一个开源命令行聊天工具,可让你与 GPT 协作编辑本地 Git 仓库中的代码。为此,aider 需要能够可靠地识别 GPT 何时想要编辑本地文件,确定它想要修改哪些文件以及保存哪些更改。这种自动化代码编辑的关键在于使用系统提示来告诉 GPT 如何在其响应中构建代码编辑。

Aider 目前要求 GPT 使用基于简单文本的”编辑格式”,但 OpenAI 的新函数调用 API 看起来是创建更结构化编辑格式的有前途的方式。在实现了几个基于函数的编辑格式后,我想通过基准测试来衡量将它们设为 aider 默认编辑格式的潜在好处。

基于此,我开发了一个基于 Exercism python 编码练习的基准测试。该基准测试评估 aider 和 GPT 将自然语言编码请求转换为可执行代码并保存到通过单元测试的文件中的有效性。它不仅评估 GPT 的编码能力,还评估其编辑现有代码格式化这些代码编辑的能力,以便 aider 可以将编辑内容保存到本地源文件中。

我在所有 ChatGPT 模型(除了 gpt-4-32k)上运行了基准测试,使用了多种编辑格式。结果很有趣:

  • 纯文本编辑格式效果最好。要求 GPT 在标准 markdown 代码块中返回整个文件的更新副本,被证明是所有 GPT-3.5 和 GPT-4 模型中最可靠和有效的编辑格式。图中实心蓝色条显示了这种 whole 编辑格式的结果。
  • 函数调用表现较差。对于所有模型,使用新的函数 API 进行编辑的表现都不如上述整个文件方法。GPT-3.5 尤其产生了较差的代码,并经常破坏此输出格式。这令人惊讶,因为引入函数 API 是为了增强结构化输出的可靠性。图中图案条(绿色和蓝色)显示了这些 ...-func 编辑方法的结果。
  • 新的 6 月 GPT-3.5 模型比旧 6 月模型表现稍差。新的 6 月 (0613) 版本的 GPT-3.5 的表现似乎比 2 月 (0301) 版本稍差。如果你查看前三个实心蓝色条上的”首次尝试”标记,并比较前三个实心绿色 diff 条,就可以看到这一点。
  • GPT-4 比 GPT-3.5 表现更好,这是预期的。

定量基准测试结果与我对提示 GPT 执行编码等复杂任务的直觉一致。最小化响应格式的”认知开销”是有益的,这可以让 GPT 专注于手头的编码任务。

作为一个思维实验,想象一下与编辑器开发人员的 slack 对话,你要求他们编写代码为你的应用添加一些新功能。他们将通过聊天手动输入回复。他们应该输入代码并将其包装在普通的 markdown 代码块中吗?还是应该输入一个正确转义且语法正确的 json 数据结构,其中包含新代码的文本?

使用更复杂的输出格式似乎会导致两个问题:

  • 它使 GPT 编写的代码更差。保持输出格式简单似乎可以让 GPT 将更多注意力放在实际的编码任务上。
  • 它降低了 GPT 对输出格式的遵守,使得像 aider 这样的工具更难以准确识别和应用 GPT 尝试进行的编辑。

我原本期望开始在 aider 中为 GPT-3.5 和 GPT-4 使用基于函数调用的编辑。但鉴于这些基准测试结果,我目前不会采用函数 API。我当然计划在未来版本的模型上再次对函数进行基准测试。

下面将详细讨论基准测试、编辑格式和结果的更多细节。

基准测试

该基准测试使用了 Exercism Python 代码库中的 133 个实践练习。这些练习旨在帮助个人学习 Python 并磨练编程技能。

每个练习包含:

测试目标是让 GPT 阅读说明文档,实现提供的函数/类骨架,并通过所有单元测试。基准测试衡量的是 133 个练习中有多少百分比能成功完成,即所有相关单元测试都能通过。

对于每个练习,aider 会向 GPT 发送:

  • 实现文件的初始内容
  • Exercism 的说明文档
  • 以及最终指令:
根据上述说明修改提供的文件:<实现文件>
保留并实现现有的函数或类桩代码,它们将被单元测试调用。
仅使用标准 Python 库,不要建议安装任何包。

aider 会根据 GPT 的回复更新实现文件并运行单元测试。如果所有测试通过,则认为练习完成。如果部分测试失败,aider 会向 GPT 发送第二条消息,包含测试错误输出(仅发送前 50 行以避免超出较小模型的上下文窗口)。aider 还会附加这条最终指令:

查看上面的测试错误。
测试是正确的。
修复<实现文件>中的代码以解决错误。

要求 GPT 根据测试失败情况修正其初始实现,是该基准测试检验代码编辑能力的另一种方式。第二次机会也很重要,因为它让 GPT 有机会在说明文档与单元测试具体要求存在偏差时进行调整。许多练习都有多段说明文档,大多数人类程序员在首次尝试时也可能无法通过某些测试。

图表中的条形图显示了每个模型与编辑格式组合完成的练习百分比。完整条形高度表示经过两次编码尝试后的最终结果。每个条形图中还有一条水平标记线,显示仅经过第一次编码尝试(不含测试错误输出)的中间表现。

值得注意的是,在基准测试中 GPT 始终看不到单元测试的源代码,只能看到失败测试的错误输出。当然,所有这些代码很可能都是其原始训练数据的一部分!

总结来说,通过一个练习意味着 GPT 能够:

  • 编写所需代码(可能在查看测试错误输出后)
  • 正确将所有代码编辑打包成编辑格式,以便 aider 处理并保存到实现文件中

反之,未能通过练习只需上述任一步骤出现故障即可。实际上,GPT 在不同练习中会在不同环节失败:有时它只是编写了错误代码;有时则未能将代码编辑格式化为符合编辑格式,导致代码无法正确保存。

需要记住的是,更改编辑格式通常会影响 GPT 表现的两个方面。复杂的编辑格式往往会导致 GPT 编写更差的代码,并且更难以正确格式化编辑内容。

编辑格式

我对4种不同的编辑格式进行了基准测试,具体描述如下。 每个描述都包含了一个GPT可能对用户请求”将打印内容从hello改为goodbye”的示例响应。

whole格式

whole格式要求GPT返回包含所有更改的完整文件副本。文件应采用普通的Markdown三重反引号标记,内联在响应文本中。

这种格式与ChatGPT在普通聊天中返回代码片段的方式非常相似,只是在开头的三重反引号前添加了文件名。

这是更新后的demo.py文件:

demo.py
```python
def main():
    print("goodbye")
```

diff格式

diff格式同样要求GPT在响应文本中以简单的diff格式返回编辑内容。每个编辑都是一个带围栏的代码块,指定文件名和ORIGINAL原始代码与UPDATED更新代码块。GPT提供文件中一些原始行,然后是新更新的行集。

这是对demo.py请求的更改:

```python
demo.py
<<<<<<< ORIGINAL
    print("hello")
=======
    print("goodbye")
>>>>>>> UPDATED
```

whole-func格式

whole-func格式要求通过函数调用API返回整个文件的更新副本。

{
    "explanation": "将hello改为goodbye。",
    "files": [
        {
            "path": "demo.py",
            "content": "def main():\n    print(\"goodbye\")\n"
        }
}

diff-func 格式

diff-func 格式要求通过函数调用 API 返回原始/更新风格的编辑列表。

{
    "explanation": "将 hello 改为 goodbye",
    "edits": [
        {
            "path": "demo.py",
            "original_lines": [
                "    print(\"hello\")"
            ],
            "updated_lines": [
                "    print(\"goodbye\")"
            ],
        }
    ]
}

GPT-3.5 的表现

0613 模型似乎表现更差?

GPT-3.5 的基准测试结果让我相当确信,新的 gpt-3.5-turbo-0613gpt-3.5-16k-0613 模型在代码编辑方面比旧的 gpt-3.5-turbo-0301 模型稍逊一筹。

这可以从每个结果的”首次尝试”部分看出(在 GPT 获得第二次编辑机会之前)。观察前三个蓝色条中间的白色水平线:使用 whole 编辑格式时,2 月模型的成功率为 46%,而 6 月模型仅为 39%。

同时请注意实心绿色的 diff 条在 2 月和 6 月 GPT-3.5 模型之间的显著下降,从 30% 降至约 19%。

在早期版本的基准测试中,我也观察到了这种性能下降的其他迹象。

diff 的病态使用

当 GPT-3.5 能够正确生成 diff 编辑格式时,它经常以一种病态的方式使用该格式。它会将整个原始源文件放在 ORIGINAL 块中,而将整个更新后的文件放在 UPDATED 块中。这比直接使用 whole 编辑格式更糟糕,因为 GPT 发送了两个完整的文件副本。

幻觉函数调用

当 GPT-3.5 使用 functions API 时, 它容易忽略指定有效函数的 JSON Schema。 经常会出现一个全新的、语义上无效的 function_call 片段, 其中包含 "name": "python"

arguments 属性本应是一组键/值对, 包含 name 字段中指定函数的参数。 然而 GPT-3.5 经常直接将整个 Python 文件 塞入该字段中。

        "function_call": {
          "name": "python",
          "arguments": "def main():\n    print(\"hello\")\n"
        },

这似乎可能是由于为 ChatGPT 代码解释器插件进行的微调 导致了混淆?

随机性

该基准测试力求保持确定性,在重复运行时会为每个练习发送完全相同的请求。作为实现这一目标的一部分,当向 GPT 发送测试错误输出时,它会移除通常由 unittest 模块包含的挂钟计时信息。

基准测试工具还会记录所有 OpenAI API 请求和响应的 SHA 哈希值。这使得可以检测基准测试过程中的随机性或非确定性行为。

事实证明,OpenAI 的聊天 API 并不具有确定性,即使在 temperature=0 的情况下也是如此。完全相同的请求会产生多个不同的响应,通常不超过 5-10 种变体。这表明 OpenAI 可能正在将他们的 API 负载均衡到多个略有不同的模型实例上?

对于某些练习,这些可变响应中的一部分能通过单元测试,而其他变体则不能。因此,这类处于”临界状态”的练习结果会有些随机性,取决于 OpenAI 返回的是哪个变体。

考虑到这一点,理想情况下应该为每个模型/编辑格式组合多次运行所有 133 个练习,并报告平均性能。这将平均掉 API 变异带来的影响。但这也会显著增加此类基准测试的成本。所以我并没有这样做。

针对 133 个练习进行基准测试已经提供了一定的稳健性,因为我们正在测量跨多个练习的性能表现。

但为了了解 API 变异对基准测试结果的影响程度,我使用 whole 编辑格式对 gpt-3.5-turbo-0613 进行了 10 次完整的 133 个练习测试。您将在图表中看到一组误差条,显示了这 10 次运行的结果范围。

OpenAI API 的随机性似乎不会对整体基准测试结果造成很大的变异。

结论

基于这些基准测试结果,aider 将继续为 GPT-3.5 使用 whole(完整文件)编辑格式,而为 GPT-4 使用 diff(差异)格式。

GPT-4 在使用 wholediff 编辑格式时获得的结果相当,但使用 whole 会显著增加成本和延迟,相比 diff 格式而言。

使用 whole 格式时,流式传输每个编辑文件的完整更新副本的延迟是一个实际挑战。GPT-3.5 模型响应相当迅速,能够以合理的速度流式传输整个文件。Aider 会显示进度条和文件流式传输时的实时差异,这有助于消磨等待时间。

GPT-4 模型则慢得多,每次请求时等待即使是小文件被完全”重新键入”也可能是不可接受的。


目录