288 of 584

AI 时代的软件基本功

访谈/演讲概述

AI 编程工具虽然被过度炒作,但同时也极其强大。如果使用得当,它们会表现得非常出色;但如果使用不当,它们会比任何人类团队更快地让你陷入“面条式代码”(混乱代码)的泥潭。造成这种差异的原因不在于工具本身,而在于流程。

在过去 18 个月指导开发者使用 AI 智能体进行构建的过程中,Matt Pocock 观察到了一个共同的规律:那些取得成功的开发者,既不是把所有事情都外包给 AI 的人,也不是完全不用 AI 的人;而是那些回归软件工程基础的人。在本次演讲中,他分享了他的学生们利用 AI 智能体集群交付高质量应用程序的迭代流程,并解释了为什么让这一切奏效的原则(统一语言、垂直切片、测试驱动开发、深度模块)——这些几十年来经久不衰的理念——在当下变得比以往任何时候都更加重要。


专用词语翻译列表

英文原词 中文翻译 语境说明
Spaghetti code 面条式代码 / 意大利面条式代码 指结构混乱、难以维护的代码。
AI agent swarms AI 智能体集群 指多个协同工作的 AI 代理程序。
Ubiquitous language 统一语言 领域驱动设计(DDD)中的概念,指团队共享的标准术语。
Vertical slices 垂直切片 软件开发中按功能特性而非技术层级进行划分的方法。
TDD (Test-driven development) 测试驱动开发 一种先写测试后写功能代码的软件开发方法。
Deep modules 深度模块 指接口简单但内部封装了大量复杂功能的软件模块。
Shallow modules 浅层模块 指接口复杂但内部功能单一的软件模块。
Specs-to-code 需求转代码 一种直接将需求说明书交给 AI 生成代码的开发模式。
Software entropy 软件熵 借用物理学概念,指软件系统随着修改不断趋于混乱无序的现象。
Design concept 设计概念 指多人协作设计时,团队之间形成的关于产品的抽象共识。
Thinking traces 思考轨迹 指 AI 在得出最终结论前,展示出的中间推理和思考过程。
PRD (Product Requirements Document) 产品需求文档 详细描述软件产品功能和非功能需求的文档。

正文翻译

讲者:Matt Pocock

大家好!到目前为止大家在这次大会上感觉如何?很棒,太好了。

我想给大家传达一个信息。对于那些认为自己的技能在这个新时代已经毫无价值的人来说,我希望这个信息能给你们带来一丝安慰:我认为,软件基础知识现在比以往任何时候都更重要。

我是一名讲师,最近我一直在教授一门名为《面向真正工程师的 Clojure Code》(注:据语境推断,可能是 AI 编程工具 Cursor 识别错误所致,此处保留原词)的课程。这个名字听起来挺有挑衅意味的。在筹备这门课程的过程中,我必须想出一套关于 AI 编程的教学大纲。这其实挺让人头疼的,因为技术总是在不断变化。AI 是一个全新的范式,我们理应抛弃所有旧规则,以此来引入新东西,对吧?

围绕这一点,出现了一场被称为“需求转代码”(specs-to-code)的运动。这个运动的观点是:“你可以写一份关于应用程序应该如何运行的规格说明书。然后,你可以用 AI 将其转化为代码。如果应用程序出了问题,你就回去修改说明书。你不需要真的去看代码,只需修改需求,再次运行编译器,就能得到新的代码。”听说过这个概念的请举手。如果你自己也尝试过,请继续举着手。

好吧,我也尝试过。大家可以把手放下了。

我当时注意到的情况是,我会运行它,并尽量强忍着不去看代码;但当我最终还是看了代码时,我发现:首先,它输出了代码;然后我运行它,结果得到了更糟的代码。我再试一次,代码变得更烂了。我不断重复这个过程,不断让编译器运行,最后得到的完全是一堆垃圾。

有过类似经历的请举手。是的。我不认为这种方法行得通。那种认为我们可以忽略代码、让代码自己管理自己的想法,换个说法其实就是“凭直觉编程”(vibe coding)。

但我当时并不相信这一点。我想:“我该如何修复这个‘编译器’?我该如何让它每次不再产生糟糕甚至更差的代码?”于是我决定:“我需要用通俗的语言向大语言模型(LLM)解释一个好的代码库应该是什么样子的。”我翻出了我最喜欢的一本旧书,也就是约翰·奥斯特豪特(John Ousterhout)写的《软件设计哲学》(A Philosophy of Software Design)。大家可以去亚马逊上买来看看。

