Hacker News 热门(buzzing.cc 中文翻译)
精选
70AI 编辑部评分,满分 100

编写智能体时,哪种编程语言最合适?

2026-08-11 12:58· 24分钟前· chaychoong
AI 导读

针对“动态语言比静态语言更省 LLM token”的流行说法,作者用 GPT-5.6 Sol 让智能体实现 zstd 解码器进行实测。结果显示,medium 努力度下动态语言表现更好,ultra 下静态语言反而更优,且此前评测存在测试路径错误等缺陷。作者认为,琐碎任务上的性能无法推广到更大问题。

推荐理由

该实验将语言效率的讨论从微基准拉回实际工程任务,用Zstd和Pandoc实现揭示主流语言更可靠,可能改变开发者在AI辅助下选择技术栈时对动态语言优势的固有认知。

正文 · AI 翻译

这篇被引用得相当广泛的文章(反正我老是看到有人引用它)提出,动态语言和/或能更简洁地表达事物的语言,其 token 效率更高。它似乎被引用得足够多,以至于大语言模型的搜索结果也认同这一点。例如,当我搜索“dynamic vs static language token cost”(不带引号)时,谷歌的 AI 摘要开头是这样写的:

动态类型语言通常比传统静态类型语言具有更低的 LLM token 成本,因为省略显式类型声明会使代码更紧凑。

谷歌的 AI 引用了同一篇文章,该文表明,一些简洁的动态语言其 token 成本可能只有 Rust、Go、C++ 等静态语言的 1/2 到 1/3。作者说:

在我比较的语言中,C(token 效率最低的语言)和 Clojure(效率最高的语言)之间存在 2.6 倍的显著差距。

之后他们又尝试了 J 语言,并表示:

它平均仅需 70 个 token 就占据主导地位,几乎是 Clojure(109 个 token)的一半。数组语言在避免使用奇特符号集时,可以做到极高的 token 效率。如果 token 效率被证明是一个关键驱动因素,这或许是语言演进的一个非常有趣的路径。

我找到的另一个关于动态与静态语言 token 成本对比的帖子是这一篇,它支持相同的结论。如果你想把这当作本系列关于基准测试、评估和实验设计的练习之第 8 部分,你可以先点击链接思考一下评估问题,然后再继续往下读。

在没有运行我们自己的评测的情况下,第一个实验存在的一个问题是题目过于简单,这从上面的引文中就能看出来;一个用 J 语言 70 个 token 就能解决、用 Clojure 109 个 token 就能解决的问题根本算不上什么问题(作者用的是 Rosetta Code)。正如我们在审视其他“穴居人模式”评测与我们自己的评测时所看到的,对于大部分工作在于打印答案的琐碎问题,与那些稍微不那么琐碎、实际上需要一定“真功夫”的问题相比,你可能会得到截然不同的结果;当你开始关注那些需要不止几个 token 才能解决的问题时,“穴居人模式”所宣称的巨大收益以及复现实验中所展示的效果就会消失。总的来说,在琐碎任务上的表现并不能推广到更复杂的情况。

第二个链接中的问题要更微妙一些,所以我们把大部分细节放到附录里,但其中包括这样的问题:其中一个测试执行了错误的路径(该路径不存在),导致测试失败。后来某个智能体将该不存在的路径符号链接到它自己的可执行文件上,这在那个用例中能行得通,但也导致之后所有测试都运行那个智能体的可执行文件,而不是正确的可执行文件。作者试图就 Rust 出现一些失败得出某些结论,但实际上这仅仅意味着 Rust 的评分是在 Go 智能体将该损坏测试上的所有评分符号链接到 Go 可执行文件之前运行的。

与其依赖这些评测,我们可以尝试运行一些我们自己的评测。从这些评测以及我们上一轮评测练习中讨论过的评测可以看出,要做出一个评测结果并不反映评测创建者所认为的含义的评测,是非常容易的。毫无疑问,这些评测也不会例外,同样会存在缺陷(更多细节见下方附录)。

作为建立直觉的一种方式,我喜欢在查看结果之前预先登记猜测¹。我预先登记并与朋友分享的一些猜测包括:

  • High confidence (95%): the overall dynamic vs. static language claim won't hold
    • 基于上述原因:这感觉类似于“穴居人”评测,其结果充其量只会随着问题规模的增大而被稀释。
  • Low confidence (60%): static languages will be somewhat better than dynamic at ultra effort
    • 在超高强度(ultra effort)下,对“训练框架(harness)能将反馈更快地传递给模型,并因此对正确性或效率带来某种好处”这一点的信心非常弱;但同时,由于种种原因,这种情况不成立也似乎合情合理。例如,我注意到 Codex 在调用 Rust 编译器时,经常犯完全相同的错误,然后不得不去修复它;也许这类问题远比“假设中更快的反馈循环”之类的事情影响更大。
  • High confidence (98%): the "weird" language supremacy of something like J won't hold
    • 这与整体上“静态语言 vs. 动态语言”论断的推理逻辑相同,另外还考虑到一点:AI 实验室在冷门语言上投入的合成数据 RL 环境工作量将会少得多(甚至可能为零)。

Zstd

对于第一个评测,我尝试给智能体提供 zstd 的 RFC(以及勘误表),并告诉它们实现一个完整的 zstd 解码器(智能体被限制在一个无法访问互联网的容器中)。测试用例没有提供给智能体。对于 zstd 这样体量的东西,期望测试覆盖每一种可能的情况其实不太现实。例如,尽管 zstd 是一个经过相当充分测试的软件,但我曾经在 zstd 中发现过一个数据损坏的 bug。测试套件的本意并不是要找出那些可能潜伏多年、极其极端的边界情况,而是用来检查那些“容易”从 RFC 推导出来、理应能正常工作的各种情况。

下图中,x 轴是成本,y 轴是正确性得分(越靠左上越好 / 越靠右下越差);这是 GPT-5.6 Sol 在中等(medium)和超高(ultra)强度下的平均结果。如果我们只看中等强度(并忽略不同任务上结果往往差异巨大的事实),我们可能会得出类似 Alderson 评测那样的结论:在使用 LLM 时,动态语言更高效、更好,因为(忽略相对冷门的语言)动态语言集群落在静态语言集群的左上方(我们采用了 Alderson 的静态 vs. 动态颜色编码,以便一眼就能比较)。但如果我们看超高强度的结果,情况就相当混杂了,有几种静态语言表现最好,而且在较好的结果中,静态语言的数量多于动态语言。

下面的图表还有一个开关,可以把 x 轴从成本切换为时间。mame/ai-coding-lang-bench 指出,更快拿到结果是有价值的(我个人不这么认为,因为结果耗时太长,我通常会同时做别的事而不是干等),所以我们也可以看看这个维度。同样地,我们观察到两种语言类型并没有哪一方占据绝对优势,不过在这个特定任务的中等投入水平下,最佳动态语言的结果再次优于最佳静态语言的结果(不过,同样地,两者差距相当接近)。

我们可以观察到,就像我们之前把完全琐碎的“原始模式”评测与稍微不那么琐碎的“原始模式”评测进行对比时一样,在琐碎评测中成立的强关联并不能推广到这个更大的案例中。和之前的情况一样,极端性能差距在这些更大的评测中消失了,除非是在我们预期表现不佳的情况下,比如使用汇编语言(对人类来说会显著更耗时、更困难),以及使用相对冷门的语言——这些语言我们不太可能预期 AI 实验室会投入精力去生成合成 RL 环境数据。

请注意,这与第一项评测的结论相反——那项评测认为像 J 这样密度极高的语言出于效率考虑是有意义的。也许,如果你有非常庞大的预算,并且可以针对你偏爱的语言训练或微调一个模型,那么使用一种冷门(且“古怪”)的语言是有意义的;但如果你是一个普通的 LLM 用户,坚持使用主流语言似乎比使用冷门的高密度语言更稳妥。

而事实证明,如果我们绘制语言流行度与这项评测表现的关系图(未展示),我们会观察到一种弱到中等的正相关关系:越流行的语言最终得到的解决方案既更正确,也更便宜。

正如我们之前所指出的,非常相近的评测也可能产生截然不同的结果。例如,我们在这里的 Optimization 1 与 Optimization 2 评测中看到了显著不同的结果,而这两项评测分别是在优化 wasm 中的 bzip2 压缩与解压缩,就评测而言,这算是相当接近的任务了。要想提出一个强有力且普适的论断,比如“动态语言比静态语言更高效”,我们就必须在众多任务上运行评测。然而,要证明像下面这样的论断——

动态类型语言通常比传统静态类型语言具有更低的 LLM token 成本,因为省略显式类型声明会使代码更加紧凑。

——充其量只是大致方向正确,并且实际上与任何特定案例都不太相关,或许也不足以强到能普遍适用,我们只需要尝试几个案例,看看这个论断是否普遍成立即可。在上面,我们看到在某个努力水平下,这个论断似乎勉强有点道理,但存在例外;而在更高的努力水平下,这个论断似乎就不太成立了,这就足以说明该论断可能并非普遍正确,前提是我们的评测不存在使其完全失效的混杂因素。

Pandoc

但是,为了观察一个截然不同且呈现方式也不同(更偏向测试驱动开发,而非“阅读规格说明”)的任务,接下来的这项评测采用了 Pandoc ProgramBench 评测,并针对我们的用例进行了修改。我们没有采用 ProgramBench 所呈现的逆向工程任务,而是向智能体提供 ProgramBench 的材料以及 ProgramBench 的测试,然后根据一组留出的测试来对智能体进行评分,以衡量每种条件下的表现2。

在下面的结果中,x 轴同样是成本,y 轴是留出测试上的得分。

和之前一样,我们没有看到成功与否或成本与语言是静态、动态还是高度紧凑之间存在非常强的关联。我们再次发现,相对冷门的语言往往表现不佳(尽管 Clojure 在本次评测中的表现比在 Zstd 上要好得多)。此外,汇编语言的表现要差得多,这似乎是意料之中的——我们可以预期,人类用汇编语言实现 Pandoc 会比实现 Zstd 处于更大的劣势,而且似乎没有充分理由认为 LLM 在这方面会有所不同。

这一切意味着什么?

谁知道呢?

关于使用 LLM 时哪些做法效果好,我有很多疑问(比如,哪些测试技术效果好、哪些语言效果好、哪些软件架构效果好、修复 bug 的成本是否因语言而异、一般程序维护成本是否因语言而异,等等)。这些问题大多在公开数据中没有答案,即使 AI 实验室内部已经回答了,这些信息也大多没有公开。

大多数关于某种语言特别适合 LLM 使用的说法似乎都是错误的(例如,上面链接的评测中提到的 Ruby、Clojure 和 J 特别适合 LLM 的说法,以及相当常见的 Elixir 特别适合 LLM 的说法),但目前还不清楚什么才是正确的。

2014 年,我们研究了静态类型与动态类型的相关文献,发现除了少数案例研究之外,综述这些文献并没有提供太多有价值的信息。举一个典型学术研究的例子,我们看到了论文《静态类型系统能否提高软件系统的可维护性?一项实证研究》,我当时对此评论道:

研究对象被安排参加了一些课程,在这些课程中,他们要么需要修复现有代码中的错误,要么需要填充存根方法。Java 使用静态类,Groovy 使用动态类。在类型错误(以及相应的无方法错误)的情况下,开发者在 Java 中解决问题更快。对于语义错误,则没有差异。该研究采用了被试内设计,33 名研究对象以随机顺序完成任务。一个显著的局限性是,该研究避免使用“复杂的控制结构”,例如循环和递归,因为这些会增加解决问题时间的方差。因此,所有错误都是琐碎的错误。这可以从解决任务的中位时间(以数百秒计)看出。任务可能包含多个错误,因此每个错误的解决时间相当短。

选择那些避免“复杂控制结构”(如循环和递归)的任务,且任务耗时数百秒,这使得结果对于真正消耗专业程序员时间的任务而言毫无意义,就像我们看到的第一个评估一样,其任务耗时在几十到几百个 token 之间。然而,借助 LLM,我们实际上可以给它们提供非琐碎的任务,并比较它们的表现。这里存在结果如何推广到不同任务的问题,但我们在人类研究中也会遇到完全相同的问题,而且情况更糟(LLM 的方差很大,但人类的方差更大,因为你无法让同一个人用不同的随机种子去完成一系列任务)。虽然花 20 美元让 LLM 实现一个 Zstd 解码器,一旦乘以语言数量以及每种语言每个条件下的迭代次数,并不算便宜,但如果你想想雇佣一个能阅读 zstd RFC 并实现它的专业程序员要花多少钱,那么同等的研究根本不可能进行,因为成本会使其完全不可行。对于 Pandoc 任务来说,这一点更是如此。

有了大语言模型,很多问题从实际上无法回答,变成了只要花点功夫、消耗一些 token 就能回答。由于当前存在的种种激励机制,我们未必很快就能得到这类问题的答案,但至少现在可以试着去啃一啃了。

我见过很多流传的说法,这些评测既无法证实也无法证伪它们(原因如上所述:由于不同问题之间的方差很大,需要尝试的任务数量要多得多),但确实能对它们有所揭示,例如:

  • Languages with a lot of bad code out there (e.g., PHP) will perform worse
    • 在这些任务上似乎不成立
  • Because it's so easy to re-write now, you should use a powerful language (like Haskell)
    • 在这些任务上似乎不成立
  • You should use a popular language
    • 这一说法得到的支持较弱

