🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度
1. 为什么用 OpenHands 跑 SWE-bench Verified 的 Python 单选例
跑 Agent 最怕的不是任务复杂,而是模型 API 地址换来换去。这次我用 OpenHands 复现 SWE-bench Verified 的 Python 修复链路,把默认模型供应商指向 TaoToken 的统一 API Base URL,挑三个能通过单测复现的用例,记录同一把 Key 下每个用例的修复步数和最终通过数。SWE-bench Verified 里有 500 个人工复核过的真实 GitHub issue 与 PR 对,但本地逐个复现比直接看排行榜百分比更有诊断价值:把仓库拉进沙箱,告诉 Agent 哪个测试挂了,看它能不能把 fail-to-pass 单测修到通过。
OpenHands 的 Agent 循环是「读任务 → 在沙箱里执行命令 → 看输出 → 改文件 → 再验证」,天然适合这类复现。每个 SWE-bench 实例本质上是三件事:一个 issue 描述、一个基础 commit、一组由 PR 引入的测试。本地复现时我不看 gold patch,只把 issue 描述和失败测试作为初始任务交给 Agent。它需要自己定位到具体模块,写 patch,再跑测试确认。这个过程中模型对 Python traceback 的理解能力、上下文窗口的利用效率,都会直接影响修复步数。
为什么选 Python 链路?Python 实例在 Verified 里数量多,环境依赖相对集中,通常 conda 或 pip 装完就能跑;相比 C++ 或 JavaScript 项目,省去一层编译噪音。更重要是 Python 报错堆栈可读性强,Agent 能快速从 AssertionError 和文件名定位到问题函数,减少「反复读文件但不动代码」的无效动作。我选的三个用例都满足同样的前提:单测稳定复现、不依赖外部网络、patch 范围集中在单个 Python 模块。这样环境噪音被压到最低,剩下才是模型修复能力的真实反馈。
还要说明白一点:本文不引 SWE-bench Verified 的排行榜分数,所有通过数和失败记录只对本次运行有效。同一个模型、同一把 Key,在不同 OpenHands 版本和不同 sandbox 镜像下结果可能不一样,所以下面这张表是「一次运行」而不是「公榜复现」。
2. 把 TaoToken 配成 OpenHands 的默认模型供应商
OpenHands 的模型供应商设置可以用 Web UI 手动填,也可以通过环境变量注入。为了可复现,我选择了后者:在 Docker 启动时把 LLM_MODEL、LLM_API_KEY、LLM_BASE_URL 三个变量传给 OpenHands,等于把默认供应商指向 TaoToken 这个统一网关。官网创建的 Key 从控制台复制后填进去,模型 ID 不自己猜,直接从模型广场复制完整字符串。
docker run -it --rm \
-e LLM_MODEL=YOUR_MODEL_ID \
-e LLM_API_KEY=YOUR_API_KEY \
-e LLM_BASE_URL=https://taotoken.net/api \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:latest \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
docker.all-hands.dev/all-hands-ai/openhands:latest
命令里两个 latest 镜像标签建议按你拉到的实际版本固定,避免隔几天重跑时基础镜像漂移。/var/run/docker.sock 挂载是 OpenHands 创建 sandbox 容器的前提,~/.openhands 用来持久化会话记录。启动后打开 http://localhost:3000,在设置页面能看到模型供应商已经变成刚才环境变量指向的地址。YOUR_API_KEY 在官网控制台创建,YOUR_MODEL_ID 以模型广场展示为准,这两个占位符不要直接拿去跑。
TaoToken 在这里只承担两个角色:提供 Key 和提供 Base URL。它不进 sandbox,不修改 OpenHands 运行时,也不替换任何模型权重。之后想换模型,只需要改 LLM_MODEL 重新启动,不用重装 OpenHands。这比每次去找不同厂商的独立 API 地址更省事,也方便在同一个 Agent 任务里做横向对照。
需要特别留意 Base URL 的写法:尾巴上是 https://taotoken.net/api,不要加 /v1。OpenHands 会按 OpenAI 兼容客户端自己拼接补全路径,多写一个 /v1 反而会 404。这个坑我这次跑的时候遇到过,后文排障部分会再展开。市面上有些临时 API 通道会给一串很短的地址,但响应头、限流策略、计费明细全部不透明,出了问题连对账都难;用统一网关的好处是控制台能看到每次调用的记录,至少知道跑了多少请求、花了多少量。
另外强调一点安全边界:OpenHands 默认把命令执行限制在 sandbox 容器里,我没有把生产库或生产机器的路径挂进去。评测只给了它仓库代码和单测命令,所有数据库操作都发生在隔离环境里。如果你要在自己的服务器上复现,也建议先确认挂载路径不会覆盖重要数据。
3. 三个 Python 用例的修复步数与通过记录
记录口径先定清楚:修复步数指 OpenHands 的 Agent 从拿到任务到提交可验证 patch 的关键动作数,包括读文件、跑测试、改代码、再跑测试;它不是 token 数,也不是工具调用的原始条数。最终以 F2P 单测是否 PASS 为准。三个用例各自单独会话,避免互相污染上下文。运行中使用的都是同一把 TaoToken Key,同一个模型 ID,OpenHands 的 sandbox 重启过但配置没有改动。
| 用例 | 问题类型 | 修复步数 | 最终 F2P 单测 |
|---|---|---|---|
| Case 1:函数在空输入时返回错误值 | 边界条件 | 12 | 通过 |
| Case 2:异常分支没有清理中间状态 | 异常处理 | 26 | 通过 |
| Case 3:集合比较逻辑与等价条件不一致 | 逻辑重写 | 31 | 失败 |
这张表只是本次单次运行记录,不代表公榜。同一模型在不同采样温度、不同上下文截断策略下可能给出不同 patch。下面的日志摘要是 OpenHands 输出里的关键动作,省略了中间大量重复读取文件的步骤。
3.1 Case 1:函数在空输入时返回错误值
这个用例的问题很典型:函数在传入空列表时没有走默认分支,而是落进了一个只适用于非空数据的循环,最终返回错误结果。Agent 第一步先跑失败测试,定位到 test_empty_input 的断言失败,然后顺着调用链找到函数入口,在进入主逻辑之前补了一个空输入判断。整个过程比较顺,没有出现反复试错。
[agent/step 01] run: pytest tests/test_edge.py -k test_empty_input
[agent/step 02] observed: AssertionError at test_edge.py:42
[agent/step 03] edit: add early return when input is empty
[agent/step 04] run: pytest tests/test_edge.py -k test_empty_input
[result] PASS
修复步数 12 步,说明 Agent 在边界条件类问题上能快速收敛。日志里从第三步直接跳到第四步,是因为中间的 git diff 和语法检查都被我省略了。这类错误的修复成本本来就不高,真正影响步数的是能不能快速从 traceback 里定位到函数,而不是在无关文件里翻找。
3.2 Case 2:异常分支没有清理中间状态
第二个用例是异常分支里的状态残留。函数在处理一批数据时,如果中途抛异常,缓存里会留下半成品状态,导致下一次调用读到脏数据。Agent 一开始只在 except 里补了日志,跑测试仍然失败;后来它意识到必须在 finally 里统一清理缓存,而不是在 except 里修修补补。这个认知转变花了大约十步。
[agent/step 08] run: pytest tests/test_state.py::test_exception_cleanup
[agent/step 09] observed: second call raises RuntimeError
[agent/step 12] edit: reset cache in finally block
[agent/step 15] run: pytest tests/test_state.py::test_exception_cleanup
[result] PASS
这个用例比第一个难在问题不在报错那一行,而是报错之前的状态污染。Agent 如果只看 traceback 会误以为问题在 RuntimeError 抛出的位置,实际上要往前追一层,找到异常发生前写入缓存的代码。26 步里至少有 8 步是在确认「为什么第二次调用还会失败」。最终 patch 把清理动作挪到 finally,才算真正修复了 F2P 测试。
3.3 Case 3:集合比较逻辑与等价条件不一致
第三个用例是三个里唯一失败的。问题出在一个集合比较函数上:它用交集是否为空来判断两个集合是否等价,但函数名和调用方都暗示应该用子集关系。Agent 前面十几步都在复现和阅读调用方,后半段开始改代码时,第一次只改了其中一个分支,第二次把 intersection 换成了 <=,但没有覆盖空集合边界,F2P 单测仍然失败。
[agent/step 14] run: pytest tests/test_set_ops.py -k test_equivalent
[agent/step 15] observed: expected {1, 2}, got {1, 2, 3}
[agent/step 21] edit: use subset instead of intersection
[agent/step 24] run: pytest tests/test_set_ops.py -k test_equivalent
[agent/step 28] observed: assertion still fails
[result] FAIL
31 步停在上限,没有继续跑下去。回看日志,问题不是模型不知道集合运算,而是它对「最小改动」的克制不够:改完一个分支后没有立即回到测试验证,又顺手重构了相邻代码,导致 regress。这个结果至少说明一点:在长上下文任务里,Agent 需要更频繁地把当前 diff 和原始失败测试对照,而不是凭感觉扩大修改面。
4. 复现这次运行时最常踩的三个配置错
如果你按上面的命令复现,遇到 401 或 404,先检查这三个地方,都跟本次配置直接相关。第一个是 Base URL 多写 /v1。OpenHands 会在 Base URL 后面自动拼接 chat/completions 之类的路径,写成 https://taotoken.net/api/v1 会导致找不到端点。正确写法就是 https://taotoken.net/api,这个地址也用于所有工厂兼容场景。
第二个配置错是模型 ID 没有从模型广场复制,而是自己拼写。比如在商业模型名前加日期、把大小写写错,或者把版本号写成别家的格式。OpenHands 会把 LLM_MODEL 原样传给通道,通道不认识就返回 model not found。解决方法是打开官网模型广场,复制当前展示的模型 ID 原文,不要手动改动。
第三个配置错是 API Key 在复制过程中夹带了不可见字符。从网页控制台复制到终端时,偶尔会带上换行或零宽空格,docker run 拿到后会把整个 Key 当成一个带空格的值。建议创建 Key 后先用引号包住环境变量,再把 Key 原文粘贴到双引号里。这三个问题我在本次复现时都实际遇到过,修正后同一把 Key 就能稳定跑完三个用例。
复现步骤本身不复杂:先在官网创建 Key,启动 OpenHands 容器,打开 Web UI,在任务输入框里粘贴 issue 描述和失败测试命令,等 Agent 跑完,再单独执行一次 F2P 测试确认结果。三个用例不要在同一个会话里连续给,否则上下文会越来越长,模型容易把前面用例的代码片段混进后面的 patch。每个用例独立会话,得到的数据才干净。
复现完这张表后,打开 模型对话 确认刚才的调用是否入账、模型 ID 是否和模型广场一致;要换模型重跑第 3 节,只需要改 LLM_MODEL 环境变量,再到 控制台创建 Key 复制一把新 Key。如果你计划把 OpenHands 跑成每日任务,Coding Plan 的包月口径比按次调用更好控制预算。用同一套供应商配置继续复现,比每次重新搭环境要省事得多。
🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度




被折叠的 条评论
为什么被折叠?