书中对“什么是糟糕的代码”下了一个定义。他称之为“复杂代码”。复杂性是指软件系统结构中任何使得系统难以理解和修改的因素。也就是说,一个糟糕的代码库是一个难以修改的代码库。如果你不能在不引发 Bug 的前提下修改它,那它就是个糟糕的代码库。好的代码库应该是非常容易修改的。

我觉得这个观点很好。接着我尝试了另一本书,叫《程序员修炼之道》(The Pragmatic Programmer)。大家也可以去买来看看。

这本书里有整整一章在探讨一个叫做“软件熵”(software entropy)的概念。这正是我当时所看到的现象。熵的概念是,事物总是趋向于灾难、彼此分离并最终崩溃。这也是大多数软件系统的表现:每次你对代码库进行修改时,如果你只考虑当前的改动,而不考虑整个系统的设计,你的代码库就会变得越来越糟。这就是我当时观察到的。在“需求转代码”理念下,你一遍遍地运行编译器,这只会让代码越来越差。

推动“需求转代码”运动的一个核心理念是:“代码是廉价的”。听过这句话的请举手。是的。

但我认为这是不对的。我认为代码并不廉价。事实上,糟糕的代码现在比以往任何时候都要昂贵。因为如果你的代码库难以修改,你就无法享受 AI 带来的所有红利。要知道,在优秀的结构中,AI 实际上能发挥得非常非常出色。这意味着优秀的代码库比以往任何时候都重要,也意味着软件基础知识比以往任何时候都重要。这就是我今天演讲的主题。

那么,让我们来点实际的内容。

我将谈谈你在使用 AI 时可能已经遇到过,或者将来可能会遇到的各种“失败模式”,以及如何通过回归经典著作、参考优秀的软件实践来避免这些问题。听起来不错吧?

第一个失败模式是:AI 并没有按照我的意愿去做。

我有了一个好主意,但 AI 却做出了完全不同的东西;或者它写出了一些根本不是我想要的规范。遇到过这种情况的请举手。很好。

《程序员修炼之道》里是这么说的:没有人确切地知道自己想要什么。在这一过程中,你和 AI 之间存在着沟通障碍。因此,当你和 AI 对话时,其实就像是 AI 在做需求收集。它本质上是在向你弄清楚你到底需要什么。

我发现另一本书,弗雷德里克·布鲁克斯(Frederick P. Brooks)的《设计之设计》(The Design of Design)中谈到了一个叫做“设计概念”(design concept)的想法。它是指,当超过一个人共同设计某样东西时,你们之间会漂浮着一个关于正在构建的产品的模糊概念。这个你们正在构建的东西,或者说它的概念,就是“设计概念”。它不是一份资产,不是你可以写进 Markdown 文件的东西,它是关于你正在构建的系统的一种无形的理论。

于是我想:“原来如此,我和 AI 之间并没有共享的设计概念。”所以我发明了一个技能(Skill)。

这个技能非常非常简单,叫做“严厉盘问我”(Grill Me),它是这样写的: “无情地盘问我关于这个计划的每一个细节,直到我们达成共识。顺着设计树(这也是布鲁克斯提到的概念)的每一个分支走下去,逐一解决决策之间的依赖关系。”

包含这个技能的代码库在 GitHub 上获得了大约 13,000 颗星。它彻底火了,人们非常喜欢这个东西。这短短几行指令会让 AI 问你 40 个、甚至 60 个问题。我曾见过它在确认双方达成共识之前,问了开发者 100 个问题。这把 AI 变成了一个类似“对手”的角色,它会不断地向你抛出想法,试图达成共识。

通过这种对话,你可以把生成的内容转化成产品需求文档(PRD)之类的信息。或者如果只是一个小改动,你可以直接把它转化成任务 Issue。然后,你的后台挂机智能体(AFK agent)就会接手处理它。

别在这个观点上跟我抬杠,但我个人认为这比我使用的工具(Clojure Code)里默认的“计划模式”要好得多。“计划模式”非常急于创建资产,它总想立刻生成一个计划然后马上开工。而我认为,首先达成一个共享的设计概念要好得多。

这是第一个建议。

第二个失败模式是:AI 过于啰嗦。

你和 AI 仿佛在鸡同鸭讲。感受过这一点的请举手。是的。AI 仿佛用了太多的词汇试图沟通它在做什么,你们之间并没有在使用相同的语言交流。

