导语:世界级编程大师、《代码整洁之道》作者“Bob大叔”(Robert C. Martin)近日与知名 TypeScript 教育家 Matt Pocock 直播对谈,分享他与 AI Agent 协作编程的最新实践:不再逐行审阅 AI 生成的代码,而是依靠单元测试、变异测试、质量指标等确定性工具自动把关。他同时提醒开发者:软件基础从未过时,轻视它的人“会吃苦头,而且不会等太久”。
“我完全不看 Agent 写出来的任何代码。”大约一个月前,这句话从 Robert C. Martin 口中说出后,很快在开发者社区里引发了不小的讨论。这位全球开发者熟知的“Bob 大叔”,是《代码整洁之道》的作者,也是编程生涯超过五十年的世界级编程大师。让人意外的是,进入 AI 时代后,他开始尝试一件许多程序员都难以理解的事:让 Agent 写代码,而自己不再逐行检查。
对于习惯了 Code Review 流程的开发者来说,这种做法显然很难接受。很多人忍不住发问:如果 AI 生成的代码不再需要人类逐行阅读,程序员凭什么判断它写得对不对?事实上,Bob 大叔并不是打算“闭着眼睛”全盘接收 AI 的输出。他给 Agent 套上了一层又一层的约束:单元测试、Gherkin 测试、QA 流程、质量指标、变异测试、测试覆盖率,以及大量其他自动化检查。只有当代码顺利通过这些测试与质量关卡,他才会对最终结果建立起“很高的信心”。
近日,Bob 大叔与知名 TypeScript 教育家 Matt Pocock 进行了一场直播对谈,围绕 AI 时代的软件基础,以及他过去几个月与 AI Agent 一起写代码的真实经历展开。在他看来,关键并不在于放弃软件工程,而在于改变人与代码之间的分工:与其让人逐行检查 Agent 产出的代码,不如建立一套确定性的规则和工具,让它们自动判断代码是否达标。而谈到如何识别“垃圾代码”时,Bob 大叔坦言,他能看出 Agent 什么时候开始“挣扎”,因为作为程序员,他也经历过同样的挣扎——问题在于,一个刚入行的程序员,可能根本识别不出这种挣扎。
对谈开场,Matt 提到了 Bob 大叔广为人知的“浴袍梗”。Bob 大叔解释说,这件事大约发生在两年前:当时是早上六点,他穿着浴袍坐在前廊,忽然想到 SQL 注入实在是一件糟糕透顶的事——让一种文本语言直接承担访问数据库的角色,在安全性上本身就很不合理。他越想越觉得不对劲,于是掏出手机发了一通牢骚,这就是后来所谓的“晨间浴袍吐槽”的由来。没想到反响还不错,之后他又陆续做了几次,浴袍也逐渐成了他的个人标志。
Bob 大叔还分享了自己的编程起点:1964 年,他 12 岁,写下了人生中的第一个程序。那台机器是母亲为他 12 岁生日买的小型模拟电脑,编程方式是把白色的小管子套在插桩上,本质上是一个 3 位的有限状态机——但在当时的他看来,这已经足够令人着迷。此后,父亲给他买了 Fortran、Cobol 和 PL/I 的书,他把这些书全部读完,只是当时没有机器可以运行程序,只能把代码写在纸上,然后在脑子里把程序“运行”一遍。16 岁时,他找到一份可以写代码的临时工作;18 岁那年,他得到了第一份真正的程序员工作,从此一直做到今天。
谈到自己与 AI 的接触,Bob 大叔说,去年 12 月左右,这件事确实让他吃了一惊。圣诞节期间,他开始尝试 ChatGPT、Grok 之类的工具,最初没觉得有多惊艳,后来才逐渐意识到这些东西可能比想象中更有意思。他找了最早一批相对早期的 Grok 智能体帮自己写代码,结果写得确实不怎么样,但至少,它真的把代码写出来了。当时他正忙着一个项目,便试着让它参与工作,那个阶段他一直在给智能体“擦屁股”——它速度确实快,但总会留下隐患,甚至经常把局面搞得一团糟。他的感受很矛盾:一方面觉得有意思,因为确实快;另一方面又很沮丧,因为它反而让自己变慢了。后来他转念一想:既然它这么快,那它其实可以去做一些人类自己根本做不到的事情。
于是,一些早年搁置的想法重新浮出水面。早在 2000 年代初,Bob 大叔就有一个自己觉得很棒、但当时完全不现实的想法——CRAP。这个缩写把代码测试覆盖率与每个函数的圈复杂度结合起来,通过一个比较复杂的公式算出一个分数,用来衡量一个函数到底有多糟糕。他曾经在一个大型项目上跑过一次,确实找出了大量糟糕的函数,但问题在于当时只能一个一个去修,还得重新编写测试,成本实在太高,最后只能把想法搁置下来。另一个让他感兴趣的创新是“变异测试”:让一个小程序自动修改源代码,比如把负号改成加号、把小于号改成大于号、把等号改成不等号,每修改一次就运行一遍完整的测试套件,预期测试应当失败——因为代码已经被故意改坏了。如果测试没有失败,就说明出现了一个“存活的变异体”,需要把问题处理掉。2000 年时,他曾在项目上试过变异测试,当时得让它跑一整晚:完整测试套件每次要运行 4 分钟,还要重复几百次。而现在,让智能体来做这件事,以前要跑一整晚的任务 30 分钟就能完成,它还能把所有测试漏洞补上。
Bob 大叔由此想到,这或许是清理代码残留垃圾的好办法:AI 写代码时确实会留下很多“碎屑”和“浮毛”,但它们自己也许就是清理这些东西的好工具。他不断加入更多工具,让智能体互相配合,到现在它们已经做得相当不错。他现在的原则是:让智能体去干活、去运行这些工具,并努力达到一种状态——不需要亲自去看代码,也可以信任它们。当然,他仍会通过其他方式验证代码质量,比如检查 CRAP 分数、抽查代码、运行其他测试;既然智能体处理代码的速度比他快得多,那就让它们负责写代码,自己处理更高层面的事情,确保一切正常运转。
那么,AI 生成的“烂代码”该怎么办?Matt 提出一个假设:既然 AI 这么快,为什么还要在意烂代码?为什么不能顶着 Bug 一路往前冲,直到把 Bug 冲掉?Bob 大叔的回答很直接:他早就发现,如果一直让 Agent 往下做,却不去清理它留下的“烂摊子”,它的速度反而会越来越慢——它会陷入一种困境,改了一个地方,却无意中破坏了另一个地方,为了修复又破坏了其他地方,最后开始不停地绕圈子。这些智能体虽然快、也确实聪明,但它们和人类一样会受到烂代码的影响,也许容忍程度和人类不同,但这个阈值依然存在。代码烂到一定程度,Agent 就处理不了了,会开始原地打转,把烂摊子越搞越大,他甚至遇到过某个智能体直接对他说:“我处理不了了。”
谈到为什么不靠“堆指令”来引导,Bob 大叔坦言,最初他确实是这么做的:给智能体的 Prompt 基本都是测试驱动开发(TDD)的方法、整洁代码的要求、代码应该长什么样、应该遵循哪些规则,最后可能得到一份长达 10 页的文档,专门告诉它什么是“好代码”。但他很快发现,这些模型对待规则的态度,特别像《加勒比海盗》里的“海盗法典”——听起来是规则,对它们来说却更像是一堆“建议”。这背后有技术原因:随着上下文窗口越来越大,放在最前面和最后面的内容往往比夹在中间的内容更容易被模型关注,这个现象叫“中段丢失”(Lost in the Middle)。比如一份很长的 Prompt,开头前三句话模型可能记得很清楚并当作高优先级指令,但第 50 句、第 80 句的内容就可能被丢到上下文中某个角落。当模型面对海量上下文时,中间的信息更容易被忽略甚至“消失”。但确定性工具不会这样——它们不会因为规则写在第 50 行还是第 80 行,就突然把规则当成“建议”。因此他认为,使用智能体的关键,就是尽可能把初始提示词精简到绝对最小值、让它留在优先级区域,之后再用确定性工具接手。
Matt 也分享了自己的观察:他把上下文窗口称作“聪明区”和“愚蠢区”——这个说法并非他原创,而是来自 Dex Horvath。在上下文窗口前段,比如前 15 万个 Token,模型通常表现得相当聪明;但随着上下文不断变长,Transformer 的注意力机制会越来越吃力,信息逐渐被稀释。这就像一个越来越拥挤的房间,每个 Token 都在大声说话,人越来越多,最后你很难从一片噪音中分辨出真正重要的信号。Bob 大叔认同这个比喻,并补充道,他正在研究自动化检查是否也存在“太多了”的临界点:如果检查最终让智能体慢到还不如人类,那就输了;但只要它的生产力仍然高于人类,就依然处于领先地位。根据他目前的观察,这种生产力优势大概可以维持在 2 到 4 倍左右。当然,确定性工具会明显拖慢智能体,因为本质上你把它放进了一个循环里:要求它不断修改代码,直到工具最终告诉它“OK”。于是 Agent 会不停循环——改这个、改那个、增加更多测试、降低圈复杂度、拆分函数——花很长时间才能达到预先设定的合规标准。本质上,这是在牺牲一部分生产力换取更高的代码质量。虽然还没找到这种方式的极限,但他正在尝试让多个智能体彼此协作、相互交接:一个负责写代码,下一个负责审查,再下一个负责测试和强化。这样做会带来巨大的通信开销,但即便如此,它们的整体速度仍然比人类快得多。
在 Bob 大叔看来,采用多 Agent 有两个明显优势。第一,可以并行运行多个 Agent——比如同时运行三个编码 Agent,他的笔记本电脑甚至还能支持更多。第二,当 Agent 的任务聚焦到单一任务时,就能更好地控制上下文窗口:“中间丢失”的问题会减轻,还能在顶部堆叠更多规则,让 Agent 执行得更好。他甚至设置过这样一套机制:让 Agent“出生、完成任务、然后消失”,这样下一个 Agent 进来时,面对的就是一个干净的上下文窗口;缺点是启动时间更长,一个 Agent 可能需要 10 到 15 秒才能启动并理解当前上下文。他倾向于尽可能把任务拆得足够细、足够聚焦。
他详细介绍了自己的一套五角色流水线:先运行一个“规格定义器”,把人类编写的文档转换成 Gherkin 语言和 QA 流程——QA 流程本质上是一套系统测试,要求从人类用户视角出发,在 UI 层面操作系统,证明系统能正常工作;随后这两份文档交给“编码器”,负责编写单元测试、实现故事逻辑、让 Gherkin 测试跑通;完成后交给“清理器”,负责复杂度分析和常规代码审查,把实现者留下的“烂摊子”清理干净;接着交给“强化器”,运行变异测试,这个 Agent 非常无情,会通过修改等号、小于号之类的细节反复验证测试是否真的有效,并确保达到 100% 的测试覆盖率,这个过程需要很长时间;最后交给“QA Agent”,把书面的 QA 文档转换成可执行脚本,用脚本直接操作系统并给出确定性的测试结果。如果能通过这一整套流程,最终得到的程序质量会非常高。这种方式的性价比如何?Bob 大叔说,给单个 Agent 一个任务,可能 5 分钟就能完成,但结果是否可靠很难说;而采用整套流程可能需要一个小时,但依然划算——因为让人类完成同样的工作,可能需要半天。
谈到代码库的内部结构,Bob 大叔说,大约在一个月前,这部分工作他还是手动完成的:先让 Agent 构建一个东西,然后通过不断提问来“审问”它们——这里是什么结构?这个模块和那个模块怎么关联?模块到底是什么?等到得到那些“令人恐惧的答案”后,他会亲自设计模块结构,告诉 Agent 模块应该如何划分、之间应该如何通信,再给它们一个实现计划去执行。这个过程非常辛苦,所以他让 Agent 为自己构建了一个“架构查看器”:它能在屏幕上弹出类似 UML 的图表,展示整个系统的模块结构和依赖关系,点击某个模块就能查看它的子模块,甚至直接查看对应代码。此外,他还构建了另一个确定性工具,用来定义哪些模块可以依赖哪些模块、哪些模块绝对不能互相依赖,最终形成一份 Agent 无法违反的规范文件——如果违反,就必须通过反转依赖、提取接口等方式修复。他正在尝试把整个规划过程自动化,但目前还没有取得太大进展。
Matt 还引用了 John Ousterhout 提出的“深层模块”概念:模块大致有两种形式,一种是“浅层模块”,接口复杂但内部实现很少;另一种是“深层模块”,接口简单却在内部隐藏了大量信息。这种设计对 Agent 来说非常理想,因为它们只需要读取接口,而不必理解具体实现。Bob 大叔完全认同——模型非常关注接口的名称和结构,这意味着它们不必阅读下层代码,这既是风险也是优势,只要代码保持连贯一致就没有太大问题。它们也会通过阅读测试来理解系统的功能,因此任何有助于优化代码结构的做法,都会帮助模型更好地理解代码。他还顺带提到,《代码整洁之道》的附录里记录了与 John Ousterhout 之间的一场长篇辩论,那真的非常有意思。模块化的好处在于:任何划分良好、接口严谨的东西都是人类可以理解的,因为人类大脑擅长碎片化处理,模型和 Agent 也是如此——如果一个模块的“轨迹”足够清晰,模型不会被该模块内部的话题所混淆;反之,如果在一个模块里塞进天底下所有的东西,可怜的 Agent 就会纳闷“我在这里到底在干什么”。这与他提到的“咖啡与肥皂剧”论点一致:如果模型和用户愉快地聊冲咖啡时,旁边又有人聊起肥皂剧并进入上下文窗口,之后再提到咖啡,模型就可能莫名其妙地把咖啡和肥皂剧联系起来——只要能让模型保持正确方向、尽可能保证上下文内容一致,就能避免这些奇怪的幻觉,至少也能减少对齐偏差。
那么,传统编程规范里有哪些需要重新定义?Bob 大叔首先谈到“阈值”。在他看来,Agent 能处理的复杂度与人类开发者并不一样——它们的短期记忆比人类强得多,而且非常精准。因此他会相应调整 CRAP 分值、放宽对函数规模的限制:对人类开发者,他要求 CRAP 分值控制在 4 以下;对智能代理,他目前把阈值放到了 6,甚至在考虑提高到 8。他一直在寻找最合适的阈值,但这并不容易。CRAP 分值本质上取决于圈复杂度,也就是一个函数内部存在多少条不同的执行路径:如果测试覆盖率达到 100%,CRAP 分值为 6,就意味着这个函数有 6 条执行路径,而且每一条都经过了测试——这正是 CRAP 的核心目标:确保测试覆盖率足够高,同时限制圈复杂度。他还提到自己曾就这个问题和 Agent 争论过很多次,虽然不能完全相信 Agent 在辩论中的结论,但它们似乎认为 6 是一个相当不错的标准。
另一个需要重新审视的,是测试驱动开发(TDD)。Bob 大叔曾是 TDD 的坚定拥护者,但他现在认为,那是一种“人类的纪律”,是根据人类的思维方式演化出来的——他并不打算把这种纪律强加给 Agent:强迫 Agent 写一行测试、再写一行生产代码、然后再写下一行测试,没有什么意义。对人类来说这样做的收益非常大,但对 Agent,他更愿意允许它们采用 John Ousterhout 提倡的方式:先写出一个函数,再为这个函数编写测试。事实上,即使他要求 Agent 严格按照 TDD 的纪律来做,它们最终也总会回到“先写代码、再写测试”的模式。他的结论是:把人类的纪律强加给智能 Agent,可能是一个错误——我们不需要强加纪律,但需要坚持“人类的价值观”,只是具体的阈值需要根据智能代理的特点进行调整。
在任务规划上,Bob 大叔提醒,现在最大的诱惑是过度编写规格说明:让开发者不断完善需求和计划、写成规格说明,再一次性交给 Agent。这是一个源自 70 年代的古老诱惑,后来瀑布流开发模式就是这种思路的典型代表,而敏捷开发的兴起,很大程度上就是对这种做法的反思和回击。过于沉重的前期规划往往会把事情搞得一团糟,因为最终做出来的东西几乎不可能和最初设想的一模一样。他说自己这周就尝试过这种方式,结果一次次证明是一场灾难——作为人类,你很快就会发现 Agent 根本无法完全按照你制定的计划执行,因为你不可能提前考虑到所有细节,而 Agent 也没有你那么强的判断能力,于是它们很容易朝着错误的方向一路跑偏,你只能叫停、回滚、重新制定计划、从头开始。所以他已经放弃这种做法,回到敏捷的方式:先让 Agent 完成一两个具体任务,看看整体架构有没有问题;如果需要,就手动介入做一些调整,再让它继续完成几个任务。他坦言,可能永远都无法完全摆脱最后这一步“人工组织”的工作,虽然一直在想办法解决,但目前还不确定这是否真的可行。
Matt 提到,现在很多人热衷于“计划极大化”:拿着一份规格说明,让七个不同的 Agent 分别跑一遍,不断完善计划,然后才真正开始执行。Bob 大叔对此并不乐观:Agent 非常喜欢写计划,它们会不断修饰计划,把它写得越来越华丽、完美、细节丰富,但到了真正执行的最后阶段,往往还是会崩盘。现在整个行业都能看到这种趋势,也就是所谓的“规格驱动开发”(Spec-Driven Development,SDD),但他的直觉是,这条路行不通。我们应该重新审视敏捷开发的核心思想:先做一点,获取反馈,再调整和重新组织,然后继续做一点。他以前做敏捷开发演讲时常举一个例子:假设改建一栋房子的成本只需要 1 美元——包括打地基、修屋顶以及之后所有的修改,每次都只需要 1 美元——那么你会怎么盖这栋房子?是先花几千美元请建筑师设计一套完美的方案,再花 1 美元交给承包商一次性盖出来,还是直接走到承包商面前边建边改:“地基打在这里,做成这个形状。噢,不对,改一下。厨房放这边,客厅放那边。等等,还是换个位置吧”?显然后一种方式更合理。而现在软件修改的成本已经大幅下降、几乎接近于零,为什么还要花大量时间和精力做昂贵的前期规划?为什么不直接不断尝试、调整和修改,直到它看起来正确为止?
谈到规格说明与源代码的关系,Bob 大叔认为,规格说明是转瞬即逝的,它们会消失,也会频繁变化,并不等同于源代码。过去人们常说“人类编写了源代码,所以代码就是最终的规格说明”,但现在这个说法已经站不住脚了——源代码依然存在,只是已经不再由人类编写。很多人因此感到失落,认为必须有某种由人类定义的东西放在最前面,但归根结底,即使是 Agent 产出的东西,最初也是由人类驱动的。他现在不再编写一份用来定义“我想要什么”的规格说明,而是直接看最终结果,那个结果本身就是规格说明。他手里有很多工具,比如针对 Clojure、Java 和 Go 的 CRAP 工具、变异测试工具,以及 Agent Harness,但他会告诉别人:不要直接下载我的工具,因为那是我为自己写的;你应该让自己的 Agent 去研究这些工具,然后根据你的需求量身定制一个——这才是更好的方式:真正定义事物的本质,并根据具体需求进行定制。
Matt 还谈到一个有趣的现象:如果你给某人发一份详尽的技术规范,可能只能得到 20% 的阅读率——20% 可能还说多了,也许只有 5%;但如果你把规范交给智能体,它们大概率真的会读完。Bob 大叔补充道,反过来也一样:Agent 写出来的东西,人类反而不读了。这完全是一种不对等的关系。而这场对谈的重头戏,是给新生代开发者的忠告。Bob 大叔说,程序员学习编程的方式(无论在学校还是其他地方)都应该是写代码:你应该先写上一年代码(具体多久他也说不好),这样你才能真正知道 Agent 究竟在处理什么。下一步,当你入职一家大量使用 Agent 的公司时,作为一个刚完成培训的年轻人,你应该被当作一个 Agent 来对待——那位手下运行着一堆智能体、自己负责战略决策的首席工程师,应该给你分配和智能体一样的任务,让你接受和智能体一样的确定性工具约束。你应该在这种状态下待上几个月,虽然产出会非常低,但能学到非常多的东西;等你通过了这种严酷的考验,也许才会被信任去亲自运行一个智能体。你绝对不能完全丢掉代码。
他回忆道,十年前,他经常告诉人们:如果你从来没有写过汇编语言,那就应该花一个周末写写汇编,这样你才能知道后台到底发生了什么——如果你整天只写 Java,那你就是生活在一个幻觉世界里,那里仍然存在很多你不理解的“魔法”。他认为这一点在今天依然成立:在这条学习路上,你必须从最基础的东西——二进制——开始,一路经过汇编语言、像 C 这样的基础编程语言、像 Python 这样的高级语言,然后再进入处理智能体级别的工作,学习使用确定性工具,最后才能在监督下真正开始战略性地运行智能体。Matt 提到 John Ousterhout 对编程的分类:战术编程像地面作战的士官,真正身处战场、负责具体战斗;战略编程则更像将军,从更高层面指挥整场战争。智能体非常擅长战术,却非常不擅长战略。传统上,战略编程的反馈循环非常长,一个人干六个月就辞职,可能永远学不会战略编程,因为他犯下的错误可能要九个月之后才会暴露出来;但有了智能体,一切都被大大加速,你实际上可以更早获得关于错误的反馈。
那么,Bob 大叔是怎么知道自己的智能体在犯错的?他说,早期他只是看着代码,发现里面那些“不好的代码”,但那其实不是最重要的部分,更重要的是下一步:他会看着它们“瞎忙”——他能看出 Agent 什么时候在挣扎,因为自己也经历过同样的挣扎;问题在于,一个刚入行的人可能根本识别不出这种挣扎。他是通过艰苦的实践学会这一点的。他建议去读那些老书,比如 Tom DeMarco、Ed Yourdon 的著作,以及《程序员修炼之道》,这些书里关于更高层面战略博弈的内容非常丰富,只是因为年代久远已经没人读了。当然,你需要过滤掉一些过时的内容,因为很多书写于 20 世纪 70、80 年代,但有意思的是,很多重要的经验和教训恰恰就是在那个时候总结出来的。他最初推荐的学习方式是:先通过阅读这些书建立基本的理解,再通过亲身实践真正掌握它;也正因为如此,他认为新人应该先“扮演”几个月的智能体,亲自经历一下智能体所处的状态,才能真正理解那是什么感觉。
谈到软件基础为何依然重要,Bob 大叔引用了迪杰斯特拉(Dijkstra)的话:软件是人类迄今为止尝试过的最复杂的东西,比我们做过的任何其他事情都要复杂。而基础,其实就是我们组织这种复杂性的一种方式,让复杂的东西变得可以理解——不仅让人类能够理解,也让我们的模型能够理解,毕竟模型终究也是模仿人类建立起来的。正因如此,基础在今天依然适用。他认为,现在有些人觉得软件基础已经不重要了,他们会吃到苦头,而且不会等太久——虽然可能会比想象中更久一点,因为智能体确实很厉害,但他已经见过它们撞墙了,知道那堵墙就在那里,所以不想再撞一次。
这场对谈还描绘了一个有意思的演变过程:我们现在已经站在编译器之上了——过去,抽象层是编译器;在那之前是汇编语言;再往前是二进制;现在,这个抽象层又向上提升到了模型。而每一次抽象层向上提升,处在更低层级的人都会抱怨:“这会毁了一切。我们甚至都没工作可做了。编程变得这么简单,五岁小孩都能写代码了。”每一次都是同样的故事,现在,我们又进入了下一个阶段,下面的人开始说“这会毁了一切”。但 Bob 大叔的回答很笃定:不,它不会。同样的规则依然适用,所有这些基础之所以存在,都是出于同样的原因——你今天扔掉的那些规则,一年之后,很可能还是会从地上把它们捡起来,掸掉上面的灰尘,然后重新想起:当初为什么需要这些东西。
免责声明:本文内容整理自公开报道,AI 产品动态与进展以官方发布为准,仅供参考,不构成任何投资建议。
发表评论 取消回复