ARTICLE · 1154606
代码从来不是软件最贵的部分
AI 正在让一件曾经相当昂贵的事情变得便宜:生产代码。
这当然令人兴奋。但它也暴露了软件行业一个长期被混淆的问题:我们总以为编写程序和创造软件是同一件事。
代码可以今天生成,明天重写。软件却需要保存昨天的订单,遵守上个月的协议,并在明年继续处理今天还没想到的情况。
前者是实现问题。后者还涉及时间、规则、历史和责任。
我因此开始怀疑,软件工程长期以来最重要的工作,是否被源代码这种最显眼的产物遮住了?
如果未来代码几乎可以随时重新生成,那些不能轻易重新生成的东西,究竟是什么?
一、复制一个网站以后,麻烦才刚刚开始
假设今天我想复制一个购物网站。
我把网页截图交给 AI,要求它重新实现相同的界面。很快,一个看起来几乎一样的网站就出现了。商品图片排列整齐,购物车可以添加商品,付款按钮也能正常点击。
这在过去可能需要不少开发时间。现在,至少对于某些相对简单的界面和交互,AI 已经可以大幅降低实现成本。
可如果我真的想经营这个网站,问题才刚刚开始。
用户付款成功了,但库存同时被另一个订单扣减,应该怎么办?
用户提交订单以后,连续点击了三次付款按钮,系统应该创建三个订单,还是只处理一次?
商品已经发货,用户却申请退款,运费、优惠券和库存应该怎样处理?
更麻烦的是,这些规则并不是永远不变的。
上个月满减优惠可以与会员折扣叠加,这个月不行了。去年某类商品允许七天退货,今年因为政策变化需要调整。但系统仍然必须正确处理那些发生在旧规则下的订单。
此时,真正困难的已经不是画出一个购物车按钮,而是决定按钮背后的每一种状态变化究竟意味着什么。
当然,我不是说 AI 无法实现这些功能。只要规则足够明确,很多功能同样可以由 AI 编写。
问题在于:规则从哪里来?如果不同部门对同一规则理解不同,谁来决定正确答案?当一项新需求和过去的业务约定发生冲突,又应该保留什么、改变什么?
复制一个网站的外观,和重新创造一个能够承担相同责任的业务系统,显然是两项不同的工作。
而我越来越觉得,这个区别可能比我们过去想象的更重要。
二、软件工程到底在和什么作战?
1987 年,Fred Brooks 发表了著名的《No Silver Bullet》。
他把软件开发的困难区分为两类:一类来自实现手段,例如编程语言、工具和开发环境;另一类则来自软件所要表达的概念结构本身,包括复杂性、与外部环境的符合性、持续变化以及结构难以直观呈现等问题[1]。
这个区分放到今天,突然有了新的意义。
AI 编程首先在改变什么?
至少有相当一部分,是把人的想法转换成具体代码的成本。
过去开发者需要查阅文档、编写接口、处理语法细节、寻找函数、修改样板代码。现在,这些活动中的许多工作可以交给 AI。
但一个更困难的问题仍然存在:我们如何知道自己究竟想让程序做什么?
比如,一项业务需求写着:
“会员可以获得优先退款服务。”
这句话对于人类产品经理可能已经足够用来开会了,对于一个必须准确运行的软件系统却远远不够。
优先多久?
所有会员都优先,还是按等级排序?
退款申请已经进入人工审核队列的订单怎么办?
如果付款来自第三方支付服务,退款失败又如何处理?
这些不是语法问题,而是业务规则尚未被完整定义。
不过,Brooks 的区分也不能直接拿来证明:只要 AI 消除了编程成本,剩下的一切就必然无法自动化。
2006 年,Ben Moseley 和 Peter Marks 在《Out of the Tar Pit》中提出了一个值得重视的不同判断。他们认为,许多软件系统的复杂性其实来自可变状态、控制流程以及其他实现选择,并不全是问题本身不可避免的困难[2]。
这个反驳很有意思。
假设一个业务规则本来就很复杂,我们当然不能靠换一种编程语言让它消失。
但如果复杂性来自同一份订单状态被五个组件分别保存、更新和解释,那么通过重新设计数据关系与状态处理方式,复杂性就有可能显著下降。
也就是说,复杂性未必永远固定地属于“问题”或“实现”。我们如何表示问题,本身就会改变我们必须面对的复杂性。
这让我觉得,软件工程最重要的任务之一,或许不是不断寻找更强的编程工具,而是分辨:哪些复杂性确实来自现实,哪些是我们表达现实的方式额外制造出来的。
三、抽象究竟替我们省掉了什么?
软件工程一直在发明各种抽象。
数据库让开发者不必自己管理磁盘上的每一个数据块。前端框架让开发者通过组件组织界面。身份认证服务可以承担密码存储、令牌签发和登录状态管理。云计算服务让团队不必亲自采购、安装和维护服务器。
这些技术确实非常有价值。
但它们降低的成本并不相同,也不是简单地让复杂性消失。
比如使用第三方支付接口,开发者可能不再需要自己处理银行卡清算的全部细节,却必须理解支付服务的回调、失败状态、重复通知和退款规则。
数据库隐藏了底层存储的很多细节,却没有替业务系统决定什么叫一笔有效订单。
云计算服务减少了服务器采购和日常运维的负担,却可能引入特定服务接口、配置方式、计费规则与数据迁移约束。
John Ousterhout 在《A Philosophy of Software Design》中强调,良好的模块设计应当通过简单接口封装较多实现复杂性,从而降低其他开发者需要面对的认知负担[3]。
这个思想非常有说服力。
但我想继续问:一个复杂性被隐藏起来以后,它究竟在什么意义上消失了?
如果接口准确、稳定,且能够覆盖使用者需要关心的行为,隐藏实现细节是一种真正的进步。
可如果一个接口看上去非常简单,却隐含了大量没有说清楚的使用条件,那么开发者得到的可能只是暂时的轻松。
直到某天出现故障,他才突然发现,自己必须去理解那些原本被告知“不需要关心”的细节。
这并不意味着抽象是一件坏事。
恰恰相反,好的抽象非常珍贵。
只是它的价值不能只看隐藏了多少代码,还应该看:它是否准确保存了使用者真正需要依赖的约定。
我甚至觉得,抽象最值得研究的地方,并不是它能够藏起多少东西。
而是它知道什么东西不能藏。
四、源代码真的保存了一个软件的全部知识吗?
1985 年,Peter Naur 写过一篇令我印象很深的文章:《Programming as Theory Building》。
他的核心观点是,编程活动的重要成果不仅是程序文本,更是程序员在工作过程中建立起来的、关于问题如何通过程序得到解决的理解。他把这种理解称为 theory[4]。
这里的 theory 并不是物理学意义上的宏大理论,而是一种能够帮助开发者解释系统、判断修改是否合理、继续应对新问题的知识。
这个观点尤其适合用来理解软件维护。
假设两支团队拿到了完全相同的源代码。
第一支团队参与过系统设计,知道为什么某个接口必须保留旧字段,为什么一种看似多余的检查不能删除,以及为什么两项功能不能同时上线。
第二支团队只拿到了代码仓库。
两支团队拥有相同的程序文本,却未必拥有相同的修改能力。
第二支团队可能觉得:
“这段逻辑看起来没有用了,删掉。”
第一支团队则知道:
“别动。三年前有一类订单还依赖它。”
问题在于,后者知道的那件事,未必完整写在源代码里。
它可能散落在设计文档、历史需求、数据库记录、测试用例、用户反馈和过去的工程决策中。
当然,Naur 的观点并不能证明软件知识永远无法被形式化,或者必须永久依附于某位程序员。
恰恰相反,我觉得今天更值得思考的,是我们究竟能把多少这样的理解保存下来。
业务约束能否写成机器可检查的规范?
设计决策能否和代码一起维护?
历史规则能否在发生变化时被明确记录?
测试能否不仅验证某项功能正常工作,还能保护那些不能轻易改变的行为?
当 AI 能够持续阅读代码、分析提交历史、生成测试并解释系统结构时,它或许也有机会参与维护这种理解。
但这要求我们承认:代码仓库里的程序文本,并不自动等于关于系统的全部知识。
五、如果代码不是中心,我们应该保存什么?
其实,软件工程很早就在探索把重要信息从具体代码中分离出来。
模型驱动工程就是一条路线。
它尝试用更贴近业务领域的模型来表达系统的重要结构、行为或约束,再利用转换工具生成部分实现[5]。
这样一来,同一套模型可能成为理解和生成不同实现的依据。
但这里有个问题:如果模型本身遗漏了一条关键业务规则,那么从它自动生成的代码即使完全符合模型,也仍然可能不符合现实需求。
我们只是把错误从代码里提前搬进了模型。
另一条有意思的研究路线来自终端用户软件工程。
2011 年,Ko 等人的综述讨论了大量并非专业程序员的人如何创建程序,例如教师使用电子表格处理成绩、其他领域的工作者编写辅助自己工作的工具。这些人同样会遭遇需求理解、设计、测试、调试和维护等问题[6]。
这意味着,一个人不需要成为专业程序员,也能开始创造软件。
但能够创造程序,并不会让软件工程问题自动消失。
今天的 AI 把这条路线又向前推了一大步。
以前,不懂编程的人可能需要通过电子表格公式、可视化编辑器或特定领域的配置工具表达需求。
现在,他可以直接使用自然语言描述自己想要什么。
这当然是一次巨大的能力扩展。
可是当一个从未接受过软件工程训练的人,能够独立创建越来越复杂的业务系统时,谁来帮助他发现需求中的矛盾、识别重要的状态转换、检查安全约束,以及判断一次修改是否破坏了过去的行为?
如果这些工作同样能够由 AI 完成,那很好。
但这说明我们需要让 AI 承担更完整的软件工程任务,而不只是提高代码生成速度。
例如,2024 年的 SWE-agent 研究表明,为语言模型提供适合操作代码仓库、编辑文件、运行测试的交互接口,能够提高它在特定软件工程基准任务上的表现[7]。
这说明自动化的边界确实正在扩大。
但完成一个代码仓库中的修复任务,与长期负责一个真实业务系统,仍然不是完全相同的评价目标。
从这个角度看,我并不认为软件工程会因为 AI 而失去意义。
我更怀疑,它会被迫重新解释自己的意义。
六、软件最贵的部分,可能是它已经发生过的事情
到这里,我想回到文章开头的问题。
如果未来代码越来越容易重新生成,那么什么东西仍然昂贵?
我认为至少有三种。
第一,是对现实规则的准确理解。
一笔订单什么时候成立、什么时候可以取消、退款金额如何计算,这些规则不是由代码的语法决定的。它们来自业务、制度、用户关系和具体情境。
第二,是无法随意清零的历史。
系统已经处理过的交易、签订过的约定、保存的数据和向用户作出的承诺,不能因为开发者想重写代码就自动重新开始。
代码可以重构,财务账本没有“重新开一局”的选项。
第三,是维持系统行为一致的能力。
一个成熟软件系统需要让过去与未来相容:新功能不能随意破坏旧数据,新接口不能在没有协调的情况下改变约定,新的自动化工具也不能因为运行得更快,就免除对结果的验证。
这些工作并不意味着每一步都必须由人亲手完成。
相反,它们同样可能成为下一代软件自动化的重要对象。
我们完全可以想象这样的开发过程:业务规则被明确记录并持续更新,重要约束能够自动验证,AI 根据规范生成或修改代码,测试和运行记录不断帮助系统发现自身行为与预期之间的差异。
但即使有一天这一切都高度自动化,我们依然需要回答:谁来确定系统应当遵守什么规则?发生冲突时,什么才算正确?自动化运行的结果,最终由谁承担责任?
这些问题不会仅仅因为编程速度提高就自行得到答案。
所以,《代码从来不是软件最贵的部分》并不是一个严格的成本统计结论。对于某些技术难度极高、业务规则却相对明确的软件,代码实现当然可能非常昂贵。
我真正想表达的,是另一种判断:
我们过去太容易把软件看成程序员生产出来的代码,而忽略它其实还保存着一套不断变化的现实关系。
代码是这些关系的一种实现。
却不是这些关系本身。
当 AI 让代码生产越来越便宜时,也许我们终于有机会重新分配注意力:少关心一点今天写出了多少行代码,多关心一点,我们是否真正知道系统应该做什么,以及明天修改它时,哪些东西绝对不能被随手丢掉。
想到这里,我对软件工程的兴趣反而变得更大了。
因为我开始怀疑,软件工程最值得研究的下一步,可能不只是怎样更快地创造程序。
而是怎样让一个复杂系统在不断变化中,仍然能够保存自己需要保存的东西。
毕竟,写出一个程序只是开始,让它在不断变化的世界里继续正确地工作,才是软件工程更漫长的任务。
参考文献
[1] Brooks, F. P. (1987). No Silver Bullet—Essence and Accidents of Software Engineering. Computer, 20(4), 10–19.
[2] Moseley, B., & Marks, P. (2006). Out of the Tar Pit.
[3] Ousterhout, J. (2018). A Philosophy of Software Design. Yaknyam Press.
[4] Naur, P. (1985). Programming as Theory Building. Microprocessing and Microprogramming, 15(5), 253–261.
[5] Schmidt, D. C. (2006). Model-Driven Engineering. Computer, 39(2), 25–31.
[6] Ko, A. J., et al. (2011). The State of the Art in End-User Software Engineering. ACM Computing Surveys, 43(3), Article 21.
[7] Yang, J., et al. (2024). SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering. Advances in Neural Information Processing Systems, 37.