随笔

在 AI 时代,编程语言还重要吗?

Java、四堵墙,以及为什么"语言即验证契约"是一种升迁

我做了多年 Java 开发者,我喜欢这门语言。我想在任何话之前先直白地说出这一点,因为这篇文章剩下的部分会听起来像一封分手信,而分手信只有在从好的部分开始时才是诚实的。Java 是好的。语法不挡你的路,工具链是一流的,而二十年来它让我构建了赚钱且基本上不会倒的系统。

然后我开始追逐机器的最后一点性能,我最喜欢的语言变成了我的敌人。

三堵墙

第一堵墙是JNI 税。当你活在延迟曲线的底端,迟早你需要 JVM 不给你的东西 — 一个系统调用、一个 mmap 的区域、一个带恰好你想要的排序的原子操作。Java 的答案是 JNI,而 JNI 的答案是边境上的收费站:编组、钉住、过渡记账。较新的 downcall 机制缩窄了过路费;它没有废除收费站,因为平台的整个设计假设"受管"和"原生"之间的穿越是罕见且粗粒度的。对我的调用模式来说它两者都不是。边界坐在热路径上,每秒穿越数百万次,它的固定过渡成本是每次穿越的一部分。

第二堵墙是我从未要求的线程。我的系统按设计是单线程的 — 一个热循环、一个核、钉住且隔离。JVM 对我的设计的意见是:不管怎样,给你一打管家线程。GC 工作线程、JIT 编译线程、引用处理器、finalizer、各种 VM 内部件。你可以调优它们、缩小它们、有时饿死它们 — 你无法移除它们。而它们每一个都是又一次调度器交互,其时机我无法控制,等着抢占我关心的那唯一一个线程 — 而在延迟曲线的底端,最坏的一次发生远比平均的一次重要。

第三堵墙是预热。没有快速预热 JVM 的方法,而且 — 更糟 — 没有可靠地保持热的方法。分层编译最终会给你快的代码,按它的时间表,在足够多的 profile 累积之后。然后一个不走运的分支、一个新的调用点形状,JIT 就在交易日中段把你反优化回解释器。执行你方法的机器码不是一个事实;它是一种心情。我花过真实的工程周去构建预热框架,重放合成负载来哄 JIT 编译对的东西 — 这些基础设施的全部工作就是和我自己的运行时谈判。

每堵墙有相同的形状:平台在你的源码和机器之间插入了行为,而那个行为不是你能移除的。 你没有安排的 GC 暂停,你没有 spawn 的线程,在一个你没碰过的方法底下变化的机器码。对大多数软件来说这是一笔极好的买卖。在延迟曲线的底端,它是一个趁你睡觉时不停重新装修你公寓的房东。

第四堵墙

这是我没预料到的部分:AI 撞上了和我一样的墙。

当我开始认真和编码 agent 一起工作时,我注意到它们写 Java 很出色,却在优化它时奇怪地糟糕。我花了比愿意承认的更长时间才看清为什么。agent 在一个循环里工作:改点什么、测量、对差值推理、重复。那个循环假设目标静止不动。在 JVM 上,目标从不静止。把基准跑两次,JIT 编译得不一样,GC 在不同时刻触发,一个管家线程偷走不同的微秒。agent 看到噪声,对噪声发明一套理论,"修复"它,而修复什么也没改变 — 或者改了别的东西,又催生一套新理论。我看着一个 agent 追一个幽灵回归追了三次重构,而实际变量是编译层。它不是蠢。它是在不可靠的数据上做可靠的推理。

人类也撞这个 — 我们只是有多年伤疤组织告诉我们何时不该信一个数字。agent 没有伤疤组织。一个移动的目标不会减慢 AI;它主动地迷惑它,因为 agent 的核心能力是从证据中得出结论,而平台在制造假证据。

那为我重新定义了一切。我为了延迟原因想移除的那些特性和正在迷惑我的 agent 的那些特性是同一批特性。非确定性调度、依赖 profile 的代码生成、带隐形延迟代价的分配 — 它们每一个都拉宽了代码说什么和机器做什么之间的差距。人类在调试时间里付这个差距。agent 在被腐蚀的推理里付它。

