2025 年初,我写了一篇《Claude 是编译器吗?》的文章。当时我的回答是:我不知道。
现在我很确定答案是“不,这是个范畴错误,它比编译器更厉害。”但这需要稍加解释。
计算机程序以极其复杂和挑剔著称。程序以极高的精度运行。不存在“挥手”这样的 CPU 指令。与此同时,高层次的目标又极度缺乏具体说明。
在一个高度模式化的世界观里,软件是按层级构建的,每一层都增加具体说明,并隐藏“不必要的”细节。愿景变成战略,产品计划变成编码计划,代码变成二进制文件。每一步都由不同的角色处理:高管、副总裁、产品经理、架构师、工程师、编译器。
关键在于,每一步都涉及做出大量决策。这就是提高具体说明程度的意义所在。(这也是我招聘工程师的两个关键指标之一是判断力的原因。另一个指标是和气。)
最底层,从源代码到二进制文件,是编译器的工作。编译器会做出大量决策!内联、寄存器分配、是发出警告还是直接拒绝程序。而这些决策至关重要:它们影响着性能、系统稳定性、可预测性和故障模式。编译器工程师的工作就是让编译器持续做出良好的决策。
一个优秀且值得信赖的编译器能让软件工程师免于做出这些决策。大多数工程师对编译器的工作原理知之甚少;他们不需要了解这些也能高效工作。
在 2025 年,我们处于一个使用大语言模型生成小块代码的世界。在这种思维模式下,编码智能体可能会作为一个新层级,插入在软件工程师和传统编译器之间。它将自然语言“编译”成代码,做出决策,从而让工程师无需操心。它的价值与其可靠性及其所能做出的决策规模成正比。
问题是,这种高度模式化的世界观是错误的。抽象会泄露,层级会摩擦。即使它们不会,我们也会故意在上面戳洞。
跨层级工作极具价值;对机械原理的感知至关重要。
帝国大厦之所以能在一年内、预算内(!!)建成,部分原因在于系统性地跨层级协同工作。例如,在决定外墙铬镍钢覆层时:
建筑师、建造商和分包商都认为,没有充分协商,谁也无法独立处理这个复杂的施工技术问题。因此,经过充分的前期讨论后,召开了一次全面会议,参会者包括业主代表、建筑师和建造商代表、负责轧制材料的分包商、负责制造和安装的金属工人,以及在各个准备阶段对所有板材进行检测的检验员。
这话说出来听着特别理所当然。
然而在实践中,我们却系统性地做不到这一点。我简直能想象那些金属工人有多高兴,因为他们有机会引导设计走向一个不会让他们干得又慢又痛苦的方向。
我们失败的部分原因在于,根本不知道哪些事情值得去问。最优秀的高管之所以对自己行业有深刻理解,是有原因的。我也怀疑其中一部分是轻视心理("一个流水线上的金属工人能告诉我什么?")。但很大一部分原因还在于沟通和组织上的开销。层级的存在是有原因的——信息隐藏使得组织能够规模化。
Claude 比编译器更胜一筹,因为它能垂直贯穿整个技术栈。大语言模型现在能讨论战略、产品、架构、代码和机器码。它(目前还?)不能像经验丰富、专注的人类专家那样出色地完成大多数单项任务,但它能完成所有这些任务,而且无需安排会议或请求许可。
这里有一个具体例子。
exe.dev 的虚拟机有漂亮的域名:vm-name.exe.xyz。当我们启动一台新虚拟机时,我们会添加一条或几条 CNAME 记录。很简单,对吧?
但我们的虚拟机启动得很快,快到即使我们在创建虚拟机之前就创建了 DNS 记录,用户仍然需要干等着 DNS 传播,有时这需要几分钟,而不是几秒钟。
我们做了显而易见的事:自己编写了 DNS 服务器,这样 DNS 总能立即与数据源保持一致。从此生活就美好了。
但延迟至关重要,因此我们增加了区域。就这样,DNS 再次成为瓶颈,因为所有 DNS 都从俄勒冈州提供服务。此外,部署操作还导致了短暂的 DNS 中断。为了解决这个问题,我们现在所需要的只是一个地理分布但完全一致的 DNS 服务器。
我们做了任何明智的工程师在面对难题时都会做的事:走捷径。我们通过“氛围工程”构建了一个针对我们特定需求定制的分布式 DNS 服务器。
目标很明确:降低远离俄勒冈州的用户的延迟,并提高正常运行时间的弹性。但除此之外,其他一切都不明确。我们必须弄清楚所有事情,从我们想要的确切行为(尤其是在各种故障条件下),到它如何融入我们公司的整体计划,再到能够最好地实现这些目标的架构,一直深入到具体的实现细节。
我们当面敲定了最高级别的战略和架构决策。我们将构建一个相当通用的 DNS 服务器,然后在其上叠加我们特定的行为调整,采用中心辐射模型,使用仅追加的复制策略,并在边缘节点实现持久化。
剩下的就是实际构建它了。
我让大语言模型研究分布式 DNS 系统的标准设计,向我讲解 DNS 的内部机制和特性,指出历史上的安全缺陷,探索替代实现策略(AXFR/IXFR?不用了,谢谢),研究开源方案,推演故障模式,并规划测试策略。
一旦我有了一个看起来有希望的初步设计草图,我就提示多个并发的智能体循环来构建整个系统,包括测试和对抗性代码审查。它们提出了大量问题——涉及各个细节层面,从主要的架构方法到逐行的代码问题。当我回答这些问题(或撤销那些让我后悔的回答)时,我慢慢地将所学到的东西转化为非常简洁的书面指导,并将那些被证明是重要的决策固定下来。
然后,我让新的智能体比较已完成的实现,并寻找有趣的差异。令人震惊的是,有多少重要的决策是智能体从未询问过就自行做出的——而且做出的方式各不相同。
这里有一个例子。复制采用了相当直观的方法:通过请求自上一个已知条目以来的所有内容来追赶,然后通过长轮询获取新条目。但有一个棘手的变数:数据库回滚。这种情况很少见,但确实会发生,并且会破坏“仅追加”的约定。
智能体们注意到了这一点,并以截然不同的方式解决了它。我最终确定的设计是给每一行一个“时间线”字段,就像“你生活在哪条时间线?”一样。这些字段是随机生成的,每个针对“自第 N 行以来的条目”的同步请求都包含边缘服务器针对第 N 行的时间线值。如果时间线不匹配,我们就知道历史记录已被篡改,并回退到完整的全新重新同步。
不同智能体构建的系统之间也存在明显的风格差异。Claude 和 Codex 都同意 Claude 创建了一个更优雅的系统,但 Codex 更全面。
我逐一处理了已识别出的主要分歧列表,进行了实验,然后添加了更多的书面指导。
然后,我将整个差异规范分析过程重复了两次。我知道我的那些格言。
计划扔掉一个;无论如何,你都会的。
—— 弗雷德·布鲁克斯
如果你计划扔掉一个,你会扔掉两个。
—— 克雷格·泽鲁尼
到我准备好构建一个最终版本时,我已经积累了一份“伤疤文档”,它经验性地足以指导一个智能体完成大多数重要决策,涵盖各个层面,从高层目标到架构,再到偶尔的底层细节,例如承载负载的并发缓存的数据类型的确切形状。
最终系统包含了单元测试、端到端测试、用于降低生产环境部署风险的影子模式,以及一套由智能体编写、为智能体服务的简洁文档套件。
这总共花费了我大约一周的精力。我阅读的实际代码量微乎其微。
在那之后,我向团队展示了解决方案。我计划启动服务器,然后去度假。当我的同事们向我提出一连串问题——“X 是如何工作的?在 Y 条件下会发生什么?”——时,我发现我可以自信地回答所有问题。(而且我确实去度假了。一个月后的 DNS 事件数量:0。)
这里的 Claude 不仅仅是编译器。我从未把任务交出去,让智能体做出一堆决策来将其付诸实践。那是氛围式编程。
相反,Claude 是一种垂直整合的资源,一个多层级编译器。它跨技术栈工作的能力加速并增强了我做出不同层级决策的能力,包括判断哪些决策是重要的。(大多数单行代码达不到这个标准。)这才是氛围式工程。
我想说,在所有重要的方面,我都理解这段代码。当然,如果现在要我手动编辑它,会有很陡的学习曲线。但我没必要这么做。更重要的是,我能对系统进行推理,与同事交流观点,并指导智能体完成未来的工作。而且,这里留下了一个持久的产物,它封装了设计中那些足够重要、值得记录的核心意图性方面,跨越所有层级,因此应该能在 bug 修复和代码更迭中存活下来。
这个时代的问题之一是:软件工程师需要对他们所工作的系统理解到什么程度?
精心选择的抽象层能提供理解。物理基本定律看似包罗万象,但在解释为什么发生事故时坐大巴比坐小车更好这件事上,它们不如经典力学。
有些软件层正在消亡,因为它们只提供便利,却不提供额外的洞察。(抱歉,Tailwind。我曾爱过你。)但那些能让我们以可理解的方式表达重要决策的软件层呢?它们会留下来。
我们正将更多注意力转移到技术栈上层,但并没有完全放弃底层。智能体并不是一张免票,可以让我们放弃对系统更深层的所有理解。Go 标准库大部分是用 Go 写的,但少数关键例程是用汇编写的。在那里你不能依赖编译器。
软件工程师正在被拉伸。这既令人兴奋又令人疲惫。不过,越来越清楚的是,在不久的将来,氛围式工程就只是……工程。