返回 文章 build CMS 文章

Claude 3.5 Sonnet 在 SWE-bench Verified 上达到 49%,Anthropic 详解其代理脚手架设计

Anthropic 通过最小化代理脚手架,让 Claude 3.5 Sonnet 在 SWE-bench Verified 上达到 49%,展示了工具设计对 AI 编码代理性能的关键影响。

Claude 3.5 SonnetSWE-benchAI 代理软件工程
成长分 / 100 76 综合收获、行动、留存与影响

Claude 3.5 Sonnet 在 SWE-bench Verified 上达到 49%,Anthropic 详解其代理脚手架设计
为什么值得读了解如何通过精心设计的工具接口和提示,最大化大语言模型在真实软件工程任务中的表现。

获取构建高效 AI 代理的实用设计原则,如防错工具、字符串替换编辑策略等。

关键洞察
  1. SWE-bench 评估的是整个代理系统(模型+脚手架),而非孤立模型,脚手架设计对性能有显著影响。
  2. Anthropic 的设计理念是保持脚手架最小化,将控制权交给模型,仅提供 Bash 和编辑两个通用工具。
  3. 工具描述和规范需要像人类工具界面一样精心设计,通过测试和迭代来预防模型误解。
转成行动

深入阅读

正文与原文对照

原文保真覆盖:全文原文字符:16917

我们的最新模型——升级版 Claude 3.5 Sonnet——在软件工程评测 SWE-bench Verified 上取得了 49% 的成绩,超过了此前最先进模型的 45%。本文介绍我们围绕该模型构建的“智能体”,旨在帮助开发者充分发挥 Claude 3.5 Sonnet 的最佳性能。

SWE-bench 是一个 AI 评测基准,用于评估模型完成真实软件工程任务的能力。具体来说,它测试模型如何解决热门开源 Python 仓库中的 GitHub issue。对于基准中的每个任务,AI 模型会拿到一个已配置好的 Python 环境,以及该 issue 被解决前一刻的仓库检出版本(checkout,即本地工作副本)。随后,模型需要理解、修改并测试代码,再提交它给出的解决方案。

每个解决方案都会用关闭原 GitHub issue 的那个 pull request 中的真实单元测试来评分。这用来检验 AI 模型能否实现与该 PR 的人类原作者相同的功能。

SWE-bench 评估的并不只是孤立的 AI 模型,而是一整套“智能体”系统。在这里,“智能体”指的是 AI 模型与围绕它的软件脚手架(scaffolding)的组合。这套脚手架负责生成输入模型的提示词,解析模型的输出以执行动作,并管理交互循环,把模型上一步动作的结果纳入下一个提示词。即使使用相同的底层 AI 模型,智能体在 SWE-bench 上的表现也可能因这套脚手架而大不相同。

评测大语言模型编码能力的基准还有很多,但 SWE-bench 因以下几个原因越来越受欢迎:

  1. 它使用来自真实项目的真实工程任务,而不是竞赛或面试风格的题目;
  2. 它尚未饱和,还有很大的提升空间。目前还没有模型在 SWE-bench Verified 上的完成率超过 50%(不过截至撰文时,升级版 Claude 3.5 Sonnet 已达到 49%);
  3. 它衡量的是整个“智能体”,而不是孤立的模型。开源开发者和初创公司通过优化脚手架,在同一个模型上取得了大幅的性能提升,成效显著。

请注意,原始的 SWE-bench 数据集中包含一些任务,如果没有 GitHub issue 之外的额外上下文(例如关于要返回的具体错误信息),就不可能解决。SWE-bench-Verified 是 SWE-bench 中经人工审核、确保可解的 500 道题的子集,因此能最清晰地衡量编码智能体的表现。本文所指的就是这个基准。

达到业界最先进水平

使用工具的智能体

在为升级版 Claude 3.5 Sonnet 优化智能体脚手架时,我们的设计理念是把尽可能多的控制权交给语言模型本身,并保持脚手架极简。该智能体包含一段提示词、一个用于执行 bash 命令的 Bash 工具,以及一个用于查看和编辑文件与目录的编辑工具。我们持续采样,直到模型自行判定已完成,或超出其 200k 上下文长度。这套脚手架让模型可以自行判断如何解决问题,而不是被硬编码成某种特定的模式或工作流。

