银弹飞过先锋大厦

从软件工程发展历史看大模型时代的软件开发

TL;DR 本文13200+字,全文阅读约需20分钟。其中2680字的示例部分为ChatGPT生成,因此可以说本文GPT含量20%,不过该部分详细内容与本文主旨关系不大,且由GPT-4生成,可略过不看。本文简要回顾了从软件及计算机诞生到当前大模型AI时代的软件工程发展历史,并试图从软件工程历史和ChatGPT实践案例中探讨在大模型时代的软件开发模式。

本文标题来自三个典故:1、标题模式借用伍迪·艾伦的电影名称《子弹飞过百老汇》,内容与电影无关;2、“银弹”出自弗雷德·布鲁克斯的软件工程论文《没有银弹》;3、“先锋大厦”是OpenAI在旧金山的总部所在地。

未来已来,只是尚未均分。

—— 威廉·吉布森(科幻作家)

今年四月参加了QCon Beijing 2024技术大会,令我印象最深刻的是开场微软中国CTO韦青的Keynote,他畅想了在智能时代企业的发展之路。他没有谈及当前的大模型应用,也没有谈及技术或是工程方法。他说的是进化,说的是成长型思维,说的是企业或者说是人应该如何在智能时代生存。人类在迈向智能化社会,而如果企业不转型,那必将被淘汰。企业如何适应智能化时代,韦青用了下面几张对比图来说明,在智能时代,我们不能像在进入汽车时代一样用马车时代的固有思维来设计汽车。这让我为之一振,我比较关心大模型在软件工程上的应用,但之前的思维更多是在聚焦于如何使用智能化来为当前的软件工程工具提效,可当前的软件工程方法都是基于以往的开发经验总结出来的,如果基于当前开发模式来叠加智能化那不正如这张图所形容的一样吗。

carriage-n-automobile.jpg

这同样让我想到另外一个故事:据说最初美国航天飞机的火箭助推器的直径是4英尺8.5英寸。为什么这样设计呢,因为美国铁轨的宽度是4英尺8.5英寸,而运送火箭助推器是通过火车装载,并且中途会经过一个隧道,而隧道的大小刚好比铁轨大一点点。火车轨道宽度的设计是延续自电车轨道而来的,电车轨道又是根据马车宽度来定的,马车宽度又是依据古罗马人铺设的马路而制定的,至于马路的宽度,则是罗马人根据罗马战车的宽度来定的,罗马战车的宽度是依据两匹并行的马的屁股宽度而定。也就是说,古罗马战马屁股的大小决定了航天飞机火箭助推器的直径。

我没有去求证这个故事的真实性,不过从很多产品的设计延续性来看,人们倾向于以经验乃至前人的经验来做设计,就像Office的保存按钮图标是软盘、手机拨号图标是听筒电话一样。但如果需要面向未来的软件开发来设计软件工程和工具,那就需要减少对以往固有经验的依赖,换一种思维模式。我们可以尝试从三个方面来改变思维方式:一是采用“第一性原理”探究事物的本质,二是从历史学习该事物的原理,当前这些特性存在都是前人踩坑后的填充。三是通过实践来验证猜想和推论。

软件工程的本质是什么,我觉得可以引申敏捷宣言里的话语,软件工程就是为了更高质量、更快效率、更低成本的构建“可以运行的软件”。下面我将回顾软件工程的发展历史,并通过对大模型应用在软件工程中的一个小实践,来探讨大模型时代软件工程的可能变化。

一 埃达:第一位程序员

追溯软件工程的历史,先要回顾下计算机和软件的历史。如果问大家两个问题:一、计算机和软件程序哪个先出现?二、计算机(Computer)这个词最初指的是人还是机器?估计很多人会答错。正确答案分别是:软件程序、人。最早的算法由埃达·洛夫莱斯(Ada Lovelace)写成,她是大诗人拜伦的女儿。1834年,巴贝奇构想出一种通用计算机器模型,他将此称为“分析机”,当时没有人能够理解,除了埃达。因为只有埃达能够理解通用计算机器的概念,1843年,巴贝奇邀请她为分析机论文写注解。埃达在写出的大篇幅注解中提出了四个设想,这四个设想后来成为了构建现代计算机的重要理念。第一个设想是对通用计算机器的说明,埃达认为这种机器不仅可以进行预设任务,还可以根据编写和重编程序完成无限数量的可变任务。第二个设想认为通用计算机器的运算不必限制于数字,而是可以存储并计算能用符号表示的任何对象,如文学、逻辑、音乐等。第三个设想则是关于分析机的工作步骤,她以计算伯努利数为例子,描述使用子程序和递归循环等过程来进行计算,相当于我们现在所说的计算机程序或算法。第四个设想则提出了人工智能的问题:机器能思考吗?埃达认为不能。她认为分析机可以根据指令执行操作,但不能产生自己的想法和意图。

由于埃达在注解中提出的第三个设想,以及她写的关于分析机计算伯努利数算法的详细步骤,后人将埃达·洛夫莱斯称为“世界第一位程序员”。1980年左右,美国国防部决定设计一种新的语言来取代当时国防部使用的 450 多种编程语言,设计团队将该编程语言命名为Ada(埃达)。

ada-lovelace.jpg

二 从计算机到编程

19世纪末,哈佛大学天文台台长爱德华·皮克林雇佣了一批聋哑女工,对天文台拍摄的照相底片进行测量和分类工作,她们需要在计算机械上进行大量的数据运算,而这批人也被称为“哈佛计算机”。20世纪初,随着泰勒“科学工作法”的推广和福特T型车流水线作业的发明,工业生产大规模化,工厂对于计算问题的需求也越来越大。那时候一些计算工厂会有一些大房间装满了人类“计算机”(大部分是女性),一排排整齐排列组合,数据从一端输入,经过一个“计算机”计算操作后,再传给下一个“计算机”处理,就像今天计算机的进程处理管道。

