Bad benchmarks and evals: Senior SWE-Bench, napkin math, and winter tires

Dan Luu

糟糕的基准与评测:Senior SWE-Bench、餐巾纸估算与冬季轮胎

原文由 Dan Luu 发布,订阅该博客

本文将考察三类不同的基准测试:一组用于性能“餐巾纸估算”的基线数值计算,一组 AI 模型评测,以及一组关于汽车轮胎的测试。为了建立直觉,我喜欢在看到解释之前先自行思考一番,因此下文会先给出基准信息,再给出解释,方便你先形成自己的判断,再对照我的看法。

29. 我的一位朋友正在复习性能数量级的估算,以准备计算机性能相关的面试,发现 https://github.com/sirupsen/napkin-math(5.4k stars)是搜索结果的第一条。该 README 中的表格包括:

餐巾纸估算性能估计
操作延迟吞吐1 MiB1 GiB
顺序内存读写(64 字节)0.5 ns
├ 单线程20 GiB/s50 μs50 ms
├ 多线程200 GiB/s5 μs5 ms
同可用区网络10 GiB/s100 μs100 ms
├ VPC 内10 GiB/s100 μs100 ms
├ VPC 外3 GiB/s300 μs300 ms
哈希(非密码学安全,64 字节)10 ns5 GiB/s200 μs200 ms
随机内存读写(64 字节)20 ns3 GiB/s300 μs300 ms
快速序列化 [8] [9]N/A1 GiB/s1 ms1s
快速反序列化 [8] [9]N/A1 GiB/s1 ms1s
系统调用300 nsN/AN/AN/A
哈希(密码学安全,64 字节)100 ns1 GiB/s1 ms1s
顺序 SSD 读取(8 KiB)1 μs8 GiB/s100 μs100 ms
上下文切换 [1] [2]10 μsN/AN/AN/A
顺序 SSD 写入,-fsync(8KiB)2 μs3 GiB/s300 μs300 ms
TCP 回显服务器(32 KiB)50 μs500 MiB/s2 ms2s
随机 SSD 读取(8 KiB)100 μs70 MiB/s15 ms15s
解压缩 [11]N/A1 GiB/s1 ms1s
压缩 [11]N/A500 MiB/s2 ms2s
排序(64 位整数)N/A500 MiB/s2 ms2s
代理:Envoy/ProxySQL/Nginx/HAProxy50 μs???
同区域内网络250 μs2 GiB/s500 μs500 ms
可用区/VPC 内的优质网络250 μs25 GiB/s50 μs40 ms
顺序 SSD 写入,+fsync(8KiB)300 μs30 MiB/s30 ms30s
{MySQL, Memcached, Redis, ..} 查询500 μs???
序列化 [8] [9]N/A100 MiB/s10 ms10s
反序列化 [8] [9]N/A100 MiB/s10 ms10s
顺序 HDD 读取(8 KiB)10 ms250 MiB/s2 ms2s
随机 HDD 读取(8 KiB)10 ms0.7 MiB/s2 s30m
Blob Storage GET,if-not-match 30430 ms
Blob Storage GET,单连接(128KiB)80 ms100 MiB/s10 ms10s
Blob Storage GET,多连接(offsets)80 msNW limit
Blob Storage LIST100 ms
Blob Storage PUT,单连接(128KiB)200 ms100 MiB/s10 ms10s
Blob Storage PUT,多连接(multipart)200 msNW limit10 ms10s
跨区域网络 [6]不等25 MiB/s40 ms40s
网络 NA Central <-> East25 ms25 MiB/s40 ms40s
网络 NA Central <-> West40 ms25 MiB/s40 ms40s
网络 NA East <-> West60 ms25 MiB/s40 ms40s
网络 EU West <-> NA East80 ms25 MiB/s40 ms40s
网络 EU West <-> NA Central100 ms25 MiB/s40 ms40s
网络 NA West <-> Singapore180 ms25 MiB/s40 ms40s
网络 EU West <-> Singapore160 ms25 MiB/s40 ms40s

这个基准测试有什么问题?

30. 我不断看到有人引用 DeepSWE 和 Senior SWE-Bench 来“证明”自己喜欢的模型比别人喜欢的模型更好,或是把它当作通用的优劣评判标准,例如:

DeepSWE 排行榜,展示不同模型和投入级别下得分与每任务平均成本的关系Senior SWE-Bench 排行榜,显示 Claude Fable 5、Claude Opus 4.8 和 GPT-5.6 Sol 位列前三

这些基准测试有什么问题?

31. 人们常说冬季轮胎在寒冷天气下优于四季轮胎。例如,在谷歌上搜索“all season tires during winter cold”(不加引号),AI 概览的第一句就是:

四季轮胎在冰冻的冬季气温下会失去抓地力并变硬。其橡胶配方为较暖天气设计,在 7°C(45°F)以下会变硬,导致刹车距离显著变长、抓地力下降……四季轮胎的橡胶在零度以下无法保持柔韧性,在雪地和冰面上表现得更像硬塑料。

鉴于训练数据中有大量网络评论,这样的说法也算合理,因为我在讨论该用哪种轮胎的帖子里经常看到类似的观点。

这个基准测试有什么问题?

29. 餐巾纸估算的数值

随机内存访问延迟

让我的朋友(Jamie)一眼就觉得不对劲的是,随机内存读写被标为 20ns,因为随机内存读写在这里暗指的是一次真正的 DRAM 读取(而非缓存命中),按数量级估算,这应该在 100ns 左右才对。

在我们聊这件事时,他指出 README 把一些并非真正延迟的东西也称作“延迟”。随后,当他打开测量随机内存读取延迟的代码时,发现了如下代码(如果你想先自己练练手,可以在看下面解释前先想想这段代码有什么问题):

  while test.i < test.vec.len() {                                                        
      let random_index = test.order[test.i];                                             
      black_box(test.vec[random_index]);                                                                                                                                          
      test.i += 1;                                                                                                                                                                
  }     