这对我来说似曾相识。如果你做了很长时间的开发者,并且曾和领域专家合作过——假设领域专家想让你构建一个关于“微芯片”的应用,而你对微芯片一无所知。你需要建立某种共享语言,对吧?否则,他们会使用你听不懂的术语,你会把这些术语翻译成你可能自己都不懂的代码,而领域专家肯定也看不懂。

所以你和领域之间存在着语言鸿沟。于是我重新回去研究了“领域驱动设计”(DDD)。

这方面我仍在探索的边缘,但我读到的关于 DDD 的所有内容都让我深感共鸣。我太喜欢它了。DDD 中有一个核心概念叫做“统一语言”(ubiquitous language)。

使用统一语言时,开发者之间的对话、代码的表达、以及与领域专家的沟通,都源自同一个领域模型。从本质上讲,这是一个写满你和 AI 共用术语的 Markdown 文件。你非常专注地研究这些术语,确保它们与实际含义完全一致。无论是写代码时、讨论代码时,还是与领域专家沟通时——或者在我们的场景中,与 AI 沟通时——你都会一直使用它们。

所以我编写了另一个技能,即“统一语言”技能。它基本上就是扫描你的代码库,寻找术语,然后创建一个包含所有术语定义的 Markdown 文件(即统一语言词汇表表格)。然后我把它发给 AI,我自己也能看到。实际上,在我和 AI 进行“严厉盘问”和计划阶段时,我总是打开着这个文件。

通过阅读 AI 的“思考轨迹”,我发现这不仅改进了它的计划能力,还让 AI 能以更简洁(不那么啰嗦)的方式思考。这也意味着它的最终实现会与你的计划更加一致。这个方法绝对是个大杀器,效果好得难以置信。

所以这是第二个建议:与 AI 建立一套共享语言。

好了,假设你和 AI 已经对齐了思路。你知道自己该构建什么,AI 也构建出了正确的东西,但它就是跑不起来。遇到过这种情况的请举手。是的,就是跑不起来。

那么,有一个显而易见的方法可以让情况变好:我们可以使用反馈循环。我们可以使用静态类型——顺便说一句,如果你没在用 TypeScript,那简直太疯狂了。如果你在构建前端应用,却不给大语言模型访问浏览器的权限让它去查看效果,那也是不行的,它绝对需要这个权限。当然,你显然还需要自动化测试。

我在这里注意到的一点是,即使有了这些反馈循环,大语言模型使用它们的方式也不太好。它没法像资深开发者那样充分利用反馈循环。它的行为模式往往是一次性做太多事情。它会生成海量的代码,然后心想:“哦,我可能该做个类型检查了。”或者“我应该跑个测试看看”。

在《程序员修炼之道》中,他们将这描述为“车速超过大灯的照射范围”。本质上,这就是开得太快了,因为“反馈的速度就是你的限速”。

反馈的速度就是限速,这意味着你应该边做边测试,采取经过深思熟虑的小步骤。而 AI 默认情况下在这方面做得很差。

所以,第三个技能是:测试驱动开发(TDD)。

你应该使用 TDD,因为 TDD 强制大语言模型必须采取小步骤。你先创建一个测试,让该测试通过,然后再重构代码使其更优雅,并考虑设计问题。但这里的问题在于:写测试真的很难。

写测试一直都很难。原因是,当你编写测试时,需要做出大量不同的决策。你需要弄清楚:你想测试多大的单元?你需要确定要 Mock(模拟)什么?你需要弄清楚你到底想测试哪些行为?

而所有这些决策都是相互依赖的。如果你要测试一个非常大的单元,比如整个庞大的应用程序,那么它可能会非常不稳定。你可能不想测试那么多行为。如果你只测试这个小单元,你就必须 Mock 那个单元,它们全都是相互关联的。在我的整个开发生涯中,我思考这些问题已经好几年了。

我们注意到的是,优秀的代码库通常是易于测试的代码库。在这里我们又回到了“代码本身很重要”的观点。你的代码库越好,你的反馈循环就越好,因为你能够给大语言模型提供更好的反馈,从而让它生成更好的代码。

于是我想,一个好的、易于测试的代码库到底长什么样?我们再次回到约翰·奥斯特豪特的理论。他谈到了在代码库中应该拥有“深度模块”(deep modules)。不要用“浅层模块”(shallow modules),也就是不要有很多只暴露大量函数但没什么内涵的模块。