真正的现代计算机要等到1945年ENIAC的诞生。约翰·毛克利和赫尔曼·戈德斯坦设计了ENIAC,意为“电子数值积分计算机”(Electronic Numerical Integrator and Computer),而后,他们又根据冯·诺依曼提出的设想设计了世界第一台冯·诺依曼结构计算机EDVAC。EDVAC是ENIAC的继承者,建立了现代计算机的标准:它采用了二进制,并且设计了存储程序,也就是说计算机指令将被存储在存储器中。这种设计思想也将软件与硬件分离,成为独立的存在。

从历史贡献来看,格蕾丝·霍珀(Grace Hopper)是推动“软件编程”革命的第一人。在当时,负责建造ENIAC等计算机硬件的工程师都是男性,而编程工作则交由女性负责,那时候的程序指令会以线缆、开关连接的方式固定在硬件上,每当指令变化的时候,就需要人工编排接线。格蕾丝·霍珀发现了编程的意义所在,她发现软件可以是一个独立于机器硬件的东西,因而设计出了更接近人类表达层面的高级编程语言,从硬件层面抽象出来,也就是COBOL语言,她带领团队所编写的COBOL编译器也成为了世界第一款编译器。有意思的事,霍珀还是“Bug”一词的提出者。有一次她在排查机器故障时,发现是因为继电器上卡住了一只飞蛾造成线路短路导致,她将飞蛾贴在工作日志上,并注明这是首次发现的Bug(虫子),而排除故障的工作也被称为Debug(除虫)。

三 软件工程的诞生

电子计算机诞生以来,硬件能力每年都在飞速提升,尤其在晶体管和集成电路发明之后。相比之下,软件能力的提升却很有限,这导致软件能力无法匹配计算机业务的快速扩张。在上世纪五、六十年代,大型项目的软件出现了周期长、质量低、成本高等一系列问题。为此,北大西洋组织(NATO)在1968年左右组织了一系列会议来讨论软件问题,称之为“软件危机”(Software crisis)。而在会议上,也提出了“软件工程”的相关概念,试图通过一些工程方法来解决软件危机。

“软件工程”一词最早由玛格丽特·汉密尔顿(Margaret Hamilton)提出。她当时参与“Apollo 登月计划”,为了与硬件工程区分提出了软件工程这个词。奥巴马在任时,曾给玛格丽特颁发过“总统自由勋章”,当时玛格丽特的儿子在Twitter上发了张玛格丽特和她编写代码打印稿的合影,让人们了解了这段往事。

margaret-hamilton.webp

四 软件行业的兴起

二战前后,如ENIAC等计算机的构建和应用都由政府或军方主导;而后,IBM推动了计算机在商业领域的流行,商业软件也开始崛起。不过,真正让软件行业壮大的,是个人电脑时代的来临。个人电脑时代由两种力量推动而成。一种是追逐盈利的商业资本力量,在晶体管、微芯片和集成电路发明后,硬件能力每年都在提升,成本也在逐年下降。商业势力看到了这一巨大的盈利机会,硅谷大量的电脑公司如苹果、北极星等纷纷成立,他们的竞争让电脑能够降低成本进入普通老百姓家。另外一种力量是更理想主义的黑客社区力量,源自上世纪六、七十年代的嬉皮士文化,他们执着于信息的自由流通和分享,追求个性独立和自由。这两种力量既矛盾又相辅相成,共同推动了个人电脑的普及。而这两种力量也一直存在,在后来的互联网、移动互联网乃至AI时代都可以看到他们角逐的身影。

个人电脑这一概念最早由万尼瓦尔·布什(Vannevar Bush)于1945年在《诚如所思》一文中提出,他设想这种设备可以成为个人文档库和私人图书馆。布什还是《科学:无尽的前沿》报告的作者,在该报告中他提出了著名的政、企、学三角协作机制,并构建了这一体系,在计算机科学史上促进了很多发明。个人电脑最早在硅谷一个叫“家酿计算机俱乐部”(Homebrew Computer Club)的组织中流行,这是早期的黑客群体之一,很多电脑公司的创始人如乔布斯、沃兹尼亚克等都来自于这个俱乐部。微软创始人比尔·盖茨也曾参与这个组织的聚会,但后来他发表的一封公开信却触怒了整个圈子。

当时微软公司刚刚成立,他们开发的BASIC编译程序被拷贝走了,并在“家酿计算机俱乐部”的黑客群体中流传。这一行为激怒了当时年仅二十岁的比尔·盖茨,1976年,他发表了一封名为《致电脑爱好者的一封信》(An Open Letter to Hobbyists)的公开信,他在信中痛斥自由拷贝软件的行为,说:“硬件必须付钱购买,但软件却被免费共享了。谁会在意为软件辛苦工作的人是否获得了合理报酬呢?”这封信在黑客群体掀起轩然大波,并且在以后的自由软件和开源软件社区中一直被当作反面案例来宣传。但必须承认的是,比尔·盖茨看到了软件的巨大商业价值,也正是他的这一先于时代的洞见,让他引领微软成为世界第一的软件提供商,而他本人也因此获得巨大商业回报成为世界首富。

五 软件工程的发展

软件行业的兴起使得超大型软件密集型系统越来越多,软件也走向全球化。从上世纪六十年代到八十年代,软件工程随着软件行业的兴起而蓬勃发展,这段时间产生了几个重要的软件工程概念。