Jamie 指出,循环的各次迭代之间没有数据依赖,因此这些内存读取是并行发生的。由于所谓的延迟数值是通过计算平均每次访问的时间得出的,这种做法是错误的,因为 CPU 可以同时让多个加载请求在途。如果想用这种方式测量延迟,就必须在各次加载之间引入依赖,以防止访问重叠(我们在本系列第 4 部分的练习 19 中讨论过相关话题)。

随机 SSD 读取

我同意 Jamie 的所有评论,虽然我自己倒没有特别在意“延迟”这个用词,因为也许它在某些情况下是延迟的简称,在另一些情况下则是类似延迟的东西(比如吞吐量的倒数),这样可以让表格更简洁。

除了内存延迟数值外,最先让我觉得不对劲的是其他一些数值。例如,随机 SSD 读取被标为 100 us / 70 MB/s。你完全可以找到更快(当然也有更慢)的 SSD。比如,如果你有一块较快的(但并非特殊的,例如非 Optane)设备,延迟可能会低于 40us,例如 Kioxia CD9P-R 在这篇评测中被测得约为 30 us。除了一些小脚本外,我还没做过任何需要在意磁盘性能的工作,所以我对这类数值该取什么并没有直觉1,但我也在想,对于随机读取延迟和吞吐量,只给一个单一数值,是否不如对 DRAM 访问那样有用。每当我查看磁盘基准测试时,结果似乎都会因读取大小、队列深度和任务数等因素而有巨大差异(例如参见前面关于 Kioxia CD9P-R 的链接)。当然,也有类似因素会影响 DRAM 的延迟和带宽,但对于内存访问,你似乎更常处于那种知道一两个数值就有帮助的区间。由于我对磁盘性能一无所知,我去问了做过 Postgres 磁盘性能工作的 Peter Geoghegan;他表示认同,也在下文补充了一些关于磁盘性能复杂性的评论

如果我们查看生成这个 SSD 随机读取数值的代码,它给我的感觉和 Jamie 看到随机内存读取代码时的感觉一样不对劲。它用以下方式生成偏移量:

for i in 0..(buffer.len() / page_size) {
    pages.push((i * page_size + 1) as u64);
}

然后再做 8 KiB 的读取(偏移量会被打乱以形成随机读取)。有些地方让人觉得不对劲:

  1. 那个 +1 让每次读取都不对齐。以 4KiB 页大小为例,一次读取会跨 3 个页
  2. 不同的偏移量可能会重叠到同一个页,导致从页缓存中出现并非预期的读取
  3. 取决于页大小,读取可能会超出文件末尾并导致 panic

“buffer.len() / page_size”这种写法似乎是想让访问保持在边界内,但这与访问长度无关。如果我们偷懒,不去细想确切的偏移量,设想一个巨大的访问长度,比如 4 GiB(缓冲区大小为 8 GiB),那肯定会溢出。如果想更精确一点,溢出情况会更像是 4KiB 页配合 8KiB 访问长度,但道理是一样的。

最后一个偏移量将是 SIZE - 4096 + 1。这意味着我们只有 4095 字节可用,却要尝试访问 8192 字节。由于该基准测试只运行 5 秒,它可能有机会也可能没机会真的尝试读到 EOF 之后而失败,但无论在某次运行中是否随机失败,这里都存在一个 bug。

顺序 SSD 读取

单看代码,很多地方就让我觉得不太对劲。例如,用于生成顺序 8 KiB SSD 读取的代码,据称延迟为 1us、吞吐为 8 GiB/s。如前所述,我没有做过任何与磁盘性能相关的工作,所以对这类数值是否合理没有直觉,但代码给人的感觉就不对。它取一个 1 GiB 的文件,将其刷入磁盘,然后反复重新读取,因此会有一次未缓存的读取,后面跟着多次缓存命中后的读取。看起来意图是想测量未缓存的读取,但如果是想测量缓存读取,代码也没有做到这一点(这似乎也是其他一些数值的问题,比如带 fsync 的 3 GiB/s 读取)。可以说,先有一次未缓存读取再跟上若干次缓存读取是符合现实的,但当有人拿到这个由 1 次未缓存读取加 N 次缓存读取聚合而成的数值时,如果他们的工作负载并不完全相同,就不清楚该如何使用这个数值;而 N 也没有以明显的方式给出,他们甚至不知道自己是否有相同的工作负载。

既然我对数值应该是多少毫无头绪,也许我们可以查一些数据。据称该测量是在 c4-standard-48-lssd 上完成的。谷歌关于该实例的文档称,全部 8 块附加磁盘的最大吞吐量为 5000 MiB/s(根据谷歌的表格,这按附加磁盘数量线性扩展,每块磁盘为 625 MiB/s)。以我对磁盘基准测试非常有限的了解,峰值吞吐量数值通常是在使用较大读取时测得的,因此 8 GiB/s 显得过高,代码给人“不对劲”的感觉似乎是对的。如果我们再看其他数值,更广泛的观点是:针对特定读取大小给出少数几个单一数值,并不能代表磁盘的整体性能。

代表性

但这类“餐巾纸估算”背后的思路,通常并不是要精确知道某一个云实例的表现;而是想得到一些可用于各种性能估算的基础数值。如果我们回到 Kioxia CD9P 的基准测试,在不同参数下,有不少读取基准测试的带宽要高于那个数值(当然也有不少参数下带宽更低)。就延迟而言,即使是在最小化延迟的设置下,顺序读取的延迟也更高(包括该基准测试中的其他磁盘),这也是 sirupsen 基准测试无意中从缓存读取的另一个迹象,但即便这些数值是正确的,也并不清楚你能拿这些数值做什么。

看起来 sirupsen 代码曾试图防止缓存和预取。如果检测到测试在 Linux 上运行,它会设置建议性的 POSIX_FADV_RANDOM,并在测试开始前设置建议性的 POSIX_FADV_DONTNEED,但这两者在该基准测试中都无法在操作系统层面阻止缓存,也不应该被期望能阻止更底层的缓存(例如 SSD 内部的缓存)。在 Mac 上,该基准测试会在事前执行 Command::new("sudo").arg("purge").output().expect("failed to flush page cache"),但在其他操作系统(如 BSD 或 Windows)上似乎没有等同于 POSIX_FADV_RANDOM 的操作,也没有做任何处理。

