OpenAI 的一项新分析指出,流行代码基准 SWE-Bench Pro 存在会影响评测可靠性和准确性的问题。对做模型选型、Agent 平台或内部编码助手的团队来说,这不是一个“榜单小插曲”:如果 benchmark 把噪声当信号,模型排名、采购决策和产品路线都会被带偏。
代码评测最怕“题目看起来真实,答案却不可判定”
SWE-Bench 这一类评测的吸引力很明确:它把模型放进真实软件仓库,让模型修 bug、跑测试,比单文件算法题更接近工程现场。Pro 版本通常意味着更难、更接近复杂项目,也更容易被团队拿来做高阶模型比较。
问题也藏在这里。真实仓库不是干净实验室:依赖版本会变,测试可能 flaky,issue 描述可能含糊,补丁是否正确也不一定能被现有测试完整覆盖。OpenAI 的分析之所以重要,是因为它提醒我们:一个“看起来工程味很足”的 benchmark,并不自动等于一个可靠测量仪器。
对编码模型评测来说,至少要拆开两件事:
- 模型能力信号:能否定位问题、理解上下文、写出可维护补丁、通过相关测试。
- 数据集噪声:测试不稳定、任务描述歧义、评测脚本错误、参考答案不唯一、环境复现失败。
如果第二类因素没有被隔离,最终分数可能只是在衡量谁更会撞运气、谁更适配某套脆弱 harness。
“通过测试”不是终点,只是一个观测点
很多编码评测会把任务简化成一个二元结果:patch 是否通过测试。这个指标有价值,因为它可自动化、可复现、成本低。但它也有边界。
常见风险包括:
- 测试覆盖不足:模型可能写出能过现有测试、但破坏边界行为的补丁。
- 测试污染或泄漏:如果模型在训练或上下文中见过相似补丁,分数会失真。
- 环境漂移:同一任务在不同镜像、依赖锁文件或系统库下结果不一致。
- 任务标注偏差:issue 期待的修复范围和测试实际验证的范围不匹配。
- 非确定性失败:并发、时间、网络、随机种子导致结果飘动。
所以更稳的评测不该只问“过没过”,还要记录“为什么过”“为什么失败”“失败是否可复现”。这类元数据会让 benchmark 从排行榜玩具变成工程工具。
可以这样实践:给代码评测加一层噪声审计
下面是一个最小可改造的 Python 脚本。假设你有一批模型提交结果,每条记录包含任务 ID、模型名、运行轮次、是否通过、失败原因。脚本会找出两类高风险任务:
- 同一个模型多次运行结果不一致,说明任务或环境可能 flaky。
- 所有模型都集中失败在环境或依赖错误上,说明它可能不是能力题。
保存为 audit_eval_noise.py 后可直接运行。实际使用时,把内置 runs 换成你的评测 JSONL 或数据库导出即可。
from collections import defaultdict, Counter
runs = [
{"task_id": "repo-101", "model": "model-a", "attempt": 1, "passed": True, "reason": "ok"},
{"task_id": "repo-101", "model": "model-a", "attempt": 2, "passed": False, "reason": "timeout"},
{"task_id": "repo-101", "model": "model-b", "attempt": 1, "passed": False, "reason": "timeout"},
{"task_id": "repo-202", "model": "model-a", "attempt": 1, "passed": False, "reason": "dependency_error"},
{"task_id": "repo-202", "model": "model-b", "attempt": 1, "passed": False, "reason": "dependency_error"},
{"task_id": "repo-303", "model": "model-a", "attempt": 1, "passed": True, "reason": "ok"},
{"task_id": "repo-303", "model": "model-b", "attempt": 1, "passed": False, "reason": "test_failure"},
]
by_task_model = defaultdict(list)
by_task_reason = defaultdict(Counter)
by_task = defaultdict(list)
for run in runs:
key = (run["task_id"], run["model"])
by_task_model[key].append(run["passed"])
by_task_reason[run["task_id"]][run["reason"]] += 1
by_task[run["task_id"]].append(run)
flaky_tasks = set()
for (task_id, model), outcomes in by_task_model.items():
if len(set(outcomes)) > 1:
flaky_tasks.add(task_id)
infra_suspects = []
infra_reasons = {"timeout", "dependency_error", "environment_error"}
for task_id, reasons in by_task_reason.items():
total = sum(reasons.values())
infra_count = sum(count for reason, count in reasons.items() if reason in infra_reasons)
if total > 0 and infra_count / total >= 0.8:
infra_suspects.append((task_id, reasons))
print("Flaky tasks:")
for task_id in sorted(flaky_tasks):
print(f"- {task_id}")
print("\nInfrastructure-heavy tasks:")
for task_id, reasons in infra_suspects:
print(f"- {task_id}: {dict(reasons)}")
运行:
python audit_eval_noise.py
这段脚本不替代人工复核,但它能把“可能不是模型能力差异”的任务先筛出来。更完整的内部评测可以继续加字段:仓库 commit、Docker 镜像 digest、依赖锁文件哈希、测试重跑次数、patch diff 行数、人工判定标签。
内部选型不要只看总分
如果你正在用 SWE-Bench Pro 或类似 benchmark 做模型比较,可以把总分降级为一个入口指标,而不是最终结论。更可操作的评测表应该长这样:
| 维度 | 建议记录 | 为什么重要 |
|---|---|---|
| 可复现性 | 同一 patch 重跑 3 次结果 | 区分能力失败和 flaky 失败 |
| 任务质量 | issue 是否清晰、测试是否对应 | 避免用坏题惩罚模型 |
| 修复质量 | diff 是否最小、是否引入副作用 | 防止“过测但乱改” |
| 环境稳定性 | 镜像、依赖、系统版本 | 让分数可以追溯 |
| 失败分类 | 编译、测试、超时、依赖、逻辑错误 | 找到真实瓶颈 |
还可以把报告拆成几个分数:
- clean pass rate:只统计通过噪声审计的任务。
- raw pass rate:保留原始总分,便于和外部榜单对齐。
- reproducible pass rate:要求多次运行一致通过。
- human-accepted rate:对高价值任务加入人工代码审查。
这样做会增加评测成本,但能减少“模型 A 比模型 B 高 2 个点”这种脆弱结论带来的误判。
采用建议:把 benchmark 当仪表,不要当裁判
SWE-Bench Pro 的争议给工程团队的提醒很直接:代码评测不能只追求更难的题,还要追求更可信的测量过程。
落地时可以用这份检查清单:
- 固定运行环境,记录镜像 digest 和依赖版本。
- 对失败任务做分类,不把基础设施错误计入模型能力失败。
- 对关键任务重跑,标记 flaky case。
- 抽样人工审查 patch,尤其关注“过测但不合理”的修改。
- 同时报告 raw score 和 cleaned score,避免只展示漂亮数字。
- 用你自己的代码库补充评测,因为公开 benchmark 不一定覆盖你的业务约束。
一个好的 coding benchmark 应该像示波器:它不会替你做架构判断,但能让你看清信号形状。噪声越复杂,越需要把评测流程工程化,而不是把排行榜当成采购单。