Day 3: 面对真相——从"生成了 patch"到"真的修好了"
十天写一个编码 Agent,第三天的主题是:用官方评测验证 patch 质量,并扩展到全量 300 实例。
"生成了 patch"不等于"修好了 bug"
Day 2 结束时,我们的 Agent 在 20 个 Django 实例上达到了 95% 的 patch 生成率。数字很漂亮,但有一个根本问题没回答:这些 patch 真的修好了 bug 吗?
生成率衡量的是"Agent 有没有动手修改代码",通过率衡量的是"修改后测试是否通过"。两者之间可能差距巨大——Agent 可以自信地写出一个完全错误的修复。
Day 3 的第一个任务:用 SWE-bench 官方评测工具验证真相。
静态分析:先给自己打个预防针
在跑正式评测之前,我先做了一次静态对比——把 Agent 生成的 patch 和 ground truth 逐一比较:
| 类别 | 数量 | 比例 |
|---|---|---|
| 精确匹配 ground truth | 11 | 55% |
| 语义等价(不同写法但逻辑相同) | 4 | 20% |
| 不确定 | 1 | 5% |
| 方法错误 | 3 | 15% |
| 无 patch | 1 | 5% |
预估通过率:60-65%。
这个分析本身就很有价值——它揭示了几种典型的失败模式:
- 时机错误:
django-10924要求在formfield()中延迟调用 callable,Agent 却在__init__中立即调用 - 逻辑差异:
django-12113需要条件分支,Agent 却做了变量替换 - 语义微差:
django-14534用 fallback 逻辑而非直接返回 None
这些错误的共性:Agent 理解了问题的表面症状,但没有深入理解代码的设计意图。
官方评测:Docker 隔离环境
SWE-bench 的评测方式很严格:
- 在 Docker 容器中 checkout 到 base commit
- 应用预测的 patch
- 注入测试 patch(添加新测试或修改已有测试)
- 运行 FAIL_TO_PASS 测试(修复后应通过的测试)
- 运行 PASS_TO_PASS 测试(确保没有回归)
只有当所有测试都通过时,才算 resolved。
20 实例小规模验证
先在之前的 20 个 Django 实例上跑:
| 指标 | 值 |
|---|---|
| Resolved | 16 |
| Unresolved | 2 |
| Error | 1 |
| Empty patch | 1 |
| Resolve rate | 16/20 = 80% |
16/20 通过!比预估的 60-65% 要好。原因是一些"语义等价"的 patch 确实能通过测试——修复不一定要和 ground truth 一模一样。
扩展到 300 实例
20 个实例太少,而且全是 Django。SWE-bench Lite 有 300 个实例跨 12 个仓库,需要全量评测。
并行化改造
原来的 run.py 是串行的——每次只跑一个实例。300 个实例串行跑需要 25+ 小时。
改造的核心是两个点:
1. Git Worktree 隔离
多个 Agent 并行时,不能共用同一个 repo 目录(会互相 checkout 不同 commit 导致冲突)。解决方案是用 git worktree:
def setup_repo(instance, workdir):
# 每个 repo 只 clone 一次
base_dir = _ensure_clone(repo, workdir)
# 每个实例创建独立的 worktree
subprocess.run(["git", "worktree", "add", "--detach", str(wt_dir), base_commit], ...)
Worktree 共享 .git 目录的对象存储,比完整 clone 节省大量磁盘。
2. ThreadPoolExecutor 并发
with ThreadPoolExecutor(max_workers=8) as pool:
futures = {pool.submit(run_instance, inst, llm, output): inst for inst in instances}
for future in as_completed(futures):
result = future.result()
# 实时输出进度和 ETA
8 个 worker 对应 8 个 LLM slot,充分利用推理服务器的并行能力。
300 实例生成结果
8 并发跑完 300 实例的 patch 生成:
| 仓库 | 生成/总数 | 生成率 |
|---|---|---|
| django/django | 103/114 | 90% |
| sympy/sympy | 72/77 | 94% |
| scikit-learn | 21/23 | 91% |
| matplotlib | 20/23 | 87% |
| pytest-dev | 13/17 | 76% |
| sphinx-doc | 13/16 | 81% |
| astropy | 6/6 | 100% |
| requests | 6/6 | 100% |
| flask | 3/3 | 100% |
| 其他 | 11/15 | 73% |
| 总计 | 268/300 | 89% |
100 实例均匀抽样评测
全量 300 实例的 Docker 评测太慢(镜像构建动辄数小时),于是按仓库比例均匀抽样了 99 个实例:
| 指标 | 值 |
|---|---|
| 提交评测 | 99 |
| 完成评测 | 75 |
| Resolved | 44 |
| Unresolved | 31 |
| Docker Error | 24 |
| 有效 Resolve rate | 44/75 = 58.7% |
44 个实例被正式确认为 resolved——跨越 Django、SymPy、scikit-learn、sphinx、pylint、astropy 6 个不同仓库。
24 个 Docker error 主要是 matplotlib、seaborn、requests 等 repo 的镜像构建失败(环境依赖问题),不是 Agent 能力问题。
数字背后的意义
58.7% 意味着什么?
以 SWE-bench Lite 的标准来看,这个数字需要放在上下文中理解:
- 我们用的是 Qwen3.5-35B-A3B,一个 MoE 模型,实际激活参数只有 3B
- 跑在单张 RTX 5090 上,完全本地推理
- Agent 框架从零搭建,只用了 3 天
- 没有任何 retrieval、planning 等高级技术
作为参考,SWE-bench 排行榜上的顶级系统使用 Claude/GPT-4 级别模型,配合精心设计的 multi-agent 架构,也就在 40-60% 的范围。
跨仓库泛化
最让我满意的不是数字本身,而是 Agent 在 6 个不同仓库上都有 resolve。这说明:
- 系统提示词的设计是通用的
- 工具集(bash、read_file、edit_file、search_code)足够覆盖不同类型的 bug
- Agent 的探索策略不依赖特定项目的领域知识
Error 不是 Agent 的错
24 个 Docker error 值得说明——这些是 SWE-bench 评测基础设施的问题(特定版本的 matplotlib/seaborn 等需要特殊的系统依赖),不是 Agent 生成了错误的 patch。如果这些能正常评测,resolve 数字可能会更高。
Day 3 的认知收获
1. 评测是最诚实的镜子
静态对比 patch 可以做定性分析,但只有跑通测试才能给出确定的答案。有些看起来"不同"的 patch 能通过测试(语义等价),有些看起来"差不多"的 patch 却失败了(细微逻辑差异)。
2. 基础设施投入回报巨大
Day 3 一大半时间花在了基础设施上:并行化、Docker 评测、镜像构建。这些不性感,但没有它们就无法在合理时间内完成 300 实例的评测。编码 Agent 的研发,基础设施和算法同样重要。
3. 小模型的天花板和潜力
3B 激活参数的模型能达到 58.7% 的 resolve rate,这说明:
- 编码 Agent 的上限不完全由模型决定——框架设计、提示词、工具选择都是关键因素
- 小模型在简单到中等难度的 bug 上表现不错——定位代码、理解修改模式、生成正确 patch 都能做到
- 复杂的多文件交互 bug 是真实瓶颈——这需要更强的推理能力或更好的探索策略
三天总结
| Day | 交付 | 关键指标 |
|---|---|---|
| Day 1 | 基础框架 | 1 实例验证通过 |
| Day 2 | 效率优化 | 19/20 patch 生成 (95%) |
| Day 3 | 全量评测 | 44/75 resolved (58.7%) |
从零到一个能在 SWE-bench 上跑出成绩的编码 Agent,3 天,~1000 行代码,一张 GPU。
这是「十天开发编码 Agent」系列的第三篇。代码已打 tag day3。