用 Claude 3.5 Sonnet 抬高 SWE-bench Verified 的标杆
我们最新升级的 Claude 3.5 Sonnet 在软件工程评测 SWE-bench Verified 上取得了 49% 的成绩,超过了此前最先进模型的 45%。本文介绍我们围绕模型构建的 Agent,并希望帮助开发者从 Claude 3.5 Sonnet 中获得尽可能好的表现。
SWE-bench 是一项评测 AI 模型真实软件工程能力的 Benchmark。它具体测试模型能否解决流行开源 Python 项目中的真实 GitHub Issue。
对于 Benchmark 中的每个任务,AI 模型都会获得:
- 一个已经配置好的 Python 环境;
- 对应 Repository 在原始 Issue 被解决之前的本地 Checkout。
模型需要:
- 理解代码;
- 修改代码;
- 测试代码;
- 提交自己的解决方案。
每个解决方案都会使用最初关闭这个 GitHub Issue 的 Pull Request 中的真实单元测试进行评分。
也就是说,这项评测实际测试的是:
AI 模型是否能够实现与原 PR 人类作者相同的功能。
SWE-bench 并不只是孤立地评测模型本身,而是评测一个完整的 Agent 系统。
在这里,Agent 指的是:
AI 模型 + 围绕模型的软件脚手架。
这套脚手架负责:
- 生成发送给模型的 Prompt;
- 解析模型输出并执行相应动作;
- 管理交互循环;
- 把模型上一步动作产生的结果重新加入下一轮 Prompt。
即使底层使用的是同一个 AI 模型,仅仅改变 Agent Scaffold,SWE-bench 上的表现也可能出现很大差异。
目前有许多用于衡量大语言模型编程能力的 Benchmark,但 SWE-bench 逐渐变得非常流行,主要有几个原因:
- 它使用真实项目中的真实工程任务,而不是竞赛题或面试题;
- 它还没有饱和,依然存在很大提升空间。在本文写作时,还没有模型在 SWE-bench Verified 上超过 50%,而升级后的 Claude 3.5 Sonnet 已经达到 49%;
- 它评测的是完整 Agent,而不是孤立模型。开源开发者和创业公司已经通过优化 Scaffold,在完全相同的模型上获得了显著更好的表现。
需要注意的是,原始 SWE-bench 数据集中有一些任务,如果只有 GitHub Issue 本身的上下文,其实无法完成。
例如:
某些问题要求返回一个非常具体的错误信息,但这个信息并没有出现在 Issue 描述中。
SWE-bench Verified 是 SWE-bench 中一个由人工审查过的 500 题子集,用来确保任务本身确实可以完成,因此它能够提供更清晰的 Coding Agent 能力衡量。
本文后面提到的 SWE-bench,主要都指 SWE-bench Verified。
达到新的 SOTA
使用工具的 Agent
在为升级后的 Claude 3.5 Sonnet 设计 Agent Scaffold 时,我们采用的核心理念是:
尽可能把控制权交给语言模型本身,同时让 Scaffold 保持最简。
Agent 只有:
- 一个 Prompt;
- 一个 Bash Tool,用于执行 Bash 命令;
- 一个 Edit Tool,用于查看和编辑文件与目录。
系统会不断采样,直到:
- 模型自己判断任务已经完成;
- 或者超过 200K Context Length。
这种设计让模型能够自行判断:
应该如何推进问题。
而不是把它硬编码进某一种固定模式或工作流。
Prompt 会给模型一个建议性流程,但不会写得非常长,也不会对每一步规定得太死。
模型可以自行决定如何在不同阶段之间移动,而不是必须遵守严格、离散的状态转换。
如果你并不特别在意 Token 成本,那么:
明确鼓励模型生成较长、较完整的推理和回答,有时会有所帮助。
下面是我们 Agent Scaffold 中的 Prompt:
<uploaded_files>
{location}
</uploaded_files>
我已经把一个 Python 代码仓库上传到了目录 {location}
(不在 /tmp/inputs 中)。
请考虑下面这份 PR 描述:
<pr_description>
{pr_description}
</pr_description>
你能否帮助我对 Repository 做必要修改,
以满足 <pr_description> 中的要求?
我已经处理好了 <pr_description> 中描述的所有测试文件修改。
这意味着你完全不需要修改测试逻辑,也不需要修改任何测试!
你的任务是:
只对 {location} 目录中的非测试文件进行最小修改,
确保 <pr_description> 中的要求得到满足。
请按照以下步骤解决问题:
1. 第一步,可以先浏览 Repository,熟悉整体结构。
2. 创建一个脚本复现错误,并使用 BashTool 执行:
python <filename.py>
以确认错误确实存在。
3. 编辑 Repository 源代码以解决问题。
4. 再次运行复现脚本,确认错误已经修复。
5. 思考边缘情况,确保修复也能正确处理这些情况。
你的思考应该尽可能充分,因此即使非常长也没有关系。
模型的第一个工具负责执行 Bash 命令。
它的 Schema 非常简单,只包含一个需要执行的命令。
不过,更重要的是:
Tool Description。
其中加入了很多模型真正需要知道的细节,包括:
- 输入是否需要 XML Escape;
- 当前环境是否可以访问互联网;
- 如何运行后台命令;
- 如何查看特定行范围。
下面是 Bash Tool 的规格:
{
"name": "bash",
"description": "Run commands in a bash shell\n
* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n
* You don't have access to the internet via this tool.\n
* You do have access to a mirror of common linux and python packages via apt and pip.\n
* State is persistent across command calls and discussions with the user.\n
* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n
* Please avoid commands that may produce a very large amount of output.\n
* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
"input_schema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to run."
}
},
"required": ["command"]
}
}
模型的第二个工具,也就是 Edit Tool,要复杂得多。
它包含模型:
- 查看文件;
- 创建文件;
- 编辑文件;
- 查看目录;
所需要的全部能力。
和 Bash Tool 一样,我们在 Tool Description 中加入了非常具体的使用指导。
我们在很多 Agentic Task 上,对这些工具的:
- 描述;
- Schema;
投入了大量精力。
我们会主动测试:
- 模型会怎样误解 Tool Spec;
- 哪些地方容易误用;
- 哪些错误最常见。
然后再修改工具描述,提前消除这些问题。
我们认为:
为模型设计工具接口,应该像为人类设计工具接口一样认真。
下面是 Edit Tool 的描述:
{
"name": "str_replace_editor",
"description": "Custom editing tool for viewing, creating and editing files\n
* State is persistent across command calls and discussions with the user\n
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n
* The `create` command cannot be used if the specified `path` already exists as a file\n
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n
* The `undo_edit` command will revert the last edit made to the file at `path`\n
Notes for using the `str_replace` command:
* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n
* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n
* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
...
}
我们提高表现的一种方式,是让工具尽可能:
Error-proof。
例如:
模型有时会在 Agent 已经离开 Repository Root 之后,对相对路径处理错误。
我们的解决方法非常简单:
直接要求 Tool 永远使用绝对路径。
我们还测试过多种修改已有文件的方法。
最终发现,可靠性最高的是:
String Replacement。
也就是:
模型指定:
old_str
然后用:
new_str
替换。
只有当 old_str 在文件中:
恰好出现一次
时,替换才会真正发生。
如果匹配数量:
- 多于 1;
- 少于 1;
模型都会收到清晰错误信息,然后自行重试。
下面是 Edit Tool 的输入规格:
{
"input_schema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"enum": [
"view",
"create",
"str_replace",
"insert",
"undo_edit"
],
"description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`."
},
"file_text": {
"description": "Required parameter of `create` command, with the content of the file to be created.",
"type": "string"
},
"insert_line": {
"description": "Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.",
"type": "integer"
},
"new_str": {
"description": "Required parameter of `str_replace` command containing the new string. Required parameter of `insert` command containing the string to insert.",
"type": "string"
},
"old_str": {
"description": "Required parameter of `str_replace` command containing the string in `path` to replace.",
"type": "string"
},
"path": {
"description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.",
"type": "string"
},
"view_range": {
"description": "Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.",
"items": {
"type": "integer"
},
"type": "array"
}
},
"required": [
"command",
"path"
]
}
}
结果
总体来说,升级后的 Claude 3.5 Sonnet,相比我们此前的模型,在以下能力上都有明显提升:
- 推理;
- 编码;
- 数学。
它也表现出了更强的 Agentic 能力:
好的工具和 Scaffold,能够让模型本身提升后的能力被更充分地发挥出来。
| 模型 | SWE-bench Verified 分数 |
|---|---|
| Claude 3.5 Sonnet(新版) | 49% |
| 此前 SOTA | 45% |
| Claude 3.5 Sonnet(旧版) | 33% |
| Claude 3 Opus | 22% |
以上模型都使用同一套 Agent Scaffold。
Agent 行为示例
运行 Benchmark 时,我们使用 SWE-Agent Framework 作为 Agent 代码的基础。
在下面的 Log 中,我们会把:
- Agent 文本输出;
- Tool Call;
- Tool Response;
分别渲染为:
THOUGHT
ACTION
OBSERVATION
不过,我们实际上并没有强制模型必须按照固定顺序输出。
下面通过一个典型案例,展示 Claude 3.5 Sonnet 如何解决 SWE-bench 任务。
首先,模型收到的初始 Prompt 中,会把真实 SWE-bench 任务内容填入:
{pr_description}
这个任务本身包含了复现步骤,因此给模型提供了非常有价值的调查起点。
<uploaded_files>
/repo
</uploaded_files>
我已经把一个 Python 代码仓库上传到了 /repo
(不在 /tmp/inputs)。
请考虑以下 PR 描述:
<pr_description>
linear_model.RidgeClassifierCV 的 store_cv_values 参数问题
#### 描述
sklearn.linear_model.RidgeClassifierCV 的 store_cv_values 参数报错
#### 复现步骤/代码
import numpy as np
from sklearn import linear_model as lm
# 测试数据库
n = 100
x = np.random.randn(n, 30)
y = np.random.normal(size = n)
rr = lm.RidgeClassifierCV(
alphas = np.arange(0.1, 1000, 0.1),
normalize = True,
store_cv_values = True
).fit(x, y)
#### 预期结果
应该正常获得 Ridge Regression 模型输出,
并把交叉验证预测值作为属性保留下来。
#### 实际结果
TypeError:
__init__() got an unexpected keyword argument 'store_cv_values'
lm.RidgeClassifierCV 实际上没有 store_cv_values 参数,
即使它的一些属性又依赖这个参数。
</pr_description>
模型首先回复,并使用 Edit Tool 查看 Repository 结构。
下面可以看到:
THOUGHT:模型的文本输出;ACTION:工具调用;OBSERVATION:工具返回的一部分结果。
THOUGHT:
我会帮助你实现满足 PR 描述所需要的修改。
先按照你给出的步骤进行。
1. 首先,让我们浏览 Repository 结构。
ACTION (str_replace_editor):
command:
view
path:
/repo
OBSERVATION:
下面是 /repo 中最多两层深度的文件和目录,
不包含隐藏文件:
/repo
/repo/PULL_REQUEST_TEMPLATE.md
/repo/ISSUE_TEMPLATE.md
/repo/sklearn
/repo/sklearn/decomposition
/repo/sklearn/kernel_ridge.py
/repo/sklearn/model_selection
/repo/sklearn/covariance
/repo/sklearn/cluster
/repo/sklearn/base.py
/repo/sklearn/dummy.py
/repo/sklearn/naive_bayes.py
...
现在模型已经对 Repository 结构有了更好的理解。
接下来,它使用 Edit Tool 创建一个新脚本,用来:
- 复现错误;
- 验证之后的修复。
THOUGHT:
根据 Repository 结构,我们需要修改
`sklearn/linear_model` 目录中的 Ridge Classifier。
先创建一个脚本复现错误:
ACTION (str_replace_editor):
command:
create
path:
/repo/reproduce_error.py
file_text:
import numpy as np
from sklearn import linear_model as lm
#test database
n = 100
x = np.random.randn(n, 30)
y = np.random.normal(size = n)
rr = lm.RidgeClassifierCV(
alphas=np.arange(0.1, 1000, 0.1),
normalize=True,
store_cv_values=True
).fit(x, y)
OBSERVATION:
File created successfully at:
/repo/reproduce_error.py
随后,模型用 Bash Tool 执行刚刚创建的脚本,并成功复现任务中的问题:
THOUGHT:
先验证一下我们确实能够复现这个错误:
ACTION (bash):
command:
python3 /repo/reproduce_error.py
OBSERVATION:
Traceback (most recent call last):
File "/repo/reproduce_error.py", line 10, in <module>
store_cv_values=True).fit(x, y)
TypeError:
__init__() got an unexpected keyword argument 'store_cv_values'
从这里开始,模型使用 Edit Tool 修改 Repository 中的源代码,然后重新执行自己的复现脚本,确认修改是否真正解决了问题。
THOUGHT:
错误已经成功复现。
查看代码后,我发现问题在于:
虽然 `RidgeClassifierCV` 继承自 `_BaseRidgeCV`,
但它没有把构造函数里的 `store_cv_values` 参数继续传给基类构造函数。
让我们修复它。
ACTION (str_replace_editor):
command:
str_replace
path:
/repo/sklearn/linear_model/ridge.py
old_str:
def __init__(self, alphas=(0.1, 1.0, 10.0), fit_intercept=True,
normalize=False, scoring=None, cv=None, class_weight=None):
super(RidgeClassifierCV, self).__init__(
alphas=alphas, fit_intercept=fit_intercept, normalize=normalize,
scoring=scoring, cv=cv)
self.class_weight = class_weight
new_str:
def __init__(self, alphas=(0.1, 1.0, 10.0), fit_intercept=True,
normalize=False, scoring=None, cv=None, class_weight=None,
store_cv_values=False):
super(RidgeClassifierCV, self).__init__(
alphas=alphas, fit_intercept=fit_intercept, normalize=normalize,
scoring=scoring, cv=cv, store_cv_values=store_cv_values)
self.class_weight = class_weight
在这个具体案例中,模型总共工作了:
12 个步骤
然后判断自己已经可以提交答案。
随后任务测试成功通过,证明模型的修改确实解决了问题。
但有些任务并没有这么快。
一些任务需要:
超过 100 轮
模型才会提交解决方案。
另一些任务里,模型会一直不断尝试,直到:
Context 被完全耗尽。
对比升级后的 Claude 3.5 Sonnet 与旧模型的运行轨迹,我们发现:
新版 3.5 Sonnet 更经常:
主动自我纠错。
它也更擅长:
尝试多个不同方案,
而不是反复犯同一个错误,并卡在原地。
挑战
SWE-bench Verified 是一项非常强的评测,但相比简单的单轮 Eval,它运行起来也明显更加复杂。
下面是我们实际遇到的一些问题。
这些问题,其他 AI 开发者也很可能会遇到。
1. 运行时间长,Token 成本高
前面的示例只用了 12 步就成功完成任务。
但很多成功案例需要:
- 数百轮交互;
- 超过 100K Token。
升级后的 Claude 3.5 Sonnet 非常“执着”。
只要给它足够时间,它往往能够想办法绕过问题,最终找到解决方案。
但这种方式也可能:
非常昂贵。
2. Grading 本身也会出问题
在检查失败任务时,我们发现:
有些情况下,模型其实做对了。
真正的问题来自:
- 环境配置错误;
- Install Patch 被重复应用;
- 其他系统问题。
要准确评估 AI Agent 的真实性能,
就必须先把这些基础设施问题解决掉。
3. Hidden Test
模型看不到:
最终给自己评分的测试。
所以它经常会认为:
“我已经成功了。”
但真正执行隐藏测试后,任务其实仍然失败。
有些失败是合理的。
例如:
模型在错误的抽象层级解决了问题。
它可能只是做了一个:
临时补丁
而真正需要的是:
更深层的重构。
但也有一些失败显得没那么公平。
例如:
模型实际上解决了真实问题,
只是它的实现没有恰好匹配:
原始任务中的单元测试预期。
4. 多模态能力没有被真正利用
升级后的 Claude 3.5 Sonnet 本身拥有很强的:
- Vision;
- Multimodal;
能力。
但我们的 Agent Scaffold 并没有实现让它查看:
- 保存到 Filesystem 的图像文件;
- URL 引用的图像。
这让某些任务的 Debugging 变得非常困难。
尤其是:
Matplotlib 相关任务。
同时也更容易让模型产生幻觉。
这里显然还有大量容易拿到的性能提升空间。
结论
升级后的 Claude 3.5 Sonnet,在一个非常简单的配置下:
- 一个简单 Prompt;
- 两个通用工具;
就在 SWE-bench Verified 上取得了:
49%
超过此前 SOTA 的:
45%。
我们相信:
围绕新的 Claude 3.5 Sonnet 构建 Agent 的开发者,
很快就会找到:
比本文这套初始方案更好的 Scaffold 和 Tool Design,
并继续把 SWE-bench 分数推得更高。
- 点赞
- 收藏
- 关注作者
评论(0)