关于AI编程
最近也是有一段时间没有更新杂谈了。除了本人现实生活比较繁忙的缘故之外,也有最近还在写一些别的东西的因素在。不过因为还没有校对完毕,所以一时半会儿之内估计也还发不出来。总之感觉就是开的坑越多,就越容易出现顾头不顾腚的情况。按下葫芦浮起瓢之下,各种事也就都耽误了。
咳咳跑题了,让我们说回正文。那么今天促使我写下这篇杂谈的主要原因,其实是在前段时间,我终于开始上手制作了一个已经构思很久的卡牌对战游戏。之所以说“终于”,是因为这个企划已经在我的脑子里存在了相当长一段时间。熟悉我的人都知道,我因为重度手残所以通常只玩回合制游戏;但这么多年的卡牌对战游戏打下来,我又一直总是苦恼于先后手的问题。那么,能不能设计一款不分先后手的回合制卡牌对战游戏呢?这个想法自从在我的脑海中冒出之后就再也按捺不住,之后又在无数次的洗澡或者发呆的过程中逐渐编织成型。时至今日,虽然远远谈不上完成了什么严谨的设计文档,但至少也算是想出了比较完整的系统、流程,甚至是世界观和职业分配。按照正常的游戏开发流程,我下一步的做法本来应该是新建项目,然后在雄心壮志维持不到三天之后,面对空荡荡的场景树和一堆还没写的系统陷入沉默。
这当然不是因为这个游戏的想法有多么伟大,伟大到我不敢“不自量力”地去“亵渎”它,而是因为我清楚地知道自己能力有限;以及更重要的一点,时间不够。虽然这个项目听上去真的很简单,因为一个所谓“不分先后手的卡牌对战游戏”,其实就是卤蛋对撞嘛。但任何一个实际写过游戏的人(尤其是写过卡牌游戏的人)都知道,光是回合流程和卡牌效果这些纯后端的逻辑就足以让人抓狂,而前端的UI设计和交互等就更容易让我这种美术苦手的人破个大防,更何况还有数据持久化和联网对战这些东西也都需要考虑。所以在担惊受怕之下,这个企划也就一直处于“以后有空一定做”的薛定谔状态。
直到最近,我突然产生了一个十分危险的想法:既然现在到处都在吹所谓的vibe coding,那我不如就拿这个项目先试试水?反正失败了也没什么损失。所谓vibe coding,从定义上说就是指用自然语言描述需求、让AI去生成和迭代代码,而开发者主要负责表达意图、测试结果和调整方向的一种编程方式。更具体地说,就是我不用再像“古法”程序员那样一行一行地手搓代码,而是把需求直接丢给大模型让它自己去写,然后再由我来对写出来的结果进行评阅和返修。由于一直有在白嫖别人的OpenAI会员,所以我使用的模型也顺理成章的就是它旗下的Codex。而这次vibe coding的结果就是,在不到两周的时间里,大模型就根据我的需求,从一个全空的Godot项目开始写出了接近一万行C#脚本,外加若干个tscn前端和无数个json卡牌配置。更可怕的是,这还并不是我每天关起门来高强度开发的结果,而只是在做别的事情之前,先顺手把一段需求发过去,让模型自己去生成;等到我把事情干得差不多了,再回到菜园子“收菜”的结果罢了。而就是在这种拖拖拉拉的效率之下,大模型就能在不到两周的时间里完成一个庞大到让我几年都不敢开坑的“大作”,这种效率着实让我吃了一惊。更难以想象的是,整体来看AI写的代码质量其实出乎意料地还可以。虽然它写的东西总是在语义层面上突破现有框架,比如把和A方法功能相近的B方法也塞进A的类里,或者把前后端逻辑杂糅在一起,因此总是需要人类进行纠正;但不管怎么说,在大部分时候它终究还是能算得上“勉强完成需求”。在一些对代码质量要求不是太高的情况下,我估算一个Codex Agent的效率至少顶得上十个人类熟练程序员。
于是,一个很不体面的想法自然而然地冒了出来:人类程序员是不是快完蛋了?
这里所谓的“不体面”,说的并不是因为这个想法有多么愚蠢,而是因为我觉得它有点过于急功近利,有点像是教科书里写的那样,是资本主义在发展到极致的情况下,资本家总是会迫不及待地使用更高效率的生产工具来代替落后的劳动方式,从而快速获取超额剩余价值。想到这里,我不由得回想起自己之前在博客里锐评AI冲击其他行业的情景,那时候我的态度虽然不能说是幸灾乐祸吧,但至少也称得上是隔岸观火。毕竟相比绘画和音乐,AI至少还是更偏向于我这一侧的。而这一次,“先进生产力”终于结结实实地砸到了我自己未来的饭碗上,正所谓是“屠龙者终成恶龙”了。不过,如果这篇文章只是写到“程序员要失业了,我很焦虑”,那未免也太无聊了一点。所以,我还是想像之前分析AI绘画和AI音乐时那样,借着这个机会稍微细致地谈一谈自己作为一个科班程序员、半吊子AI研究者和未来准码农的一些粗浅看法。
在具体探讨AI编程这个话题之前,我们首先得承认一件事,就是它已经由来已久,并不是在最近才突然出现的。在LLM发展的早期(具体时间我说不好,也许是在GPT-3的时候吧),虽然那时候的大模型仅从它们生成的东西来看完全配得上一句“傻子”的称呼,但其实它们已经能够根据用户的需求来给出一些简单的脚本了,比如批量改文件名、写一段正则表达式,以及稍微复杂一点工作的比如生成一个简单网页之类。只不过那时候的模型其实也并不能从语义或者抽象语法树的角度理解这些代码,只是因为读过太多相似的东西,所以在概率采样的时候能够生成出一个似是而非的东西来(而生成的东西有时候连编译都过不了)。如果从这个角度说,AI编程其实可以算得上是某种天崩开局了。
然而很不幸(至少对于人类程序员来说很不幸)的是,代码这个东西天然就非常适合于被大语言模型学习。首先,互联网上存在着海量的代码,而且它们非常集中于GitHub等代码托管网站、Stack Overflow等技术问答网站,以及CSDN博客之类的少数几个地方,而且其中的大部分都是开源的,因此非常便于收集。虽然这些代码里有相当一部分都并没有很高的质量,只是一些新手的拙作、架构上的屎山、人类不可读的怪物,以及时代的眼泪,但这些都无所谓,因为对于大模型来说,它们统统都只是训练数据。其次,代码和普通自然语言不同,它有明确的语法结构。如果在自然语言中写错一个字,在语义上通常不会造成太大的影响,人类也通常能勉强看得懂;但如果在代码里写错了一个字,那编译器就会立刻翻脸。所以至少从形式上说,代码脚本要比用自然语言写就的普通文章更容易被机器检查,因为对于它们能不能运行,报什么错,能不能通过测试用例这些问题来说,它们的答案都是非常明确的且能自动化获取的。因此,只要在古早的LLM架构上增加一些编译器之类可调用的工具,那它的学习能力就能得到迅速的提升。这一点和我们之前讨论过的AI绘画、AI音乐也不太一样。绘画和音乐当然也有大量作品可以学习,但它们的评价标准通常更加暧昧。除了P站上那些高评分、高点赞的图片,以及一些广泛流行的音乐之外,我们很难设计一种方法来对一张图像或一段旋律的“好坏”进行评价。而代码在这方面的评价则至少在部分层面上非常粗暴:能不能跑?有没有报错?测试过不过?虽然“能跑”远远不等于“写得好”,但它毕竟给模型提供了一条比较清晰的梯子。然而如果仅仅说“代码适合被大模型学习”,其实还不足以说明AI编程真正特殊的地方。真正让我觉得AI编程和AI绘画、AI音乐不太一样的地方在于:AI编程生成的是程序。
这句话听起来当然像是一句废话,但问题在于AI自己也是程序。AI绘画模型输出的结果是图像,AI音乐模型输出的是音频。无论它们生成得多么惊人,这些东西在通常情况下都不会直接反过来参与生成下一个AI绘画模型或者下一个AI音乐模型。当然这么说可能也并不绝对,毕竟图像生成模型当然也可以用来辅助制作训练数据,音乐生成模型也可以用来生产更多素材,但它们生成的东西首先是作品,然后才有可能被人类重新整理、筛选、标注,最后才能间接地进入下一轮训练,这中间仍然隔着一层比较明显的媒介转换。而AI编程则完全不同,因为它生成的代码本来就是软件生产体系的一部分。它可以生成脚本,可以生成测试工具,可以生成自动化的部署管线,甚至可以生成用来改进AI自己的某些组件。也就是说,它可以自己训练自己。
当然,我在这里必须先补充一句(以免读者觉得我对于深度学习方面过于不专业):目前AI写出来的程序和“大模型自我训练”的目标之间还差得很远,并不是说今天Codex能写出几个Godot脚本,明天它就能顺手搓出一个GPT-6了。但这并不妨碍AI编程具有一种很特殊的性质:它生成的东西,至少在类型上和改进AI系统所需要的东西属于同一个世界。在一般情况下,一个工具再怎么好用,也只是帮助人类更快地生产某种产品,比如锤子让人更容易钉钉子,画笔让人更容易画画,作曲软件让人更容易编曲。可是,如果一个工具生产出来的东西能够反过来改进这个工具本身,那就不再只是“效率提高”这么简单的事,而是会进入某种正反馈循环,这就让整件事情都开始变得微妙起来了。
在中学生物中我们都学过,在一个系统当中,负反馈循环通常倾向于让整个系统稳定下来,比如温度高了就降温,温度低了就升温,最后让系统维持在一个相对稳定的范围里;而正反馈则恰好相反,它会把一个微小的变化不断放大。一个系统的输出如果能够增强这个系统自身的生产能力,那么它带来的就未必只是线性的增长,而有可能是指数级的增长,甚至在机缘巧合的情况下,最终产生“复杂系统”意义上的“涌现”现象(对于这一点不了解的读者,可以去阅读一下我之前的博客)。就算不从这么玄学的角度来讨论,我们也可以借助一个更广泛的案例,也就是现代工业基石之一的机床的生产过程来理解。现代的高精度机床当然不可能是某个原始人徒手搓出来的,因为人类总是先用简单工具制造粗糙工具,再用粗糙工具制造更稳定的零件,再用更稳定的零件制造更精密的工具。于是,在一个粗糙的机床和由它生产出来的下一代更精密一点的机床之间,就形成了一个不断自我强化的链条。到了最后,我们看到的就不再只是某一个原始人工匠的手艺有多么高超,而是整个工业体系在“左脚踩右脚”式地原地飞升,并最终在光刻机等高精尖领域中达到曾经的人类所完全无法想象的程度。这个比喻当然并不严谨,因为程序的精度和机床的精度也不是同一种东西,但它至少说明了一个很重要的问题:当生产工具开始参与生产工具自身的时候,技术进步就不再只是某一个工具被改进,而是整个工具链条都获得了自我加速的可能性。所以,当我们现在讨论AI编程时,如果还只是总盯着一些诸如“代码写得好不好”、“会不会把前后端逻辑混在一起”之类的小问题,就多少显得有点像在第一次工业革命时嘲笑蒸汽机声音太大、效率太低、锅炉还容易炸的工人一样。那些锅炉的原型机当然可能真的声音很大、效率很低,也真的会炸;但问题在于,只要这东西能够进入生产体系,并且在生产体系中继续被改进,那么它炸得越多,人类也就越有动力去研究怎么让它少炸一点。而当锅炉们终于变得不那么容易炸、变得逐渐实用起来的时候,曾经那些只会嘲笑它爆炸的人,就该开始担心自己的工作会不会被它炸飞了。
说到这里,似乎很容易滑向一种宏大叙事,好像AI编程马上就要带来新的工业革命,而人类程序员明天就要集体下岗一样。不过至少在现阶段,事情还没有这么简单,因为AI编程虽然已经足够惊人,但它依然有很多非常现实的问题。尤其是当我们从“生产工具的自我进化”这种宏观话题,回到每天写代码、改Bug、看屎山的日常时,就会发现AI编程首先冲击的,其实也是程序员最朴素的一道防线:我会写代码,而普通人不会。在过去,这件事几乎构成了程序员职业神秘性的一大来源。虽然程序员们自己都知道,绝大多数的业务代码其实并没有多么高深,在很多时候无非就是查文档、组轮子、调接口、改Bug,然后祈祷线上不要爆炸。但对完全不会编程的人来说,“能让电脑按照自己的想法运行起来”这件事本身就已经足够神奇。就像很多人看到黑乎乎的终端窗口里刷出一堆字符时,会本能地觉得你在进行某种赛博巫术一样。然而,vibe coding的出现正在快速拆掉这层神秘性。既然AI已经能写代码,那么普通人是不是也能编程了?从现阶段看,答案显然是:能,而且比过去容易太多。以前一个完全不懂编程的人,想做一个小游戏、小工具或者数据处理脚本,基本上会在安装环境、配置依赖和第一个报错之间快速结束自己的编程生涯;而现在他只要说出需求,就可以让AI生成出一个大概能跑的东西。哪怕中间出了错,他也可以把报错复制回去,让AI自己继续去修。这件事的意义其实远比大多数人想象中要大,因为它第一次让很多没有技术背景的人获得了某种“用语言生成软件”的能力;而在过去,这些人通常只能向程序员笨拙地描述自己的需求,然后等待程序员把这些需求翻译成代码。虽然这套流程还远远谈不上稳定,但在体感上,它已经足以让很多人产生一种对程序员祛魅的感觉:原来程序员的工作,就是把需求翻译成代码。现在既然AI也会翻译了,那程序员的工作是不是没什么了不起?
从某种意义上说,这种想法并不完全错误。很多时候,程序员确实是在把需求翻译成代码。只不过这句话里最容易被忽略的地方在于:需求并不是天然清楚的,代码也不是天然正确的,而“翻译”这个过程里包含了大量不显眼但非常要命的判断。于是,网上的这样一张梗图开始迅速在各个以程序员为主体的群聊中火爆起来:

这句话当然很刻薄,而且很容易被理解成程序员群体内部惯常的傲慢,但它之所以能传播开来,恰恰是因为它用一种极其粗暴的方式说出了很多人正在经历的真实恐惧。石头在猴子手里只是石头,但枪在猴子手里就很容易变成事故了。更麻烦的一点在于,AI编程给人的幻觉比枪要温和得多。枪至少看起来就很危险,拿在手里也会让人有一点基本的敬畏;但自然语言输入框看起来实在太人畜无害了,用户只需要把愿望打进去,代码就会从另一边自动输出出来。在整个过程中,既没有火药的爆炸,也没有恐怖的后坐力,甚至模型还会在语气上对你十分礼貌,这(对于程序员来说)就十分恐怖了。
在我看来,如果生成式AI能够直接赋予任何人用语言文字实现愿望的权柄,那么“猴子持枪”的隐喻就已经超越了单纯的职场吐槽,而是直指人与工具之间那古老却历久弥新的矛盾:工具越强,使用者能力的差异也会被同时放大,因此就越需要使用者理解自己在做什么。然而在现实中的情况却往往是,工具越强,就越容易让人误以为自己什么都懂。于是我们在现实生活中正越来越多地看到,无论是办公室、创业团队还是互联网开源社区,都已经逐渐变成了“猴子枪击案”频发的原始丛林。这当然不是说AI编程真的带来了什么物理意义上的伤亡,只是说有很大一批原本不懂编程、但又总是自视甚高的人(尤其是某些领导和策划),开始用他们的破烂叙述高效率地生成大量代码。这些代码也许看起来像代码,运行起来也像代码,甚至在Demo里也像是那么回事;但至于代码为什么这么写、有没有安全隐患、会不会在某个边界条件下突然爆炸?这些问题他们自己则是完全不知道,而他们所使用的大模型自然也不知道。换言之,真正危险的不是他们写不出代码,而是他们现在写得出代码了。这种情况在小项目里也许还算可控,因为如果只是一个人让AI写个自用脚本这种情况,那它的破坏力最多也就是把他的文件夹搞得鸡飞狗跳这种程度。但一旦这种方式进入团队协作、商业项目甚至关键系统,问题就会迅速变得复杂起来。因为代码不是一次性消费品,它们会被依赖,会被调用,会被修改,会在未来某个你完全想不到的地方继续产生后果。今天的人为了紧跟时代潮流,或是为了赶进度而让AI生成出来的一段逻辑,在明天就可能变成另一个程序员的Debug地狱。更可怕的是,当不懂编程的人使用AI编程时,最大的问题往往就是因为他们不懂实现,导致提示词太模糊甚至干脆自相矛盾,然后反过来又会误导本就在理解方面存在问题的大模型。比如用户只说:“帮我做一个游戏里的某某系统,要好用一点。”这句话当然能让AI生成东西;可问题在于,其中的很多关键问题都没有被说明,比如状态如何流动?哪些功能以后会扩展?哪些逻辑必须解耦?是否需要存档、同步、安全和测试?这些问题对用户来说当然并不存在,因为他们自己也不知道;而AI在面对这种模糊需求时,就只能先自行脑补,也就是按照训练数据里最常见的方式先对付一版。然后用户一看:哎,好像能跑?于是继续追加需求,AI再补丁叠补丁,特殊情况套特殊情况,前后端逻辑逐渐糊在一起,类的语义边界一点点变质,最终堆成一坨超过AI最大上下文长度、导致它自己也修理不了的不可名状的屎山。然后,这些屎山通常会被送到某个倒霉的程序员手里,与之相伴的往往还有这样一句话:“我已经写得差不多了,你稍微改改就行。”想想也是十分可怕了。
从这个意义上说,现阶段程序员使用AI的优势,也许就在于他们更知道AI的回答错在什么地方。对于那些不会编程的人来说,他们往往只能提出一个整体性的愿望;而会编程的人则更容易意识到,这个愿望背后其实包含了数据结构、架构设计、可扩展性、性能要求和用户交互等一大堆看不见的需求。于是,他们在审查AI生成的代码时,总是能更多地指出其中不对、不合理、不符合语义边界的部分,以防止项目快速滑向语义地狱。这个概念是我自己造出来的,指的是一种代码表面上还能跑,但在整体架构和命名概念上已经开始变质的情况。比如一个类本来只该负责A,但AI在完成需求的时候顺手把和A毫无关系的BCD也塞了进去;再比如一个状态本来只该由后端维护,结果AI为了省事让UI也在那里偷偷修改,这些例子在实际的AI编程中简直不要太常见。虽然刚开始时这些问题都不明显,但如果人类没能及时指出来的话,那整个项目就会以极高的速度从“能跑”滑向“没人知道它为什么能跑”。在我看来,这就是所谓“人在回路”的意义。
当然,如果就这么把所有问题都归咎于“猴子有枪”,其实也有点太简单粗暴了。毕竟,就算是一个受过正经训练、知道自己在干什么的人类程序员,在面对AI编程大项目的时候,也仍然会遇到很多让人血压升高的问题。在我看来,现在的AI编程之所以容易变成屎山,并不只是因为使用者太菜,而是因为现阶段的AI本身也确实存在一种根本上的局限:它擅长完成小目标,但不擅长完成大目标。这一点至少在我的实际使用中感受非常明显,比如让AI写一个具体函数来根据某个规则计算伤害,或者写一个json解析器之类的东西,它通常能完成得相当不错,甚至比我随手写的东西还要规范一点;但如果你让它完成一个更大的目标,比如“把整个战斗流程写完”或者“重构一下卡牌效果系统”,就算已经把需求说得很详细,AI提交的东西也仍然会变得微妙起来。这种微妙感如果非要去形容的话,那我大概会说,就是AI在大目标上经常会出现一种非常隐蔽的“偷懒”倾向。这里的偷懒当然不是说它真的有某种因为token付少了而不想干活的主观恶意,而是说它总是会倾向于优先满足提示词里那些容易实现、容易展示、容易被检查的部分,然后忽略掉另一些隐含的、结构性的约束。但项目中的很多长期约束本来就很难在一个提示词里一次性说清楚,而是就分散在整个项目的语义当中,并且会在不断积累的过程中逐渐形成一个项目的骨架。所以,AI也许在处理单个小决定时表现得不错;可一旦这些小决定之间需要形成长期一致性,它就很容易开始失忆,最后把整个项目带进一种“每一步看起来都能解释,但合起来就是一坨”的状态。
对此,一个很自然的思路是:既然AI擅长小目标而不擅长大目标,那就不要把大目标直接丢给它,而是把大目标拆成一连串的小目标,再让它一步一步完成。这其实就是所谓CoT的思想,也就是Chain of Thought,在中文里一般会翻译成“思维链”或者“链式思考”。当然严格来说,随着这个词在使用过程中的语义逐渐漂移,如今的CoT已经不完全是最初论文里那个狭义概念了。很多时候,只要是让模型不要直接给结果,而是先分步骤分析、先列计划、先拆任务、先解释思路,再逐步生成结果,都可以被宽泛地归入这种思路。而人类程序员之所以能比不熟悉编程的人更好地完成这些工作,也并不是因为他真的懂什么赛博咒语,而是因为他知道怎样把一个大到没法直接动手的目标,分解成一个个可以处理的小目标。这虽然听上去也很像是一句废话,但从来源上看其实也不过是人类被大模型逼出来的结果。因为你不能指望LLM能自动替你规划好路线,所以你只好像个老妈子一样一步一步地告诉它:先不要急着写代码,先告诉我你准备怎么做;先不要改整个项目,先列出可能受影响的模块;先不要一次性生成一大坨,先把接口和数据流定下来,等等。这种人类被现阶段大模型逼疯之后不得不总结出来的各种所谓“沟通技巧”,其实就是提示词工程的雏形。虽然在名字上叫做prompt engineering,但它并不是一种像软件工程、土木工程那样严格的engineering学科,而是研究“我应该怎么跟这个模型说话,它才更可能给出我想要的结果”这种抽象问题的经验集合。从这个角度看,CoT当然就是提示词工程的一种典型形式。也许在外行人看来,“Think step by step”真的就像是一句能够让大模型输出变好的咒语,但它其实很好地体现了提示词工程的基本精神:通过各种巧妙的“沟通技巧”,诱导大模型完成原本不擅长的任务。除此之外,我们还可以让模型扮演某个角色,比如让它“作为资深程序员”来回答问题;也可以让多个智能体进行合作;再比如我在之前文章中提到过的RAG等等。只不过,由于各家大模型的训练方式都不同,而且就算是同一个大模型的输出也带有随机性,所以这些提示词工程的效果其实也很难量化评估。
也正因如此,在我们当下这个“猴子有枪”的危险的原始丛林里,提示词工程作为一种临时的解决方案被匆忙推上了前台。有些人宣称它是在建立“射击俱乐部”,宣称既然AI已经把枪发到了所有猴子手里,那至少应该教大家怎么瞄准、怎么上保险、什么时候不该开枪。但也有很多人对此嗤之以鼻,因为在他们看来,所谓提示词工程不过是在给猴子递上更精美的子弹匣。如果一个人本来就不知道自己要做什么,也不知道自己生成的东西意味着什么,那么再精致的提示词模板也未必能让他变成一个合格的工程师;相反,它很可能只是让他以更高的效率、更大的规模,生产出更加难以收拾的错误。原本他只能手搓几行烂代码,现在他能用漂亮的提示词一次性生成几千行烂代码;原本他的破坏力还受限于打字速度,现在连这个限制也被取消了。我个人的看法大概介于两者之间。提示词工程当然有用,尤其是在现阶段的大模型还充满各种奇怪局限的时候,它几乎是所有认真使用AI的人都绕不开的东西。你不可能完全不考虑上下文,不考虑输出格式,不考虑模型什么时候会出现幻觉。只要模型还需要通过自然语言和人类互动,那么如何更好地使用自然语言去约束模型,就一定具有其现实意义。但在另一方面,提示词工程也确实不应该被神化,因为现在很多提示词中的所谓“技巧”,在本质上都只是在迁就当前模型的缺陷。比如模型上下文不稳定,所以人类要反复强调;模型容易忘记约束,所以人类要列清单;比如模型容易过度发挥,所以人类要限制输出格式;比如模型长期规划能力弱,所以人类要帮它拆小任务;比如模型自检不可靠,所以人类要让它多轮验证,等等。所以,与其说提示词是新时代的编程语言,不如说它是自然语言还没有变成真正可靠的编程接口之前的临时替代品。它在当下当然具有不可忽视的价值,但这种价值很可能只是过渡性的。随着模型能力增强、上下文窗口变大、项目级记忆更可靠,很多今天看起来很有用的提示词技巧,未来也许会像“搜索引擎高级语法”一样,从某种必备技能变成少数爱好者才会怀念的时代眼泪。
然而,如果提示词工程的价值只是过渡性的,那么一个很自然的问题就会随之出现:如果有一天,AI不再需要人类替它写提示词了呢?这个问题或许有些超前,毕竟在我们如今的直觉里,提示词仍然是人类和AI沟通的唯一接口。通常来说,人类提出问题的问题越清楚,AI给出的回答就越接近我们想要的东西。所以很多人会很自然地认为,“会写提示词”就是AI时代的新技能。而程序员哪怕不再亲手写每一行代码,至少还可以凭借自己更强的工程经验、更清楚的任务拆分能力和更准确的提示词,维持住相对于普通人的优势。如果“我会写代码”是程序员面对AI编程时失守的第一道防线,那么“我更会让AI写代码”就是如今程序员退守的第二道防线。但问题在于,这道防线真的稳固吗?至少从理论上说,它并不稳固。因为提示词本身并不是什么神圣不可侵犯的东西,它只是现阶段人类能够理解、能够编辑、能够传递给模型的一种控制信号。既然它是一种控制信号,那么只要AI系统能够通过反馈判断某种提示词是否更有效,它就完全有可能自己生成提示词、修改提示词,甚至比较不同提示词之间的效果。现在的提示词工程当然有用,但它也并没有什么超自然的成分,在本质上也只是人类根据经验总结出来的控制策略。那么,如果一个AI系统能够自己观察失败日志、自己总结哪里出错、自己调整上下文、自己生成测试、自己比较不同拆分方案,它为什么不能自己得出类似的控制策略?到那时候,prompt engineering大概就会从一种人类技能,变成系统内部的一个优化环节。甚至更进一步,所谓“提示词”本身也未必还会保持自然语言的形式。自然语言提示词的优点是人类能看懂、能修改、能解释,但这并不意味着它就是模型最喜欢、最擅长、最高效的控制方式。对模型来说,真正重要的也许不是“这句话在人类看来是否优雅”,而是“什么样的内部状态最能导向正确输出”。这种内部状态完全可能是某种高维向量、隐式上下文、工具调用策略,或者端到端学习出来的张量表示。也就是说,人类今天煞有介事地研究提示词,有点像是在研究一层专门给人类看的操作面板。这个面板当然重要,因为目前我们还需要通过它来控制机器。但如果有一天机器内部可以自己完成控制优化,那么它未必还需要保留这块面板,更未必需要按照人类可读的方式来组织自己的“思考”。
更麻烦的是,程序员的专业经验本身也未必永远是优势。它既是一种能力,也可能是一种偏见。在当下,程序员比普通人更会用AI,是因为程序员知道如何把模糊愿望翻译成工程结构。但我们也必须承认,这些经验本身是人类在特定历史条件下形成的折中方案。很多设计模式、架构原则、工程习惯,并不是宇宙真理,而是人类在自身心智限制、团队协作限制、编程语言限制、工具链限制下总结出来的生存技巧。因此,一个写Java的人,总是会倾向于把问题翻译成类、接口和继承;一个写Haskell的人,可能会倾向于把问题翻译成纯函数、组合和不可变数据;而一个写C++的人,没准会在任何问题里都先闻到内存布局、生命周期和性能开销的味道。这些对人类来说当然都可能是好方法,但这种预设立场对于AI来说却未必总是正确。这让我联想到DeepMind公司开发的围棋bot的故事,因为他们最初版本的AlphaGo还学习了大量人类棋谱,而后来的AlphaGo Zero则是从零开始自我对弈,只依靠规则和强化学习不断迭代。其结果是,完全不依赖人类经验的新版本完胜了依赖人类经验启动的旧版本。这个故事告诉我们:对于AI来说,人类经验并不总是捷径,有时也可能是上限。当然,编程和围棋并不完全一样,毕竟围棋规则封闭,胜负明确,而软件工程面对的是现实世界里一堆混乱、模糊、不断变化的需求。人类经验在软件工程中仍然极其重要,不可能被一个简单类比直接抹掉,但这个例子至少提醒我们:如果AI未来真的能够在更大的搜索空间里探索工程结构,能够自动生成、测试、验证和重构系统,那么程序员带着传统工程经验给出的提示词,未必一定比普通人朴素表达出的目标更接近最优解。到了那时候,人类程序员基于自身的专业知识给出的需求,到底是在帮助AI接近目标,还是在反过来在给AI拖后腿?这个问题其实也很难说。
如果这一步真的发生,那么“人在回路”的意义就会被进一步压缩。今天我们还需要人在回路,是因为AI还需要人类为它们拆解任务、检查结果、纠正方向。但如果未来AI自己也能拆拆解任务,自己也能比较不同架构方案,然后自己跑测试、自己修Bug,那么人类在这个系统里还剩多少必要性?在我看来,在这样的系统里人类就算不会真的完全消失,最多也只能退守到两个位置上。第一个位置是在最上游,提出那个最原始、最模糊的目标,比如:“我要一个好玩的卡牌游戏。”这些目标不是纯粹的技术问题。它们背后包含欲望、审美、商业判断、社会价值和现实约束。AI可以帮助人类拆解目标、实现目标,甚至反过来修正目标,但目标最初从哪里来,至少在目前看来仍然需要由某个主体提出。而第二个位置则是在最下游,承担最终责任,而这个位置就没有那么体面了。如果AI写出的代码出了Bug,导致银行系统崩了,自动驾驶撞人了,医疗系统误诊了,交易系统因为某个边界条件损失了几千万,这时候谁来负责?AI显然负不了责,因为它没有财产可供赔偿,没有社会身份可供惩罚,也没有真正意义上的道德人格。公司当然也不会轻飘飘地说一句“这是AI写的,所以谁都没有责任”。更可能发生的情况是,它们会拿出一整套流程文件,然后指着某个签过字的人说:“我们有完整的代码审查流程,这位工程师确认过系统可以上线。”于是,人类程序员最后的铁饭碗,可能与他们曾经赖以生存的技术不再相关,而是来自他们作为法律责任主体的存在,在AI自动生成、自动测试、自动部署之后,作为一个具备法律人格和组织身份的人类,在最后的验收文件上签一个字。这个签字当然也不是完全没有技术含量,毕竟敢签字至少就说明你知道系统大概有没有问题,测试是否充分,风险是否可接受。但从社会分工上说,这已经不再是传统意义上的“开发者”,而更像是代码审计员、系统责任人、技术背锅人,又或者说,是自动化流水线的人类担保物。我知道这件事听起来很荒诞,但其实细想起来在社会中又似乎并不陌生,因为在很多行业中其实早就是这样了。很多项目最后都会有人签字、盖章,尽管他对这个项目中的大部分内容是怎么实现的可能都毫不知情。所以签字这个动作并不一定代表这个人真的亲手完成了全部工作,而更像是一种社会制度上的责任锚点,毕竟出了事以后总得有个人来负责,而这也许才是整个行业最黑色幽默的地方。
但即便暂且不考虑这么阴暗的未来,仅仅从现实就业的角度来看,AI编程带来的冲击也已经足够明显了。一旦大量中间编码劳动被AI接管,那么即使人类仍然需要提出目标、审查结果和承担责任,公司在完成同样工作量时所需要的程序员数量,也会不可避免地下降。这其实是一个很简单的算术问题,假设过去一个项目需要十个程序员,那现在一个熟练使用AI的程序员就能完成过去十个人的工作量。哪怕由于软件开发成本下降,导致社会对软件的总需求大幅增长,也未必能够完全吸收被释放出来的人力。对此,乐观派们当然可以说,历史上的每一次技术革命都会创造新的需求、新的岗位和新的市场,比如工业革命虽然让很多传统手工业者失业,但也催生出了现代工厂、铁路、电力、化工等新的产业体系。这个说法当然有道理,但问题在于总需求增长并不等于每一个原有劳动者都能被重新吸收。如果单个程序员的生产力增长了十倍,但软件需求却只增长了200%,那剩下七个人又该何去何从呢?他们当然可以转向新的领域,承担新的职责,但这中间的转型成本、时间成本和淘汰成本,并不会因为一句“先进生产力必然创造新岗位”就自动消失。生产力革命从来都不是请客吃饭。它当然会带来更便宜的商品、更高效的生产和更大的社会总财富,但它同样会把一部分原本掌握旧技能的人抛到时代车轮底下。站在宏观历史角度看,这当然可以被概括为“社会进步的阵痛”;但如果那个正在阵痛的人正好是我自己,那事情就没那么洒脱了。
更何况,最先被冲击的很可能不是那些已经站在行业上游、负责系统架构和复杂决策的人,而是大量中低端的、增删改查的外包编程工作。这些东西从技术上说当然不算多么高深,但它们过去恰恰构成了大量程序员赖以谋生的基础,也构成了新手程序员进入行业的入口。而这又引出了一个更加棘手的问题:如果初级任务全都被AI吃掉,那公司还愿意招初级程序员吗?在过去,一个新手程序员之所以能成长起来,并不是因为他刚入行时就什么都会,而是在于他可以从一些相对简单、相对重复、相对边界清晰的工作开始做起,然后在不断的写需求、修Bug、读屎山、被骂的过程中,逐渐理解什么叫代码质量,什么叫项目约束,以及用自己的亲身经历体会到,什么样的地方猛一看没问题,但之后一定会爆炸。可如果AI把这些初级任务大量接管,那么公司就会越来越倾向于招少数能够指挥AI的熟练程序员,而不是招一批什么都不会的新手慢慢培养。这样一来,行业就可能出现一个非常尴尬的断层:公司越来越需要能驾驭AI的高级程序员,却越来越不愿意培养未来会成长为高级程序员的新手。这听起来很矛盾,但在资本主义世界里又相当合理。毕竟,公司并不是程序员学校,它没有义务替整个行业培养下一代。过去公司之所以愿意雇佣初级程序员,是因为初级程序员虽然水平有限,但至少能在低成本岗位上完成一部分实际工作。可一旦AI能用更低的成本、更快的速度完成这些工作,初级程序员的训练价值就会和他的劳动价值脱钩。公司也许仍然希望世界上存在高级程序员,但它未必愿意自己花钱培养他们。于是,一个有些荒诞的未来就出现了:所有公司都想要“有五年AI协作经验、懂架构、会审查、能背锅”的高级程序员,但没有几家公司愿意雇佣那个刚刚毕业、只会问AI怎么写正则表达式的新手。
当然,也许在未来的教育体系中,开源社区和个人项目会承担一部分培养功能,也许下一代程序员不是通过公司里的初级岗位成长起来,而是通过和AI一起构建无数个个人项目成长起来。就像我现在用Codex写游戏一样,过去需要一个小团队才能完成的东西,未来一个学生也许就能在宿舍里做出原型。但问题在于,这两种入口培养出来的人未必一样。一个人如果在真实团队中成长,那他通常会被迫面对协作、维护、责任、历史包袱和现实约束;而一个人如果在AI陪伴下独自做项目,那他就很容易沉浸在“我能让东西跑起来”的幻觉里。前者会让人学会尊重屎山,后者则可能让人学会高速制造屎山。当然这句话说得可能有点刻薄,毕竟我自己现在也正在用AI做个人项目,所以严格来说,我可能也是那个正在丛林里练枪的猴子之一。
写到这里,其实就又兜兜转转回到了我在本文开头提到的那种“不体面”的感觉。之前写AI绘画的时候,我多少还能站在旁观者的位置上,讨论画师为什么会抵触AI,讨论“创造”和“拼接”的职业特性差异;后来写AI作曲的时候,我也可以相对轻松地说,AI作曲或许不像AI绘画那样直接砸在现有从业者的饭碗上,反而更像是给普通人和音乐人都提供了一种新的灵感来源。那时候我当然知道,自己并不是什么真正置身事外的历史观察者,但至少在情绪上,我还是很容易站到“先进生产力”的一边,仿佛只要说一句“时代总是在发展”,很多具体的痛苦就可以被轻飘飘地归入宏观叙事。然而轮到AI编程时,这种轻松的姿态就很难继续维持下去了。“屠龙者终成恶龙“,这句话放在这里当然很合适,过去程序员总是挥刀斩向别人的重复劳动;如今这把刀转了一圈,终于架到了程序员自己的脖子上。这个故事听起来也没有什么不对,甚至有种非常爽快的感觉;可AI编程真正特殊的地方在于,它还能让这个故事继续走到一个更加陌生的阶段。我们总说,“屠龙者终成恶龙“,然后呢?也许之后的答案不再是“会出现新的屠龙者,杀掉屠龙者变成的恶龙”这样的循环,而是”之后再也没有新的屠龙者了“。因为就像我说的那样,AI编程生成的不是图像、音乐或者其他外部作品,而是程序本身,是AI系统、开发工具、训练框架、测试流程和自动化管线赖以存在的基础,是在参与改造替代劳动的机器本身。一旦这条链条真正闭合,故事就不会再局限于“一批人用新工具替代另一批人”,而是“工具开始参与自己的迭代”。到那时候,新的屠龙者就未必还是人,甚至“屠龙者”这个位置本身都可能被系统内部的自我优化过程吞掉。当然,这不是说今天的Codex会写Godot脚本,明天它就能自己训练出GPT-6,后天就把人类装进培养皿里。现实不会这么简单,AI的运转仍然依赖算力、数据、资本、工程系统和社会制度;但方向上的差异又已经足够明显,因为如果AI可以生成代码,代码可以改进AI工具链,而工具链又可以进一步提升AI生成代码的能力的话,那生产工具就会开始以前所未有的方式参与自身的再生产过程。而在这样的系统当中,如果继续往黑暗方向想的话,那未来人类在技术系统中的位置,甚至可能会变得有点像FGO 2.6版本的剧情里,被妖精豢养的人类那样。到了那时,人类就不再是生产系统真正的主人,而更像是某种被保留下来的资源,用来提供欲望,提供审美,提供目标,提供灵感,提供价值判断,提供训练数据,最后再提供法律和道德责任的接口。这个比喻当然有些过分夸张,但这个夸张的比喻至少提醒我们一件事:当我们说AI是先进生产力,能够优化掉某些职业的时候,不能只想象它能替别人省力,也要想象它优化到我们自己头上的那一天。
从这个角度看,我当然还是希望AI编程继续发展。毕竟,就算我嘴上说得再忧国忧民,但当Codex帮我把一个拖了几年的游戏企划从脑子里拉进Godot工程的时候,我还是会感到一种非常朴素的快乐。那种快乐和第一次用AI作曲生成BGM、第一次看到AI绘画把文字变成图像时其实并没有本质区别,它让一个原本因为时间、精力、技能和成本限制而无法实现的想法,突然变得似乎可以触摸。问题只在于,每一种这样的快乐背后,似乎都附赠了一张时代的账单。当画师看到AI绘画时,那张账单寄到了画师手里;当音乐人看到AI作曲时,那张账单也许还没有那么沉重,但它同样已经在路上;而当程序员看到AI编程时,那张账单终于也寄到了我们自己的手上。我当然希望这场AI革命来得更快、更彻底,前提是被革命的不是我。但很遗憾,时代从来不会因为我的双标而停止向前奔腾的脚步。先进生产力这种东西,念在嘴里的时候总是很轻松,只有砸到饭碗上的时候才会发出比较真实的声音。所以,如果非要给这篇文章一个不太积极但也不算完全绝望的结论,我大概会这么说:也许我们这一代程序员最终极的工作,并不是继续写代码,甚至也不是继续教AI写代码,而是在一个越来越不需要人类的系统里,找到自己还能被允许存在的位置。至于那个位置究竟是目标的提出者、灵感的供应者、责任的签署者,还是恶龙胃里负责盖章的器官,那就只能交给时间回答了。