第一个是软件项目管理,弗雷德·布鲁克斯(Fred Brooks)将他所领导的IBM OS/360项目总结成了《人月神话》一书,书中提出了软件项目管理的一些理念和方法。其中最有名的是这一句:“如果一个软件项目进度已经落后,那么向这个项目中增加人手只会让进度更加落后。”这一观点也被称为“布鲁克斯法则”(Brooks’s law)。《人月神话》揭示了软件项目管理的复杂性,其核心是大型团队的沟通成本。不同于硬件,软件是通过软件抽象来解决业务问题,对于复杂的业务问题只能切分更多的软件任务,引入更多的开发人员,而更多的开发人员也意味着更多的沟通和管理成本。布鲁克斯后来又发表了一篇论文,名为《没有银弹》,他将这种通过软件抽象来解决业务问题的工作称为本质性工作(essential task),而其他将软件抽象转变成可以在机器上运行软件的工作称为附属性工作(accidental task),后者如编译、构建、测试等。在过去的几十年里,通过高级编程语言、分布式、云化等各种技术和工具一直在改进和提升的都是附属性工作,而本质性工作并未得到有效的改进。这是因为软件本身的固有属性决定的,布鲁克斯将其总结为复杂性、隐匿性和易变性。本质性工作大多发生在人们的脑中,这加剧了团队沟通的困难度。因而,布鲁克斯在论文中总结到:“在未来十年内,没有银弹,不会有任何技术或管理上的突破能使软件工程的生产力得到数十倍的提升。”

fred-brooks-mythical-man-month.jpeg

第二个是编程模式的发展,主要是结构化编程和面向对象编程的普及。当时虽然已经开始流行高级编程语言,但编程模式还大量沿用汇编语言思想的goto语句。艾兹格·迪杰斯特拉(Edsger Dijkstra)首先发表论文《goto语句有害论》,而后他与其他软件工程大师们提出了结构化编程模式,使用子程序、条件判断、循环语句来代替goto语句。同时期,面向对象程序设计的理念开始形成。到了七十年代,施乐公司PARC实验室推出Smalltalk编程语言,将面向对象的封装、多态和继承特性引入到编程语言中,而后,C++等语言的成熟将面向对象设计推成编程的主流。结构化编程和面向对象编程的发展提升了软件抽象能力,从某种意义上讲,这是对布鲁克斯所称的本质性工作效率提升的努力。

第三个是开发流程,这时期最有名的发明是瀑布软件开发模型。温斯顿·罗伊斯(Winston Royce)在1970年左右提出了瀑布模型这一软件开发流程,虽然瀑布模型在敏捷软件开发时代备受批评,但它的实际效用是非常明显的。瀑布模型将软件开发过程分为分析、设计、实现、验证、发布等阶段,每个阶段的输出是下个阶段的输入,像分段瀑布一样流转,因此得名。瀑布模型让软件开发过程的各种工作显性化,同时也明确了各项工作的输出件标准,对于大型软件项目来说,易于管理和规范。

六 自由软件与开源软件

比尔·盖茨意识到了软件的商业价值,软件也成为商业公司赚取利润的重要来源,软件不再将源码作为免费产品随硬件售卖附赠,而是编译成二进制发布包对外出售。软件源码不再附赠的情况让一位黑客大感失望,他就是理查德·斯托曼(Richard Stallman, RMS)。他认为信息自由分享的黑客时代传统不应该被抛弃,因此在1984年发起GNU项目,要创建一个完全自由而开放的操作系统来对抗商业封闭的UNIX. 而后他又成立自由软件基金会(Free Software Foundation, FSF),并创造了通用公共许可证(GNU General Public License, GPL)这一软件授权协议。GPL对软件行业的影响是深远的,因为它不仅规定了软件自由分发的权利,还规定了其义务,这也是被称为GPL具有“传染性”的原因。但“传染性”这一词偏负面,实际上GPL对软件自由分发起了至关重要的作用,一些著名的开源软件如Linux、Git都是以GPL协议分发的。

不过斯托曼对于自由软件的执着让他近乎偏执,自由软件社区因此分裂出一批期望更加融入商业社会的人,以埃里克·雷蒙德(Eric Raymond, ESR)为代表,在1998年创立了开放源码促进会(Open Source Initiative, OSI),以商业更友好的开源软件来取代意识形态更浓重的自由软件。雷蒙德在研究了一系列软件项目后,发布了开源软件开发的经典著作《大教堂与集市》,在书中他阐述了两种开发模式,一种是自上而下构建的、集中式的、由专人或专属团队所管控的软件开发方式,这种雷蒙德称为大教堂模式;另一种则是自下而上构建的、分布式的、在互联网上分发传阅的开放的软件开发方式,雷蒙德称之为集市模式。集市模式的代表是Linux项目。

Linux的诞生源于大学时代林纳斯·托瓦兹(Linus Torvalds)暑期的个人项目,他大学放假期间在家无聊,就写了Linux并发布到了互联网新闻组,没想到在黑客圈里广受欢迎,托瓦兹将它无偿开放出来,并广泛接纳社区开发者的代码提交。到了2005年,估计有近一万人给Linux贡献过代码,Linux也成了开源社区最大的项目。Linux项目代表了软件工程的另外一种模式,它没有企业项目那种自上而下的管理,通过互联网分布式协作来实现大型软件项目开发并管控质量。托瓦兹有句名言:“只要眼睛多,Bug容易捉。”(Given enough eyeballs, all bugs are shallow.)

七 万维网及敏捷软件开发