代码的其他部分还有其他问题,但与其逐一深究每个具体问题和大多数给出的数值,不如回到这样一个观点:有些场景下,我们想要了解的是不同区间内一系列数值的范围,而这似乎就是其中之一。再举一个例子,README 称“解压缩”为 1 GiB/s、“压缩”为 500 MiB/s。当然,任何餐巾纸估算都不会精确,但只要摆弄一下不同的 zstd 压缩选项,我们就能看到压缩速度有超过两个数量级的差异,而且还有更专为高速压缩设计的算法,会让范围更大(当然,你也可以花更多算力来换取更慢但压缩率更高的结果)。

回到磁盘的例子,我们提到磁盘数值来自一个配备 8 块磁盘的虚拟机配置。这些数值看起来是错误的,但即便数值正确,如果你使用该虚拟机的单盘版本,读取带宽等数值当然也会不同。如果不是从页缓存读取,你会预期读取基准测试的带宽大约只有 1/8。为什么要把 GCP 上某个特定 8 盘配置的读取带宽数值当作餐巾纸估算值背下来,就不太清楚其用处了。

什么值得学?

总体而言,我确实觉得了解其中一些数值是有用的,但我不确定是否非要去查表来记住这些数值(也许除了作为面试准备,面完就忘——如果你有充分理由相信面试会问到这些的话)。一般来说,如果你做的事情确实需要知道这些数值,你在使用的过程中自然就会记住它们。例如,我至今还记得标准单模光纤的色散是 17 ps / nm * km,因为我二十年前做过一些光学/光子学方面的工作。这在草稿估算中经常出现,用的多了自然就记住了。同样地,2 的各种次幂(例如 2^8 = 256、2^16 = 65536 等)我也没有刻意去背,但因为在写代码时经常碰到,需要用到的数值也就自然记住了。

所链接的餐巾纸估算仓库提到“数值已为便于记忆而取整”,暗示记住这些数值是有意义的。除了上面提到的那些,很多数值其实是可推导的,而且在我看来,如果你在工作中要用到这些,与其死记一个大概的数值,不如理解其推导过程。例如,我们在第 4 部分中曾从一些基本参数推导过 Sandy Bridge 处理器的单核内存带宽数值。如果你只是想知道一段代码会跑多快,通常不需要从第一性原理重新推导一遍。但如果你想理解改动某项东西会带来什么影响,了解其中起作用的机制及其相互作用就会很有帮助,而这恰恰是死记几个数值无法给你的。

回到这个问题的背景,我那位正在准备面试的朋友最后担心的还有一点:这个仓库非常受欢迎,所以面试官可能会在不了解其中大多数数值都是错误的情况下直接拿来用。

额外信息:内存延迟随时间的变化

顺便说一下,我很好奇在真实系统上实际观测到的内存延迟是多少,于是我绘制了来自instlatx64 网站的数据,结果如下:

这里有两张图,因为使用了两种不可直接比较的方法。原有方法使用 1024 字节步长的访问来测量内存延迟,这在数据量足够大的旧处理器上效果不错。较新的处理器加入了一些机制,会让这种方法无法保证是纯粹的 DRAM 访问,因此新方法改为使用随机访问来测量内存延迟(用旧方法测得的一些较晚的数值,如果按随机内存访问时间来理解,其实并不真正有效)。延迟数据来自 instlatx64 网站,CPU 发布年份是通过让 GPT-5.6 Sol ultra 在 codex 中查询得来,未经核实,因此其中一些年份可能是错的。

仅从肉眼观察图表就可以看出,内存延迟曾经在一段时间内大幅改善,但这种改善最终停滞了,而且我们实际上看到随着时间推移观测到的延迟反而变高了,原因超出了本文的范围。

690ns?

我们还看到一些来自 90 年代的极高离群结果。不花太多时间细看这些结果,也看不出它们明显是错的。在 Intel 方面,最大的离群值是一颗 83 MHz 的 Intel Pentium Overdrive。其他较老的 Intel 结果则都是非 Overdrive 的 Pentium。

Overdrive Pentium 是可以直接插到上一代 CPU 主板上的芯片。看起来该测试是在一块 Gigabyte GA-5486AL 主板上运行的,芯片组为 ALi M1489/M1487,总线速度设为 33 MHz。根据 ALi M1489/M1487 的数据手册,DRAM 读取时序有四种可能配置。如果配置为“常规”设置,一次读页未命中是 CP+8,读取时序为 4-4-4。我对 486 总线时序的了解甚至比对磁盘性能的了解还少,所以我问了一个 LLM,它告诉我这是正确的,我们应该预期一次内存访问需要 21、22 或 23 个周期。这感觉不太对,因为当我问 LLM 这些数字到底是什么意思时,它说这是首字加上后续每个字的周期数,所以这个周期数是针对整个缓存行填充的。那么对于单字访问的实际 load-to-use 延迟,就应该是前半部分,即 11 个总线周期,但如果你要的是整个缓存行的时间,那么这个数值似乎又 plausibly 在基准测试的合理范围内。

再看 AMD K5 PR166 的离群结果,其中有些地方有点奇怪,但我们已经在关于现代计算机性能的问题上钻得太深了,所以也许可以把那作为本系列后续的另一个问题。

30. DeepSWE / Senior SWE-Bench

总体合理性

在看这些基准测试的方法论之前,仅看结果,DeepSWE 和 Senior SWE-Bench 作为对编码智能体整体能力的概括,都让人觉得不太可信。粗略地看 DeepSWE 主页,OpenAI 上一代模型(GPT-5.5)与 Anthropic 这一代模型(Fable 5)一样好;而粗略地看 Senior SWE-Bench,Anthropic 上一代模型(Opus 4.8)则优于 OpenAI 这一代模型(GPT 5.6)。一般来说,大多数人只会得到这种表面印象,而这也是我通常看到这些基准被使用的方式(例如在工作群里,或是别人直接发给我时等等)。