应该有相对较少、但体积较大、带有简单接口的“深度模块”。让我们快速比较一下。深度模块把大量的功能隐藏在简单的接口后面,隐藏了复杂性。如果你想看,你可以深入到深度模块内部去看,但你并不需要这么做,你只需使用接口即可。而浅层模块则没有太多功能,且接口极其复杂。

(停顿让观众拍照)

包含大量浅层模块的代码库看起来大概就像这样:你有一大堆零碎的小代码块,AI 必须逐一遍历和导航。这对 AI 来说探索起来非常困难。

这就是为什么你经常会看到,如果是这样一个代码库——AI 非常擅长创建这样的代码库——AI 会完全不理解你的代码在干什么。它会尝试探索代码,但由于布局糟糕、充满了浅层模块,它可能无法及时找到正确的模块,或者无法理解所有的依赖关系。它无法读懂你的代码。

那么,一个充满了深度模块的代码库长什么样呢?它看起来是这样的:代码还是那些代码,但它被结构化在了清晰的边界内,并且在顶部拥有清晰的接口。

对于这些接口,你可能需要对它们有极强的控制权,并把它们设计得非常出色。否则,AI 可能会搞砸整个设计。至于内部的实现细节,你可以稍微放手交给 AI 去做。

那么,如何将一个凌乱的代码库变成这种结构清晰的代码库呢?

我也有个相应的技能:改进代码库架构(Improve codebase architecture)。事实证明,做这件事相当复杂,但它是一套你可以一遍遍复用的步骤。你只需要探索代码库,寻找那些功能相关的代码,然后把它们全包装进一个深度模块中。

这便是一个易于测试的代码库,因为围绕这段代码的边界非常非常简单。你在接口层面进行测试,通过该接口进行验证,这就大功告成了。这样的代码库是对 TDD 开发模式的一种极大奖励。

但如果遇到第六种失败模式呢?也就是:假设你的反馈循环在正常工作,你的开发机器正在飞速运转。你能够比以往任何时候都交付更多的代码,但你的大脑跟不上了。

在开发生涯中,觉得自己现在比以前任何时候都更累的,请举手。我也是,简直让人筋疲力尽。

我认为,那种混乱的代码库实际上给你的大脑带来了沉重的负担,因为你和 AI 都需要把所有的信息塞进脑子里。相比之下,清晰的架构不仅让你读起来更容易理解,也意味着你可以把这些深度模块视为“灰盒”。

你可以这么说:“好吧,我只负责设计接口,我不会太在意或者花太多时间去审查内部的实现。”

当然,你只能在应用程序中相对不那么关键的部分这么做(比如不能在金融交易等敏感模块里这么做)。但在应用的大多数模块中,只要你在模块外部有一个可测试的边界,只要你理解它的目的并能从外部进行设计,你就不需要过多考虑其内部实现。我发现这真的拯救了我的大脑,因为我可以放心地说:“好吧,AI,大模块内部的事情交给你处理。我只负责从外部进行测试和验证。”

所以这就是第五个建议:设计接口,把实现委托给 AI。

但这也意味着,每当我们接触代码、每当我们做计划时,我们都必须思考并了解我们应用中的模块。我们需要对代码的“地图”了如指掌,它需要成为我们“统一语言”的一部分。我们还需要把它融入到我们的规划技能中。所以在编写 PRD 时,我会在 PRD 里具体说明模块的改动、模块内部的接口,以及它们是如何被修改的。我每时每刻都在思考这些。

这个观点来自肯特·贝克(Kent Beck):每一天都要投资于系统的设计。

这才是最核心的要点,对吧?因为在那种纯粹的“需求转代码”模式中,我们并没有投资于系统的设计,我们实际上是在抛弃它,在丢掉系统设计。但我认为,系统设计绝对是关键所在。

因此,代码并不廉价。这就是我想让大家带走的感悟。代码非常重要。

如果我们把 AI 看作一个非常出色的“基层程序员”,一个负责战术实施的开发人员,一个在现场执行代码修改的士兵,那么你需要一个在他们之上的人。你需要一个在战略层面进行思考的人。那个人就是你。

这需要我们运用过去 20 多年来一直在使用的软件基础技能。

如果你对我在这里提到的任何 AI 技能指令感兴趣,它们都在 GitHub 的 macpocockskills 仓库里。如果你对我的培训课程或任何免费资源感兴趣,我在 YouTube 和 Twitter 上都有账号,你也可以访问 aihero.dev,那里有我的新闻邮件可以订阅。

非常感谢大家。我希望这次演讲能给你们在这个新的 AI 时代带来信心,相信你们依然能够发挥重要的影响力。

谢谢!