1990年,英国计算机科学家蒂姆·伯纳斯-李(Tim Berners-Lee)和他的工作伙伴罗伯特·卡里奥合作制定了一份计划书《万维网:一个超文本项目的计划书》(World Wide Web: Proposal for a Hyper Text Project),这个名叫万维网(World Wide Web, WWW)的系统将彻底改变计算机行业。互联网(Internet)可以追溯到上世纪六十年代的ARPANET,但一直到了万维网的出现才让它真正成为全球互联网。万维网可以理解为是互联网上的全球信息系统,很多人在概念里其实是将万维网与互联网等同了。在伯纳斯-李的设想中,万维网具备三项技术:全球唯一的统一资源标志符(Uniform Resource Locator, URL),超文本标记语言(HTML)和超文本传输协议(HTTP)。该年年底,伯纳斯-李在他的工作电脑上开发了第一个网页浏览器,实现了HTML解析和HTTP传输,值得一提的是,伯纳斯-李所使用的工作电脑正是乔布斯被赶出苹果后创办的新公司所生产的NeXT电脑。

伯纳斯-李所任职的欧洲核子研究组织希望为万维网申请专利,但伯纳斯-李坚持认为万维网的协议应该免费开放共享,毕竟他的设计初衷就是为了促进分享和协作。后来,欧洲核子研究组织同意了他的请求并采用了GPL协议开放源码。1993年,马克·安德森(Marc Andreessen)开发了第一款商业浏览器Mosaic,网站如雨后春笋般涌现,大型网站如AOL、雅虎等。紧接着,以博客、维基百科为代表的Web2.0开始兴起,用户创造内容让万维网上的数据急剧增加,传统的网站目录方式无法满足用户查找内容的需求,自动化搜索引擎将成为万维网内容的主要查找方式。这也就造了拉里·佩奇(Larry Page)和谢尔盖·布林(Sergey Brin)的Google,取代雅虎成为一代互联网霸主。

万维网的疾速发展使得互联网软件开发呈井喷之态,大量的互联网企业需要快速开发并上线软件应用,并且要根据用户的反馈及时修改以应对需求变化。这种变化使得传统的瀑布开发模式无法满足企业需求,一种新的开发模式应运而生。2001年,十七位软件开发人员在美国犹他州雪鸟滑雪地组织了一场会面,他们中有很多是软件工程大师,包括写作《重构》的马丁·福勒(Martin Fowler)。会上,他们发表了“敏捷软件开发宣言”(Manifesto for agile software development),这其中主要是四句话:“个体和交互胜过流程和工具,可以工作的软件胜过详尽的文档,客户合作胜过合同谈判,响应变化胜过遵循计划。”敏捷软件开发鼓励快速响应变化、迭代开发与变更,它包含了一系列实践,如Scrum、极限编程等。

八 云计算与DevOps

2003年,在亚马逊工作的安迪·杰西(Andy Jassy)发现在亚马逊孵化新业务的过程中,工程师总要把大量时间投入在重复的基础设施构建工作上。他敏锐地察觉到了这一内部机会,并给亚马逊创始人兼CEO贝索斯写了一份商业计划书,在该计划书中构想了亚马逊AWS(Amazon Web Services)云的未来。安迪·杰西带着一个小团队开启了内部创业之旅,2006年发布了云服务S3和EC2。AWS在亚马逊内部的重要性越来越高,2023年利润占亚马逊整个利润的一半多。安迪·杰西一直担任AWS CEO职位,一直到2021年接替贝索斯成为亚马逊CEO. 虽然说云计算这个词在上世纪末就已出现,但AWS是真正将云计算商业化成功的第一个产品。AWS的成功也让众多互联网头部厂商看到机会,谷歌云、微软Azure云等紧追不舍。云计算成为了互联网软件乃至整个IT软件的底座。

云计算的本质是资源弹性,云服务厂商将一个个计算资源连接起来构成一个巨大的资源池,而软件用户只需要租用计算资源,根据业务需要弹性扩缩容。支撑资源弹性的是软件的架构弹性,这种架构模式就是微服务。微服务架构是将庞大的单体结构程序拆分成松耦合的独立组件,以API方式调用,这种架构解耦可以追溯到GoF的《设计模式》和Robert C. Martin的《敏捷软件开发》。另外,GitHub的出现让开源软件更容易流传,开源组件成为构筑现代软件的基本模块,开发软件像是搭积木一样,通过胶水语言将开源组件组装,便可以快速构建业务软件,这种开发模式极大地提升了开发效率。加上云平台提供的软件应用部署能力,从软件开发到部署发布及运维,可以快速在云平台上闭环。这种软件开发模式也被称为云化软件开发。

云化软件开发的工程实践是DevOps,它强调的是开发与运维之间的沟通协作以及更小粒度地发布和更频繁地变更,可以理解为是敏捷软件开发的延伸。Git、依赖包管理、docker、CI/CD等现代化开发工具成为DevOps工程实践的核心:Git的分布式开发方法和版本管理践行软件项目的“Everything as Code”的理念,如基础设施代码化(IaC)、配置代码化、流水线代码化等,团队各角色基于代码协作。诸如Ruby Bundler、Maven、NPM等语言依赖包管理工具使得组件化开发更简单,特别是对于开源组件的引用,一个Node.js工程项目,引用几百甚至上千个开源组件是非常常见的现象,因此网上也有关于NPM依赖黑洞的段子。而Docker、K8s等容器工具降低了微服务开发的门槛,结合CI/CD工具自动化部署,降低运维成本。小结一下,DevOps云化软件开发工程工具实践的几个关键词是代码化、组件化/开源、容器化、自动化,其本质是为了更频繁地变更(发布新特性和修复问题),缩短交付周期。

npm-jokes.jpg

九 大模型时代的软件工程

