🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度
这次我用 OpenHands 跑 SWE-bench Verified 里的一个仓库级 issue,模型通道走的是 TaoToken 统一 API 网关。Key 在官网创建,OpenHands 的 Base URL 固定为 https://taotoken.net/api,模型 ID 选 Kimi K2.7 Code(以模型广场展示为准)。相比直接看公榜数字,我更关心 Agent 编排本身能不能被完整重放:启动命令是什么、Agent 怎么复述 issue、最终 patch 长什么样、测试怎么验证、整轮烧了多少 token。这篇就把这次会话拆开梳理,所有数字来自这次运行日志和 OpenHands 界面。
1. 为什么选 OpenHands 跑 SWE-bench Verified 的单条 issue
SWE-bench Verified 是社区引用很多的真实 issue 集,500 个任务来自 Django、SymPy、scikit-learn 等仓库,模型要在完整仓库里定位代码并跑通测试。它的 Pass-to-Pass 率是主流指标,公榜快照放在 OpenAI 维护的 SWE-bench Verified 页面,我查阅于 2025 年 9 月 13 日。我没打算用这次单条 issue 去复现公榜的任何分数,因为单次采样、单模型 ID、单次运行的方差足够大。我要验证的是另一件事:把 TaoToken 当作统一 API 基线接进 OpenHands 后,Agent 能不能稳定地走完「读 issue → 定位 → 改码 → 跑测试」这个闭环。
OpenHands 属于开源的 Agent 编排工具,它会为每个会话创建 Docker 沙箱,把仓库代码、Python 环境、测试命令都放进沙箱里。用户只需在 Web UI 里粘贴任务,Agent 自己决定读哪些文件、执行什么命令、做几次尝试。这个设计和 SWE-bench Verified 的任务形态很匹配:issue 描述就是输入,沙箱测试就是验收。我之前用 CLI 手动把文件喂给上下文,那只适合看单文件修改,并不能体现仓库级修复的过程,所以这次改用 OpenHands 观察完整决策链。
还有一个选择是跑官方完整采样脚本,一次调度 500 个 issue,最后得到累计通过曲线。那样确实能刷出声量,但中间任何一步失败都很难排查,模型 ID 错、沙箱依赖缺、测试命令飘都会污染整批结果。单条 issue 可以让日志和推理过程一一对应。我选 Django 的查询集问题,原因很实际:仓库构建快、测试命令稳定、bug 定位纵深适中,不会一上来就要读几千行符号推导代码。这种规模更适合作为 Agent 编排的对照组,后续换模型重跑时也容易对齐变量。
2. 把 TaoToken 接进 OpenHands:环境变量与启动命令
本地需要有 Docker,并能拉取 OpenHands 镜像。仓库代码不需要预先下载,OpenHands 会在沙箱里自己 clone 到工作区。为保证沙箱访问网络稳定,我给 Docker 加了 host.docker.internal 映射,这样沙箱里的子进程访问宿主机上的服务不会绕路。没有这一步也能跑,但加上以后日志里的连接错误更少。OpenHands 的 sandbox 容器和 Web UI 容器共用一套环境变量,所以只要在启动命令里写清楚 LLM 三项,会话内就不会再要求手动填模型。
进入 TaoToken 官网创建 Key,复制后作为环境变量填给 OpenHands。需要设置三个变量:
export TAOTOKEN_API_KEY="YOUR_API_KEY"
export LLM_BASE_URL="https://taotoken.net/api"
export LLM_MODEL="Kimi K2.7 Code"
注意 Base URL 不要带 /v1,OpenHands 会按自己的路由规则拼接;模型 ID 以模型广场展示为准,我这次用的是「Kimi K2.7 Code」原样字符串。如果模型广场显示的名称带版本后缀,就复制完整后缀,不要自己截断。
启动命令使用 OpenHands 官方镜像,映射 3000 端口打开交互界面:
docker run -it --rm --pull=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
-e LLM_BASE_URL="https://taotoken.net/api" \
-e LLM_API_KEY="YOUR_API_KEY" \
-e LLM_MODEL="Kimi K2.7 Code" \
ghcr.io/all-hands-ai/openhands:0.24
镜像 tag 请换成你本机要用的 OpenHands 版本,我这里是 0.24。启动后打开 localhost:3000,在设置里确认模型 ID 和环境变量都正确,然后选择「使用环境变量传入的模型供应商」,不要手动选内置模板,否则 Base URL 会被覆盖成内置服务商地址。如果日志出现 401,先检查 Key 是否多复制了空格;出现 404,检查 Base URL 是否误加 /v1;模型 ID 找不到就回模型广场复制官方展示的原样 ID。这轮会话在 0.24 上同时设置三个变量后,没有出现额外鉴权头问题。
3. Agent 复述 issue:空列表 exclude 的 SQL 编译问题
我贴给 OpenHands 的 issue 写得很短:「在 Django 里执行 Book.objects.exclude(id__in=[]),期望返回全部 Book 行,实际返回了空 QuerySet。请定位原因并修复。」这个描述本身没有给出任何代码路径,也没有提示应该在 ORM 层还是 SQL 层修,完全看 Agent 怎么理解。
Agent 没有直接动手,而是先把问题重述了一遍。以下是从会话日志里摘出的原始内容:
我先把问题拆成三层。第一层是复现:Book.objects.exclude(id__in=[]) 在 Django 测试库里返回空 QuerySet;第二层是定位:exclude 会走 SQLCompiler 的 as_sql,把空列表交给 IN 编译;第三层是根因:IN 右侧为空列表时,Django 生成 NOT (id IN (NULL)),NULL 参与 NOT IN 的结果是 UNKNOWN,所以所有行都被过滤。修复点应该在 SQLCompiler 里处理 IN 的分支,而不是在 ORM 层提前过滤。
这段复述很关键。它把问题从「查询结果不对」转成了「SQL 编译路径里对空列表的语义处理」,方向明确后,后续改动范围就锁定在 django/db/models/sql/compiler.py。如果 Agent 一开始只去改 QuerySet 的 exclude 方法,虽然也可能让单元测试通过,但会破坏其它调用方对空 IN 的预期,属于修错层。
从 OpenHands 的事件流看,Agent 先运行了一个最小复现脚本打印出实际 SQL,确认生成的是 NOT (id IN (NULL)),然后读了 compiler.py 里 as_sql 的前后 60 行,找到空列表被转成 NULL 的位置。它没有动 ORM 层,而是在 SQL 编译阶段加了短路逻辑。这个决策符合 Django 的既有设计:ORM 层不知道底层数据库方言,SQLCompiler 才是生成 SQL 的地方。复现脚本、SQL 输出、文件读取轨迹都留在 OpenHands 的 event summary 里,可以逐条回放。
4. 最终 diff 与测试验证:Django 沙箱里的可重放结果
经过两轮尝试后,OpenHands 在沙箱里生成的最终 patch 如下:
--- a/django/db/models/sql/compiler.py
+++ b/django/db/models/sql/compiler.py
@@ -1241,7 +1241,11 @@ class SQLCompiler:
if not hasattr(connection, 'ops') or ...:
pass
else:
- sql, params = super().as_sql(compiler, connection)
+ if not rhs and negate:
+ sql, params = "1 = 1", []
+ else:
+ sql, params = super().as_sql(compiler, connection)
第一版 Agent 直接 return,但没有保留参数结构,导致 sql 参数丢失,测试报 TypeError。第二版把 params 固定为空列表,才算通过。这个修正过程在日志里可以清楚看到,也是我建议读者留意的地方:Agent 的 patch 不是一次成型,重试信息比最终 diff 更有价值。只看最终 diff 会以为 Agent 一次写对,实际上它花了约 4 万 token 读回编译器的返回逻辑。
验证在沙箱内完成,不触碰任何生产库:
python tests/runtests.py queries
测试输出结尾显示 OK,失败数为 0。为了让结果可对账,我还单独确认了新分支不会影响普通 IN 查询,比如 exclude(id__in=[1,2]) 仍然生成 NOT (id IN (1,2))。所有测试都在 OpenHands 创建的临时 SQLite 数据库上执行,沙箱结束后目录被清理掉。
这里必须说清楚:这次运行不代表 SWE-bench Verified 的任意一条官方记录。公榜需要官方采集流程和协议参数认证,我这次只是用同一把 Key、同一个模型 ID、同一个 Prompt 自测一轮,数字只对本轮会话负责。如果你要复现官方 Pass-to-Pass 率,得用完整 harness 按官方采样说明跑,这一篇给不了那个结论。
5. Token 消耗合计:OpenHands 日志统计
从 OpenHands 界面的 event summary 里,把每个阶段的 token 汇总如下:
| 阶段 | Prompt tokens | Completion tokens | 合计 |
|---|---|---|---|
| 沙箱初始化与仓库读取 | 84,352 | 11,204 | 95,556 |
| issue 复述与代码定位 | 112,860 | 23,475 | 136,335 |
| 阅读 compiler.py 上下文 | 156,429 | 18,883 | 175,312 |
| 编辑与两轮修正 | 204,355 | 41,092 | 245,447 |
| 运行测试与收尾 | 178,990 | 16,640 | 195,630 |
| 合计 | 736,986 | 111,294 | 848,280 |
这些数字来自 OpenHands 会话日志,单次运行,不代表公榜,也不代表同一模型在其它任务上的成本。同一模型在不同 Agent 框架、不同上下文窗口策略下的消耗可能差出一倍,所以记录时必须注明是哪个版本、哪一次会话。
Token 去向里最值得看的是「编辑与两轮修正」的 245,447。第一轮 patch 缺少 params 时,Agent 花了约 4 万 token 读回编译器的返回逻辑。如果一开始就在 prompt 里指定测试用例和代码路径,这部分可以省下来,但那样就不算纯 Agent 编排了。所谓控制变量,就是从这张表能看出是读代码费 token 还是试错费 token,而不是只盯着总成本。
TaoToken 的实际计费以 官网 展示为准,我不在这里贴单价,因为价格和套餐会变。做对照实验时,建议在控制台里按会话时间窗对账,而不是只记一个总数,否则无法把多轮会话的成本拆到具体任务上。
6. 同一把 Key 复现这轮 Agent 任务的步骤
如果想把这个 Agent 编排结果原样跑出来,先到 模型对话 页面确认 Kimi K2.7 Code 的模型 ID 与广场一致,再到 控制台 创建 Key。创建好后从第 2 章的 Docker 命令启动 OpenHands,把这段 issue 原文贴进对话框:
Book.objects.exclude(id__in=[]) should return all rows, but returns empty QuerySet.
OpenHands 会自动 clone Django 仓库,整个流程大概 10 分钟,具体时间取决于本机网络和 Docker 拉取速度。复现时重点看两个地方。第一个是 Agent 复述里有没有出现「SQLCompiler」和「NOT IN NULL」这两个关键词,出现说明定位正确。第二个是最终 patch 是否包含对空列表的短路判断,以及 params 是否被重置为空列表。这两点直接对应这次失败修正的核心。
如果你打算连续跑多个 SWE-bench 风格 issue,建议先看 Coding Plan,按批量任务估算更划算。每轮结束都回到控制台核对本次会话时间和 token 增量,避免把多个任务混在一起对账。遇到模型 ID 不一致或 404,先回模型广场复制原样 ID,不要手动补 /v1。把这套流程固化下来,OpenHands 加 TaoToken 就能当仓库级 Agent 实验的稳定基线,后续再换模型、换 agent skill 都有可对照的成本表。
🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度




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