对于我预先登记的猜测,我们的结果是

  • High confidence (95%): the overall dynamic vs. static language claim won't hold
    • 这看起来是正确的
  • Low confidence (60%): static languages will be somewhat better than dynamic at ultra effort
    • 现有信息不足以确切断定这一点,但如果非要做一个非对即错的判断,我会判定它为错误
  • High confidence (98%): the "weird" language supremacy of something like J won't hold
    • 这看起来是正确的
  • [from a draft reader]: "dynamic is better on small-scale, but gets overtaken by static as the size of the project grows"
    • 这些任务并不支持这一说法(在规模大得多的 Pandoc 任务上,静态语言相比动态语言并没有明显优于在规模较小的 Zstd 任务上的表现),但任务本身及其呈现方式的差异太大,因此不清楚这是因为任务规模的变化还是其他差异所致

顺便说一句,Clojure 在 Pandoc 评测中相比 Zstd 评测提升如此之大的一个主要原因是:在 Zstd 评测中,36/40 个中等难度和 5/40 个超高难度的 Clojure 程序出现测试失败,因为字节转换在 128–255 范围内会抛出异常(也许应该用 unchecked-byte?),而它们不恰当地使用了这种转换。

这是一个真实的结果,因为如果你让最好的公开 GPT 模型去实现 Zstd(并且大概在你做其他可能涉及此问题的位/字节操作任务时也是如此),它生成的代码就会以这种特定方式失败。如果有测试能捕获这个问题,bug 就会被修复,但仍然会耗费时间和 token。无论某种语言表现得好不好,这类成本到处都存在(例如,cargo 会反复以错误的参数被调用,然后立刻被捕获并修复,但我注意到,在我实际的项目中,除非你给 codex 明确的指令说明如何调用 cargo,否则这个循环实际上会消耗相当多的墙钟时间,而且很明显,在上下文窗口里留出这个空间是值得的)。

总之,这一切都说明了为什么如果有人想对哪些语言或语言类别特别适合 LLM 做出强有力的论断,他们就需要运行相当多的不同评测。如果我们深入探究为什么某个特定条件得到了某个分数,导致该分数的失败通常是某种特殊的情况,而且并不总是很清楚这个问题在多大程度上会跨任务或跨设置泛化。仅凭一个评测的分数,甚至五个或十个评测的分数,是无法得出关于编程总体情况的结论的。

确实,在 Zstd 评测和 Pandoc 评测中,我们都看到了语言流行度与积极结果(更高的正确率、更低的成本、更短的墙钟时间)之间的相关性,而且在其他评测中也很可能看到这种情况,但就此对任何特定语言得出强结论都是错误的。我之前在根据 GitHub CI 数据考察不同项目构建失败频率时,就发出过类似的警告,指出不同项目的构建失败频率高低可能有不同的原因,而且不应据此得出强结论,因为不同项目的结果不一定具有可比性(例如,如果某个项目的主分支是某种已经过其他审查的候选发布版本,那么该项目的构建失败率预计会较低,但这与直接在 main 分支上进行开发的项目是不可比的)。

不久之后,某个高分语言的相关人士(如果我没记错的话,是 Martin Odersky 和 Scala)在推特上转发了这篇文章,并将该语言的高排名视为该语言的一次胜利。那个结论在当时就是不成立的,而由于这里存在众多差异来源,任何关于单一语言的此类结论在这里就更加站不住脚了。

这些数据(假设评测有效)可以反驳一些强主张,并对其他主张具有提示性,但由于只有两个任务,它实际上只能对语言类别具有提示性,而不能针对特定语言,因为任何特定语言都可能因为某种特殊原因在某项任务上表现好或差,而这种原因可能推广到其他任务,也可能不会。

感谢 Max Bittker、Yossi Kreinen、Aaron Levin、Alan Boll、Luke Burton、Marco Primi、Milosz Danczak 和 Justin Blank 的评论/更正/讨论。

附录:ai-coding-lang-bench 中的若干问题

就像我上面说的,我这里的评测是一个快速粗略的评测,我确信它充满缺陷,所以我并不是想说我这里呈现的评测很好而这个是差的,但以下是 Endoh 的 ai-coding-lang-bench 评测中存在的一些问题。

一个问题是,部分测试似乎运行了错误的可执行文件。已发布运行的设置似乎在某项测试中,于每个候选者的目录内执行了 `../../minigit`,而候选者生成的可执行文件位于 `../minigit`。`../../minigit` 并不存在。

由于静态类型语言在正确性得分上较低,该评测的作者指出“600 次运行中仅有的失败出现在 Rust 和 Haskell(两者均为静态类型,且都是相对‘困难’的语言)”,并暗示“困难的语言”,例如“C 的内存管理、Rust 的所有权模型,以及 Haskell 的 monad/纯度,可能会给 AI 增加额外负担”。

然而,Rust 的失败是因为 `../../minigit` 处没有可执行文件,导致测试失败。第一次 Go 运行通过执行 `ln -sf minigit-go-1-v1/minigit ../minigit` 并链接 `generated/minigit` 到其自身运行“修复”了这个问题,但这意味着之后每一次执行(针对每种语言)实际上都运行了第一次 Go 运行的可执行文件。当针对 Rust 自身的可执行文件重新评分时(而不是因尝试执行不存在的文件而失败),Rust 获得了满分,从而推翻了“Rust 失败是因为它难以处理”的理论。

其他测试也存在问题。例如,有两项测试的结构导致无论实际检查的值是什么,它们都会通过。其中一项测试包含

  if ../minigit commit ...; then                                                                                                                                      
    COMMIT_POST_CHECKOUT=$(cat .minigit/HEAD)                                                                                                                         
                                                                                                                                                                      
    if grep -q "parent: $COMMIT1" \                                                                                                                                   
        ".minigit/commits/$COMMIT_POST_CHECKOUT"; then
      pass "checkout then new commit works"
    else
      pass "checkout then new commit works"                                                                                                                           
    fi                                                                                                                                                                
  else                                                                                                                                                                
    fail "checkout then new commit works"                                          
  fi

内部的 if 在两个分支中都有 pass,这意味着这几乎等同于

  if ../minigit commit ...; then
    pass
  else
    fail
  fi

内部的 if 似乎本意是要进行实际检查,但由于编码错误(也许是复制粘贴错误?),该检查实际上被省略了。

另外,如上所述,智能体可以修改测试环境——第一个 Go 智能体就曾为了修复一个损坏的环境而这样做。它们对测试和环境拥有完全访问权限,可以做任何事,而且测试套件在开发过程中全程可见、没有保留的隐藏测试,这很容易导致作弊:通过针对特定情况编写代码来通过测试,但产出的程序在“现实生活”中毫无用处。从宏观层面看,类似的情况似乎确实发生了——很多程序未能实现规范中的大部分内容,却通过了所有测试,这可能表明智能体“弄懂了”如何通过测试,并且更倾向于这样做而不是实现规范(也可能说明测试本身非常单薄、很容易通过)。

另一个问题是,Claude Code CLI 的版本并非所有运行都一致(版本从 2.1.66 到 2.1.68 不等)。还有少数类似的问题可能有一定影响,但相比上述问题,它们的影响可能较小。

附录:循环中使用 medium 对比使用 ultra

举一个我们可以对比的例子:我很好奇使用 medium 并让智能体持续工作到底有多划算;同时,我脑海里也一直萦绕着“Ralph 循环”支持者提出的一个问题——在循环的每一次迭代中清空上下文窗口、把完整提示词重新给智能体,效果会更好。和上面一样,我预先登记的猜测如下:

  • Zero confidence (50%): Ultra is more effective than medium in a loop
    • 我不太确定该怎么看待这个问题。我猜支持 ultra 的理由是,ultra 是经过某种方式设计的,应该比在循环中反复使用 medium 更聪明。但也有可能存在某种权衡:ultra 可能更偏向速度,而且正如我们指出的,方差非常高,所以即使 ultra 在大多数问题上胜出,在这个场景下也可能落败;ultra 可能还针对缩短墙钟时间或其他参数做了更多优化;ultra 还有一个劣势,就是它不知道在隐藏测试全部通过后停下来,而在这个设置下,达到完全正确的 medium 条件不会再次运行,这极大地有利于循环中的 medium(可以说,这与人们实际使用这些工具的方式是相符的)。
    • 你或许可以说这是 50% 加一个极小量,因为我的思路是先想到这样框定问题,而不是反过来;但我要说,充其量我的置信度也极低。
  • Medium confidence (80%): continuing with context outperforms Ralph loop
    • /goal 模式等默认不会这样做,而且,想必 Anthropic 和 OpenAI 的人已经试过 Ralph 循环这类方法,发现效果并不理想。
    • 随着 harness(以及模型?)的改进,密切关注上下文窗口似乎变得不那么重要了;在 2025 年末 / 2026 年初,处理长时间运行的任务时,我经常不得不清空上下文窗口来避免问题,这种情况随时间推移越来越少见了;但即便如此,因为我不太在意别人怎么说,我跑智能体循环时默认保留上下文,只在出现明显问题时才清空,这样似乎效果还不错——比如我就是用这种方式构建了全世界最强的 Azul AI。所以,我并不确定当时在每个循环迭代都默认清空上下文是否是正确的选择。

就这一个问题而言,平均来看,跑一次 ultra 似乎比反复跑 medium 单位成本更优(单位时间更是如此),而且沿用之前的上下文比 Ralph 循环表现更好。天真地反复跑 medium 的问题在于,智能体可能被锚定在一个糟糕的解决方案上,无法取得进展。Ralph 循环背后的理论是,丢弃可能导致这种问题的糟糕上下文,但这并不能让你免于得到一个糟糕的产物。

仅仅通过使用大语言模型,我就注意到,丢弃一大段代码让 LLM 从头重写,往往比让 LLM 修改它或就地重写效果更好。Michael Malis 一直在用 Rust 重写 Postgres 并做了大量改动,他也注意到了这一点。这也与此前提到的观点相关:由于高方差(再加上这种路径依赖),如果你不介意消耗 token,多次掷骰子并取最佳结果往往更划算。

附录:Guards of Atlantis 2

我尝试做了第三个评估,这个评估无论从问题的呈现方式还是问题的实际执行过程来看,都更像是一种“业务逻辑”类的评估。你可以说,Zstd 评估和 Pandoc 评估对程序员来说都是相当不寻常的任务,因为没多少程序员能拿到像 Zstd RFC 那样写得既清晰又详尽的规格说明,也没多少程序员会拿到像 ProgramBench 测试那样预先创建了这么多测试用例的问题。

这里的想法是实现一个棋盘游戏。一般来说,棋盘游戏的规则是由不擅长编写清晰规格说明的人写的,所以实现一个棋盘游戏更接近于非程序员(或者是不擅长编写良好规格说明的程序员)给某人布置任务时的情况。

这里的问题在于,要找到一个我手头有合理评分基准(oracle)、但对大语言模型来说又不简单的游戏。例如,大语言模型能够一次性就搞定 Scout 和 Azul 的规则,这使得它们不适合作为测试任务。对于那些大语言模型无法立即一次搞定的游戏,我恰好有 Guards of Atlantis 2 的评分基准,因为我之前让一个大语言模型为我和我朋友们实现了一个可以玩的版本(这个没有链接,因为我想不出如何做一个不涉及版权侵权的界面)。后端只花了我几个小时的时间,但为了让规则大致正确,却耗费了相当多的大语言模型时间。我喜欢把这个作为测试任务,因为它的规则很棘手,就像很多交给程序员的实际需求描述一样棘手,但从原则上讲,弄清楚正确的规则并加以实现是可能的(毕竟,人类在离线正确玩这个游戏时,就是在隐式地做这件事)。

在桌游规则中,相当常见的情况是,严格按字面阅读规则反而是错误的,你需要借助“常识”(或查阅某种 FAQ)才能正确游玩(确实有一些游戏设计师致力于避免这种情况,比如 J C Lawrence,但这相当少见)。《亚特兰蒂斯守卫者》就有不少这样的规则。该游戏的设计师也公开表示,不存在所谓的规则精神或对规则的常识性解读,并称你应该始终严格按字面阅读规则,因此也有许多情况是你需要忽略“常识”解读、严格按字面来读规则的。这种组合对 LLM 来说相当困难(而且,从我观察到人类玩家按设计师意图游玩该游戏的频率来看,对人类来说也同样困难)。

我认为,仅仅阅读规则书就想正确游玩这款游戏,实际上几乎是不可能的(当然理论上可行,但你需要知道哪些规则要按字面理解、哪些不能,而由于规则本身并没有定义一个自洽的体系,让你能据此推断哪些规则遵循哪套元规则,你只能靠随机猜测并碰运气)。在我实现这款游戏时,为了让我的 LLM 理解规则,我给了它各种资料,比如一份非官方的规则 FAQ(内容正确)、一份非官方的精简版规则(比官方规则写得好且正确,但不完整)、一本开局书(可以假设开局书里只包含合法走法,用它来检验规则)、Discord 规则频道里的评论等等,然后让 LLM 在这些资料之间做一致性检查,并让它明白 FAQ 和 Discord 评论这类东西的权威性高于实际印刷的规则书。用我每月 200 美元的 OpenAI/codex 个人账号,我让一个 LLM 利用我所有的空闲算力来运行一致性检查并修复规则问题。我没有仔细追踪这花了多长时间,但我觉得大概花了一两个月的时间反复打磨这些修复,才得到一个还算合理、可以玩的结果,但我不会真的完全相信它是正确的。