前文提到,埃达·洛夫莱斯认为电脑不能思考,只会根据指令执行。一个世纪以后,图灵将她的主张写到自己的论文里,称为“洛夫莱斯夫人的异议”。这篇论文便是著名的人工智能奠基论文《计算机器与智能》(Computing Machinery and Intelligence),他在文中还提出了一个被后人称为“图灵测试”(Turing Test)的思想实验,在这个测试中,测试人与人和机器同时用文字交流,但他不知道背后谁是人或机器,如果在一段交流时间内无法通过交流内容来判断区分人和机器,那么就认为该机器通过了图灵测试,也就是说该机器具备与人类相当的智能。一个冷知识是:我们上网时经常被要求输入验证码的英文单词“Captcha”即是“完全自动通过图灵测试来区分计算机或人类”(Completely Automated Public Turing test to tell Computers and Humans Apart)的首字母缩写。图灵自己将此实验称为“模仿游戏”(The Imitation Game),关于他的传记和电影名称则取之于此。

历史上对人工智能的研究主要分成了两大阵营,一派推崇知识和推理逻辑,典型的如专家系统,被称为符号主义,也有称之为“GOFAI”(Good old fashioned artificial intelligence),这一派的发展如该词所言已经是old-fashioned了。还有一派通过模拟人类的大脑构建神经网络,将知识通过机器学习存储在大量的连接中,基于数据学习来发展人工智能,被称为连接主义,这一派的代表是神经网络系统。杰弗里·辛顿(Geoffrey Hinton)在神经网络领域潜心研究三十多年,开创了深度学习这一人工智能分支,而基于深度学习的大模型在近年来发展迅速,尤其在OpenAI的ChatGPT发布后成为全球最热门的技术。ChatGPT让人们看到了AGI(通用人工智能)的可能,它的AIGC能力可以帮助到人们将AI运用在实际的工作和生活中。关于神经网络和深度学习的历史和原理可以参见我的另外一篇文章《从神经网络到 Hugging Face》,这里我想就使用ChatGPT来辅助编码的案例实践来谈一谈我对大模型时代的软件工程的理解。

一个工程实践

问题的背景是:我想做一个类似Perplexity的智能搜索前端页面,但我不太懂前端开发,所了解仅HTML、CSS、JavaScript三项基本知识以及Next.js前端框架而已,因此我希望在ChatGPT(GPT4版本)的帮助下完成前端代码的编写。其中一个场景需求是,用户在输入问题点击搜索按钮后,页面上自动追加并显示答案文字。以下引用和代码部分为我与ChatGPT交流的原始记录,没有做改动。我不是Prompt Engineer,因此Prompt写得比较简陋。(这一段对话比较长,并且回答部分都由ChatGPT生成,不需要了解细节的读者可以快速略过直到“大模型是不是银弹”章节。)

我首先问ChatGPT:

现在你是一名Next.js专家,请帮我实现一段代码,在页面上点击button,在一个div区域内显示一段文字。

ChatGPT回答:

当然可以!下面是一个简单的Next.js页面示例,它实现了你的要求。这个示例中,我们将使用React的状态来控制点击按钮后在div区域显示的文本。

首先,你需要在你的Next.js项目中创建一个新的页面。假设我们将这个页面命名为showText.js,你可以在pages目录下创建这个文件。然后,将以下代码复制到showText.js文件中:

import React, { useState } from "react";

export default function ShowText() {
  // 使用React的useState钩子来管理文本的显示状态
  const [text, setText] = useState("");

  // 定义一个函数来处理按钮点击事件
  const handleClick = () => {
    // 设置要在div中显示的文本
    setText("这是我显示的文本!");
  };

  return (
    <div>
      <button onClick={handleClick}>点击我</button>
      <div>{text}</div>
    </div>
  );
}

这段代码的工作原理是:

使用useState钩子来创建一个名为text的状态变量,这个变量将用来存储在div区域显示的文本。

创建了一个handleClick函数,当按钮被点击时,这个函数会被触发。它通过调用setText函数更新text状态,从而改变div区域内显示的文本。

页面上有一个按钮和一个div元素。按钮的点击事件被设置为调用handleClick函数。当text状态改变时,div中的内容会相应更新显示新的文本。