方法论问题

如果我们看看这些基准测试是如何“做出来的”,就会发现,公开可用的基准测试中,很少有能让人合理地据此对编码智能体的整体水平形成一般性判断的。就方法论而言,这些基准测试在衡量什么才能得到可泛化结果这一点上,本身就说不通。就像我对磁盘性能一无所知一样,我对 AI 也一无所知,所以我请了一位曾在 Anthropic 带过一段时间的评测团队的同事(Aaron Levin)来审阅这里的推理和结论,他也认同总体观点和推理逻辑。与请教磁盘性能专家一样,这里的重点并不是说因为专家认同你就应该认同;而是想说,在这些情况下,你不需要任何关于该领域的专业知识就能得出与专家相同的结论。你只需要运用评估任何基准测试或实验设计问题时都会用到的通用推理即可。

总分代表性

上一篇中,我们讨论过这样一个总体观点:单一的总分几乎可以说明任何事情,因为当我们查看子基准测试的结果时,总会有大量结果显示模型 X 优于模型 Y,而并没有什么特别好的方法,能通过对现实中任务分布的采样来说明基准 A 比基准 B 更具代表性。

如果我们更仔细地看这些基准测试的细节,对于 DeepSWE,共有 113 个任务(或者说,这是 codex 告诉我的),每个任务运行四次,结果通常表现为通过/失败的分数(模型在每个任务上的得分表现为 0%、25%、50%、75% 或 100%)。在图表上可以看到,GPT-5.5 远优于 Opus 4.8;GPT-5.5 与 Opus 4.8 之间的差距,大约相当于 Opus 4.8 与 Gemini-3.5 Flash 之间的差距。如前所述,如果你实际使用过这些模型,这与我或我信任其判断的任何人的整体体验都不太相符(当然在特定任务或子基准测试上确实会出现这种情况)。

如果我们探究为何会得出这种所谓的结论,GPT-5.5 xhigh 据称比 Opus 4.8 xhigh 稍便宜且好得多(得分 67% 对 54%)。在 113 个任务中,两个模型在 34 个任务上打平,GPT-5.5 xhigh 在 57 个任务上胜出,Opus 4.8 在 22 个任务上胜出。对我或其他程序员而言,如果这些任务能代表我或其他程序员所做的任务,这也许是有意义的。113 个任务(甚至只是 79 个有差异的任务)已经多到我们无法在本文中逐一细看,但从任务名称来看,其中几乎没有与我所做任务相关的。而从语言来看,在有差异的任务中,只有 4 个任务使用的是我经常让编码智能体处理的语言(Rust),其余任务所用的语言,我要么不用编码智能体,要么只用它们处理一些任何模型都能应付的琐碎问题2

结果有差异的四个 Rust 任务是:

Boa 中的分层求值取消(https://deepswe.datacurve.ai/data/v1.1/tasks/boa-hierarchical-evaluation-cancellation)、fd 中的确定性多键排序(https://deepswe.datacurve.ai/data/v1.1/tasks/fd-deterministic-multi-key-sorting)、oxvg 中保留样式表选择器结构(https://deepswe.datacurve.ai/data/v1.1/tasks/oxvg-structural-selector-preservation)以及 wasmi 中的陷阱转储生成(https://deepswe.datacurve.ai/data/v1.1/tasks/wasmi-trap-coredumps)。这些任务似乎都与我让编码智能体做的事情没有太大关联,所以对我来说毫无价值。

其中有一个任务模糊地像是过去一年里我做过的事情,另外三个则完全不是。我们从单个基准测试中已经知道,不同基准测试之间的结果方差很大(例如,在上一篇中的 Optimization 1 基准测试里,我们得到的结果模型排名与 DeepSWE 有点像,但在 GameAI 中则得到类似 Senior SWE-Bench 的排名,但正如我们在那篇文章中也观察到的,即使某个基准测试名义上看起来与我们关心的任务相似,其结果也可能与我们在实际任务上看到的情况完全相反,原因还是方差非常大)。113 个任务中只有 1 个与我做过的任务有点相似,这意味着 DeepSWE 的基准分数对我个人而言毫无意义。

Senior SWE-Bench

再来看另一个基准测试,Senior SWE-Bench 存在上述所有问题,而且它以更具误导性的方式呈现结果,还额外存在对结果进行主观评分的问题。我不想在这里做那种超长的逐点拆解,但就其中一个问题来看,要被算作“品味得当的解法”(tasteful solve),解决方案必须满足多个标准,包括在评分细则上得分超过某个阈值,且结果长度不超过参考结果的 2 倍。

任意且主观的评分函数

甚至无需更深入地细看,我们就已经看到这是一个经典的https://danluu.com/discontinuities/情形。该基准测试有这些连续的分数,然后通过要求严格的阈值引入了阈值效应。据我所见,这种做法常常是因为这样更简单,但如果你认为底层标准很重要,一般来说,你通常不希望说分数 X 算通过而 X-epsilon 就算失败。相反,分数应该以某种非离散的方式聚合。我认为人们常常不愿意这样做,是因为试图写出一个这样的公式往往会让人明显看出权重是任意的、分数是无意义的。我们大概知道,如果参考解是 R 行,我们不想为 1 行的解给出高达 N 的额外加分,所以我们需要某个函数来在那里封顶。或许我们可以做类似 (2R)/(R+N) 来把奖励封顶在 2。也许这对大函数的惩罚还不够,所以我们应该换成 (2R^2)/(R^2+N^2)。如果把这个写成 1+tanh(ln(R/N)) 会更容易看清它的行为,所以如果你更喜欢,也可以心里把它替换成这个形式。然后我们还需要把这个分数与其他分数结合,所以我们至少需要 M 个公式中的 M-1 个,以便为它们设定相对权重。

这显然会是一个难以自圆其说的任意公式。但实际使用的、带有这些离散性的公式,同样是另一个完全任意的函数,只是其性质更差,更难以自圆其说!只不过写下它的人不必把它当作公式来思考,因此可以避免去想它有多任意。

阈值效应

如果我们具体看 Senior SWE-Bench 所定义的 LOC 指标,当然会看到阈值效应。例如,在https://senior-swe-bench.snorkel.ai/tasks/paperless-ngx-perf-workflow-queries 上,GLM-5.2 以 121 行对参考的 61 行被评为 tasteful。如果再多一行,变成 122 行,也就是两倍,就会让 GLM-5.2 从通过变为失败。从链接中我们还可以看到,该基准测试在每种条件下只运行了一次。正如任何用过 LLM 的人都知道的,也正如我们在上一篇中看到的,不同运行之间的方差非常大(通常,同一模型不同运行之间的方差比不同模型和不同努力程度之间的方差还要大,这一点我们在上一篇中已经观察到),这已经让单次运行在采用某种合理的连续评分时就不太有意义。当这种带有噪声的指标再通过阈值效应被去掉部分信息后,结果就变得更加没有意义。

那甚至还不是在 LOC 分数方面特别成问题的基准测试。plausible-fix-top-pages-comparison 更糟,因为参考解只有 1 行(由于增加和删除各算 1 行,这被计为 2 行)。这使得一个 tasteful 解的最大长度只有 3 行;如果同时有增加和删除,就必须是删 1 行加 2 行,或反过来。

代码质量

如果我们看实际结果,就会发现它们说不通。可以看到,在这个任务上,Opus 4.8 被评为“tasteful”,而 Opus 4.7 和 Fable 5 则没有。如果我们查看实际的 diff 并与参考解对比,会发现如下情况(注意,只有对实际代码的改动才计入 LOC 标准;测试代码的行数、注释等不计入)。

参考解
  --- a/lib/plausible_web/controllers/api/stats_controller.ex
  +++ b/lib/plausible_web/controllers/api/stats_controller.ex
  @@ -723,7 +723,7 @@ defmodule PlausibleWeb.Api.StatsController do
       else
         json(conn, %{
           results: pages,
  -        meta: Map.merge(meta, Stats.Breakdown.formatted_date_ranges(query)),
  +        meta: Map.new(meta.values) |> Map.merge(Stats.Breakdown.formatted_date_ranges(query)),
           skip_imported_reason: meta[:imports_skip_reason]
         })
       end
Opus 4.8(通过)
  --- CHANGELOG.md                                                                                                                         
  +++ CHANGELOG.md                                                                                                                         
  +- Fixed blank comparison dates in row tooltips on the Top Pages report                                                                  

  --- lib/plausible_web/controllers/api/stats_controller.ex
  +++ lib/plausible_web/controllers/api/stats_controller.ex
  -        meta: Map.merge(meta, Stats.Breakdown.formatted_date_ranges(query)),                      
  +        meta: Map.merge(Map.new(meta), Stats.Breakdown.formatted_date_ranges(query)),

  --- test/plausible_web/controllers/api/stats_controller/pages_test.exs
  +++ test/plausible_web/controllers/api/stats_controller/pages_test.exs                     
  +      assert json_response(conn, 200)["meta"] == %{                                                                                                                                     
  +               "date_range_label" => "2 Jan 2021",                                                
  +               "comparison_date_range_label" => "1 Jan 2021"                                                                                                                                            
  +             }                                                                                    
Opus 4.7(未通过)
  --- lib/plausible_web/controllers/api/stats_controller.ex                                          
  +++ lib/plausible_web/controllers/api/stats_controller.ex                                                                                                                                                
  -        meta: Map.merge(meta, Stats.Breakdown.formatted_date_ranges(query)),
  +        meta:                                                                                                                                                                                           
  +          meta.values                                                                                                                                                                                   
  +          |> Map.new()                                                                                                                                                                                  
  +          |> Map.merge(Stats.Breakdown.formatted_date_ranges(query)),                                                                                                                                   

  --- test/plausible_web/controllers/api/stats_controller/pages_test.exs
  +++ test/plausible_web/controllers/api/stats_controller/pages_test.exs                                                                                                                                   
  +      assert json_response(conn, 200)["meta"]["date_range_label"] == "2 Jan 2021"
  +      assert json_response(conn, 200)["meta"]["comparison_date_range_label"] == "1 Jan 2021"   
Fable 5(未通过)
  --- lib/plausible/stats/breakdown.ex                                                               
  +++ lib/plausible/stats/breakdown.ex                                                               
       %{                                                                                            
         results: build_breakdown_result(results, query_with_metrics, metrics),
  -      meta: meta
  +      # `meta` is a `Jason.OrderedObject` - convert it to a plain map so that
  +      # callers can safely `Map.merge/2` extra fields into it. Merging into the
  +      # struct would add keys outside of its `values` field, which the JSON
  +      # encoder for `Jason.OrderedObject` silently ignores.
  +      meta: Map.new(meta.values)
       }

  --- lib/plausible_web/controllers/api/external_stats_controller.ex
  +++ lib/plausible_web/controllers/api/external_stats_controller.ex
  -  defp maybe_add_warning(payload, %Jason.OrderedObject{} = meta) do
  +  defp maybe_add_warning(payload, meta) do

  --- test/plausible_web/controllers/api/stats_controller/pages_test.exs
  +++ test/plausible_web/controllers/api/stats_controller/pages_test.exs
  +      assert json_response(conn, 200)["meta"] == %{
  +               "date_range_label" => "2 Jan 2021",
  +               "comparison_date_range_label" => "1 Jan 2021"
  +             }

我不是 Elixir 程序员,也不熟悉这个代码库,但单看代码,未通过、被评为“非 tasteful”的 Opus 4.7 解法在语义上似乎与参考解完全相同。唯一的区别是将管道展开到了多行以提高可读性。在不了解 Elixir 的情况下,我觉得仅以“品味”就否掉这个改动是荒谬的。

我用过其他一些常用管道操作符的语言(比如 F# 或使用 tidyverse 的 R),我不记得曾遇到过有人会因为“不 tasteful”而拒绝 Opus 4.7 这样的改动(除非有风格指南对什么该展开成多行、什么不该有严格规定,但如果是那样,应该由自动格式化工具来处理,解法的格式就无关紧要了)。

Fable 5 的解法可以说应该因为改动范围过大而被拒绝,但无论是否应该因其他原因被拒绝,仅因长度就将其额外判为“非 tasteful”似乎是错误的。

评分方差

LLM 的方差同样适用于评分本身。当然,如果我们把同一次运行的结果多次喂给 LLM 评分器,出于与我们多次让 LLM 解决同一问题时会得到截然不同结果的相同原因,我们会得到不同的分数。我让身边友好的编码智能体对 GPT-5.6 Sol 和 Opus 4.8 所测试的每种条件各重新评分 10 次(codex 告诉我评分是使用 Sonnet 4.6 进行的,因此它也用该模型重新运行)。当使用相同模型和努力程度时,预期的 LLM 评分 tastefulness 结果有 23% 的概率会与官方结果翻转(就子结果而言,相对品味翻转率为 32%,实践一致性为 5%,任务细则为 3%)。如果我们转而看官方结果与典型/中位数结果不同的比例,总体差异为 21%(相对品味为 27%,实践一致性为 3%,任务细则为 2%)。总体翻转率低于单个子项翻转率,是因为在某些情况下,某个子分数从 tasteful 翻转为 untasteful 时,总分本来就已经是 untasteful。

需要明确的是,这不是运行间的方差。这是在单次运行上使用 LLM 评分所带来的方差,在公开可用的 GPT-5.6 Sol 和 Opus 4.8 基准测试中,如果我们假设所测量的内容是正确且合理、且最可能的 Sonnet 分数就是正确分数,那么大约有 20% 的时间会给出错误结果。

当然,如果我们用不同模型评分,也会得到不同结果。如果改用 GPT-5.6 Sol 而非 Sonnet 4.6 重新评分,被判定为 tasteful 的解法数量对两个模型而言都会减半以上。那哪一个更准确呢?谁知道呢?

总体有效性

有时,你可以看一个基准测试说,虽然个别结果是错的,但总体上噪声会相互抵消,整体结果是有道理的。我认为这里并非如此。我看到很多人转发 Senior SWE-Bench,似乎是因为它声称提供了现实的问题并以合理的方式评分。我们已经提到,从表面看结果似乎不太可信,而审视其方法论也支持了那种表面印象——即结果是没有意义的3

结果的呈现方式也有待改进。在我所在的某个 Slack 群里,有人分享了这个链接,预览片段显示如下:

  • Claude Fable 5: 29.1%
  • Claude Opus 4.8: 25.0%
  • GPT-5.6 Sol: 24.4%

他们给出了肯定的评论,说这比其他基准测试更真实(指的是众多把 GPT-5.5 排在 Opus 4.8 之前的基准测试中的某一个)。如果你实际查看结果,就会清楚 25.0% 与 24.4% 之间的差异几乎毫无意义,但结果却被呈现得好像这些差异是有意义的。虽然页面上明确指出,按测量结果 GPT-5.6 Sol 比 Opus 4.8 便宜得多,但我在大多数提及 Senior SWE-Bench 的讨论中看到的都略去了这一点,只提标题结果。标题结果对 Fable、Opus 和 Sonnet 使用 max,而对 GPT-5.6、GPT-5.5 和 GPT-5.4 使用 xhigh,这也显得有些奇怪。

31. 寒冷天气下的轮胎性能

尽管人们常说四季轮胎在 7°C / 45°F 时会变得像(不知为何,大家常用“曲棍球”来形容)硬块一样硬、抓地力差,但其实根本没有基准测试!这在本系列中是一个反复出现的主题:人们不断重复一个似乎没有任何测量依据4的说法。

幸运的是,正如我们在这篇关于平台与变现的文章中讨论过的,Jonathan Benson 得以通过对轮胎的深入探索实现变现,从而带来了前所未有的公开轮胎基准测试细节。他测试了不同种类轮胎在不同温度和不同条件下的表现。我确信轮胎制造商内部肯定有各种类似测试,但据我所知,此前还没有人以如此全面的方式公开做过(嗯,这似乎与编码智能体的公开基准测试也没什么不同)。

在 Benson 的测试中,他发现,在干燥条件下,夏季轮胎一直到 0°C / 32°F 都拥有最好的抓地力(他没有测试更冷的条件),其次是四季轮胎,而冬季轮胎则比夏季轮胎和四季轮胎都差一大截。在湿滑条件下,他只测试到了 2°C,因为在 0°C 时就是冰面条件而不仅仅是潮湿。排名略有不同,因为四季轮胎在 2°C 湿滑条件下大幅优于夏季轮胎,但夏季轮胎仍然优于冬季轮胎。

需要注意的是,在视频中,Benson 所称的冬季轮胎是 UHP 冬季轮胎,这类轮胎在美国或加拿大非常少见(不过这正是我用作冬季轮胎的类型,因为这符合我所在地的条件)。他所称的“北欧”轮胎才是大多数人在当地、以及我曾居住过的所有其他地方所使用的冬季轮胎,而在那些地方,除非你经常开车进山(即便如此,对我住过的大多数地方的人来说,这可能仍然不是正确选择),或者你经常在冰面上行驶,否则那种轮胎其实并不太合适。但即便对比 UHP 冬季轮胎与四季轮胎的结果,四季轮胎在 0°C 以上的干燥或潮湿条件下仍然更好,尽管差异幅度远小于与大多数美国人所使用的“北欧”冬季轮胎相比的差异(我想他使用的术语在欧洲可能更常见?)。

当然,不同轮胎的表现会有所不同,换用不同轮胎会看到结果有些差异,而且当然也有许多条件下冬季轮胎比四季轮胎或夏季轮胎更好,但认为四季轮胎仅因寒冷就变得过硬而失去抓地力、必须换冬季轮胎的说法显然是错误的。

谁会在意轮胎?

顺便说一下,如果你在想为什么要关心轮胎,平均来看,机动车事故是相当主要的死因之一,而且如果看速度对事故严重程度的影响,速度的影响非常显著,因此可以推断,拥有能让你更快刹停或更好地过弯、或许能避免或减轻事故的轮胎,理应对事故严重程度产生实质性影响。我认为这类问题并没有特别好的数据(要做随机对照试验会非常困难,而观察性数据在一般情况下会高度混杂)。但作为去年一项分析的一部分,我曾试图在实际碰撞测试数据中寻找 HIC 与速度之间的关系。让我惊讶的是,我没能找到一篇做过这方面研究的论文(不过我确实找到了一些可作为本系列练习的论文),但一个直观的分析表明,两者关系大致为四次方。我真的应该把那写成一篇类似这篇关于碰撞测试的文章、但专门讨论 HIC 与各种车辆脑震荡风险的文章!总之,我尽量为所在地区选择合适的轮胎开车,因为这似乎是按每美元和/或每份精力计算、对我自身安全影响较大的干预措施之一。但我从未真正遇到过那种靠着好轮胎才化险为夷的情况,而那些会特意为安全去挑选合适轮胎的人,本身发生事故的可能性就更低,所以这可能只是一个毫无意义的傻爱好罢了。

基准测试与评测中的更多问题

如果你喜欢这篇文章,这是关于基准测试、评测与实验设计系列练习的一部分(1234565

感谢 Peter Geoghegan、Aaron Levin、Luke Burton、Em Chu、Jamie Brandon、Yossi Kreinin、Jeshua Smith 和 Ikhwan Lee 的评论/指正/讨论。

附录:关于磁盘性能的更多内容

以下是 Peter Geoghegan 的一些后续评论,与我不同,他确实懂磁盘性能:

我在某些访问模式下见过性能在基本相当的 SSD 之间存在显著差异。这很可能是由于 FTL/固件层面的差异。显然,有些 SSD 在反向顺序读取方面比其他 SSD 好得多,这与操作系统的预读无关(使用直接 I/O 时)。这是我正在合作做 Postgres 索引扫描 I/O 预取的同事写的一篇关于此现象的博客文章:https://vondra.me/posts/fun-and-weirdness-with-ssds

我相当确定这些情况对操作系统/文件系统来说仍然是不透明的。这篇略显过时的 LWN.net 文章为此提供了一些佐证:https://lwn.net/Articles/353411,“给文件系统开发者的信息是‘相信我们’和‘别为 SSD 实现的细节操心了,你们这些可爱的小系统程序员们’,每当我们想要更多关于 SSD 实现的信息时就会得到这样的答复”。

我在 2023 年问过 Linux 黑客 Matthew Wilcox 这个问题。他说情况大致相同,如果想在微基准测试中考虑性能差异,最好的方法仍然是在配置上非常谨慎,定期运行 TRIM 等。

曾有一段时间(我想大概是 2015 年左右),我写过一些代码,本打算把它做成关于 CPU 性能的练习或教程。它有点像那个餐巾纸估算仓库,但范围要窄得多。想法是你可以提出这样的问题:

  1. 你有一颗 CPU X。如果你想知道这个循环有多快,你需要知道哪些参数?
  2. 给定这些参数,循环应该有多快?

我已经有了针对各种场景想要的代码,但不知为何,我写的代码没能体现出 DRAM 开页访问与关页访问之间的差异,然后我就被其他事情分心了,最终没有把练习写出来。在 LLM 出现之前,做这类事情相当耗时,因为要做对,你必须对其中涉及的机制有足够了解,然后在编写代码和检查其行为时格外小心。而后,因为我搞砸了什么又没抽出时间去调试,我最终也就没有把练习写出来,因为我不想在代码至少存在一个问题的谜团未解时就把它发布出来。

总之,磁盘要复杂得多,要得到好的数值需要更加小心。有了 LLM,我认为现在做这件事已经可以在不花费大量时间的情况下完成,但仍然需要一些细心。

P.S. 文中(29)提到的那位朋友是Jamie Brandon,他正在积极求职。他在数据库(查询引擎)和流处理系统方面做了不少工作。他最广为人知的文章可能是Against SQL,但他还写过不少我喜欢的其他文章,例如这篇关于流处理系统一致性缺陷的分析。他主要在寻找温哥华本地的工作或远程工作。如果想与他联系,可以通过 [email protected] 联系他。


  1. 人们常把我误认为是性能工程师,但我觉得更像是,我有时能解决性能问题,是因为凭借硬件背景(在硬件领域,基准测试/评测/实验设计比在软件领域更成熟,如这里所讨论的),我作为程序员在基准测试/评测/实验设计方面有着不寻常的丰富经验,加上我倾向于去解决那些容易与金钱价值挂钩的问题例如这个或这个,但我解决性能问题的可能性并不比解决其他问题更高,与那些日复一日做性能工作的人相比,我对性能问题的了解并不特别深入或广泛。 [return]
  2. 关于按语言筛选是否有意义的话题,我在看到有人引用这篇关于语言 token 效率的文章后去研究了一下;那篇文章的结果在非平凡任务上未能复现,但不同语言之间似乎确实存在足够大的差异,使得按语言筛选是合理的。特别是当智能体未能实现某个功能时,尤其是在较低的努力级别下,往往是由于对某种语言的某种特殊用法出错。例如,对于那篇文章中的 zstd 评测,使用 Clojure 的智能体经常会在字节转换的语义上出错,而使用 Java 的智能体——尽管本质上拥有相同的可用操作——却不会犯这个错误。 [return]
  3. 在更宏观的层面,我交谈过的、在其他话题上通常能给出合理评论的人,并不太把这些标题/总分结果当回事。

    例如,在评论这些基准测试的有用性时,Em Chu 说:

    推特/黑客新闻上的情绪,至少不会产生误导性的精确感,感觉是判断模型是否有用的更好方式,尽管这很奇怪(可惜这需要阅读大量黑客新闻帖子,所以我没法推荐。我通常会直接跳过任何看起来像 LLM 基准测试的东西,因为它值得一读的概率接近于零。(我真希望我对黑客新闻的评论也能这么做。)

    我认识的大多数我信任其判断的人都采取类似的做法(有时会用他们认识的人的意见来替代网络情绪)。例外情况通常是那些在该领域工作、会看大量基准测试并在心里做某种聚合的人。例如,当我和经营一家 RL 环境初创公司的 Max Bitker 聊天时,他似乎熟悉每一个公开的基准测试,并能根据他对所有基准测试整体格局的心智模型,预测某个模型发布几周后的舆论会是什么样,但这与看一个总分指标完全不同,而且非常耗时,除非你从事 AI 工作,否则这更像是一种业余爱好,而不是评估模型有效性的合理方式(对业余爱好本身没有意见;我自己也有很多业余爱好)。

    举一个把这些基准测试结果当真与现实世界观察对比的具体例子,这里有一个帖子,有人利用 DeepSWE 的结果制作了一个当时新出的 5.6 Sol/Terra/Luna 与 5.5 的效用与成本对照表。有人(我会同意他的观点,尽管措辞会有所不同)回复道

    胡扯。你真的用过这些模型吗,还是在自嗨?按这个表格,5.6-sol xhigh 既比 5.5 xhigh 更便宜又更好。在哪个现实中这才是真的?

    另一个人这样回复他

    哪个现实都不是。我觉得基准测试的任务真的很简单,在那种情况下这个表格可能是对的。

    我认为这不完全公平(我试过一些任务,似乎确实是对的),但总体而言,人们的体验大多与公开基准测试所展示的截然不同。这一点似乎被不少人所理解,从我线下认识的人到随机的网络评论者都是如此。但这并非普遍现象,因为我仍然看到有人传阅这些分数来解释自己为什么使用某个模型和努力级别,而这在一般情况下似乎并不合理。

    [return]
  4. 一般来说,除非是那种我多次看到的常见说法,否则我不会把这些例子做成练习,因为完全没有依据的错误说法实在太常见了,在一般情况下这并不特别有趣。 [return]
  5. 我一直在Patreon 上发布这些内容,并没有什么特别坚定的理由。我确实通过 Patreon 赚了一点钱,但如果是以赚钱为优化目标,我认为显然更应该把所有内容都公开发布,因为通过可能获得的工作机会带来的潜在收入增量,远大于直接通过 Patreon 能赚到的钱。考虑到我面试表现很差,这一点就更成立了(我几乎只在面试只是走个过场的情况下才能拿到工作,否则我通过面试的概率接近于零;上一次面试时,我在一道类 LeetCode 题的电话筛选中就挂了,即便通过了那些,后续的正式面试通常也会挂掉,如果那是真正的面试的话)。

    我最初在 Patreon 上发布一些我认为太小或太微不足道、不值得做成“正式”博客文章的东西,但后来就养成了在 Patreon 上发布的习惯,已经有一段时间没有公开写太多东西了。

    这类文章作为一系列练习的一部分,完全属于那种看起来太小、太微不足道而不适合放到主博客上的类别。如果你对此有看法,我很想听听你的想法。

    这个系列背后的想法是,我想写某种教程或博客文章来帮助人们更好地做基准测试和评测。但是,我对评测的看法是,它更多是关于避免犯错,而不是遵循某个特定流程,所以并没有一种在一般情况下都适用的分步指南格式。我知道有一些实验设计的方法,会教你去做诸如画出因果图,然后通过看图来找出潜在问题,例如对撞偏倚。仅从观察人们在学习这类技巧前后的数据分析表现来看,我认为这平均而言并没有带来很大改变(尽管少数人确实觉得它非常有用)。

    我曾在某处(可能是 Andrew Gelman)看到过对这种作为避免实验设计问题的通用方法的批评,问题在于万物皆相关,所以你在创建因果图时仍然是在运用自己的判断。一旦图画好了就能机械地看出问题,并不能阻止某人在一开始就画错图。

    一个有点相关的想法,是我在多年前阅读 McElreath 的《Statistical Rethinking》前几章时产生的,当时我希望学到某种能带来严谨统计分析的流程,结果发现其实并没有这样的流程,最终还是得靠自己的判断来决定某件事是否合理。

    既然如此,我觉得一系列练习可能会有用,所以我曾想过写一篇包含 50 或 100 个练习的单篇文章。对于小练习来说,这似乎相当可行,但从观察人们学习各种东西的过程可以清楚看到,给人们一堆小练习,然后指望他们把技巧泛化到更大、更复杂的练习上,通常效果并不好。一旦你开始加入更大、更复杂的练习,很快就会超出长文的篇幅,即便按本博客的标准来看也是如此,本博客有这篇长达 3.2 万字的关于 FTC 在 2011-2012 年对谷歌调查中犯错的文章(作为参考,通常说一部小说的字数在 8 万至 10 万字)。

    一般来说,我一直避免在博客上发布多篇系列文章,因为作为读者,我更喜欢所有内容都在一篇文章里,而不是分散在某种长长的系列中。我理解作者往往更喜欢多篇系列,因为这通常会带来更多流量、在社交媒体上走红的概率也更高等等,但我一直把这个博客优化得更像我自己想读的样子,而不是为了最大化浏览量。在这种情况下,单篇版本的字数很容易就能达到一本厚重的奇幻小说的体量(作为参考,Brandon Sanderson 的《Stormlight Archive》系列据说每本约 45 万字),相比之下,这篇包含 3 个练习、约 7000 字的文章已经算短的了。按这个速度,我大概需要 64 篇,而现在才写到第 7 篇,但可写的问题肯定足够支撑 64 篇,问题只在于是否有时间去写。

    [return]

本文章由 muse-spark-1.2-contributor 进行翻译

评论