我之所以对它有一定信任,唯一的原因是 Pedro Oliveira 也实现了《亚特兰蒂斯守卫者》,而且他们用了完全不同的方法(一种更常规的方法,由人来驱动 LLM,而不是试图让 LLM 自己摸索)。当我们对比实现时,发现各自大概有 10 个左右的 bug。可能还有一些残留的 bug,是我们两个实现都犯了同样的错误,也可能有些地方我们的实现不同,但检查系统没有注意到,不过我认为我们两个实现的规则现在都已经相当扎实了。这就是我为什么能拥有这款游戏的“神谕”的原因。

我喜欢把这个当作一项任务,因为它更接近现实世界中你会遇到的那种“规格说明”——规格模糊、自相矛盾,有时干脆就是错的,而你需要借助其他信息才能得出正确结果。对于这个评测,为了避免变成对 LLM 能否以恼人格式获取数据的测试(比如把开局库从一组图片转换成某种结构化数据、把规则扫描件转成文本等),我把两样东西都给了智能体:凡是我让 LLM 提取数据的地方,既给原始材料(提取过程本身也需要各种一致性检查才能做对),也给提取后的数据(原始材料也一并提供,这样 LLM 如果想检查提取错误,可以自行核对原文)。

虽然我是用较老的模型完成这项任务的(其中一部分我用的是 GPT-5.1 或 5.2,另一部分用的是 5.4 或 5.5),但即使用较新的模型、且不给它们我提供给老模型的那种引导,这项任务仍然太难了。无论使用什么语言,智能体在这项任务上的得分都约为 0。

顺便说一句,如果你好奇 LLM(以及人类)会在什么地方卡壳,这里有几个例子。有一张卡牌,其文本写着:“目标:与你相邻的一个单位。攻击后:可对另一个敌方英雄再发动一次。”

在这款游戏中,英雄是一种单位类型。严格按字面理解,并在完全掌握规则的前提下(例如知道“攻击后”是什么意思等),这应该意味着你可以要么攻击一个单位,要么攻击两个英雄(毕竟,要对另一个敌方英雄重复攻击,就意味着第一个目标单位是英雄;否则,那就会是另一个“单位”是英雄,而不是另一个“敌方英雄”)。

这张卡实际上印有相当于勘误的内容,因为人们抱怨它表述不清;勘误内容为“(即使原始目标是随从,你也可以重复)”。这对大语言模型(以及一些人)来说已经很令人困惑了,但真正的致命之处在于,还有其他卡牌使用了相同的句式结构,却没有这个修正。要正确打出其他具有相同结构的卡牌,你需要知道,每当这种结构出现时,都应按照这张卡上的勘误来操作。游戏设计师喜欢使用多种具有特定非字面含义的结构,你必须牢记这些含义。

另一个不应按表面意思来打的规则示例是,一张角色卡牌上写着“选择一项,或在不同目标上选择两项:A、B”。严格按字面理解,人们会期望能够在不同目标上执行A或B,或者同时执行A和B。但游戏精神的一部分是元规则:一张卡牌不能对另一个角色进行多次攻击,因此“按卡面所说,在不同目标上同时执行A和B”的解释是不成立的。基于类似的推理以及类似结构的用法,这张卡牌的正确解读应为“选择一项,或在不同目标上选择两项”,这可以说仍然存在歧义,更清晰的写法可以是“选择一项或两项(若选两项,则必须作用于不同目标)”。

作为人类,一旦你理解了“游戏精神”是什么,就能解决这类问题。但按照设计,这些并没有明确写在规则里,人们必须从Discord讨论中推断出来,而这似乎超出了当今模型的能力范围——尽管在许多专业任务上被当今模型超越的人类,却能够做到这一点。

在我监督实现规则的大语言模型时,大语言模型之所以触达天花板、无法收敛到完全正确的规则,是因为大语言模型会观察到某条规则不一致或不正确。然后它会尝试修复这条规则,同时也会修改其他内容,试图让它们保持一致和正确。这有时会让事情变得更正确,有时则会让事情变得更不正确。当事情变得更不正确时,大语言模型有时会修改一条原本正确的测试,把它变成一条不正确的测试。因此,过了一段时间后,大语言模型并没有真正在提升正确性,只是在哪些规则不正确这件事上反复折腾。这还是在我提供了关于检查什么以及如何检查的某些指导的情况下;如果没有这些指导,即使是使用当今更先进的模型,大语言模型也无法以合理的方式驾驭这个问题。

我确信存在一款规则复杂度恰到好处的棋盘游戏,可以在这里做成一个很好的评测基准,但顾名思义,要创建这样的“神谕”(oracle)需要花费一些功夫,而我手头并没有一个规则合适的棋盘游戏的现成“神谕”(我认为这实际上是可行且可扩展的,因为人们可以创建几十个甚至几百个这样的游戏,而所需工作量并不会比创建一个多太多,因此人们可以为数百款游戏获得一个相当正确的“神谕”,然后检查哪些游戏的难度水平适合作为当今大语言模型的有趣测试;Guards 之所以实现起来费力,是因为没有现成的实现和回放数据可用;如果有人依赖回放数据来测试每款游戏的正确性,那么相对容易就能批量生成大量这样的环境)。

这可以说是一个有点意思的问题:给定一份清晰的规格说明,例如一套写得很明确的规则,那么比《亚特兰蒂斯守卫者》更复杂的产物,大语言模型也能实现(我认为 Zstd RFC 就更复杂,Pandoc 当然也更复杂;甚至 Pandoc 支持的单个文档格式,比如 PDF,都比《亚特兰蒂斯守卫者》更复杂)。所以问题不在于找到一款规则复杂到让大语言模型吃力的游戏,而更多在于找到一款规则模糊或矛盾到让大语言模型吃力、但又不至于让大语言模型完全无从下手的游戏。但这确实是一个现实世界中的真实问题,因为人类通常并不擅长写出清晰的规格说明,而模型和工具链在多大程度上能处理好人类那种不清晰、自相矛盾、有时甚至完全错误的规格说明,对典型用户来说,可能比大语言模型能否根据一份像 Zstd RFC 那样写得极好的规格说明来实现某个东西,或者大语言模型拿到 4800 个 ProgramBench Pandoc 测试用例加文档后能否实现某个问题,更相关。