提示词为模型概述了一种建议的做法,但对这项任务来说,它既不会过长,也不会过于详细。模型可以自由选择如何从一步走到下一步,而不是遵循严格、离散的转换。如果你对 token 用量不敏感,明确鼓励模型给出较长的回答可能会有帮助。

以下代码展示了我们智能体脚手架中的提示词:

<uploaded_files>
{location}
</uploaded_files>
I've uploaded a python code repository in the directory {location} (not in /tmp/inputs). Consider the following PR description:

<pr_description>
{pr_description}
</pr_description>

Can you help me implement the necessary changes to the repository so that the requirements specified in the <pr_description> are met?
I've already taken care of all changes to any of the test files described in the <pr_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!

Your task is to make the minimal changes to non-tests files in the {location} directory to ensure the <pr_description> is satisfied.

Follow these steps to resolve the issue:
1. As a first step, it might be a good idea to explore the repo to familiarize yourself with its structure.
2. Create a script to reproduce the error and execute it with `python <filename.py>` using the BashTool, to confirm the error
3. Edit the sourcecode of the repo to resolve the issue
4. Rerun your reproduce script and confirm that the error is fixed!
5. Think about edgecases and make sure your fix handles them as well

Your thinking should be thorough and so it's fine if it's very long.

模型的第一个工具用于执行 Bash 命令。它的模式(schema)很简单,只接收要在环境中运行的命令。不过,工具的描述分量更重:其中包含给模型的更详细的说明,涉及输入的转义、无法访问互联网,以及如何在后台运行命令。

接下来,我们展示 Bash 工具的规范:

{
   "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"]
   }
}

模型的第二个工具(编辑工具)复杂得多,包含模型查看、创建和编辑文件所需的一切。同样,我们的工具描述中包含了关于如何使用该工具的详细信息。

我们针对各种各样的智能体任务,在这些工具的描述和规范上投入了大量精力。我们对它们进行测试,找出模型可能误解规范的各种方式,以及使用工具时可能遇到的陷阱,然后修改描述以预先化解这些问题。我们认为,应当在为模型设计工具接口上投入更多的关注,就像我们在为人类设计工具接口上投入大量关注一样。

以下代码展示了我们编辑工具的描述:

{
   "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
\n
Notes for using the `str_replace` command:\n
* 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`",
...

我们提升性能的一种方式,是对工具做“防错”设计。例如,有时在智能体离开根目录之后,模型会搞混相对文件路径。为防止这种情况,我们干脆让工具始终要求使用绝对路径。

我们尝试了多种不同的策略来指定对现有文件的编辑,其中字符串替换的可靠性最高:模型指定 `old_str`,在给定文件中将其替换为 `new_str`。只有当 `old_str` 恰好匹配一处时,替换才会发生。如果匹配数多于或少于一处,模型会看到相应的错误信息,以便重试。

我们编辑工具的规范如下所示:

...
   "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 展现出比我们此前的模型以及此前最先进的模型更强的推理、编码和数学能力。它的智能体能力也有所提升:工具和脚手架帮助这些提升后的能力得到最佳发挥。

模型 Claude 3.5 Sonnet(新) 此前的 SOTA Claude 3.5 Sonnet(旧) Claude 3 Opus
SWE-bench Verified 得分 49% 45% 33% 22%

我们部分模型在 SWE-bench Verified 上的得分,均使用这套智能体脚手架。

智能体行为示例

运行基准测试时,我们以 SWE-Agent 框架为基础来编写智能体代码。在下面的日志中,我们把智能体的文本输出、工具调用和工具响应分别呈现为 THOUGHT、ACTION 和 OBSERVATION,尽管我们并不限定模型必须遵循固定的顺序。

下面的代码块将演示 Sonnet 3.5 解决一道 SWE-bench 问题的典型过程。

在第一个代码块中,你可以看到给模型的初始提示词的一部分,其中 `{pr_description}` 已被替换为 SWE-bench 任务中的真实内容。重要的是,这个任务包含复现该问题的步骤,这会给模型提供一个宝贵的调查起点。

<uploaded_files>
/repo
</uploaded_files>
I've uploaded a python code repository in the directory /repo (not in /tmp/inputs). Consider the following PR description:

<pr_description>
linear_model.RidgeClassifierCV's Parameter store_cv_values issue
#### Description
Parameter store_cv_values error on sklearn.linear_model.RidgeClassifierCV

#### Steps/Code to Reproduce
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)

#### Expected Results
Expected to get the usual ridge regression model output, keeping the cross validation predictions as attribute.

#### Actual Results
TypeError: __init__() got an unexpected keyword argument 'store_cv_values'

lm.RidgeClassifierCV actually has no parameter store_cv_values, even though some attributes depends on it.

模型作出响应,首先使用编辑工具查看仓库结构。你可以在 THOUGHT 和 ACTION 下看到模型的文本输出和工具调用参数,在 OBSERVATION 下看到工具输出的一部分:

THOUGHT:
I'll help you implement the changes to satisfy the PR description. Let's follow the steps you outlined.

1. First, let's explore the repo structure:
ACTION (str_replace_editor):
command:
view

path:
/repo

OBSERVATION:
Here's the files and directories up to 2 levels deep in /repo, excluding hidden items:
/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
...

在对仓库结构有了更好的了解之后,模型使用编辑工具创建了一个新脚本,用来复现该问题并测试其修复:

THOUGHT:
Based on the repository structure, we'll need to modify the Ridge Classifier in the `sklearn/linear_model` directory.

Let's create a script to reproduce the error:
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 工具执行它编写的脚本,并成功复现了任务中的问题:

THOUGHT:
Let's verify that we can reproduce the error:
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'

此后,模型使用编辑工具修改仓库中的源代码,并重新运行脚本,以验证这一修改是否解决了问题:

THOUGHT:
The error is reproducible. Looking at the code, I can see that the issue is that while the `RidgeClassifierCV` class inherits from `_BaseRidgeCV`, it fails to pass the `store_cv_values` parameter from its constructor through to the base class constructor.

Let's fix this:
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 轮;在另一些任务中,模型一直尝试,直到上下文耗尽。

对比升级版 Claude 3.5 Sonnet 与旧模型的尝试记录可以发现,升级版 3.5 Sonnet 更常进行自我纠正。它还展现出尝试多种不同解决方案的能力,而不是反复犯同一个错误而卡住。

挑战

SWE-bench Verified 是一项强大的评测,但运行起来也比简单的单轮评测更复杂。以下是我们在使用它时遇到的一些挑战,其他 AI 开发者也可能会遇到。

  1. 耗时长与 token 成本高。上面的示例来自一个在 12 步内成功完成的案例。然而,许多成功的运行需要模型花数百轮、超过 100k 个 token 才能解决。升级版 Claude 3.5 Sonnet 很有韧性:只要时间足够,它往往能找到绕过问题的办法,但这可能代价高昂;
  2. 评分。在检查失败的任务时,我们发现有些情况下模型的行为是正确的,但存在环境配置问题,或安装补丁被应用了两次的问题。解决这些系统问题,对于准确了解 AI 智能体的表现至关重要。
  3. 隐藏的测试。由于模型看不到用来给它评分的测试,它常常“认为”自己成功了,而任务实际上是失败的。其中一些失败是因为模型在错误的抽象层级上解决了问题(打了个补丁,而不是做更深层的重构)。另一些失败则显得不太公平:模型解决了问题,但与原任务的单元测试并不匹配。
  4. 多模态。尽管升级版 Claude 3.5 Sonnet 拥有出色的视觉和多模态能力,我们并没有实现让它查看保存在文件系统中或以 URL 引用的文件的方式。这使得调试某些任务(尤其是来自 Matplotlib 的任务)格外困难,也容易导致模型产生幻觉。这里无疑有开发者可以改进的“低垂果实”,而且 SWE-bench 已经推出了一项聚焦多模态任务的新评测。我们期待开发者在不久的将来用 Claude 在这项评测上取得更高的分数。

升级版 Claude 3.5 Sonnet 仅凭一段简单的提示词和两个通用工具,就在 SWE-bench Verified 上取得了 49% 的成绩,超过了此前最先进的 45%。我们相信,使用新版 Claude 3.5 Sonnet 进行开发的开发者,很快就会找到新的、更好的方法,把 SWE-bench 得分提升到我们最初展示的水平之上。

致谢

Erik Schluntz 优化了 SWE-bench 智能体并撰写了这篇博客文章。Simon Biggs、Dawn Drain 和 Eric Christiansen 协助实现了该基准测试。Shauna Kravec、Dawn Drain、Felipe Rosso、Nova DasSarma、Ven Chandrasekaran 以及许多其他人为训练 Claude 3.5 Sonnet 在智能体编码方面表现出色做出了贡献。