你可以通过运行Next.js应用来查看效果:在你的项目目录中,使用npm run dev(或者如果你使用Yarn的话,就是yarn dev),然后在浏览器中访问对应的页面(通常是http://localhost:3000/showText,取决于你的文件名和配置)。点击按钮后,你应该能在页面上看到指定的文本显示出来。

ChatGPT不但生成了整段代码,还解释每个步骤,这样我可以理解并按照自己的期望修改里面的逻辑,再按照ChatGPT的调试指导步骤可以直接在本地开发环境中运行。

接着,我将需求更进一步描述:

我需要再显示复杂点,点击button后,div内显示三段文字,每一段延时200ms显示出来

ChatGPT紧接着给出一段:

为了实现点击按钮后在div内逐步显示三段文字,每段文字之间延时200毫秒,我们可以通过使用setTimeout函数来实现。下面是一个简化的实现方式:

首先,定义三段不同的文本作为要显示的内容。

当按钮被点击时,通过设置多个setTimeout,依次在不同的时间间隔后更新状态,从而控制文本的显示。

更新后的showText.js代码如下:

import React, { useState } from "react";

export default function ShowText() {
  const [texts, setTexts] = useState([]); // 使用数组来存储要显示的文本段落

  const handleClick = () => {
    // 清空文本,以防多次点击
    setTexts([]);

    // 定义三段文本
    const textSegments = ["第一段文本显示", "第二段文本显示", "第三段文本显示"];

    // 依次显示每段文本
    textSegments.forEach((segment, index) => {
      setTimeout(
        () => {
          // 更新状态以添加当前文本段落
          setTexts((prev) => [...prev, segment]);
        },
        200 * (index + 1),
      ); // 每段文本延时200ms显示
    });
  };

  return (
    <div>
      <button onClick={handleClick}>点击我</button>
      <div>
        {texts.map((text, index) => (
          <p key={index}>{text}</p> // 使用段落标签<p>来分隔文本段落
        ))}
      </div>
    </div>
  );
}

在这个实现中:

使用了一个数组texts来存储需要显示的文本段落。

每次按钮点击时,首先清空texts数组以重置显示内容。

通过forEach循环和setTimeout,计划在200毫秒、400毫秒、600毫秒后依次更新状态,添加对应的文本段落到texts数组中。

使用map函数在div中为texts数组中的每一段文本渲染一个<p>标签,以此来分隔显示的文本段落。

每次点击按钮时,你会看到三段文本依次出现,每段之间有200毫秒的延迟。

现代前端软件工程推荐强类型语言TypeScript,我让ChatGPT改成TypeScript:

这一段代码请用typescript写一下吧

ChatGPT马上就给我转成TypeScript:

将之前的例子转换为使用TypeScript的形式,主要变化是添加类型注解来增加代码的类型安全性。这不仅有助于防止类型相关的错误,还能提高开发效率,因为TypeScript提供的类型检查和自动完成特性。

下面是使用TypeScript重写的showText.tsx代码:

import React, { useState } from "react";

const ShowText: React.FC = () => {
  const [texts, setTexts] = useState<string[]>([]); // 明确指出texts是字符串数组

  const handleClick = (): void => {
    // 清空文本,以防多次点击
    setTexts([]);

    // 定义三段文本
    const textSegments: string[] = [
      "第一段文本显示",
      "第二段文本显示",
      "第三段文本显示",
    ];

    // 依次显示每段文本
    textSegments.forEach((segment, index) => {
      setTimeout(
        () => {
          // 更新状态以添加当前文本段落
          setTexts((prev) => [...prev, segment]);
        },
        200 * (index + 1),
      ); // 每段文本延时200ms显示
    });
  };

  return (
    <div>
      <button onClick={handleClick}>点击我</button>
      <div>
        {texts.map((text, index) => (
          <p key={index}>{text}</p> // 使用段落标签<p>来分隔文本段落
        ))}
      </div>
    </div>
  );
};

export default ShowText;

这里的主要变化包括:

使用React.FC来声明ShowText组件的类型。React.FCReact.FunctionComponent的缩写,它是一个泛型接口,用于类型化函数式组件。

useState<string[]>([])表明texts状态是一个字符串数组。

handleClick函数声明中使用: void来指明这个函数没有返回值。

使用TypeScript编写Next.js页面组件,可以让你在编译时就捕获到潜在的类型错误,提高代码质量和可维护性。

在我将这段代码复制粘贴后,遇到了编译错误:

编辑器提示报错 “Property ‘forEach’ does not exist on type ‘string | string[]’. Property ‘forEach’ does not exist on type ‘string’.”

让我惊讶的是ChatGPT非常自信的认为代码没有问题,而是给了我其他的debug思路:

这个错误表明TypeScript编译器不能确定textSegments变量在调用forEach方法时是一个数组。在你提供的上下文中,textSegments被明确声明为一个字符串数组(string[]),所以正常情况下不应该出现这个错误。错误信息“Property ‘forEach’ does not exist on type ‘string string[]’. Property ‘forEach’ does not exist on type ‘string’.”暗示在某处textSegments可能被推断为string | string[]类型,即它可能是一个字符串或字符串数组。

考虑到你的代码片段应该没有问题,这个错误可能是由于TypeScript或编辑器状态的某种混乱造成的。尝试以下方法可能会有所帮助:

1. 重启编辑器:有时编辑器或其TypeScript服务可能会进入异常状态,重启编辑器通常能解决这类问题。

2. 检查TypeScript版本:确保你的项目使用的TypeScript版本与编辑器的TypeScript服务版本一致。版本不匹配有时会导致奇怪的类型推断问题。

3. 检查其他代码:确保没有其他地方对textSegments进行修改,使其类型可能被推断为string | string[]。特别是在大型项目中,变量名重复或错误的类型声明可能导致难以发现的错误。

4. 显式声明类型:尽管你已经显式声明了textSegmentsstring[],考虑在调用forEach之前再次确认其类型,虽然这种情况下似乎多余。这可以通过类型断言实现,例如:

(textSegments as string[]).forEach((segment, index) => {
  // ...
});

如果上述步骤仍然不能解决问题,那么可能需要更详细地审查项目的配置和代码上下文。确保tsconfig.json中的配置正确无误,且没有其他可能干扰类型推断的因素。此外,尝试在命令行运行tsc来看是否TypeScript编译器也报相同的错误,这有助于确定问题是由编辑器引起的还是由TypeScript配置引起的。

我发现果然是因为ChatGPT所说的第3点原因,我在拷贝代码时并没有给textSegments 声明类型。不过很快我又遇到了新问题:

另外报错: Argument of type ‘(prev: never[]) => string[]’ is not assignable to parameter of type ‘SetStateAction<never[]>’. Type ‘(prev: never[]) => string[]’ is not assignable to type ‘(prevState: never[]) => never[]’. Type ‘string[]’ is not assignable to type ‘never[]’. Type ‘string’ is not assignable to type ‘never’.

ChatGPT又给了解法:

这个错误提示表明TypeScript无法正确推断setTexts更新函数中回调的返回类型。具体来说,当你尝试使用函数形式的setState更新状态时,TypeScript认为这个回调函数应该返回一个never[]类型,这通常意味着它不能正确推断状态的初始类型。这个问题往往出现在状态的初始值设置或类型注解方面。

错误中提到的never[]类型表示一个数组,但它的元素类型为never,即TypeScript认为这个数组不应该包含任何元素。当你尝试向这样的数组中添加字符串时,会出现类型不匹配的错误,因为string[]不是never[]的子类型。

解决方案

问题的根源在于useState的类型推断。当你使用useState时,如果你没有明确指定状态的类型,并且给定的初始值让TypeScript无法正确推断出来,就可能导致这种情况。在你的例子中,显然TypeScript没有从初始值(空数组[])中推断出正确的类型。

为了解决这个问题,你需要在使用useState时明确指定状态的类型。看看下面的代码,特别注意useState<string[]>([])这一行:

import React, { useState } from "react";

const ShowText: React.FC = () => {
  // 明确指定useState的类型为string[]
  const [texts, setTexts] = useState<string[]>([]);

  const handleClick = (): void => {
    setTexts([]);

    const textSegments: string[] = [
      "第一段文本显示",
      "第二段文本显示",
      "第三段文本显示",
    ];

    textSegments.forEach((segment, index) => {
      setTimeout(
        () => {
          setTexts((prev) => [...prev, segment]);
        },
        200 * (index + 1),
      );
    });
  };

  return (
    <div>
      <button onClick={handleClick}>点击我</button>
      <div>
        {texts.map((text, index) => (
          <p key={index}>{text}</p>
        ))}
      </div>
    </div>
  );
};

export default ShowText;

在这个例子中,通过使用useState<string[]>([]),我们明确告诉TypeScript状态texts是一个字符串数组。这样,当你更新状态时,TypeScript知道你是在向字符串数组中添加元素,而不会尝试将其推断为never[]类型。

确保在使用useState时始终提供足够的类型信息,特别是当状态的初始值可能让TypeScript感到困惑时。这不仅可以避免此类错误,还可以提高代码的可读性和可维护性。

ChatGPT特别贴心的指出,让我特别注意useState<string[]>([])这一行,我检查了代码,发现问题确实就出在这里。经过几轮对话,ChatGPT帮助我实现了需求并Debug代码,最后顺利运行成功。(整个项目的源码开放在GitHub上:hutusi/fellini ,欢迎Star。)

虽然此前我也有使用GitHub Copilot来做补全代码等工作,但没有像这次使用ChatGPT来实现一个完整需求的感觉惊艳。我也曾使用ChatGPT写过文案,甚至写过格律诗,但都不太满意,而此次使用ChatGPT来写代码却完全超出我的预期。它除了能够生成可以运行的代码,还能够准确识别我的意图,解释每段代码的含义,并且提醒我注意可能出错的代码行。不过,上面的例子还只是一个需求的片段,GitHub CEO 在去年5月Web Summit上演示18分钟借助GitHub Copilot X来编写一个贪吃蛇小游戏的例子会让人更加惊叹GPT的能力。这也让人们看到了软件工程的一个可能的发展方向。

十 大模型是不是银弹?

纵观整个软件工程历史,虽然软件的概念早于计算机出现,但真正的软件产业是跟随计算机硬件其后发展出来的。计算机硬件能力发展速度非常快,这一规律被总结成了“摩尔定律”,即芯片的性能每18个月会提升一倍。但软件的能力却不会有这么快速的发展,不但如此,还因为业务需求不断变化而导致软件复杂度急剧增加,软件的复杂性吞噬了硬件的性能。以至于另外一个定律应运而生——安迪-比尔定律:安迪给的,比尔就会拿走。安迪是时任英特尔CEO安迪·格鲁夫,比尔是微软创始人比尔·盖茨。也就是说“摩尔定律”揭示下的芯片提升的性能最终又被软件吃掉了。

究其原因,是软件固有属性——复杂性决定的,软件的复杂性体现在其不可见性、可变性等,这导致软件项目的质量、成本、进度都很难控制,软件工程正是为了解决这些问题而生。通俗来讲,软件工程的目的就是希望用更低的成本、更可控的周期来交付更符合用户期望的软件。瀑布开发模式或者说V模型,其明确分工协作、确定各阶段交付标准是为了使得软件项目进度可控、软件质量可控;而敏捷软件开发也是为了使软件项目通过快速迭代、响应变化来命中用户需求,符合用户期望。DevOps、CI/CD、自动化测试等工具带来了软件构建效率的提升,使得代码可以更快的编译交付上线,而开发人员也可以得到更快的质量反馈。从大型机到个人电脑,从互联网到云计算,软件工程也会随着计算机产业的变化而更新改进,以契合软件行业的发展。不过,弗雷德·布鲁克斯在《没有银弹》中认为,这些改进都属于对软件附属性工作的改进,软件的本质性工作并没有得到真正的改进,这些本质性工作是抽象成计算机可理解的软件所做的工作,大多发生在开发人员的脑子里。而依据布鲁克斯的观点(该文写于1986年),过去几十年乃至未来十年,没有一项技术或方法能大幅减少本质性工作,这也就意味着软件工程生产力不能得到实质性的提升。

不过我认为,在过去的软件工程实践里,有两项技术在软件本质性工作上做出了较大贡献,一项是高级编程语言,一项是可复用的软件对象设计(如设计模式、开源组件),虽然这两者布鲁克斯在文中并不认同。高级编程语言(如面向对象语言)拉近了实际业务与软件抽象的距离,使得开发人员可以使用更符合人类思维的编程语言来与计算机交流,不过我同意布鲁克斯的观点:高级语言对于某些(不擅长抽象的)用户在某种程度上增加了脑力劳动的负担。可复用的软件对象从某种意义上讲节省了可能的软件开发工作。

布鲁克斯还认为人工智能也不能给软件生产力带来数量级的增长,他在文中所说的人工智能是指专家系统,而专家系统也因为其规则和推理无法支撑复杂场景的增长在上世纪八十年代后逐渐式微,引领现代人工智能方向的是深度学习神经网络,现今就是以GPT为代表的大模型。布鲁克斯于2022年去世,不知道如果他看到ChatGPT等大模型应用在软件工程会做如何判断。但从目前业界的共识看,大模型技术将改变软件工程和整个软件行业。

有人将此称为“软件工程3.0”,我不太认可这种说法,因为所谓的“软件工程2.0”(敏捷、DevOps)也只是对传统软件开发模式的迭代改进。我更愿意称大模型前的软件工程为“传统软件工程”,而大模型后的软件工程为“智能软件工程”。在智能软件工程时代,大模型应用可以帮助用户直接将需求转换为代码,这就省去了中间的分析、设计、编码、调试等环节。(当然,对于一些复杂的需求,目前的一些大模型应用可能需要用户做需求的分解,包括提供更详细的上下文,以便于大模型理解用户意图。)分析、设计、编码、调试等环节是基于传统开发模式的任务分工,大模型不需要这样的分工。我们甚至可以把大模型理解为一个承接外包任务的智能全栈工程师,用户只需要给大模型下发任务,而大模型外包公司则负责将任务完成并交付可运行的软件代码(包括测试用例集)。

因此,我觉得在设计软件开发的大模型应用时,基于现有工具延伸、优化用户交互模式都不是首要考虑的因素,而是看两个方面。一是从第一性原理看,该应用是不是瞄准了软件最本质的产品——代码。二是从布鲁克斯的理论看,该应用是不是在解决软件开发的本质性工作。从布鲁克斯发表《没有银弹》以降,没有人或技术敢声称是银弹,敏捷流行的时期最常见的文章是《敏捷不是银弹》,其他如DevOps、微服务等技术出现时都如是说,因为学习或经历过历史的人都知道软件的根本问题其实是人的问题。但大模型的AI时代正在来临,我大胆预测大模型是软件开发的银弹,这颗银弹从先锋大厦(OpenAI总部所在地)射出,飞过福尔松大街,飞过太平洋。

十一 后记

在写作本文时,我正巧看到GitHub CEO Thomas Dohmke前不久在TED上发表的演讲,名为With AI, Anyone Can Be a Coder Now。他预测到2030年,GitHub上的软件开发者将从目前的1亿增加到超过10亿,也就是说全球超过10%的人都能够编写软件。因为在像GitHub Copilot这种大模型应用的帮助下,编写软件将变得像骑自行车一样容易。主持人问:既然AI这么强而且还在加速增强,那人类还需要参与软件开发其中吗?Thomas的回答是:这就是为什么我们给产品起名为“Copilot”(副驾驶员)的原因,因为还需要“Pilot”(驾驶员)。我们需要有创造力的Pilot决定做什么。

这个问题可能很多人有不同的看法,我想到最近看的一本哲学小书《苏菲的世界》。主人公苏菲在一位导师的引导下学习哲学史,苏菲(Sophie)在希腊语中即“智慧”之意,而哲学(Philosophy)即“爱智慧”。苏菲在学习“智慧”的过程中成长,但到最后她和导师意识到自己不过是一位上校写给女儿的书中的角色而已。她们决定逃出书本,融入到现实中的女孩的世界。

附 参考资料

书籍

《创新者》沃尔特·艾萨克森【著】,中信出版社,2017 (英文原版 The Innovators, Walter Isaacson, Simon & Schuster, 2015)

《睡鼠说:个人电脑之迷幻往事》 约翰·马科夫【著】,黄园园【译】,电子工业出版社,2015

《智慧的疆界:从图灵机到人工智能》 周志明【著】,机械工业出版社,2018

《人月神话》 弗雷德·布鲁克斯 【著】,汪颖【译】,清华大学出版社,2002

《设计模式:可复用软件面向对象软件的基础》 GoF【著】, 机械工业出版社, 2000

《敏捷软件开发:原则、模式与实践》Robert C. Martin【著】, 清华大学出版社,2003

《大教堂与集市》埃里克·雷蒙德【著】,卫剑钒【译】,机械工业出版社,2014

《科学:无尽的前沿》万尼瓦尔·布什【著】,崔传刚【译】,中信出版社,2021

《苏菲的世界》乔斯坦·贾德【著】,萧宝森【译】,作家出版社,2007

论文、演讲及文章等

The History of Software Engineering, Grady Booch, IEEE Software, 2018

《诚如所思》万尼瓦尔·布什 (As We May Think, Vannevar Bush) , 1945

《没有银弹:软件工程的本质性与附属性工作》弗雷德·布鲁克斯 (No Silver Bullet—Essence and Accidents of Software Engineering, Fred Brooks), 1986

《致电脑爱好者的一封信》比尔·盖茨 (An Open Letter to Hobbyists, Bill Gates), 1976

Building an app in 18 minutes with GitHub Copilot X, Thomas Dohmke, Web Summit Rio, 2023

With AI, Anyone Can Be a Coder Now, Thomas Dohmke, TED, 2024

《看不见的大猩猩——智能时代的企业生存和发展之路》 韦青(微软中国 首席技术官),QCon Being Keynote, 2024

《从神经网络到 Hugging Face》 胡涂说 hutusi.com, 2024

《改变世界的一次代码提交》 胡涂说 hutusi.com, 2020

《自由的代价》 胡涂说 hutusi.com, 2015

软件工程 神经网络 深度学习 大模型 软件开发

知识共享许可协议 本文采用「CC BY-SA 4.0」知识共享许可协议,如果还喜欢其他文章, 欢迎订阅“胡涂说”博客
公众号
微信公众号同步更新,欢迎关注😊
对我博客最大的鼓励来自于你的评论,欢迎选择 来回复, 也可以在 GitHub discussion 留言。