← 文章 / 编程开发
Lobsters 1小时前 · 2026-08-11 03:10:19 · 0 阅读

编程语言如何影响Token效率与正确性?

这篇被广泛引用的文章(反正我老是看到有人引用)提出,动态语言或表达更简洁的语言在 token 效率上更占优势。这篇文章被引用的次数之多,连 LLM 搜索结果都认同这一观点。比如,我搜索“dynamic vs static language token cost”(不带引号)时,Google 的 AI 摘要开头就是这么说的:

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

Google 的 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可执行文件之前就已经完成了。

与其依赖这些评估,我们可以尝试运行一些自己的评估。正如从这些评估以及我们上次关于评估的练习中讨论的评估所看到的,很容易制作出一个评估,其结果并不像评估创建者所认为的那样。毫无疑问,这些评估也不会例外,会存在缺陷(详见下文附录)。

为了建立直觉,我喜欢在查看结果之前预先登记猜测1。我与朋友们预先登记的一些猜测包括:

  • 高置信度(95%):整体上动态语言与静态语言的对比说法不会成立
    • 基于上述原因:这感觉类似于穴居人评估,结果最多只会随着问题规模的增大而被稀释
  • 低置信度(60%):在极限努力下,静态语言将比动态语言略胜一筹
    • 对于在极限努力下,测试平台能更快地向模型提供反馈,并因此带来正确性或效率上的某种好处,我的信心非常弱;但同样有理由认为情况并非如此,原因多种多样。例如,我注意到 codex 在调用 Rust 编译器时,经常犯完全相同的错误,然后不得不修复它;也许这类问题比假想的更快反馈循环更为重要。
  • 高置信度(98%):像 J 这样的“奇特”语言不会保持其优势
    • 这与整体静态与动态的论断理由相同,另外还考虑到,AI 实验室在晦涩语言上的合成数据强化学习环境投入将非常少(甚至可能为零)。

Zstd

在第一次评估中,我尝试给代理提供 zstd 的 RFC(以及勘误表),并指示它们实现一个完整的 zstd 解码器(代理被限制在无互联网的容器中)。测试用例并未提供给代理。对于像 zstd 这样复杂的系统,期望测试覆盖所有可能的情况并不现实。例如,尽管 zstd 是一个经过充分测试的软件,我曾发现 zstd 中的一个数据损坏错误。测试套件并非旨在发现可能潜伏多年的极端边界情况,而是用于检查那些可以从 RFC 中“轻松”推导出的各种应能正常工作的场景。

下图横轴为成本,纵轴为正确性得分(左上为优,右下为劣);显示的是 GPT-5.6 Sol 在中等和极限努力下的平均结果。如果我们只看中等努力(并忽略不同任务上结果往往差异巨大的事实),可能会得出类似 Alderson 评估的结论:在使用 LLM 时,动态语言更高效、更优,因为(忽略相对晦涩的语言)动态语言集群位于静态语言集群的左上方(我们采用了 Alderson 的静态与动态颜色编码,便于一目了然地比较)。但若观察极限努力,结果则相当混杂,少数静态语言表现最佳,且在较好结果中静态语言多于动态语言。

下方的图表还提供了一个切换选项,可将横轴从成本改为时间。`mame/ai-coding-lang-bench` 指出,快速获得结果是有价值的(我个人不这么认为,因为结果耗时较长,我通常会同时处理其他任务而非干等),因此我们也可以从这个角度审视。同样,我们观察到两种语言类型并未有一方占据绝对优势,尽管在这项特定任务的中等投入下,最佳动态语言的结果再次优于最佳静态语言(尽管差距依然不大)。 我们可以注意到,就像之前将完全琐碎的“原始模式”评估与稍复杂的同类评估对比时一样,那些在琐碎评估中成立的强关联性并未延续到这个更大的案例中。正如之前所见,极端性能差异在这些更大规模的评估中消失了,除非是在我们预期表现不佳的情况下,比如使用汇编语言(对人类而言会显著增加时间和难度)或使用相对冷门的语言,这些语言可能并非AI实验室重点生成合成强化学习环境数据的对象。 值得注意的是,这与第一项评估的结论相反,后者曾建议像J这样的高密度语言在效率上具有优势。或许,如果你拥有极其充裕的预算,并能针对你偏爱的语言训练或微调模型使其高效运作,那么使用一种冷门(且“奇特”)的语言是合理的;但如果你是一位普通的LLM用户,坚持使用主流语言似乎比采用冷门高密度语言更为稳妥。 此外,若将语言的热门程度与本次评估中的表现绘制成图(未展示),我们会发现存在弱到中等的正相关关系,即越流行的语言往往能产生更正确且成本更低的解决方案。 正如我们之前提到的,非常相似的评估可能产生截然不同的结果。例如,在优化bzip2压缩与解压缩的wasm实现时,我们观察到优化1和优化2的评估结果存在显著差异,尽管这些任务在评估中算是相当接近的。要提出一个强有力且普遍适用的论断,比如“动态语言比静态语言更高效”,我们需要在众多任务上运行评估。然而,要证明像这样的论断:

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