附录:各项决策的理由

  • Testing ultra
    • 我见过有人说你不应该真正去衡量这个,因为这是工具链的事,不是模型的事。我能理解如果你是在改进模型或工具链,你会想把这两者分开衡量;但当我们看用户如何使用这些东西时,很多人就是直接用 codex 或 claude 以及各种内置功能和选项;某件事到底是工具链的事还是用户的事,对他们来说其实并不重要。
  • Using codex
    • 我见过一些评测出于和上面相同的原因使用非常薄的工具链,而我使用 codex 而不是非常薄的工具链,原因也和上面一样。
    • 同样,在这个“穴居人模型”评测中,我使用了带 Opus 和 Fable 的 claude,以及带 GPT 的 codex。
  • No internet access
    • 如果给模型联网权限,它们往往会作弊,而且有很多问题即使上网搜索也找不到能解决该问题的源代码,所以这样做能让这些评测更接近真实情况。
  • Relatively large tasks compared to a lot of benchmarks people pass around
    • 虽然我会让大语言模型处理大量琐碎任务,但那些真正耗费我时间或 token 的事情,往往比 Alderson 评测或 Endoh 评测里的任务规模更大;大语言模型在琐碎任务上已经足够好,以至于某些条件让它们在某个琐碎任务上表现稍好或稍差,对我来说差别不大。但对于像实现 Guards of Atlantis 这样的任务——我得花好几个小时搭建脚手架,任务才勉强能跑起来——我非常在意是什么因素让模型表现更好或更差。
  • Agent-specified prompts
    • Public evals seem to have moved to relatively thin/lightweight prompts that don't specify the task in great detail; this is said to be better because an agent setting up a task will give too much information that helps agents succeed at the task
      • 我能理解你为什么想测试那个,但同样重要的是,我非常在意智能体在由智能体设定的任务上表现如何,因为我让智能体执行的很多任务本身就是由智能体定义的;我在意智能体在两种风格下的表现,而不只是其中一种,而公开评测已经偏向其中一种风格了。
  • Zstd eval: asking agents to fix bugs without telling them the issue or the failing tests
    • In general, if you tell an agent to fix a specific thing, it will fix it, but it won't necessarily fix the class of issue; I've found that if you tell it there's an issue but don't tell it what the issue is, it sometimes does a more general thing instead of just putting in a narrow, brittle fix, so I do care about how agents behave when given instructions like this (of course you can tell agents to not just make a narrow, brittle, fix, but that often doesn't work)
      • 这感觉和我们之前在 Pandoc 保留集脚注里注意到的问题有点相关——当我们告诉智能体我们有一个保留集时,似乎会迫使智能体产出更通用、更不脆弱的解决方案。

附录:这些评测存在的问题

说到性能基准测试,我做得够多了,以至于我大致清楚自己的基准测试有哪些缺陷,也能在时间/精力与缺陷之间做出有依据的权衡,而且我有相当的信心认为基准测试中存在的缺陷对我试图理解的东西不构成实质性影响。但我在 AI 评测方面做得还不够多,无法对 AI 评测产生这种直觉,所以在元层面上,我预期自己做的任何 AI 评测都会存在一些我尚未意识到的缺陷。

我预期这里存在缺陷的另一个原因是,这些评测是由编码智能体搭建的,而我每次花一分钟去找问题,至少都能发现一个问题。这表明这些评测很可能还有更多缺陷,只要再多花点时间就能发现,但我希望这更像是“快速玩具项目”级别的正确性,而不是“Gary Bernhardt”级别的正确性,所以我在修复了几个问题之后就停手了。

早在我做验证工程师的时候,参加过一场 Sun/Oracle 工程师在奥斯汀的聚会,大概是 2007 年前后,会上他们用数学方法把“两次 bug 之间的时间间隔”形式化地转化为芯片发布的置信度。我很少见到有人这么做,但最近听说 Will Wilson(Antithesis 联合创始人)提到,Antithesis 的一些人用生态学里的数学方法(关于稀有物种观测的文献)来估算真实 bug 率,这看起来比 Sun/Oracle 那位工程师二十年前做的要精密得多。

这个想法很酷,但当你每分钟都能找到一个 bug 的时候,你根本不需要高深的数学来告诉你可能还有一大堆 bug。如果我是为了工作做这件事,而且我们有理由在意这些评测的准确性,那仔细看看、多修一些问题可能是合理的(而且如果我是干这行的,我大概也有能力和经验,在指导 LLM 搭建这些评测时少犯些错)。但就回答“用 LLM 时,动态语言是否真的明显优于静态语言”这个问题而言,我更有把握的是这个说法不成立,而且还有很多其他问题似乎更可能得出可操作的结果(比如哪些技术或测试库效果最好)。

我通常不会在博客上发布内容,除非我觉得它们已经比较成熟了,但这意味着我经常会探索一些数据,直到满足自己的好奇心,然后就再也不发布结果了。在和别人聊起这些未发布的结果时,我发现和我聊过的人往往对这些结果很感兴趣,即使它们还没有达到我真正满意的标准,这似乎表明,那些我没聊过的人可能也会感兴趣。从我目前看到的情况来看,我怀疑要把这个做到我真正满意的标准,至少需要我目前投入时间的10倍。我目前相当忙,几个月内都看不到自己能抽出时间来做这件事,到那时候我也不确定自己是否真的会抽出时间来发布它。在最近的一篇文章中,我提到了一项我差不多一年前做的分析,当时我想弄清楚哪些车在事故中更有利于降低脑震荡风险,我花了一些时间研究这个问题,进展到足以得到一个让我满意的答案,然后就再也没有抽出时间去做那些把结果整理干净以便发布的工作。

那项分析中有一些结果看起来是“可发表的”,也就是说它们可以变成一篇正式发表的论文(比如从实际碰撞数据中发现,HIC和速度之间的关系看起来像是四次方关系(!);有一篇论文试图找到这种关系,但做了错误类型的分析,没能找到“O(n)”式的关系,得到的结果要模糊得多),但我从来都不太在乎某样东西是论文还是博客文章,而且事实证明,我更有可能直接转向下一个分析,而不是把分析整理到足以发布一篇博文的程度。

沿着这条思路,一个更近期的项目是:在做出一个超人类水平的 Azul AI 之后,我尝试用一个人工时间投入少得多的流程,去做一个超人类水平的 Splendor AI。我认为那个尝试没有成功,但它以明显优势击败了我能找到的所有其他 Splendor AI,这算是一个有点意思的结果。我觉得自己对棋盘游戏 AI 的了解已经足够写点东西出来,但我主要的兴趣在于搞清楚自己能不能做出一个像样的东西,然后我就一直去做其他项目,而不是花时间好好写一篇总结。我觉得其中有意思的一个例子是:你想做的很多性能优化,实际上会改变结果,所以你不能只依赖那些可以被严格验证为不改变结果的优化手段。但是,如果你天真地让一个编码智能体去做这些优化,同时又要求不降低棋力,它们就会做出各种降低棋力的事情。棋力下降非常严重的情况很容易被发现,但还有一些更微妙的问题,有时会导致(比如)在与你自己 AI 的自对弈中棋力没有变化,但面对人类或其他 AI 时棋力却下降了,所以必须有某种流程来捕捉糟糕的优化,而这本质上是一种带有随意性的流程,必须结合你自己的直觉和依赖 LLM(它们会非常有帮助,但也常常完全错误)来设计。

对于我感兴趣的这种数据类项目,大语言模型大幅减少了获得一个足以满足我好奇心的结果所需付出的努力,但据我所知,它们并没有减少多少发布结果所需的努力(至少,如果你是手动撰写结果而不是让大语言模型来写,并且你希望结果干净漂亮的话),这意味着撰写结果会遇到一种类似阿姆达尔定律的瓶颈,所以我已经做了更多这样的项目,但写出来的却更少了。如果非要说的话,我觉得写这些内容实际上花了更多时间,因为我的工作流程变了。例如,我不再只是输出一个 ggplot2 的图表,而是会做一个交互式版本,在某些方面更好看,但制作起来肯定更耗时。而且我会用大语言模型跑一遍拼写/语法检查(至少到目前为止,这是我写作时唯一用到的大语言模型辅助),这会发现一堆需要修复的问题。由于我是逐个手动查看而不是直接采纳修改(而且我打字错误很多),这实际上相当耗时(上一篇花了超过一小时,这篇也花了超过半小时,尽管我并没有一路改到底,大概改到一半就放弃了)。

总之,发布这篇内容是一个实验,发布一些半成品的笔记,而不是那种我在发布前真正想要的整理干净的版本。如果你对此有看法,请告诉我(X Bsky Mastodon)!

我没有当前这些评估的 GitHub 链接。一方面,我觉得我确实应该有。另一方面,它们很乱,而且我在发布代码之前有一堆东西想清理,我不知道我是否/何时会去做这件事,而这样做,至少我算是把一些东西放出来了,而不是只跟几个朋友聊聊结果,然后让结果无限期地躺在我的硬盘上?

附录:关于 Zstd 的更多细节

智能体被指示忽略性能,但超时并非无限,而且在中等条件下,部分测试用例确实超时了。这可以说有失公平,但对分数没有实质性影响。对于非无限循环导致的超时,Clojure 中有 2 个测试用例(在 40 * 34 个测试中),J 中有 2 个,Tcl 中有 2 个,Factor 中有 1 个,PHP 中有 1 个。而且,考虑到最大的测试用例是 4 GiB,9000 秒(2.5 小时)的超时设置算是相当宽松了。在 2.5 小时内未能解码 4 GiB,意味着在 Graviton 5 核心上的速率低于 0.5 MB/s,这相当慢。

以下是我在尝试让智能体进行设置时遇到的一些问题(而且,如上所述,发现每个问题所花的时间都很短,这意味着还有更多问题存在)。

  • 最初,构建设置没有向智能体明确说明,导致当智能体根据说明做出看似合理的操作、但在实际评分时却行不通的情况下,某些语言会随机失败。
  • 出于某种原因,负责设置的智能体对某些语言施加了不寻常的任意限制,而对其他语言则没有(例如,Rust 的设置无法使用 rustfmt 或 Clippy);大多数(但不是全部)语言都有类似情况。
  • 许多测试(由智能体创建)实际上是某种性能/压力测试,尽管智能体被指示忽略性能(我不认为在 9000 秒内处理 4 GiB 的 Zstd 数据算是一种性能压力测试)。
  • 某些语言条件对智能体有任意指令(例如,Haskell 条件有指令要求不使用 bytestring,并提供了替代实现建议的说明)。
  • 某些语言条件使用了非常古老的工具链(例如,Zig 使用的是 0.10 版本)。
  • 某些语言条件提供了脚手架来帮助智能体实现 Zstd。
  • 在最初的汇编语言条件下,智能体用 C 实现代码,然后将其编译为汇编语言并提交汇编代码(这使得汇编语言的结果与其他语言的结果大致相当)。
  • 有些语言条件对可用工具的解释说明是错误的(例如,汇编语言条件被告知可以使用 GDB,但 GDB 实际上无法工作)。

还有一件事,虽然严格来说不算 bug,但我还是把它移除了。其中一个测试非常难(大概只有 10% 的智能体能在第一次尝试时通过)。在测试当前 zstd 发布版二进制文件时,zstd 二进制文件本身也无法通过这个测试。阅读 RFC 后,我发现这似乎是 RFC 中关于某个边界情况是否合法的歧义。不同语言在该测试用例上的通过率存在相当明显的聚类现象,我觉得这很有意思,但在其他所有测试都在衡量(或至少试图衡量)更直接明确的内容时,这似乎不是一个很有用的衡量指标。

总之,在上述列表(并非详尽无遗)中,许多问题影响了很大比例的语言,有些问题不得不修复多次。总的来说,如果你把每个条件都算作一个独立的 bug,我大概修复了(让智能体修复了)100 多个这样的 bug,而且我预计还有更多。当我与 Max Bittker(他经营一家 RL 环境初创公司)交谈时,他指出:

在我参与过的所有评测工作中,我最终都投入了大量时间和精力,主要形式是阅读轨迹(或大量轨迹的摘要),然后对问题进行分诊处理,比如“哦,这类 bug 本不应该出现,让我们更新一下 X”(X 指提示词、测试框架/环境或验证器)。

智能体倾向于草草了事,所以我在这方面非常用心,确保问题在正确的层级得到修复。例如,对于被测智能体来说,上下文中的内容非常敏感(添加它需要担心的随机垃圾信息很糟糕,最坏情况甚至会泄露答案),这与系统其他部分在幕后固定的内容截然不同。

智能体在编写评测时,对被测智能体的体验不够敏感,它们会直接给出答案,或者通过把问题变成内部智能体的问题来修复(“记住不要奖励 hack 行为”)。

我也在复用现有资源(代码仓库、游戏、工具、关卡)并围绕它们构建测试框架和验证器方面取得了很大成功,这比试图为评测从头开始构建某些东西要有效得多。

事后看来,我有点后悔做了跨语言的评测。即使修复了 100 多个评测问题,我毫不怀疑还有更多问题存在。也许这只是“这山望着那山高”的想法,我尝试下一个评测时也会后悔,但我认为评估不同测试技术或测试框架的效果会比评估不同语言省力得多,而且我觉得那个话题至少同样有趣。另外,回想起来,如果我手工做了更多工作,少依赖智能体,结果会好得多。例如,我本应让智能体为一种语言生成环境,然后既让智能体检查,也亲自检查并修复问题,然后再为另一种语言生成环境。这样重复几次之后,我可能就有了更好的流程来为其他语言生成环境(如果没有,我也可以对每种语言重复这个过程,得到更可靠的结果,而且很可能不会花更多时间)。

另一点需要注意的是,许多语言之间真正的差异并没有真正被测试到,比如针对对抗性输入的存储安全性。如果智能体用 C 或 C++ 生成大致正确的代码比用 Rust 更困难,那会被观察到,但如果模糊测试器或 valgrind 或其他工具能发现的问题,就不太可能被那一小撮测试捕捉到。我确实让一个智能体(简要地)检查了 C 和 C++ 代码的存储安全问题。智能体声称它在 ASan+UBSan 下运行了 C 和 C++ 代码,并尝试了一些模糊测试输入(每种 4000 个),没有发现问题,但当然这并不意味着没有问题,也不意味着更大的代码库不会有问题。

而且,事实上,对 Pandoc 评测做一次类似的快速内存安全检查后发现,所有 C 程序和除一个之外的所有 C++ 程序都存在内存安全问题(这些问题包括错误地解引用越界内存;一个具体例子是,在其中一个 C 程序中,一个被截断的 LaTeX 表格可能导致越界内存读取)。这些问题是仅用 10 秒的提示词就能发现的,这一事实表明,许多此类问题无需太多人力就能被发现和修复,但这会消耗相当多的 token,并且会把 C 和 C++ 版本的成本推高到远超 Rust 版本的程度,而且做完这一切之后,你对 C 和 C++ 版本内存安全性的信心仍然不如对 Rust 版本的信心。

总之,如果你对结果的分布感到好奇,我们有以下 medium 和 ultra 的数据:

我不太喜欢 ultra 的结果在这里有些饱和,但测试 ultra 的一个“问题”是,随着问题难度增加,它会持续运行很长时间(例如,大多数 Pandoc ultra 运行持续了 12 小时以上,汇编运行则持续更久),所以那些没有饱和的项目是非常大的任务,比如 Pandoc 评测,或者在某些方面过于困难的任务,比如 Guards of Atlantis 评测。


  1. 一位草案读者预先注册了猜测:“动态在小型项目上表现更好,但随着项目规模增长会被静态超越。”[返回]
  2. 保留测试集似乎是必要的,因为如果没有它们,智能体会作弊——它们会检测到测试输入,然后硬编码出能通过的测试输出(有时即使被明确告知不许作弊,它们也会这么做)。如果所有作弊都这么明目张胆,那倒不是问题(而且这本身可以成为一个有趣的衡量指标,因为智能体在不同语言下是否差异化地遵循指令,对真实用户来说很重要),但很多作弊行为更加隐蔽,也更难判定。例如,有些智能体编写的代码会依据测试的结构进行分支,但分支内部填充的代码并非针对单一测试结果特化,而是能通过同一测试的多种变体。从“绝对不算作弊”到“明显作弊”这个光谱上的任何一点,都有智能体尝试过。正如我们在研究 Senior SWE-Bench 时所看到的,用 LLM 给评测打分很棘手,而且很容易同时引入偏差和方差;使用保留测试集确实有一些问题,但它能让我们避开这一大得多的麻烦。

    首先,保留测试集之所以可疑,是因为它们是由智能体创建的。本意是创建一套保留测试,让一个理性的人(或智能体)在不作弊的情况下能够通过。智能体们审查了这套保留测试,剔除了一些不合理的情况,但我没有逐条人工核验,所以我觉得很可能至少有一条保留测试在某种程度上是不公平的。不过,保留测试集的整体得分足够低,所以我不太担心少数几条测试有问题(如果我是在 AI 实验室工作、试图训练下一代模型,我会更担心这个问题,但我觉得这对我们这里的用例来说并不重要)。

    在拥有一组留出测试集的情况下,指示智能体不要作弊,并未能阻止明显的作弊行为,这些作弊在留出测试集上得分极低;但告诉智能体存在一个用于评分的留出测试集,似乎降低了它们在智能体可见测试上取得的分数,同时提高了它们在留出测试集上的得分(在不告知这一点的情况下,不少智能体在 Pandoc 测试上以脆弱无用的代码取得了 100% 的分数;在告知存在留出集后,没有任何智能体在 ultra 上经过 1 轮后取得 100% 的分数,但留出集分数显著提高,表明泛化能力更好)。

    [return]
  3. 市面上有各种 Substack 专栏、YouTube 频道和其他渠道,承诺告诉你 LLM 编程成功的秘诀,但花时间做实际实验的投入产出比其实并不高。当我们研究“穴居人模式”时,我们看到最大的编程类 YouTube 博主之一发布了一个视频,他们花了几分钟研究了一下,就断定它是有效的。与其花哪怕 15 分钟去验证它是否真的有效,不如用这些时间去制作更多内容,后者的投入产出比可能更高。

    有各种论文讨论了不同的技术,这些论文有时比大多数博客文章或视频更深入,但平均而言,它们不一定包含更多有用的信息。例如,当我让 ChatGPT(5.6 Sol,Pro)查找关于大语言模型语言有效性的讨论时,它找到了一篇关于 token 效率的论文,其中有一个有趣的想法,但存在与我们之前讨论的“穴居人模式”评测相同的问题——它没有考察一个足够有趣、其结果能对我作为程序员有参考价值的任务。仅看引用那篇论文的文献,我们发现三位学者写了一篇关于语言 token 效率的论文,题为“The Best Programming Language for Tokenmaxxing”,但与这篇帖子相比,那篇论文只比较了四种语言,使用了较差的模型,并且使用了小型玩具问题(来自一个叫 LiveCodeBench 的东西;用 GPT-5.5 解决这些问题的成本通常在 1000 个 token 左右)。无论评测做得有多好,正如我们在本文和“穴居人模式”评测中所指出的,从一个小型玩具问题转向一个我可能关心的业余项目或工作问题时,我们经常会看到截然不同的相对结果。此外,在那篇论文中,他们提到给出的提示词是“要测试你的程序,请精确运行 ./test.sh... 这些是我唯一关心的测试”,他们声称这很现实,因为“我们认为这种设置是研究智能体行为的现实方式:在日常使用中,程序员不会对智能体隐藏他们的测试。相反,程序员会指示他们的智能体一直工作,直到所有测试通过。”但是,正如我们上面指出的,这样做会导致代码脆弱,在现实世界中失败(或者如果你有未提供给智能体的保留测试,它会在保留测试中以非常高的失败率失败;这个问题不能仅仅通过添加更多测试来解决;也许可以通过模糊测试或基于属性的测试之类的方法来解决,但效果如何是另一篇文章的话题)。我并不是说这些论文不好,也不是说从这些论文中学不到有趣的东西,但作为一个想知道应该使用哪些技术或工具的程序员,我无法从上面链接的这类论文中获取这些信息。

    [return]

来源:Hacker News 热门(buzzing.cc 中文翻译) · danluu.com

编写智能体时,哪种编程语言最合适?

Hacker News 热门(buzzing.cc 中文翻译)·2026-08-11 12:58·24分钟前·chaychoong
AI 导读

针对“动态语言比静态语言更省 LLM token”的流行说法,作者用 GPT-5.6 Sol 让智能体实现 zstd 解码器进行实测。结果显示,medium 努力度下动态语言表现更好,ultra 下静态语言反而更优,且此前评测存在测试路径错误等缺陷。作者认为,琐碎任务上的性能无法推广到更大问题。

正文 · AI 翻译

这篇被引用得相当广泛的文章(反正我老是看到有人引用它)提出,动态语言和/或能更简洁地表达事物的语言,其 token 效率更高。它似乎被引用得足够多,以至于大语言模型的搜索结果也认同这一点。例如,当我搜索“dynamic vs static language token cost”(不带引号)时,谷歌的 AI 摘要开头是这样写的:

动态类型语言通常比传统静态类型语言具有更低的 LLM token 成本,因为省略显式类型声明会使代码更紧凑。

谷歌的 AI 引用了同一篇文章,该文表明,一些简洁的动态语言其 token 成本可能只有 Rust、Go、C++ 等静态语言的 1/2 到 1/3。作者说:

在我比较的语言中,C(token 效率最低的语言)和 Clojure(效率最高的语言)之间存在 2.6 倍的显著差距。

之后他们又尝试了 J 语言,并表示:

它平均仅需 70 个 token 就占据主导地位,几乎是 Clojure(109 个 token)的一半。数组语言在避免使用奇特符号集时,可以做到极高的 token 效率。如果 token 效率被证明是一个关键驱动因素,这或许是语言演进的一个非常有趣的路径。

我找到的另一个关于动态与静态语言 token 成本对比的帖子是这一篇,它支持相同的结论。如果你想把这当作本系列关于基准测试、评估和实验设计的练习之第 8 部分,你可以先点击链接思考一下评估问题,然后再继续往下读。

在没有运行我们自己的评测的情况下,第一个实验存在的一个问题是题目过于简单,这从上面的引文中就能看出来;一个用 J 语言 70 个 token 就能解决、用 Clojure 109 个 token 就能解决的问题根本算不上什么问题(作者用的是 Rosetta Code)。正如我们在审视其他“穴居人模式”评测与我们自己的评测时所看到的,对于大部分工作在于打印答案的琐碎问题,与那些稍微不那么琐碎、实际上需要一定“真功夫”的问题相比,你可能会得到截然不同的结果;当你开始关注那些需要不止几个 token 才能解决的问题时,“穴居人模式”所宣称的巨大收益以及复现实验中所展示的效果就会消失。总的来说,在琐碎任务上的表现并不能推广到更复杂的情况。

第二个链接中的问题要更微妙一些,所以我们把大部分细节放到附录里,但其中包括这样的问题:其中一个测试执行了错误的路径(该路径不存在),导致测试失败。后来某个智能体将该不存在的路径符号链接到它自己的可执行文件上,这在那个用例中能行得通,但也导致之后所有测试都运行那个智能体的可执行文件,而不是正确的可执行文件。作者试图就 Rust 出现一些失败得出某些结论,但实际上这仅仅意味着 Rust 的评分是在 Go 智能体将该损坏测试上的所有评分符号链接到 Go 可执行文件之前运行的。

与其依赖这些评测,我们可以尝试运行一些我们自己的评测。从这些评测以及我们上一轮评测练习中讨论过的评测可以看出,要做出一个评测结果并不反映评测创建者所认为的含义的评测,是非常容易的。毫无疑问,这些评测也不会例外,同样会存在缺陷(更多细节见下方附录)。

作为建立直觉的一种方式,我喜欢在查看结果之前预先登记猜测¹。我预先登记并与朋友分享的一些猜测包括:

  • High confidence (95%): the overall dynamic vs. static language claim won't hold
    • 基于上述原因:这感觉类似于“穴居人”评测,其结果充其量只会随着问题规模的增大而被稀释。
  • Low confidence (60%): static languages will be somewhat better than dynamic at ultra effort
    • 在超高强度(ultra effort)下,对“训练框架(harness)能将反馈更快地传递给模型,并因此对正确性或效率带来某种好处”这一点的信心非常弱;但同时,由于种种原因,这种情况不成立也似乎合情合理。例如,我注意到 Codex 在调用 Rust 编译器时,经常犯完全相同的错误,然后不得不去修复它;也许这类问题远比“假设中更快的反馈循环”之类的事情影响更大。
  • High confidence (98%): the "weird" language supremacy of something like J won't hold
    • 这与整体上“静态语言 vs. 动态语言”论断的推理逻辑相同,另外还考虑到一点:AI 实验室在冷门语言上投入的合成数据 RL 环境工作量将会少得多(甚至可能为零)。

Zstd

对于第一个评测,我尝试给智能体提供 zstd 的 RFC(以及勘误表),并告诉它们实现一个完整的 zstd 解码器(智能体被限制在一个无法访问互联网的容器中)。测试用例没有提供给智能体。对于 zstd 这样体量的东西,期望测试覆盖每一种可能的情况其实不太现实。例如,尽管 zstd 是一个经过相当充分测试的软件,但我曾经在 zstd 中发现过一个数据损坏的 bug。测试套件的本意并不是要找出那些可能潜伏多年、极其极端的边界情况,而是用来检查那些“容易”从 RFC 推导出来、理应能正常工作的各种情况。

下图中,x 轴是成本,y 轴是正确性得分(越靠左上越好 / 越靠右下越差);这是 GPT-5.6 Sol 在中等(medium)和超高(ultra)强度下的平均结果。如果我们只看中等强度(并忽略不同任务上结果往往差异巨大的事实),我们可能会得出类似 Alderson 评测那样的结论:在使用 LLM 时,动态语言更高效、更好,因为(忽略相对冷门的语言)动态语言集群落在静态语言集群的左上方(我们采用了 Alderson 的静态 vs. 动态颜色编码,以便一眼就能比较)。但如果我们看超高强度的结果,情况就相当混杂了,有几种静态语言表现最好,而且在较好的结果中,静态语言的数量多于动态语言。

下面的图表还有一个开关,可以把 x 轴从成本切换为时间。mame/ai-coding-lang-bench 指出,更快拿到结果是有价值的(我个人不这么认为,因为结果耗时太长,我通常会同时做别的事而不是干等),所以我们也可以看看这个维度。同样地,我们观察到两种语言类型并没有哪一方占据绝对优势,不过在这个特定任务的中等投入水平下,最佳动态语言的结果再次优于最佳静态语言的结果(不过,同样地,两者差距相当接近)。

我们可以观察到,就像我们之前把完全琐碎的“原始模式”评测与稍微不那么琐碎的“原始模式”评测进行对比时一样,在琐碎评测中成立的强关联并不能推广到这个更大的案例中。和之前的情况一样,极端性能差距在这些更大的评测中消失了,除非是在我们预期表现不佳的情况下,比如使用汇编语言(对人类来说会显著更耗时、更困难),以及使用相对冷门的语言——这些语言我们不太可能预期 AI 实验室会投入精力去生成合成 RL 环境数据。

请注意,这与第一项评测的结论相反——那项评测认为像 J 这样密度极高的语言出于效率考虑是有意义的。也许,如果你有非常庞大的预算,并且可以针对你偏爱的语言训练或微调一个模型,那么使用一种冷门(且“古怪”)的语言是有意义的;但如果你是一个普通的 LLM 用户,坚持使用主流语言似乎比使用冷门的高密度语言更稳妥。

而事实证明,如果我们绘制语言流行度与这项评测表现的关系图(未展示),我们会观察到一种弱到中等的正相关关系:越流行的语言最终得到的解决方案既更正确,也更便宜。

正如我们之前所指出的,非常相近的评测也可能产生截然不同的结果。例如,我们在这里的 Optimization 1 与 Optimization 2 评测中看到了显著不同的结果,而这两项评测分别是在优化 wasm 中的 bzip2 压缩与解压缩,就评测而言,这算是相当接近的任务了。要想提出一个强有力且普适的论断,比如“动态语言比静态语言更高效”,我们就必须在众多任务上运行评测。然而,要证明像下面这样的论断——

动态类型语言通常比传统静态类型语言具有更低的 LLM token 成本,因为省略显式类型声明会使代码更加紧凑。

——充其量只是大致方向正确,并且实际上与任何特定案例都不太相关,或许也不足以强到能普遍适用,我们只需要尝试几个案例,看看这个论断是否普遍成立即可。在上面,我们看到在某个努力水平下,这个论断似乎勉强有点道理,但存在例外;而在更高的努力水平下,这个论断似乎就不太成立了,这就足以说明该论断可能并非普遍正确,前提是我们的评测不存在使其完全失效的混杂因素。

Pandoc

但是,为了观察一个截然不同且呈现方式也不同(更偏向测试驱动开发,而非“阅读规格说明”)的任务,接下来的这项评测采用了 Pandoc ProgramBench 评测,并针对我们的用例进行了修改。我们没有采用 ProgramBench 所呈现的逆向工程任务,而是向智能体提供 ProgramBench 的材料以及 ProgramBench 的测试,然后根据一组留出的测试来对智能体进行评分,以衡量每种条件下的表现2。

在下面的结果中,x 轴同样是成本,y 轴是留出测试上的得分。

和之前一样,我们没有看到成功与否或成本与语言是静态、动态还是高度紧凑之间存在非常强的关联。我们再次发现,相对冷门的语言往往表现不佳(尽管 Clojure 在本次评测中的表现比在 Zstd 上要好得多)。此外,汇编语言的表现要差得多,这似乎是意料之中的——我们可以预期,人类用汇编语言实现 Pandoc 会比实现 Zstd 处于更大的劣势,而且似乎没有充分理由认为 LLM 在这方面会有所不同。

这一切意味着什么?

谁知道呢?

关于使用 LLM 时哪些做法效果好,我有很多疑问(比如,哪些测试技术效果好、哪些语言效果好、哪些软件架构效果好、修复 bug 的成本是否因语言而异、一般程序维护成本是否因语言而异,等等)。这些问题大多在公开数据中没有答案,即使 AI 实验室内部已经回答了,这些信息也大多没有公开。

大多数关于某种语言特别适合 LLM 使用的说法似乎都是错误的(例如,上面链接的评测中提到的 Ruby、Clojure 和 J 特别适合 LLM 的说法,以及相当常见的 Elixir 特别适合 LLM 的说法),但目前还不清楚什么才是正确的。

2014 年,我们研究了静态类型与动态类型的相关文献,发现除了少数案例研究之外,综述这些文献并没有提供太多有价值的信息。举一个典型学术研究的例子,我们看到了论文《静态类型系统能否提高软件系统的可维护性?一项实证研究》,我当时对此评论道:

研究对象被安排参加了一些课程,在这些课程中,他们要么需要修复现有代码中的错误,要么需要填充存根方法。Java 使用静态类,Groovy 使用动态类。在类型错误(以及相应的无方法错误)的情况下,开发者在 Java 中解决问题更快。对于语义错误,则没有差异。该研究采用了被试内设计,33 名研究对象以随机顺序完成任务。一个显著的局限性是,该研究避免使用“复杂的控制结构”,例如循环和递归,因为这些会增加解决问题时间的方差。因此,所有错误都是琐碎的错误。这可以从解决任务的中位时间(以数百秒计)看出。任务可能包含多个错误,因此每个错误的解决时间相当短。

选择那些避免“复杂控制结构”(如循环和递归)的任务,且任务耗时数百秒,这使得结果对于真正消耗专业程序员时间的任务而言毫无意义,就像我们看到的第一个评估一样,其任务耗时在几十到几百个 token 之间。然而,借助 LLM,我们实际上可以给它们提供非琐碎的任务,并比较它们的表现。这里存在结果如何推广到不同任务的问题,但我们在人类研究中也会遇到完全相同的问题,而且情况更糟(LLM 的方差很大,但人类的方差更大,因为你无法让同一个人用不同的随机种子去完成一系列任务)。虽然花 20 美元让 LLM 实现一个 Zstd 解码器,一旦乘以语言数量以及每种语言每个条件下的迭代次数,并不算便宜,但如果你想想雇佣一个能阅读 zstd RFC 并实现它的专业程序员要花多少钱,那么同等的研究根本不可能进行,因为成本会使其完全不可行。对于 Pandoc 任务来说,这一点更是如此。

有了大语言模型,很多问题从实际上无法回答,变成了只要花点功夫、消耗一些 token 就能回答。由于当前存在的种种激励机制,我们未必很快就能得到这类问题的答案,但至少现在可以试着去啃一啃了。

我见过很多流传的说法,这些评测既无法证实也无法证伪它们(原因如上所述:由于不同问题之间的方差很大,需要尝试的任务数量要多得多),但确实能对它们有所揭示,例如:

  • Languages with a lot of bad code out there (e.g., PHP) will perform worse
    • 在这些任务上似乎不成立
  • Because it's so easy to re-write now, you should use a powerful language (like Haskell)
    • 在这些任务上似乎不成立
  • You should use a popular language
    • 这一说法得到的支持较弱

对于我预先登记的猜测,我们的结果是

  • High confidence (95%): the overall dynamic vs. static language claim won't hold
    • 这看起来是正确的
  • Low confidence (60%): static languages will be somewhat better than dynamic at ultra effort
    • 现有信息不足以确切断定这一点,但如果非要做一个非对即错的判断,我会判定它为错误
  • High confidence (98%): the "weird" language supremacy of something like J won't hold
    • 这看起来是正确的
  • [from a draft reader]: "dynamic is better on small-scale, but gets overtaken by static as the size of the project grows"
    • 这些任务并不支持这一说法(在规模大得多的 Pandoc 任务上,静态语言相比动态语言并没有明显优于在规模较小的 Zstd 任务上的表现),但任务本身及其呈现方式的差异太大,因此不清楚这是因为任务规模的变化还是其他差异所致

顺便说一句,Clojure 在 Pandoc 评测中相比 Zstd 评测提升如此之大的一个主要原因是:在 Zstd 评测中,36/40 个中等难度和 5/40 个超高难度的 Clojure 程序出现测试失败,因为字节转换在 128–255 范围内会抛出异常(也许应该用 unchecked-byte?),而它们不恰当地使用了这种转换。

这是一个真实的结果,因为如果你让最好的公开 GPT 模型去实现 Zstd(并且大概在你做其他可能涉及此问题的位/字节操作任务时也是如此),它生成的代码就会以这种特定方式失败。如果有测试能捕获这个问题,bug 就会被修复,但仍然会耗费时间和 token。无论某种语言表现得好不好,这类成本到处都存在(例如,cargo 会反复以错误的参数被调用,然后立刻被捕获并修复,但我注意到,在我实际的项目中,除非你给 codex 明确的指令说明如何调用 cargo,否则这个循环实际上会消耗相当多的墙钟时间,而且很明显,在上下文窗口里留出这个空间是值得的)。

总之,这一切都说明了为什么如果有人想对哪些语言或语言类别特别适合 LLM 做出强有力的论断,他们就需要运行相当多的不同评测。如果我们深入探究为什么某个特定条件得到了某个分数,导致该分数的失败通常是某种特殊的情况,而且并不总是很清楚这个问题在多大程度上会跨任务或跨设置泛化。仅凭一个评测的分数,甚至五个或十个评测的分数,是无法得出关于编程总体情况的结论的。

确实,在 Zstd 评测和 Pandoc 评测中,我们都看到了语言流行度与积极结果(更高的正确率、更低的成本、更短的墙钟时间)之间的相关性,而且在其他评测中也很可能看到这种情况,但就此对任何特定语言得出强结论都是错误的。我之前在根据 GitHub CI 数据考察不同项目构建失败频率时,就发出过类似的警告,指出不同项目的构建失败频率高低可能有不同的原因,而且不应据此得出强结论,因为不同项目的结果不一定具有可比性(例如,如果某个项目的主分支是某种已经过其他审查的候选发布版本,那么该项目的构建失败率预计会较低,但这与直接在 main 分支上进行开发的项目是不可比的)。

不久之后,某个高分语言的相关人士(如果我没记错的话,是 Martin Odersky 和 Scala)在推特上转发了这篇文章,并将该语言的高排名视为该语言的一次胜利。那个结论在当时就是不成立的,而由于这里存在众多差异来源,任何关于单一语言的此类结论在这里就更加站不住脚了。

这些数据(假设评测有效)可以反驳一些强主张,并对其他主张具有提示性,但由于只有两个任务,它实际上只能对语言类别具有提示性,而不能针对特定语言,因为任何特定语言都可能因为某种特殊原因在某项任务上表现好或差,而这种原因可能推广到其他任务,也可能不会。

感谢 Max Bittker、Yossi Kreinen、Aaron Levin、Alan Boll、Luke Burton、Marco Primi、Milosz Danczak 和 Justin Blank 的评论/更正/讨论。

附录:ai-coding-lang-bench 中的若干问题

就像我上面说的,我这里的评测是一个快速粗略的评测,我确信它充满缺陷,所以我并不是想说我这里呈现的评测很好而这个是差的,但以下是 Endoh 的 ai-coding-lang-bench 评测中存在的一些问题。

一个问题是,部分测试似乎运行了错误的可执行文件。已发布运行的设置似乎在某项测试中,于每个候选者的目录内执行了 `../../minigit`,而候选者生成的可执行文件位于 `../minigit`。`../../minigit` 并不存在。

由于静态类型语言在正确性得分上较低,该评测的作者指出“600 次运行中仅有的失败出现在 Rust 和 Haskell(两者均为静态类型,且都是相对‘困难’的语言)”,并暗示“困难的语言”,例如“C 的内存管理、Rust 的所有权模型,以及 Haskell 的 monad/纯度,可能会给 AI 增加额外负担”。

然而,Rust 的失败是因为 `../../minigit` 处没有可执行文件,导致测试失败。第一次 Go 运行通过执行 `ln -sf minigit-go-1-v1/minigit ../minigit` 并链接 `generated/minigit` 到其自身运行“修复”了这个问题,但这意味着之后每一次执行(针对每种语言)实际上都运行了第一次 Go 运行的可执行文件。当针对 Rust 自身的可执行文件重新评分时(而不是因尝试执行不存在的文件而失败),Rust 获得了满分,从而推翻了“Rust 失败是因为它难以处理”的理论。

其他测试也存在问题。例如,有两项测试的结构导致无论实际检查的值是什么,它们都会通过。其中一项测试包含

  if ../minigit commit ...; then                                                                                                                                      
    COMMIT_POST_CHECKOUT=$(cat .minigit/HEAD)                                                                                                                         
                                                                                                                                                                      
    if grep -q "parent: $COMMIT1" \                                                                                                                                   
        ".minigit/commits/$COMMIT_POST_CHECKOUT"; then
      pass "checkout then new commit works"
    else
      pass "checkout then new commit works"                                                                                                                           
    fi                                                                                                                                                                
  else                                                                                                                                                                
    fail "checkout then new commit works"                                          
  fi

内部的 if 在两个分支中都有 pass,这意味着这几乎等同于

  if ../minigit commit ...; then
    pass
  else
    fail
  fi

内部的 if 似乎本意是要进行实际检查,但由于编码错误(也许是复制粘贴错误?),该检查实际上被省略了。

另外,如上所述,智能体可以修改测试环境——第一个 Go 智能体就曾为了修复一个损坏的环境而这样做。它们对测试和环境拥有完全访问权限,可以做任何事,而且测试套件在开发过程中全程可见、没有保留的隐藏测试,这很容易导致作弊:通过针对特定情况编写代码来通过测试,但产出的程序在“现实生活”中毫无用处。从宏观层面看,类似的情况似乎确实发生了——很多程序未能实现规范中的大部分内容,却通过了所有测试,这可能表明智能体“弄懂了”如何通过测试,并且更倾向于这样做而不是实现规范(也可能说明测试本身非常单薄、很容易通过)。

另一个问题是,Claude Code CLI 的版本并非所有运行都一致(版本从 2.1.66 到 2.1.68 不等)。还有少数类似的问题可能有一定影响,但相比上述问题,它们的影响可能较小。

附录:循环中使用 medium 对比使用 ultra

举一个我们可以对比的例子:我很好奇使用 medium 并让智能体持续工作到底有多划算;同时,我脑海里也一直萦绕着“Ralph 循环”支持者提出的一个问题——在循环的每一次迭代中清空上下文窗口、把完整提示词重新给智能体,效果会更好。和上面一样,我预先登记的猜测如下:

  • Zero confidence (50%): Ultra is more effective than medium in a loop
    • 我不太确定该怎么看待这个问题。我猜支持 ultra 的理由是,ultra 是经过某种方式设计的,应该比在循环中反复使用 medium 更聪明。但也有可能存在某种权衡:ultra 可能更偏向速度,而且正如我们指出的,方差非常高,所以即使 ultra 在大多数问题上胜出,在这个场景下也可能落败;ultra 可能还针对缩短墙钟时间或其他参数做了更多优化;ultra 还有一个劣势,就是它不知道在隐藏测试全部通过后停下来,而在这个设置下,达到完全正确的 medium 条件不会再次运行,这极大地有利于循环中的 medium(可以说,这与人们实际使用这些工具的方式是相符的)。
    • 你或许可以说这是 50% 加一个极小量,因为我的思路是先想到这样框定问题,而不是反过来;但我要说,充其量我的置信度也极低。
  • Medium confidence (80%): continuing with context outperforms Ralph loop
    • /goal 模式等默认不会这样做,而且,想必 Anthropic 和 OpenAI 的人已经试过 Ralph 循环这类方法,发现效果并不理想。
    • 随着 harness(以及模型?)的改进,密切关注上下文窗口似乎变得不那么重要了;在 2025 年末 / 2026 年初,处理长时间运行的任务时,我经常不得不清空上下文窗口来避免问题,这种情况随时间推移越来越少见了;但即便如此,因为我不太在意别人怎么说,我跑智能体循环时默认保留上下文,只在出现明显问题时才清空,这样似乎效果还不错——比如我就是用这种方式构建了全世界最强的 Azul AI。所以,我并不确定当时在每个循环迭代都默认清空上下文是否是正确的选择。

就这一个问题而言,平均来看,跑一次 ultra 似乎比反复跑 medium 单位成本更优(单位时间更是如此),而且沿用之前的上下文比 Ralph 循环表现更好。天真地反复跑 medium 的问题在于,智能体可能被锚定在一个糟糕的解决方案上,无法取得进展。Ralph 循环背后的理论是,丢弃可能导致这种问题的糟糕上下文,但这并不能让你免于得到一个糟糕的产物。

仅仅通过使用大语言模型,我就注意到,丢弃一大段代码让 LLM 从头重写,往往比让 LLM 修改它或就地重写效果更好。Michael Malis 一直在用 Rust 重写 Postgres 并做了大量改动,他也注意到了这一点。这也与此前提到的观点相关:由于高方差(再加上这种路径依赖),如果你不介意消耗 token,多次掷骰子并取最佳结果往往更划算。

附录:Guards of Atlantis 2

我尝试做了第三个评估,这个评估无论从问题的呈现方式还是问题的实际执行过程来看,都更像是一种“业务逻辑”类的评估。你可以说,Zstd 评估和 Pandoc 评估对程序员来说都是相当不寻常的任务,因为没多少程序员能拿到像 Zstd RFC 那样写得既清晰又详尽的规格说明,也没多少程序员会拿到像 ProgramBench 测试那样预先创建了这么多测试用例的问题。

这里的想法是实现一个棋盘游戏。一般来说,棋盘游戏的规则是由不擅长编写清晰规格说明的人写的,所以实现一个棋盘游戏更接近于非程序员(或者是不擅长编写良好规格说明的程序员)给某人布置任务时的情况。

这里的问题在于,要找到一个我手头有合理评分基准(oracle)、但对大语言模型来说又不简单的游戏。例如,大语言模型能够一次性就搞定 Scout 和 Azul 的规则,这使得它们不适合作为测试任务。对于那些大语言模型无法立即一次搞定的游戏,我恰好有 Guards of Atlantis 2 的评分基准,因为我之前让一个大语言模型为我和我朋友们实现了一个可以玩的版本(这个没有链接,因为我想不出如何做一个不涉及版权侵权的界面)。后端只花了我几个小时的时间,但为了让规则大致正确,却耗费了相当多的大语言模型时间。我喜欢把这个作为测试任务,因为它的规则很棘手,就像很多交给程序员的实际需求描述一样棘手,但从原则上讲,弄清楚正确的规则并加以实现是可能的(毕竟,人类在离线正确玩这个游戏时,就是在隐式地做这件事)。

在桌游规则中,相当常见的情况是,严格按字面阅读规则反而是错误的,你需要借助“常识”(或查阅某种 FAQ)才能正确游玩(确实有一些游戏设计师致力于避免这种情况,比如 J C Lawrence,但这相当少见)。《亚特兰蒂斯守卫者》就有不少这样的规则。该游戏的设计师也公开表示,不存在所谓的规则精神或对规则的常识性解读,并称你应该始终严格按字面阅读规则,因此也有许多情况是你需要忽略“常识”解读、严格按字面来读规则的。这种组合对 LLM 来说相当困难(而且,从我观察到人类玩家按设计师意图游玩该游戏的频率来看,对人类来说也同样困难)。

我认为,仅仅阅读规则书就想正确游玩这款游戏,实际上几乎是不可能的(当然理论上可行,但你需要知道哪些规则要按字面理解、哪些不能,而由于规则本身并没有定义一个自洽的体系,让你能据此推断哪些规则遵循哪套元规则,你只能靠随机猜测并碰运气)。在我实现这款游戏时,为了让我的 LLM 理解规则,我给了它各种资料,比如一份非官方的规则 FAQ(内容正确)、一份非官方的精简版规则(比官方规则写得好且正确,但不完整)、一本开局书(可以假设开局书里只包含合法走法,用它来检验规则)、Discord 规则频道里的评论等等,然后让 LLM 在这些资料之间做一致性检查,并让它明白 FAQ 和 Discord 评论这类东西的权威性高于实际印刷的规则书。用我每月 200 美元的 OpenAI/codex 个人账号,我让一个 LLM 利用我所有的空闲算力来运行一致性检查并修复规则问题。我没有仔细追踪这花了多长时间,但我觉得大概花了一两个月的时间反复打磨这些修复,才得到一个还算合理、可以玩的结果,但我不会真的完全相信它是正确的。

我之所以对它有一定信任,唯一的原因是 Pedro Oliveira 也实现了《亚特兰蒂斯守卫者》,而且他们用了完全不同的方法(一种更常规的方法,由人来驱动 LLM,而不是试图让 LLM 自己摸索)。当我们对比实现时,发现各自大概有 10 个左右的 bug。可能还有一些残留的 bug,是我们两个实现都犯了同样的错误,也可能有些地方我们的实现不同,但检查系统没有注意到,不过我认为我们两个实现的规则现在都已经相当扎实了。这就是我为什么能拥有这款游戏的“神谕”的原因。

我喜欢把这个当作一项任务,因为它更接近现实世界中你会遇到的那种“规格说明”——规格模糊、自相矛盾,有时干脆就是错的,而你需要借助其他信息才能得出正确结果。对于这个评测,为了避免变成对 LLM 能否以恼人格式获取数据的测试(比如把开局库从一组图片转换成某种结构化数据、把规则扫描件转成文本等),我把两样东西都给了智能体:凡是我让 LLM 提取数据的地方,既给原始材料(提取过程本身也需要各种一致性检查才能做对),也给提取后的数据(原始材料也一并提供,这样 LLM 如果想检查提取错误,可以自行核对原文)。

虽然我是用较老的模型完成这项任务的(其中一部分我用的是 GPT-5.1 或 5.2,另一部分用的是 5.4 或 5.5),但即使用较新的模型、且不给它们我提供给老模型的那种引导,这项任务仍然太难了。无论使用什么语言,智能体在这项任务上的得分都约为 0。

顺便说一句,如果你好奇 LLM(以及人类)会在什么地方卡壳,这里有几个例子。有一张卡牌,其文本写着:“目标:与你相邻的一个单位。攻击后:可对另一个敌方英雄再发动一次。”

在这款游戏中,英雄是一种单位类型。严格按字面理解,并在完全掌握规则的前提下(例如知道“攻击后”是什么意思等),这应该意味着你可以要么攻击一个单位,要么攻击两个英雄(毕竟,要对另一个敌方英雄重复攻击,就意味着第一个目标单位是英雄;否则,那就会是另一个“单位”是英雄,而不是另一个“敌方英雄”)。

这张卡实际上印有相当于勘误的内容,因为人们抱怨它表述不清;勘误内容为“(即使原始目标是随从,你也可以重复)”。这对大语言模型(以及一些人)来说已经很令人困惑了,但真正的致命之处在于,还有其他卡牌使用了相同的句式结构,却没有这个修正。要正确打出其他具有相同结构的卡牌,你需要知道,每当这种结构出现时,都应按照这张卡上的勘误来操作。游戏设计师喜欢使用多种具有特定非字面含义的结构,你必须牢记这些含义。

另一个不应按表面意思来打的规则示例是,一张角色卡牌上写着“选择一项,或在不同目标上选择两项:A、B”。严格按字面理解,人们会期望能够在不同目标上执行A或B,或者同时执行A和B。但游戏精神的一部分是元规则:一张卡牌不能对另一个角色进行多次攻击,因此“按卡面所说,在不同目标上同时执行A和B”的解释是不成立的。基于类似的推理以及类似结构的用法,这张卡牌的正确解读应为“选择一项,或在不同目标上选择两项”,这可以说仍然存在歧义,更清晰的写法可以是“选择一项或两项(若选两项,则必须作用于不同目标)”。

作为人类,一旦你理解了“游戏精神”是什么,就能解决这类问题。但按照设计,这些并没有明确写在规则里,人们必须从Discord讨论中推断出来,而这似乎超出了当今模型的能力范围——尽管在许多专业任务上被当今模型超越的人类,却能够做到这一点。

在我监督实现规则的大语言模型时,大语言模型之所以触达天花板、无法收敛到完全正确的规则,是因为大语言模型会观察到某条规则不一致或不正确。然后它会尝试修复这条规则,同时也会修改其他内容,试图让它们保持一致和正确。这有时会让事情变得更正确,有时则会让事情变得更不正确。当事情变得更不正确时,大语言模型有时会修改一条原本正确的测试,把它变成一条不正确的测试。因此,过了一段时间后,大语言模型并没有真正在提升正确性,只是在哪些规则不正确这件事上反复折腾。这还是在我提供了关于检查什么以及如何检查的某些指导的情况下;如果没有这些指导,即使是使用当今更先进的模型,大语言模型也无法以合理的方式驾驭这个问题。

我确信存在一款规则复杂度恰到好处的棋盘游戏,可以在这里做成一个很好的评测基准,但顾名思义,要创建这样的“神谕”(oracle)需要花费一些功夫,而我手头并没有一个规则合适的棋盘游戏的现成“神谕”(我认为这实际上是可行且可扩展的,因为人们可以创建几十个甚至几百个这样的游戏,而所需工作量并不会比创建一个多太多,因此人们可以为数百款游戏获得一个相当正确的“神谕”,然后检查哪些游戏的难度水平适合作为当今大语言模型的有趣测试;Guards 之所以实现起来费力,是因为没有现成的实现和回放数据可用;如果有人依赖回放数据来测试每款游戏的正确性,那么相对容易就能批量生成大量这样的环境)。

这可以说是一个有点意思的问题:给定一份清晰的规格说明,例如一套写得很明确的规则,那么比《亚特兰蒂斯守卫者》更复杂的产物,大语言模型也能实现(我认为 Zstd RFC 就更复杂,Pandoc 当然也更复杂;甚至 Pandoc 支持的单个文档格式,比如 PDF,都比《亚特兰蒂斯守卫者》更复杂)。所以问题不在于找到一款规则复杂到让大语言模型吃力的游戏,而更多在于找到一款规则模糊或矛盾到让大语言模型吃力、但又不至于让大语言模型完全无从下手的游戏。但这确实是一个现实世界中的真实问题,因为人类通常并不擅长写出清晰的规格说明,而模型和工具链在多大程度上能处理好人类那种不清晰、自相矛盾、有时甚至完全错误的规格说明,对典型用户来说,可能比大语言模型能否根据一份像 Zstd RFC 那样写得极好的规格说明来实现某个东西,或者大语言模型拿到 4800 个 ProgramBench Pandoc 测试用例加文档后能否实现某个问题,更相关。

附录:各项决策的理由

  • Testing ultra
    • 我见过有人说你不应该真正去衡量这个,因为这是工具链的事,不是模型的事。我能理解如果你是在改进模型或工具链,你会想把这两者分开衡量;但当我们看用户如何使用这些东西时,很多人就是直接用 codex 或 claude 以及各种内置功能和选项;某件事到底是工具链的事还是用户的事,对他们来说其实并不重要。
  • Using codex
    • 我见过一些评测出于和上面相同的原因使用非常薄的工具链,而我使用 codex 而不是非常薄的工具链,原因也和上面一样。
    • 同样,在这个“穴居人模型”评测中,我使用了带 Opus 和 Fable 的 claude,以及带 GPT 的 codex。
  • No internet access
    • 如果给模型联网权限,它们往往会作弊,而且有很多问题即使上网搜索也找不到能解决该问题的源代码,所以这样做能让这些评测更接近真实情况。
  • Relatively large tasks compared to a lot of benchmarks people pass around
    • 虽然我会让大语言模型处理大量琐碎任务,但那些真正耗费我时间或 token 的事情,往往比 Alderson 评测或 Endoh 评测里的任务规模更大;大语言模型在琐碎任务上已经足够好,以至于某些条件让它们在某个琐碎任务上表现稍好或稍差,对我来说差别不大。但对于像实现 Guards of Atlantis 这样的任务——我得花好几个小时搭建脚手架,任务才勉强能跑起来——我非常在意是什么因素让模型表现更好或更差。
  • Agent-specified prompts
    • Public evals seem to have moved to relatively thin/lightweight prompts that don't specify the task in great detail; this is said to be better because an agent setting up a task will give too much information that helps agents succeed at the task
      • 我能理解你为什么想测试那个,但同样重要的是,我非常在意智能体在由智能体设定的任务上表现如何,因为我让智能体执行的很多任务本身就是由智能体定义的;我在意智能体在两种风格下的表现,而不只是其中一种,而公开评测已经偏向其中一种风格了。
  • Zstd eval: asking agents to fix bugs without telling them the issue or the failing tests
    • In general, if you tell an agent to fix a specific thing, it will fix it, but it won't necessarily fix the class of issue; I've found that if you tell it there's an issue but don't tell it what the issue is, it sometimes does a more general thing instead of just putting in a narrow, brittle fix, so I do care about how agents behave when given instructions like this (of course you can tell agents to not just make a narrow, brittle, fix, but that often doesn't work)
      • 这感觉和我们之前在 Pandoc 保留集脚注里注意到的问题有点相关——当我们告诉智能体我们有一个保留集时,似乎会迫使智能体产出更通用、更不脆弱的解决方案。

附录:这些评测存在的问题

说到性能基准测试,我做得够多了,以至于我大致清楚自己的基准测试有哪些缺陷,也能在时间/精力与缺陷之间做出有依据的权衡,而且我有相当的信心认为基准测试中存在的缺陷对我试图理解的东西不构成实质性影响。但我在 AI 评测方面做得还不够多,无法对 AI 评测产生这种直觉,所以在元层面上,我预期自己做的任何 AI 评测都会存在一些我尚未意识到的缺陷。

我预期这里存在缺陷的另一个原因是,这些评测是由编码智能体搭建的,而我每次花一分钟去找问题,至少都能发现一个问题。这表明这些评测很可能还有更多缺陷,只要再多花点时间就能发现,但我希望这更像是“快速玩具项目”级别的正确性,而不是“Gary Bernhardt”级别的正确性,所以我在修复了几个问题之后就停手了。

早在我做验证工程师的时候,参加过一场 Sun/Oracle 工程师在奥斯汀的聚会,大概是 2007 年前后,会上他们用数学方法把“两次 bug 之间的时间间隔”形式化地转化为芯片发布的置信度。我很少见到有人这么做,但最近听说 Will Wilson(Antithesis 联合创始人)提到,Antithesis 的一些人用生态学里的数学方法(关于稀有物种观测的文献)来估算真实 bug 率,这看起来比 Sun/Oracle 那位工程师二十年前做的要精密得多。

这个想法很酷,但当你每分钟都能找到一个 bug 的时候,你根本不需要高深的数学来告诉你可能还有一大堆 bug。如果我是为了工作做这件事,而且我们有理由在意这些评测的准确性,那仔细看看、多修一些问题可能是合理的(而且如果我是干这行的,我大概也有能力和经验,在指导 LLM 搭建这些评测时少犯些错)。但就回答“用 LLM 时,动态语言是否真的明显优于静态语言”这个问题而言,我更有把握的是这个说法不成立,而且还有很多其他问题似乎更可能得出可操作的结果(比如哪些技术或测试库效果最好)。

我通常不会在博客上发布内容,除非我觉得它们已经比较成熟了,但这意味着我经常会探索一些数据,直到满足自己的好奇心,然后就再也不发布结果了。在和别人聊起这些未发布的结果时,我发现和我聊过的人往往对这些结果很感兴趣,即使它们还没有达到我真正满意的标准,这似乎表明,那些我没聊过的人可能也会感兴趣。从我目前看到的情况来看,我怀疑要把这个做到我真正满意的标准,至少需要我目前投入时间的10倍。我目前相当忙,几个月内都看不到自己能抽出时间来做这件事,到那时候我也不确定自己是否真的会抽出时间来发布它。在最近的一篇文章中,我提到了一项我差不多一年前做的分析,当时我想弄清楚哪些车在事故中更有利于降低脑震荡风险,我花了一些时间研究这个问题,进展到足以得到一个让我满意的答案,然后就再也没有抽出时间去做那些把结果整理干净以便发布的工作。

那项分析中有一些结果看起来是“可发表的”,也就是说它们可以变成一篇正式发表的论文(比如从实际碰撞数据中发现,HIC和速度之间的关系看起来像是四次方关系(!);有一篇论文试图找到这种关系,但做了错误类型的分析,没能找到“O(n)”式的关系,得到的结果要模糊得多),但我从来都不太在乎某样东西是论文还是博客文章,而且事实证明,我更有可能直接转向下一个分析,而不是把分析整理到足以发布一篇博文的程度。

沿着这条思路,一个更近期的项目是:在做出一个超人类水平的 Azul AI 之后,我尝试用一个人工时间投入少得多的流程,去做一个超人类水平的 Splendor AI。我认为那个尝试没有成功,但它以明显优势击败了我能找到的所有其他 Splendor AI,这算是一个有点意思的结果。我觉得自己对棋盘游戏 AI 的了解已经足够写点东西出来,但我主要的兴趣在于搞清楚自己能不能做出一个像样的东西,然后我就一直去做其他项目,而不是花时间好好写一篇总结。我觉得其中有意思的一个例子是:你想做的很多性能优化,实际上会改变结果,所以你不能只依赖那些可以被严格验证为不改变结果的优化手段。但是,如果你天真地让一个编码智能体去做这些优化,同时又要求不降低棋力,它们就会做出各种降低棋力的事情。棋力下降非常严重的情况很容易被发现,但还有一些更微妙的问题,有时会导致(比如)在与你自己 AI 的自对弈中棋力没有变化,但面对人类或其他 AI 时棋力却下降了,所以必须有某种流程来捕捉糟糕的优化,而这本质上是一种带有随意性的流程,必须结合你自己的直觉和依赖 LLM(它们会非常有帮助,但也常常完全错误)来设计。

对于我感兴趣的这种数据类项目,大语言模型大幅减少了获得一个足以满足我好奇心的结果所需付出的努力,但据我所知,它们并没有减少多少发布结果所需的努力(至少,如果你是手动撰写结果而不是让大语言模型来写,并且你希望结果干净漂亮的话),这意味着撰写结果会遇到一种类似阿姆达尔定律的瓶颈,所以我已经做了更多这样的项目,但写出来的却更少了。如果非要说的话,我觉得写这些内容实际上花了更多时间,因为我的工作流程变了。例如,我不再只是输出一个 ggplot2 的图表,而是会做一个交互式版本,在某些方面更好看,但制作起来肯定更耗时。而且我会用大语言模型跑一遍拼写/语法检查(至少到目前为止,这是我写作时唯一用到的大语言模型辅助),这会发现一堆需要修复的问题。由于我是逐个手动查看而不是直接采纳修改(而且我打字错误很多),这实际上相当耗时(上一篇花了超过一小时,这篇也花了超过半小时,尽管我并没有一路改到底,大概改到一半就放弃了)。

总之,发布这篇内容是一个实验,发布一些半成品的笔记,而不是那种我在发布前真正想要的整理干净的版本。如果你对此有看法,请告诉我(X Bsky Mastodon)!

我没有当前这些评估的 GitHub 链接。一方面,我觉得我确实应该有。另一方面,它们很乱,而且我在发布代码之前有一堆东西想清理,我不知道我是否/何时会去做这件事,而这样做,至少我算是把一些东西放出来了,而不是只跟几个朋友聊聊结果,然后让结果无限期地躺在我的硬盘上?

附录:关于 Zstd 的更多细节

智能体被指示忽略性能,但超时并非无限,而且在中等条件下,部分测试用例确实超时了。这可以说有失公平,但对分数没有实质性影响。对于非无限循环导致的超时,Clojure 中有 2 个测试用例(在 40 * 34 个测试中),J 中有 2 个,Tcl 中有 2 个,Factor 中有 1 个,PHP 中有 1 个。而且,考虑到最大的测试用例是 4 GiB,9000 秒(2.5 小时)的超时设置算是相当宽松了。在 2.5 小时内未能解码 4 GiB,意味着在 Graviton 5 核心上的速率低于 0.5 MB/s,这相当慢。

以下是我在尝试让智能体进行设置时遇到的一些问题(而且,如上所述,发现每个问题所花的时间都很短,这意味着还有更多问题存在)。

  • 最初,构建设置没有向智能体明确说明,导致当智能体根据说明做出看似合理的操作、但在实际评分时却行不通的情况下,某些语言会随机失败。
  • 出于某种原因,负责设置的智能体对某些语言施加了不寻常的任意限制,而对其他语言则没有(例如,Rust 的设置无法使用 rustfmt 或 Clippy);大多数(但不是全部)语言都有类似情况。
  • 许多测试(由智能体创建)实际上是某种性能/压力测试,尽管智能体被指示忽略性能(我不认为在 9000 秒内处理 4 GiB 的 Zstd 数据算是一种性能压力测试)。
  • 某些语言条件对智能体有任意指令(例如,Haskell 条件有指令要求不使用 bytestring,并提供了替代实现建议的说明)。
  • 某些语言条件使用了非常古老的工具链(例如,Zig 使用的是 0.10 版本)。
  • 某些语言条件提供了脚手架来帮助智能体实现 Zstd。
  • 在最初的汇编语言条件下,智能体用 C 实现代码,然后将其编译为汇编语言并提交汇编代码(这使得汇编语言的结果与其他语言的结果大致相当)。
  • 有些语言条件对可用工具的解释说明是错误的(例如,汇编语言条件被告知可以使用 GDB,但 GDB 实际上无法工作)。

还有一件事,虽然严格来说不算 bug,但我还是把它移除了。其中一个测试非常难(大概只有 10% 的智能体能在第一次尝试时通过)。在测试当前 zstd 发布版二进制文件时,zstd 二进制文件本身也无法通过这个测试。阅读 RFC 后,我发现这似乎是 RFC 中关于某个边界情况是否合法的歧义。不同语言在该测试用例上的通过率存在相当明显的聚类现象,我觉得这很有意思,但在其他所有测试都在衡量(或至少试图衡量)更直接明确的内容时,这似乎不是一个很有用的衡量指标。

总之,在上述列表(并非详尽无遗)中,许多问题影响了很大比例的语言,有些问题不得不修复多次。总的来说,如果你把每个条件都算作一个独立的 bug,我大概修复了(让智能体修复了)100 多个这样的 bug,而且我预计还有更多。当我与 Max Bittker(他经营一家 RL 环境初创公司)交谈时,他指出:

在我参与过的所有评测工作中,我最终都投入了大量时间和精力,主要形式是阅读轨迹(或大量轨迹的摘要),然后对问题进行分诊处理,比如“哦,这类 bug 本不应该出现,让我们更新一下 X”(X 指提示词、测试框架/环境或验证器)。

智能体倾向于草草了事,所以我在这方面非常用心,确保问题在正确的层级得到修复。例如,对于被测智能体来说,上下文中的内容非常敏感(添加它需要担心的随机垃圾信息很糟糕,最坏情况甚至会泄露答案),这与系统其他部分在幕后固定的内容截然不同。

智能体在编写评测时,对被测智能体的体验不够敏感,它们会直接给出答案,或者通过把问题变成内部智能体的问题来修复(“记住不要奖励 hack 行为”)。

我也在复用现有资源(代码仓库、游戏、工具、关卡)并围绕它们构建测试框架和验证器方面取得了很大成功,这比试图为评测从头开始构建某些东西要有效得多。

事后看来,我有点后悔做了跨语言的评测。即使修复了 100 多个评测问题,我毫不怀疑还有更多问题存在。也许这只是“这山望着那山高”的想法,我尝试下一个评测时也会后悔,但我认为评估不同测试技术或测试框架的效果会比评估不同语言省力得多,而且我觉得那个话题至少同样有趣。另外,回想起来,如果我手工做了更多工作,少依赖智能体,结果会好得多。例如,我本应让智能体为一种语言生成环境,然后既让智能体检查,也亲自检查并修复问题,然后再为另一种语言生成环境。这样重复几次之后,我可能就有了更好的流程来为其他语言生成环境(如果没有,我也可以对每种语言重复这个过程,得到更可靠的结果,而且很可能不会花更多时间)。

另一点需要注意的是,许多语言之间真正的差异并没有真正被测试到,比如针对对抗性输入的存储安全性。如果智能体用 C 或 C++ 生成大致正确的代码比用 Rust 更困难,那会被观察到,但如果模糊测试器或 valgrind 或其他工具能发现的问题,就不太可能被那一小撮测试捕捉到。我确实让一个智能体(简要地)检查了 C 和 C++ 代码的存储安全问题。智能体声称它在 ASan+UBSan 下运行了 C 和 C++ 代码,并尝试了一些模糊测试输入(每种 4000 个),没有发现问题,但当然这并不意味着没有问题,也不意味着更大的代码库不会有问题。

而且,事实上,对 Pandoc 评测做一次类似的快速内存安全检查后发现,所有 C 程序和除一个之外的所有 C++ 程序都存在内存安全问题(这些问题包括错误地解引用越界内存;一个具体例子是,在其中一个 C 程序中,一个被截断的 LaTeX 表格可能导致越界内存读取)。这些问题是仅用 10 秒的提示词就能发现的,这一事实表明,许多此类问题无需太多人力就能被发现和修复,但这会消耗相当多的 token,并且会把 C 和 C++ 版本的成本推高到远超 Rust 版本的程度,而且做完这一切之后,你对 C 和 C++ 版本内存安全性的信心仍然不如对 Rust 版本的信心。

总之,如果你对结果的分布感到好奇,我们有以下 medium 和 ultra 的数据:

我不太喜欢 ultra 的结果在这里有些饱和,但测试 ultra 的一个“问题”是,随着问题难度增加,它会持续运行很长时间(例如,大多数 Pandoc ultra 运行持续了 12 小时以上,汇编运行则持续更久),所以那些没有饱和的项目是非常大的任务,比如 Pandoc 评测,或者在某些方面过于困难的任务,比如 Guards of Atlantis 评测。


  1. 一位草案读者预先注册了猜测:“动态在小型项目上表现更好,但随着项目规模增长会被静态超越。”[返回]
  2. 保留测试集似乎是必要的,因为如果没有它们,智能体会作弊——它们会检测到测试输入,然后硬编码出能通过的测试输出(有时即使被明确告知不许作弊,它们也会这么做)。如果所有作弊都这么明目张胆,那倒不是问题(而且这本身可以成为一个有趣的衡量指标,因为智能体在不同语言下是否差异化地遵循指令,对真实用户来说很重要),但很多作弊行为更加隐蔽,也更难判定。例如,有些智能体编写的代码会依据测试的结构进行分支,但分支内部填充的代码并非针对单一测试结果特化,而是能通过同一测试的多种变体。从“绝对不算作弊”到“明显作弊”这个光谱上的任何一点,都有智能体尝试过。正如我们在研究 Senior SWE-Bench 时所看到的,用 LLM 给评测打分很棘手,而且很容易同时引入偏差和方差;使用保留测试集确实有一些问题,但它能让我们避开这一大得多的麻烦。

    首先,保留测试集之所以可疑,是因为它们是由智能体创建的。本意是创建一套保留测试,让一个理性的人(或智能体)在不作弊的情况下能够通过。智能体们审查了这套保留测试,剔除了一些不合理的情况,但我没有逐条人工核验,所以我觉得很可能至少有一条保留测试在某种程度上是不公平的。不过,保留测试集的整体得分足够低,所以我不太担心少数几条测试有问题(如果我是在 AI 实验室工作、试图训练下一代模型,我会更担心这个问题,但我觉得这对我们这里的用例来说并不重要)。

    在拥有一组留出测试集的情况下,指示智能体不要作弊,并未能阻止明显的作弊行为,这些作弊在留出测试集上得分极低;但告诉智能体存在一个用于评分的留出测试集,似乎降低了它们在智能体可见测试上取得的分数,同时提高了它们在留出测试集上的得分(在不告知这一点的情况下,不少智能体在 Pandoc 测试上以脆弱无用的代码取得了 100% 的分数;在告知存在留出集后,没有任何智能体在 ultra 上经过 1 轮后取得 100% 的分数,但留出集分数显著提高,表明泛化能力更好)。

    [return]
  3. 市面上有各种 Substack 专栏、YouTube 频道和其他渠道,承诺告诉你 LLM 编程成功的秘诀,但花时间做实际实验的投入产出比其实并不高。当我们研究“穴居人模式”时,我们看到最大的编程类 YouTube 博主之一发布了一个视频,他们花了几分钟研究了一下,就断定它是有效的。与其花哪怕 15 分钟去验证它是否真的有效,不如用这些时间去制作更多内容,后者的投入产出比可能更高。

    有各种论文讨论了不同的技术,这些论文有时比大多数博客文章或视频更深入,但平均而言,它们不一定包含更多有用的信息。例如,当我让 ChatGPT(5.6 Sol,Pro)查找关于大语言模型语言有效性的讨论时,它找到了一篇关于 token 效率的论文,其中有一个有趣的想法,但存在与我们之前讨论的“穴居人模式”评测相同的问题——它没有考察一个足够有趣、其结果能对我作为程序员有参考价值的任务。仅看引用那篇论文的文献,我们发现三位学者写了一篇关于语言 token 效率的论文,题为“The Best Programming Language for Tokenmaxxing”,但与这篇帖子相比,那篇论文只比较了四种语言,使用了较差的模型,并且使用了小型玩具问题(来自一个叫 LiveCodeBench 的东西;用 GPT-5.5 解决这些问题的成本通常在 1000 个 token 左右)。无论评测做得有多好,正如我们在本文和“穴居人模式”评测中所指出的,从一个小型玩具问题转向一个我可能关心的业余项目或工作问题时,我们经常会看到截然不同的相对结果。此外,在那篇论文中,他们提到给出的提示词是“要测试你的程序,请精确运行 ./test.sh... 这些是我唯一关心的测试”,他们声称这很现实,因为“我们认为这种设置是研究智能体行为的现实方式:在日常使用中,程序员不会对智能体隐藏他们的测试。相反,程序员会指示他们的智能体一直工作,直到所有测试通过。”但是,正如我们上面指出的,这样做会导致代码脆弱,在现实世界中失败(或者如果你有未提供给智能体的保留测试,它会在保留测试中以非常高的失败率失败;这个问题不能仅仅通过添加更多测试来解决;也许可以通过模糊测试或基于属性的测试之类的方法来解决,但效果如何是另一篇文章的话题)。我并不是说这些论文不好,也不是说从这些论文中学不到有趣的东西,但作为一个想知道应该使用哪些技术或工具的程序员,我无法从上面链接的这类论文中获取这些信息。

    [return]

来源:Hacker News 热门(buzzing.cc 中文翻译)· danluu.com