减法,再一次

所以我构建了 SuperJ:Java 的语法,提前编译到原生代码。没有 JVM,没有 JIT,没有 GC,没有线程。我在别处写过设计哲学 — 读起来像 Java,行为像 C — 但有一个我想在这里说得更尖锐的点,因为人们一直在客气地搞错它。

当人们听说一门年轻语言砍掉了线程、反射、lambda 和自动装箱,他们假设这些特性被砍是因为实现它们很难。其中一些确实难。那不是它们消失的原因。SuperJ 移除的每个特性都是因为移除它是对的而移除的,而上面的墙就是证据:

  • 没有线程意味着没有内存模型、没有你没写的 fence、没有管家抢占 — 第二堵墙,按构造消除。
  • 没有 JIT意味着机器码在构建时决定且永不再变 — 第三堵墙不是被解决了,它是无法被提出的。一个 SuperJ 二进制的第一个请求运行和它的第一百万个请求相同的指令。硬件和 OS 仍然有自己的心情 — 缓存、中断、调度 — 但语言运行时从不在你底下重写实验。
  • native 是直接调用 C 没有编组层,因为内存布局是固定且可知的 — 第一堵墙,消失。
  • 没有 GC、没有反射、没有动态加载:运行时里没有任何东西有意见、有议程、或有后台工作。

而第四堵墙和其余三堵一起倒了。确定性结果是我能给 AI 合作者的最大礼物。 我应该精确说明我指的是哪种确定性,因为这个词覆盖太多:不是保证每次运行花费相同纳秒的承诺 — 缓存、中断和 OS 调度器仍然存在 — 而是移除运行时生成的代码、隐藏的并发和延迟的内存工作。这给 agent 买的是一个稳定的实验表面:控制或记录外部输入,一个失败就变成可复现的而非概率性的。在 SuperJ 自己的代码库里测试套件是金标准输出测试:编译程序,运行它,把 stdout 和一个签入的文件做 diff。那种测试风格只在这样一个表面上才可能 — 同一个程序,同样的输入,同样的字节输出,每次运行。当这里的一个 agent 改了编译器而一个金标准测试失败时,diff 是信号 — 不是一次不稳定的线程交错,不是一次 GC 打嗝,不是一次预热伪影。agent 的证据是可信的,所以它的推理收敛而非游荡。我看过这个代码库里的 agent 不靠协助把误编译一路追到 LLVM IR 里一条错误指令。它们能做到是因为没有东西对它们撒谎。

看着那工作是我从一个实践观察推向一个哲学观察的推力。

那么 — 语言现在不重要了吗?

有一个时髦的论点是这样的:如果人类写英文而 AI 写代码,编程语言只是一个没人读的中间表示。随便选一个。语法之争、表达力之争、语言战争 — 全部过时,全部在为人类手指和人类工作记忆优化,而两者都在离开循环。

其中一半是对的,而那是无聊的一半。正在死去的是语言作为人类人体工程学。正在上升的是语言作为验证契约 — 而那是一种升迁,不是退休。

这里是它的核心:一个 LLM 是一个概率性生成器,所以整个系统的价值坍缩到任何检查它输出的确定性层上。 英文无法成为那一层;歧义正是英文用于的东西。提示词不是记录的产物。编程语言是 AI 对你意图的诠释停止是感觉、变成一个精确的可执行承诺的地方。它仍然可能是错误的诠释 — 但现在它是一个可以被检查、测试、拒绝、对照规范比较的东西。移除人类作者,语言就不再是书写工具,而成为意图和执行之间的契约。那是一份比它之前那份更重要的工作。

一旦你把语言看作验证契约,设计准则就以这个项目诚实撞入的方式重新洗牌:

诊断通道变成一个公共 API。 SuperJ 的编译器错误带着稳定的代码 — error[E_BOXED_PRIMITIVE]: ... — 而贡献者文档直说:根据代码分支,绝不根据消息散文,因为散文可能改进但代码永远不会被重命名。那是被设计成 agent 循环的线协议的错误输出。二十年前错误消息是给人类的 UX。现在它们是语言 ABI 的一部分。