这种说法最多只是方向性的大致正确,并不适用于任何特定案例,或许也不足以在普遍意义上具有相关性,我们只需尝试几个案例,看看这一论断是否普遍成立。在上文中,我们看到在某种努力水平下,这一论断似乎勉强成立,但存在例外;而在更高的努力水平下,这一论断似乎并不特别成立,这足以说明该论断可能并非普遍正确,除非我们的评估存在完全使其失效的混淆因素。

Pandoc

不过,为了从另一个截然不同的任务视角(更偏向测试驱动开发而非“阅读规格说明”)来观察,接下来的评估采用了Pandoc ProgramBench评估,并针对我们的使用场景进行了调整。我们不再执行ProgramBench所呈现的逆向工程任务,而是向代理提供ProgramBench材料及测试,然后根据一组保留的测试来评分,以衡量每种条件下的性能表现2。 在以下结果中,横轴再次表示成本,纵轴表示在保留测试上的得分。 和之前一样,我们并未观察到语言的成功率或成本与其静态、动态或高度紧凑的特性之间存在显著关联。我们再次发现,相对冷门的语言往往表现不佳(尽管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 任务更是如此。

借助 LLM,许多原本几乎无法回答的问题,现在只需稍加努力和消耗一些 token 就能得到答案。由于当前激励机制的作用3,我们短期内未必能获得此类问题的答案,但至少现在可以尝试一探究竟。

网上流传的许多说法,这些评估既无法证实也无法反驳(原因如上所述,由于不同问题间的差异,需要尝试更多任务),但它们确实能揭示一些问题,例如:
  • 存在大量劣质代码的语言(如PHP)表现会更差
  • 在这些任务中似乎并非如此
  • 既然现在重写代码如此容易,你应该使用功能强大的语言(如Haskell)
  • 在这些任务中似乎并非如此
  • 你应该使用流行的语言
  • 对此说法支持较弱
至于我预先登记的猜测,我们有:
  • 高置信度(95%):动态与静态语言的总体差异说法不成立
  • 这似乎正确
  • 低置信度(60%):在极限努力下,静态语言会比动态语言略胜一筹
  • 信息不足,难以定论,但如果必须二选一,我会认为此说法不正确
  • 高置信度(98%):像J这样的“奇特”语言不会占据优势
  • 这似乎正确
  • [来自草稿读者]:“动态语言在小规模上占优,但随着项目规模增大,静态语言会超越”
  • 这些任务不支持此观点(在更大的Pandoc任务中,静态语言并未比动态语言表现显著更好,相较于较小的Zstd任务),但由于任务及其呈现方式差异巨大,难以判断这是否源于任务规模扩展还是其他差异。
顺便一提,Clojure在Pandoc评估中比在Zstd评估中提升显著的一个主要原因是,在Zstd评估中,36/40的中等和5/40的极限Clojure程序因`byte`转换在`128–255`范围内抛出异常(或许应使用`unchecked-byte`?)且不当使用此转换而导致测试失败。

这是一个真实的结果,因为如果你让目前最优秀的公开GPT模型来实现Zstd(并且如果你处理其他可能涉及位/字节操作的任务,情况也类似),它生成的代码会以这种特定方式出错。如果有测试能捕捉到这个问题,bug会被修复,但依然会耗费时间和token。无论某种语言表现如何,这类成本无处不在(例如,cargo经常被以错误的参数调用,虽然会立即被发现并修复,但我注意到,在我实际项目中,除非你明确指示codex如何调用cargo,否则这个循环会消耗相当多的墙钟时间,而且很明显,在上下文窗口中为这些指示留出空间是值得的)。

总之,这一切都说明了为什么如果有人想对哪些语言或语言类别特别适合LLM提出有力论断,他们需要运行相当多的不同评估。如果我们深入探讨某个特定条件为何得到某个分数,导致该分数的失败通常是某种特殊原因,而且并不总是清楚这个问题在任务或设置之间有多大的普遍性。仅凭一个评估,甚至五个或十个评估的分数,是无法得出关于编程整体结论的。

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

不久之后,某位高得分语言的相关人士(如果没记错的话,是 Scala 的 Martin Odersky)在推特上转发了那篇文章,并将该语言的高排名视为其胜利。但那个结论在当时就缺乏依据,而鉴于这里涉及的众多变量来源,任何针对单一语言的此类结论在此处更站不住脚。

这些数据(假设评估有效性成立)可以反驳一些强主张,并对其他主张有所暗示,但由于只有两个任务,它实际上只能对语言类别提供暗示性参考,而非针对特定语言——因为任何特定语言都可能因某些特殊原因在某个任务上表现优异或糟糕,而这些原因未必能推广到其他任务。

感谢 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 的 monads/纯度,可能给 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循环”支持者常说的一个观点:每次循环迭代时清空上下文窗口,并重新给智能体完整提示词,效果会更好。

与上述一样,我预先注册的猜测如下:

  • 零信心(50%):在循环中,ultra比medium更有效
    • 我不太确定该怎么想。我猜支持这一点的理由是,ultra在设计上应该比在循环中反复使用medium更聪明。但也可能存在某种权衡,ultra更偏向速度,而正如我们注意到的,方差非常高,即使ultra在大多数问题上胜出,也可能在这里落败;ultra可能更优化于缩短墙钟时间或其他参数;ultra还有一个劣势,它不会在隐藏测试达到正确性后“知道”停止,而medium在完全正确后不会在此设置下再次运行,这极大地有利于循环中的medium(这可以说更接近人们实际使用这些工具的方式)
    • 你或许可以说这是50%加epsilon,因为我的思维恰好倾向于这样构建而非相反,但我最多只能说信心极低
  • 中等信心(80%):延续上下文优于Ralph循环
    • /goal模式等默认不这样做,而且,想必Anthropic和OpenAI的人尝试过类似Ralph循环的方法,发现效果不佳
    • 密切关注上下文窗口的重要性似乎随着工具链(以及模型?)的改进而降低;在2025年末至2026年初,处理长时间任务时我常不得不清空上下文窗口以避免问题,这种情况随时间推移越来越少,但即便如此,由于我不太关注他人说法,我运行代理循环时默认保留上下文,仅在出现明显问题时才清空,这似乎效果不错,例如,我通过这种方式构建了世界上最强的Azul AI,因此我不太确定当时默认每次循环都清空上下文是否是正确的选择

针对这一个问题,平均而言,运行一次ultra比按单位成本重复运行medium效果更好(按单位时间更是如此),且延续之前的上下文优于Ralph循环。天真地重复运行medium的问题在于,代理可能被锚定在糟糕的解决方案上而无法取得进展。Ralph循环背后的理论是,丢弃可能导致此问题的糟糕上下文,但这并不能避免你得到糟糕的产物。

仅从使用LLM的经验来看,我注意到,丢弃一段代码让LLM从头重写,往往比让LLM修改或原地重写效果更好。Michael Malis,一位一直在用Rust重写Postgres并进行重大改动的人,也注意到了这一点。这与先前提到的观点相关:由于高方差(加上这种路径依赖性),如果你不介意花费token,多次掷骰子并取最佳结果往往更有利。

附录:亚特兰蒂斯守卫2

我尝试了第三次评估,这次在问题的呈现和实际执行上都更贴近“业务逻辑”。你可能会说,Zstd 和 Pandoc 的评估对程序员来说相当不寻常,因为很少有程序员能拿到像 Zstd RFC 那样详尽清晰的规格说明,也很少有人能像 ProgramBench 测试那样,一开始就拥有大量预置测试。

这次的想法是实现一个棋盘游戏。通常,棋盘游戏的规则由不擅长撰写清晰规格的人编写,因此实现棋盘游戏更接近非程序员(或不擅长写规格的程序员)给他人布置任务时的情形。

这里的问题在于,要找到一个既适合 LLM 评分又不过于简单的游戏。例如,LLM 能一次性搞定 ScoutAzul 的规则,这让它们成了不太理想的测试任务。对于 LLM 无法立即一次搞定的游戏,我恰好有 Guards of Atlantis 2 的评分标准,因为我曾让 LLM 为我及朋友们实现了一个副本(这里没有链接,因为我不确定如何做出不侵犯版权的界面)。后端只花了我几个小时,但要让规则大致正确,却耗费了相当多的 LLM 时间。我喜欢这个任务,因为它的规则复杂,就像许多交给程序员的实际需求那样棘手,但原则上,还是有可能理清正确规则并实现它们(毕竟,人类在离线正确玩游戏时,就是在隐式地做这件事)。

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

我认为,仅凭阅读规则就能正确游玩几乎是不可能的(当然理论上可行,但需要知道哪些规则应按字面理解、哪些不应如此,而这只能靠随机猜测并碰运气,因为规则本身并未定义一套一致的系统来推断哪些规则遵循哪种元规则集)。在我实现这款游戏时,为了让我的LLM理解规则,我提供了各种资源,比如非官方的规则FAQ(内容正确)、非官方的精简版规则(比官方规则写得更好且正确,但不完整)、开局手册(可用来在假设开局手册只包含合法走法的前提下测试规则)、Discord规则频道中的评论等,并让LLM在这些资源之间进行一致性检查,同时明确FAQ和Discord评论的权威性高于实际印刷的规则。以我每月200美元的OpenAI/codex个人账户……

原始来源: Lobsters

评论 (0)