




























据我对自己过去的总结,以及对比我更晚进入软件领域的人的观察,我发现新来者更容易陷入对技术问题的纠结,工程问题则被忽略了。我并不是说技术问题不如工程问题重要,我仅仅是指出,有不少人并不区分这两类问题,工程问题甚至不在他们的认知范围内,更别提去思考它了。本文我想来总结我对这两类问题的认识,并试图总结一些方法论。我也不是专家,所以肯定会有疏漏和错误,还请不吝赐教。
The central question in how to improve the software art centers, as it always has, on people.
提升软件之道的中心问题,一直都是,以人为中心。
——Frederick Brooks《No Silver Bullet》
技术的定义或许更接近科学原则和科学事实,比方说,Go 语言中有缓冲的 channel 会在缓冲区满的时候阻塞试图向其中写入新数据的协程1。倘若这个表述变成「需要严格控制并发的场合,应该使用带缓冲的 channel」,那么我们就更接近工程的范畴了,但这依旧更像是技术细节。如果说的是「尽管协程很轻量,但这个项目里每个协程的任务都占用 100MB,而每个实例运行在 1C1G 的服务器上,再加上操作系统和程序本身占用的内存,应该用带缓冲的 channel 把并发数控制在 8 以下」,这显然就是工程问题了,尽管依旧是局部的细节。
[1]
协程指代 goroutine,是用户态的、轻量级、装载到系统线程上的并发单元。 ↩︎
如此看来,技术和工程的区别之一可能是接地气与否,也就是该问题是否涉及真实的软件需求、限制和外部环境。我们再来考量一下「工程化」这个词。过去人们常谈「前端工程化」这个问题,如今有不少人痛骂越来越臃肿的 Web,怒吼「 手写 HTML 难道他妈的不就是个网站了吗? 」。前端的过度工程当然是个问题,但后者的愤怒也有失公正。主流的前端库 React ,尽管我不太喜欢,但它在刚推出时的确解决了不少问题。组件(Components)在 HTML 之上增加了一层抽象,很好地包装并隔离了大型 Web 应用的复杂度。手写 HTML 当然能用,但用老三件套手写 Facebook 的用户界面实在强人所难——团队不方便分工,太多的细节都被挤在一起,所有人都有可能不小心碰到别人的代码。如果一个 HTML 页面冗长得不可理喻,要修改根本无从下手,这就使得工程无法进行下去。我从来不觉得 React 等前端库的出现是「技术进步」,它们只是「工程优化」。
真正的问题是,某些网站根本不能算得上是 Web 应用,竟然也要用一大堆 NPM 依赖写 SPA,这反而是对工程问题没有思考导致的——由于没有明确问题的规模(以及考虑未来是否有可能扩大规模),甚至根本没有考虑规模问题,就使用流行的、过度抽象的前端框架,消耗过多的计算资源。在过度抽象和极端简化的两边,都能观察到对工程考量的缺失。
接地气要考虑的东西其实很多,而技术问题顶多关乎设计和实现。我不得不说,尽管我很讨厌 Java 这门语言,但在具体工程中使用 Java 有明显优势:会 Java 的人实在是太多了,招人和组建团队非常容易;同时,遇到问题也更容易搜索到已有的解决方案;生态更成熟,JVM 也是很稳定的运行环境。当然也不能只看好处,选用 Java 的代价就是:JVM 启动需要时间,如果需要频繁停启,就会成为瓶颈;Java 的语法很啰唆,不仅如此,语言本身也有诸多限制,不够灵活,需要编写和维护的代码量很多,结构复杂,详见《
从 CLOS 审视 Java 面向对象编程
》;会被 Eltrac 嘲讽;Java 的内存占用不小,尤其是线程本身的体积,在高并发场景下表现远不如 Go 语言。如果服务跑起来就很少停止,不排斥 Java 的语法并能使用清晰的分层设计,业务逻辑大部分都是增删改查,并不消耗多少资源,那选择 Java 就是合理的,没必要花钱雇佣熟悉其他技术栈的软件工程师。
很丑陋,不是吗?没办法,现实世界就是丑陋的。我在以前很容易陷入某种精神洁癖(或许现在也是),我不允许丑陋的东西出现在我构造的软件中,二进制文件就一定比 .jar 好,Lambda 表达式就一定比函数指针好,一切都应该语义化,以至于我总是在思考软件本身,而不是软件应该要解决的问题。同样的精神洁癖也出现在年轻的作家当中:
当我在二十多岁开始写作时,我认为文学的目的是改变现实的样貌,剥离其物质层面的东西,无论如何都不应该写人们所经历过的事情。比如,那时我认为我的家庭环境和我父母作为咖啡杂货店店主的职业,以及我所居住的平民街区的生活,都是“低于文学”的。
——安妮·埃尔诺《写作是把刀》
软件不能是纯粹的软件(至少那些要写出来给人用的不是),就像文学很难是纯粹的文学(文学总是和社会现实、历史环境、哲学思想和真实的人的情感纠缠在一起,没有办法把人的事情剥离得只剩文字)。有很多人试图把软件变得不像软件,把文学变得不像文学,但另一个极端也同样值得批评。
接地气其实是面对真实业务场景的权衡取舍,对此,图灵奖得主 Frederick Brooks 在上世纪八十年代就写文探讨过。《 No Silver Bullet 》在软件工程领域也算是经典之作了。银弹(Silver bullet)源自欧洲民间神话,在很多故事里,用银制成的子弹是对付狼人、吸血鬼和女巫等超自然生物的唯一手段2,如今用于指代应对复杂问题的简单且万能的答案。《No Silver Bullet》说的是:开发软件没有简单且万能的解决方案。
(一种尖细、刻意、做作的声音)“现在‘AI’时代不一样了,什么都能用‘AI’写!”“上 K8S 就好了,用 Kafka 就好了!”“我必须要用 Vue / React / Hono / Ember.js / Modern.js / HTMX 写这个网站!”“你他妈就不能用纯 HTML?你看我这他妈不是个网站吗?”“你他妈的就不能用 Go 写吗?”“我们要燃烧几百美元的 Token 把这个能跑的玩意儿用 Rust 重写一遍!”
等一下,让我再确认一遍…… 嗯,没错,Brooks 的那篇论文是 1986 年发的,那个时候 Taylor Swift 还没有出生。现在…… 是的,我没记错,已经是 2026 年了,而 Taylor Swift 已经发了十五张专辑……
吸气四秒,屏息七秒,吐气八秒。
呼,我们继续。
Brooks 在文中引用了亚里士多德在《物理学》3中提出的偶然性(accidents)和自发性(essence)4两个概念,这篇论文的副标题就叫作 Essence and Accidents of Software Engineering(软件工程的自发性和偶然性)。他认为软件工程无银弹的事实是由软件的内在属性决定的,这种内在属性源于软件的本质:一种抽象的概念构造。
[3]
这里的 Physics 译作物理学可能不够准确,读者只需明白这是个形而上学概念就好。我印象中物理学这个译名貌似是个错译,又或者是误会,我没深究。 ↩︎
[4]
偶然性和自发性两个译名参照这篇中国的 论文 。不过,essence 译作「内在性」或许更容易理解。 ↩︎
I believe the hard part of building software to be the specification, design, and testing of this conceptual construct, not the labor of representing it and testing the fidelity of the representation. We still make syntax errors, to be sure; but they are fuzz compared to the conceptual errors in most systems.
If this is true, building software will always be hard. There is. inherently no silver bullet.
我相信构建软件的难点在于为这种概念构造制定规格、设计和测试,而不是将它表示出来并测试这种表示是否准确所需的劳动。
如果这是真的,构建软件永远都会很难。软件在本质上,就是没有银弹。
在 Brooks 眼中,软件工程的自发性问题无法得到解决,这些问题主要有四个:
[5]
「随机的」原文为 arbitary,这个词其实很难翻译成中文,它对应的中文翻译还有「武断的」「偶然的」「随心所欲的」。我对 arbitary 的理解是:如果某个现象没有更深层次的解释,没有理由地存在着,换成别的东西似乎也合理,不一定非得是它,那它就是 arbitary 的。 ↩︎
以上是软件内在的自发性问题,而那些偶然性问题,据 Brooks 称,已经被解决了。高级编程语言屏蔽了底层细节,消除了低级的复杂性;分时系统极大减少了软件的编译和执行时间,总的来说是缩短了系统响应时间;统一的编程环境使得人们可以一起使用、集成和复用相同的软件库6。这些解决方案我们已经习以为常,不觉得是问题,以至于我们看到新问题的新解决方案时,以为他们是银弹,而实际上它们仅仅针对偶然性问题。
[6]
顺带一提,Brooks 在这里举的例子是 Unix 和 Interlisp,后者是围绕 Lisp 语言构建的编程环境。 ↩︎
近乎每次考古计算机软件领域的论文时,都会忍不住再次引用安德烈·纪德:一切被言说之物都已经被说过了,而人们会忘记,所以一切都必须再次言说。Brooks 在接下来的一章里讨论了「银弹的希望」(Hopes for the silver),其中涉及人工智能、专家系统和自动化编程。上世纪人们对 Artificial Intelligence 这个词的印象和现在当然完全不同,此处的 AI 有两种定义,而 Brooks 采用的是第一种定义「使用计算机解决在以往只有人类智能才能解决的问题」,第二种定义的 AI 被它称作「专家系统」,也就是启发式的基于规则的计算机程序,本质上是通过观察现实中专家的行为方式在总结并抽象成的一套规则集。那个时候神经网络算法其实已经出现了,也被应用到了计算机中,但大模型还没有影子,所以文中没有提及。不过他的一些建议放在如今的 LLM 辅助编程背景下,也依然成立。
Brooks 没能想象能自己设计软件并自己写代码的编程智能体(Coding agents)的出现,但文中的「专家系统」和「自动化编程」,若是组合起来,就和如今的 Agents 很相似了。专家系统是对软件设计和完整工程进行总结后,用于给软件开发给出指导建议的系统;自动化编程则顾名思义,即输入软件规格的描述,然后从已有的代码库中选择使用,加速开发。如果把专家系统的输出直接喂给自动化编程系统,我们就得到了如今会自己做计划、写 ToDo,然后写代码实现,甚至还会自己做测试和验证的编程智能体,尽管结构上很不一样。
对于专家系统,或者说 LLM 给建议和制定设计方案的功能,在 Brooks 看来也不是银弹。他写道,这种系统可以帮助没有经验的程序员积累经验,分发好的软件开发实践。他认为开发这种系统的前提是要有这样的专家存在,而从如今 LLM 训练的方式来看,激进的爬虫从互联网各个角落扒拉下来的内容很难说得上是可靠的。我们也知道 LLM 生成的代码不一定是最佳实践。即便是理想中的,能够给出最佳建议的专家系统,帮助也非常有限。
对于自动化编程,Brooks 表示在 40 年前(换算到现代,应该有 80 年了)人们就开始想象能够自动编程的系统,对此,他引用了 David Parnas(另一位软件工程领域的先锋人物)的话:
In short, automatic programming always has been a euphemism for programming with a higher-level language than was presently available to the programmer.
简单来说,自动化编程一直是对用比程序员如今可以用到的语言更高层次的语言编程的委婉语。
Brooks 给“自动化”打了引号,因为编程不可能是自动化的。哪怕是如今的 LLM 编程,程序员也需要用自然语言(或许是软件领域里抽象层次最高的一种语言)输入指令。只要程序员还需要思考和输入,就不可能解放双手。软件的内在属性(一种概念构造)就意味着开发软件永远不可能不思考和描述。
If, as I believe, the conceptual components of the task are now taking most of the time, then no amount of activity on the task components that are merely the expression of the concepts can give large productivity gains.
只要任务的概念部分是眼下最消耗时间的(我也这么相信),那么无论在仅仅是表达这种概念的部分上费多少时间精力,都不能产生巨大的生产力提升。
也就是说,无论怎写代码上费多少心思,都无法影响软件在需求和设计阶段所耗费的时间,而这些时间才是真正的瓶颈。在真实软件开发中,人们可能花费数日甚至数周敲定一个所有人都挑不出毛病的方案,即便他们使用了 LLM 辅助设计。如果软件有明确的客户,那么在与客户的沟通上会出现更复杂的问题,LLM 对此唯一有用的帮助,就是快速生成原型产品(Prototype)并多次迭代,以确认用户的需求,或者验证技术方案的可行性。
Brooks 认为为数不多有可能直击软件工程的自发性问题的东西有几个:
前面三条或多或少成为了行业共识,但最后一条是最重要的。
A little retrospection shows that although many fine, useful software systems have been designed by committees and built by multipart projects, those software systems that have excited passionate fans are those which are the products of one or a few designing minds, great designers. Consider Unix, APL, Pascal, Modula, the Smalltalk interface, even Fortran; and contrast with Cobol, PL/I, Algol, MVS/370, and MS-DOS.
稍微回顾历史就能发现,尽管许多良好、有用的软件系统是由委员会设计并由多部分项目构建的,但那些令富有激情的粉丝为之激动的软件,是一位或几位懂设计的头脑的产物,那些优秀的设计师。想想 Unix、APL、Pascal、Modula、Smalltalk 接口,甚至 Fortran;反例是 Cobol、PL/I、Algol、MVS/370 和 MS-DOS。7
[7]
我貌似还没有见过不批评微软的优秀软件工程师。 ↩︎
段落的最后出现了一些如今人们已经不熟悉的概念,或许现代的版本是这样的:想想 Linux、Lisp、Go、Rust、WebAssembly,甚至 Python;反例是 Java、JavaScript、PowerShell 和 Windows。
Brooks 认为软件工程领域能做的最重要的事情,就是培育更多优秀的设计师,从目前的大学教育和行业现状来看,大概没人把他的话当回事儿。
软件工程的自发性问题或许与工程问题相关,而技术细节仅仅是偶然性问题。当人们在不断迭代技术方案时,他们就能同时体会到软件的复杂性、遵从性、易变性和不可见性——必须花很多时间描述系统的设计,并在沟通交流中确认彼此的意思,因为软件系统很复杂;必须考虑到外部的硬件限制和使用场景,保证某项技术的选用是合理的,因为软件需要遵从外部环境;技术方案要考虑到后续软件变更的难易程度,以及可扩缩性(scalability),因为软件一定是易变的;架构层次图、时序图和数据流图都很难描述清楚软件的实际结构,UML 仅仅是作为沟通工具存在,因为软件在本质上是不可见的。这些软件工程的自发性问题(或者说本质问题)都是工程问题。
在设计阶段不会考虑或者很少考虑的具体实现和技术细节,往往都是偶然性问题,找到现成的解决方案很容易,就算没有,也可以花时间找到解决方案(而之后遇到相同的问题时,还可以复用这个方案)。比方说,一个并发系统有多个线程执行数据库写入操作,频繁等待锁使得 CPU 的利用率下降。这看起来是工程问题,但实际上只是容易解决的技术细节,引入一个专门的线程用来写入数据,其他线程把写入操作放进消息队列就好,只有一个写者就不用等待锁了。这些技术细节往往不需要在设计阶段被考虑,几乎不会影响整体项目的进展。
前些日子和朋友闲聊,他看到有人用 “AI” “一句话”生成了完整的游戏,他感到非常震撼。我其实有很多想要细讲的,但想着还是不要破坏轻松的氛围,所以就把这些思考写进博客里。我想说,LLM 编程并没有解决工程问题和自发性问题。软件的复杂性并没有被消除,只是不再被 Vibe-coders 关心。软件的易变性依旧存在,而容易改变不代表改变起来很容易,倘若设计者本人都不理解软件的复杂性(数据结构、模块划分和各个局部的设计细节),那变更时只会塞满 LLM 的上下文、消耗更多的 Token,直到那一整块奇形怪状的软件结构已经没办法被他的 “AI” 理解。
在 LLM 之前,编写游戏程序早就不是难事。游戏的难点一直是玩法设计和所有软件都要考虑的架构设计。就像 Parnas 说的,“自动化”编程仅仅是用更高层次的语言编程而已,在不能用自然语言编程的年代,高级编程语言和游戏引擎也达成了某种“自动化”。事实是,谁都写得出来,但不是谁都写得下去。想想《僵尸毁灭工程》这个历经十五年还处在 Beta 阶段的游戏吧。
React 写起来并不难,任何编程基础到位的人,都能在不借助 LLM 的情况下写一个简易的虚拟 DOM 引擎,但不是所有人都能统筹大量的开源贡献、与开发团队紧密沟通、收集用户反馈,并且维护一个代码库十三年。好的软件应该是被长期维护的,而这意味着在软件的整个生命周期里,软件工程师都要与复杂性、遵从性、易变性和不可见性作对。无论用手写还是用 LLM 写,扔下一段能跑的代码并为自己感到骄傲,实际上相当幼稚。这段代码再有用,也只是解决了偶然性问题,搞清除了某个技术细节,对工程和软件的自发性问题做出的贡献微乎其微。
我在《 我(不)想让 AI 帮我做 》一文中简略地提及过几类程序员的差别:一类只想要完成和交付他被分配的任务,如果能减少工作量,用什么工具都行;另一类更富有热情,但比起写代码,他们更喜欢创造的感觉,不写代码也能获得成就感;最后一类是我所属的一类,喜欢写代码,理解软件的复杂性本身就是一种智识锻炼和享受。
让我们忽略第一类,他们大多只想谋得生计,对软件工程本身的兴趣不大。后两类尽管有差异,但在某一点上殊途同归:我们都觉得创造软件令人愉悦,只不过一边更在意结果,另一边重视过程和体验。在这层意义上,用一个周末 Vibe code 了一个给自己用的 RSS 阅读器并为自己感到骄傲的开发者,和用几个晚上手写写出了一个已经有更成熟解决方案的软件或算法并为自己感到骄傲的开发者,创造的价值都为 0。后者可能有值得称赞的点,比方说他没有使用大量昂贵的计算资源,也没有给 “AI” 公司花钱,促使他们去消耗更多的土地资源、矿物资源、电力和水资源建造数据中心、把硬件价格炒得没人买得起,而且他完全理解他的软件,要整合到其他系统里就相对容易。可我们不能比烂,事实就是,两者都没有为整个软件生态做出任何贡献。
我不是说没有贡献就是坏的。总所周知,程序员十分热衷于重复造轮子,尽管共识与之矛盾。所有人都需要娱乐,编写软件对我们提到的两类人而言的确是很好的娱乐。如果要把娱乐的体验和经验应用在真实的软件开发中,就要三思后而行了。Vibe code 的确很快,但若是不理解软件的结构,要维护起来就很困难,所以不能在没有任何设计的情况下用几句提示词编写软件,更不能完全不读代码;重复造轮子的确很爽,反复重构和优化代码结构也很爽,但在实际工程中,若是有现成且合适的方案却不用,在影响项目继续推进的情况下过早优化,也不明智。
在我的观察中,有不少程序员是完全无法与彼此沟通的。一个在谈论他的私人项目中使用的新鲜技术,另一个则思考这种技术在实际工程中会遇到的问题;一个在思考某个项目更适合用哪个模型开发,另一个在思考使用什么样的架构和设计模式;一个试图说服对方用 Rust,另一个试图解释自己的团队里没人会 Rust,还有一个半路杀过来让他用 LLM 写 Rust…… 为了更好地沟通和相互理解,也为了更清晰地认识软件工程本身,必须要区分工程问题和技术问题,区分自发性问题和偶然性问题,区分娱乐与职责。
当然,这最多也只能是一种自律罢了。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。