语法仍然重要 — 但对模型,不是对人类。 SuperJ 看起来像 Java,而在 AI 时代那不是怀旧,那是工程:Java 是每个模型训练语料里最重度出现的语言之一。"语法不重要"的人群在人类不再关心这一点上是对的 — 而在模型这一点上错了,对模型来说熟悉的语法意味着更强的先验和更少的生成错误。语法不再是人体工程学,而变成了和训练分布的兼容层。"Java 的语法,没有 Java 的运行时"几乎是定义上的 AI 时代语言:在表面最大化模型流畅度,在底下用你可以验证的语义替换它。

但熟悉是一笔贷款,不是礼物。Java 形状的语法也迁移了在 SuperJ 有意分歧之处错误的先验 — 一个 agent 会自信地去拿一个线程、一个 lambda、一个这里不存在的自动装箱的 Integer。这就是为什么语义边界必须异常显式,也是为什么上面稳定的错误代码不是锦上添花:熟悉让 agent 快速到达候选;诊断阻止熟悉硬化为虚假信心。

agent 能检查的语义胜过人类能欣赏的语义。 单线程执行、AOT 编译、arena 内存、确定性重放 — 它们每一个都用一种便利换取行为的可复现和可检查。验证,而非生成,是 AI 构建软件中的稀缺资源。一门给 agent 稳定实验表面的语言,是在那里它们的工作可以被廉价检查的语言 — 也是在那里它们自己的推理保持可靠的语言,因为它们的证据是真的。

而语言不是独自做到这一点的。 源码语言本身不是让 agent 有效的东西。工作单元是语言加上 bolt 在它上面的一切:编译器的限制、稳定的诊断代码、面向 agent 的文档、金标准预言机和单元门、重放日志。有一个词描述这个组装体:harness。编程语言不再仅仅是编码 agent 发射的目标;和它的编译器、诊断、测试和运行时约束一起,它是允许 agent 在其中行动的 harness。"语言作为验证契约"是原则;harness 是用它造出来的机器。

而经济学翻转了。 构建一门语言过去意味着一家平台公司和十年;AI 把一个能用的编译器、SDK 和工具链的成本砍了一个数量级。如果语言正在变得无关紧要,那个成本坍缩不会产出任何东西。相反它在产出语言。未来不是一门通用语言或没有语言;它是更多语言,每一门都是它利基市场的一份有主见的契约,因为铸造一份新契约的成本刚刚跌穿了地板。一个诚实的限定:AI 坍缩了铸造一门语言的成本 — 还没有坍缩赢得信任的成本。语义稳定性、可移植性、一个生态系统、多年不破坏人:那些仍然花费它们一直花费的。

诚实的尾声

在 AI 时代语言无关紧要吗?这和"既然我们有了计算器,数学符号还重要吗?"是同一个问题。作为书写的记号那部分 — 当然,让它去吧。作为精度的记号那部分变成全部的游戏。

我没有着手构建一门 AI 时代的语言。我着手停止付 JNI 税,杀掉我从未 spawn 的线程,在第一纳秒和第十亿纳秒运行相同的机器码。但我为延迟拆掉的每堵墙结果都是我的 agent 和可靠推理之间的一堵墙,而我不再相信那是巧合。给读代码的人类的可预测性和给验证它的 agent 的确定性是同一个属性。Java 不再给我任何一个。所以我构建了一门给我两者的语言,而我奇怪的新同事 — 从未爱过 Java、从未爱过任何东西的它们 — 在其中做着它们最好的工作。

我无法收尾的一个问题:阅读能存活吗 — 而且在哪一层?上面的一切都假设人类有时仍然打开文件。也许那会停止。也许人类最终只读实现之上的层 — 规范、能力、不变式、失败追踪 — 而 agent 拥有下面的源码,源码语言漂向压缩的、机器最优的形式。但注意即便在那个未来里什么存活了下来:必须保留某一层,人类能在其上理解、质疑、并为机器所做的事承担责任。语言在那个世界里不会消失;它向上移动,到人类判断仍然存活的最低层。我怀疑我们很快到那里 — 当东西真的坏了,仍然得有人去看。但那是断层线,而我宁愿命名它也不愿假装它不在。

← 所有文章 尝试 SuperJ →