---
area: "work-career"
category: ai-ml
companies_orgs:
- Netflix
- Cloudflare
date: '2025-12-20'
draft: true
guest: ''
insight: ''
layout: post.njk
media_books:
- The Mythical Man-Month
people:
- Fred Brooks
products_models:
- AI
- Co-pilot
- Cursor
- Claude
- Codeex
- Gemini
project: []
series: ''
source: https://www.youtube.com/watch?v=eIoohUmYpGI
speaker: AI Engineer
status: evergreen
summary: 本次演讲探讨了在AI工具加速代码生成的同时，如何应对日益增长的软件复杂性。演讲者Jake Nations指出，AI通过提供“容易”的解决方案，可能导致开发者忽视“简单”的本质，从而积累难以理解的代码。他回顾了软件工程史上的类似危机，并提出了“上下文工程”或“规范驱动开发”的方法，强调在AI时代，人类的思考、规划和深入理解系统仍然是不可或缺的，以避免知识鸿沟和系统维护的困境。
tags:
- science
- software-development
title: The Infinite Software Crisis – Jake Nations, Netflix
---
大家好，下午好。我将以一个坦白开始我的演讲。我曾提交过自己并不完全理解的代码，生成它、测试它、部署它，却无法解释它是如何工作的。而事实是，我敢打赌你们每个人都这样做过。

### 承认现实

所以，现在我要承认，我们都在提交自己不再理解的代码。我想带大家踏上一段旅程，看看这一切是如何发生的。首先，回顾历史，我们会发现历史往往会重演。其次，我们陷入了一个陷阱：将‘容易’与‘简单’混淆了。最后，有一个解决方案，但这需要我们不外包自己的思考。

### AI加速与风险

我在Netflix工作了几年，帮助推广AI工具的应用，我必须说，这种加速是真实存在的。过去需要几天才能完成的待办事项，现在只需几个小时；那些搁置多年的大型重构项目，也终于得以完成。但问题在于，大型生产系统总是会以意想不到的方式失败。就像最近发生的**Cloudflare**（云雀网络：一家美国公司，提供CDN、DNS、DDoS防护等网络服务）事件。当这种情况发生时，你最好能理解你正在调试的代码。而现在的问题是，我们生成代码的速度和数量如此之快，以至于我们的理解能力难以跟上。说实话，我自己也这样做过。我生成了一堆代码，看了看，心想：我完全不知道这玩意儿是干嘛的。但测试通过了，它能运行。于是，我就把它提交了。

### 历史的循环

关键在于，这并非新鲜事。每一代软件工程师最终都会遇到一个瓶颈，即软件的复杂性超出了他们管理的能力。我们不是第一批面临软件危机的。我们是第一批以这种无限的代码生成规模来面对它的人。所以，让我们回顾一下这一切是如何开始的。在20世纪60年代末、70年代初，一群当时的顶尖计算机科学家聚集在一起，说道：“嘿，我们正面临一场软件危机。我们对软件有着巨大的需求，但却无法跟上步伐，项目耗时过长，进展缓慢，我们做得不够好。”

于是，**Edsger W. Dijkstra**（埃兹赫尔·迪杰斯特拉：荷兰计算机科学家，图灵奖得主，在算法和结构化编程领域贡献卓著）说了一句非常深刻的话：“当我们的计算机还很弱小的时候，编程只是一个小问题；而现在，随着计算机变得无比强大，编程已经变成了一个巨大的难题。”他解释说，随着硬件能力增长千倍，社会对软件的需求也随之增长，这使得程序员必须在方法和手段上找到出路，以支持如此庞大的软件需求。

这种模式不断循环。70年代，我们有了C编程语言，以便编写更大的系统。80年代，个人电脑普及，人人都能编写软件。90年代，我们迎来了面向对象编程，以及“地狱般的继承体系”（感谢Java）。2000年代，敏捷开发、冲刺（sprints）和Scrum大师指导我们工作，瀑布模型不再流行。2010年代，我们拥有了云计算、移动开发、DevOps等等。软件真正地“吞噬了世界”。

### AI时代的新挑战

而今天，我们有了AI。比如**Co-pilot**（微软推出的AI编程助手）、**Cursor**（一款AI优先的编辑器）、**Claude**（Anthropic公司开发的大型语言模型）、**Codeex**、**Gemini**（Google开发的多模态AI模型），等等。我们可以以描述它的速度来生成代码。这个模式仍在继续，但规模已经彻底改变——它现在是无限的。

那么，**Fred Brooks**（弗雷德里克·布鲁克斯：美国计算机科学家，以《人月神话》闻名），你可能从他的著作**《The Mythical Man-Month》**（《人月神话》）中认识他。他还在1986年发表了一篇题为“**No Silver Bullet**”（并非银弹）的文章。在文中，他论证了不会有单一的创新能够带来软件生产力数量级的提升。为什么？因为他说，困难的部分从来不是编码的机械操作——语法、输入、模板代码。困难在于理解实际问题并设计解决方案。没有任何工具能够消除这种根本性的难度。我们迄今为止创造的每一个工具和技术，都让机械操作变得更容易。然而，核心挑战——理解要构建什么、它应该如何工作——仍然一样困难。

### 混淆“容易”与“简单”

那么，如果问题不在于机械操作，我们为何还要不断优化它？经验丰富的工程师为何会写出自己不理解的代码？我认为，答案在于我们常常混淆的两个词：“简单”（simple）和“容易”（easy）。我们倾向于将它们互换使用，但它们实际上意味着完全不同的东西。在演讲者晚宴上，我被揭露是**Closure**（一种函数式编程语言）的爱好者，所以这里可能有点明显。但**Rich Hickey**（理查德·希基：Closure编程语言的创建者），在他2011年的演讲“Simple Made Easy”（简单即容易）中解释了这一点。他将“简单”定义为“单一折叠、单一编织、无纠缠”。每个部分只做一件事，不与其他部分交织。他将“容易”定义为“邻近”——触手可及，无需努力即可访问。复制粘贴，然后提交。“简单”关乎结构，“容易”关乎可及性。

关键在于，我们无法凭空让某件事变得简单。“简单”需要思考、设计和解耦。但我们总能让某件事变得“容易”。你只需把它放在手边——安装一个包，用AI生成它，或者从**Stack Overflow**（一个流行的程序员问答网站）复制一个解决方案。走“容易”的路是人之常情，我们天生如此。就像我说的，从Stack Overflow复制东西，它就在那里。或者使用一个神奇地为你处理一切的框架，安装即用。但“容易”不等于“简单”。“容易”意味着你可以快速地向系统中添加功能；“简单”意味着你能理解你所做的工作。每一次我们选择“容易”，都是在选择“现在的速度”和“未来的复杂性”。老实说，这种权衡过去是有效的。代码库中的复杂性积累得足够缓慢，使我们能够在需要时进行重构、反思和重建。我认为AI打破了这种平衡，因为它提供了终极的“容易按钮”。它让“容易”的路径如此顺畅，以至于我们不再考虑“简单”的选项。当代码瞬间出现时，谁还会去思考架构呢？

### 案例：AI与认证流程

让我展示一下这是如何发生的：一个简单的任务，如何通过我们都喜爱的对话式界面，演变成一团糟的复杂性。这是一个人为设计的例子，假设我们有一个应用，想添加一些认证功能。我们输入“添加OAuth”，得到一个干净的`oauth.js`文件。迭代几次后，它变成了一个`message.js`文件。你可能会想：“好的，很酷。”然后我们又说：“现在也添加OAUTH”，于是我们有了`oauth.js`和`oauth_token.js`。我们不断迭代，然后发现会话（sessions）出错了，出现了一堆冲突。到迭代20次时，你不再是进行讨论，而是在管理一个极其复杂的上下文，甚至你自己都不记得添加的所有约束。遗留了来自被放弃方案的死代码；测试被“修复”，只是为了让它们通过；可能还存在三个不同解决方案的碎片，因为你不断地说“等等，实际上……”。每一个新指令都在覆盖架构模式。我们说“让OAuth在这里工作”，它做到了。我们说“修复这个错误”，它也做到了。糟糕的架构决策没有任何阻力，代码只会为了满足你最新的请求而变形。每一次交互都是在选择“容易”而非“简单”。而“容易”总是意味着更多的复杂性。我们本应知道得更清楚，但当“容易”的路径如此轻易可得时，我们就会选择它。复杂性将不断累积，直到为时已晚。

AI将“容易”推向了逻辑的极致：决定你想要什么，瞬间获得代码。但危险在于，生成的代码会以同样的方式对待你代码库中的每一个模式。当一个AI代理分析你的代码库时，每一行都变成了一个需要保留的模式。第47行的认证检查是一个模式；那个奇怪的、表现得像GraphQL的gRPC代码（可能是我在2019年写过的），那也是一个模式。技术债务不会被识别为债务，它只是更多的代码。

### 定义复杂性

这里真正的核心问题是“复杂性”。我知道我在演讲中反复提到这个词，但没有真正定义它。理解它的最佳方式是：它是“简单”的反义词。它意味着“相互交织”。当事物复杂时，一切都相互关联。你无法改变一件事而不影响其他十件事。

回到**Fred Brooks**的“No Silver Bullet”论文。他指出了系统中两种主要的复杂性：**“本质复杂性”（essential complexity）**，这是你试图解决的实际问题的根本性难度。例如，用户需要付款，订单必须被履行。这是你的软件系统存在的根本原因所带来的复杂性。其次，是**“偶然复杂性”（accidental complexity）**。这是我们在此过程中添加的一切：变通方法、防御性代码、曾经有意义的框架和抽象层——所有这些都是为了让代码本身能够工作的附加物。

在真实的 कोड库中，这两种复杂性无处不在，并且它们会纠缠在一起，以至于分离它们需要上下文、历史和经验。而AI生成的代码却无法做出这种区分，因此每一个模式都会被保留下来。

### Netflix案例：授权重构

这是一个来自Netflix的真实案例。我有一个系统，在五年前编写的旧授权代码和一个新的集中式认证系统之间，存在一个抽象层。当时我们没有时间重建整个应用，所以我们只是在中间加了一个“垫片”（shim）。现在有了AI，这是一个重构代码以直接使用新系统的绝佳机会。听起来是个简单的请求，对吧？然而，事实并非如此。旧代码与它的授权模式紧密耦合。例如，权限检查渗透在业务逻辑中，认证假设被硬编码到数据模型中，认证调用散布在数百个文件中。AI代理开始重构，处理了几个文件后，就会遇到一个无法解耦的依赖，然后彻底失控并放弃。更糟糕的是，它可能会试图保留旧系统中的一些现有逻辑，并用新系统来重现它，我认为这也不是一个好结果。

关键是，AI看不到“缝隙”。它无法区分业务逻辑在哪里结束，认证逻辑在哪里开始。一切都如此纠缠在一起，即使拥有完美的信息，AI也找不到一条清晰的路径。当你的“偶然复杂性”变得如此纠缠时，AI并非最佳帮手，它只会增加更多的层级。

### 人类视角的重要性

我们能够区分这一点，或者说，当我们放慢速度去思考时，我们就能做到。我们知道哪些模式是“本质的”，哪些只是几年前某个解决方案的产物。我们拥有AI可以推断出的上下文信息，但这只有在我们花时间在开始之前做出这些区分的前提下才行。

### 解决方案：上下文工程

那么，我们该如何做到这一点？在面对庞大的代码库时，如何区分“偶然复杂性”和“本质复杂性”？我在Netflix工作的代码库有大约一百万行Java代码，其中主服务大约有500万个token。我所能访问的任何上下文窗口都无法容纳它。所以，当我需要处理它时，我首先想到：“也许我可以将代码库的大部分内容复制到上下文中，看看模式是否会浮现，看看它是否能自行理解发生了什么。”但就像之前的授权重构一样，输出结果完全迷失在自身的复杂性中。因此，我不得不采取不同的方法：我必须选择要包含的内容——设计文档、架构图、关键接口等等，并花时间写下组件应如何交互以及遵循何种模式的要求。我写了一个规范，将500万token浓缩成2000字的说明。更进一步，我将这个规范转化为一套精确的代码执行步骤。没有模糊的指令，只有精确的操作序列。我发现这产生了更清晰、更集中的代码，并且我能够理解它。当我先定义好并规划好执行过程后，这便是我之前称之为“**上下文压缩**”（context compression）的方法。你可以称之为“**上下文工程**”（context engineering）或“**规范驱动开发**”（spec-driven development），名字不重要。重要的是，思考和规划成为了工作的主体。

下面我将详细介绍实践中的具体操作。第一步，第一阶段：研究。我会将所有相关信息一次性输入给AI，包括架构图、文档、Slack讨论等。我反复强调这一点，但关键是尽可能多地提供与你正在进行的更改相关的上下文。然后，使用AI代理分析代码库，绘制出组件和依赖关系图。这不应是一次性的过程。我喜欢进行探究，比如“缓存是如何处理的？”“它如何处理失败？”当它的分析错误时，我会纠正它；如果它缺少上下文，我会提供。每一次迭代都会优化其分析。最终输出的是一份研究文档：它说明了现有内容、它们之间的连接关系，以及你的更改将影响哪些部分。数小时的探索被压缩成几分钟的阅读。

我知道Dex今天早上提到了这一点，但这里的“**人类检查点**”（human checkpoint）至关重要。这是你将AI的分析与现实进行验证的时刻。这是整个过程中杠杆率最高的时刻。在此处捕获错误，可以避免日后发生灾难。

进入第二阶段。在你掌握了有效的研究成果后，我们创建一个详细的实现计划。包括真实的**代码结构**、函数签名、类型定义、数据流。你希望这份计划详细到任何开发者都能遵循。我将其比作“**数字填色**”（paint by numbers）。你应该能够把它交给最初级的工程师，并说：“去照着做。”如果他们逐行复制，代码就应该能正常工作。这一步是我们做出许多重要架构决策的地方。确保复杂逻辑的正确性，确保业务需求遵循良好实践，确保有良好的服务边界、清晰的隔离，并防止任何不必要的耦合。我们能在问题发生前就发现它们，因为我们经历过。AI没有这个选项，它将每一个模式都视为一个需求。这一步真正的魔力在于评审速度。我们可以在几分钟内验证这个计划，并确切知道将要构建什么。为了跟上我们想要生成代码的速度，我们也需要同样快速地理解我们正在做的事情。

最后是实现阶段。现在我们有了清晰的计划和明确的研究支持，这一阶段应该相当简单。这就是重点。当AI遵循清晰的规范时，上下文保持干净且专注。我们避免了冗长对话带来的复杂性螺旋。我们不会得到50条迭代式代码的回复，而是得到三个专注的输出，每个输出在进行下一步之前都经过验证。没有被放弃的方案，没有冲突的模式，也没有那些导致死代码随处可见的“等等，实际上……”的时刻。对我来说，真正的回报是，你可以让AI代理完成大量工作，因为你已经提前完成了所有的思考和艰苦工作。它可以直接开始实现。你可以去做别的事情，然后再回来评审，并且可以快速评审，因为你只是在验证它是否符合你的计划，而不是试图理解它是否“发明”了什么。

### AI是加速器，而非思考者

关键在于，我们不是让AI替我们思考。我们是利用它来加速机械部分的工作，同时保持我们理解它的能力。研究更快，规划更周全，实现更干净。而思考、综合和判断，仍然掌握在我们手中。

还记得我之前提到的那个AI无法处理的授权重构吗？现在我们实际上正在着手处理它，并取得了一些进展。但这并非因为我们找到了更好的提示词。我们发现，我们甚至无法直接进入研究、规划或实现阶段。我们不得不亲自动手进行这项更改。没有AI的介入，只是阅读代码、理解依赖关系，并进行更改以观察会发生什么。坦白说，那次手动迁移很痛苦，但至关重要。它揭示了所有隐藏的约束、必须保持不变的规则，以及如果认证发生变化会导致哪些服务崩溃。这些是任何代码分析都无法为我们揭示的。然后，我们将这次手动迁移的Pull Request输入到我们的研究流程中，让它作为后续研究的种子。AI随后就能看到一个干净的迁移示例。但关键是，这些实体（指代码中的各个部分）都略有不同。所以我们必须逐一审视，询问“我们该如何处理？”有些东西被加密了，有些则没有。每次都需要通过大量的迭代来提供额外的上下文。只有这样，我们才可能生成一个可能一次性成功的计划。而“可能”是这里的关键词——我们仍在验证、调整，并发现边缘情况。

这种三阶段方法并非魔法。它之所以有效，是因为我们手动完成了这次迁移。我们必须“赢得”理解，才能将其融入我们的流程。我仍然认为没有“银弹”。我不认为存在更好的提示词、更好的模型，甚至更好的规范。真正重要的是深入理解你的系统，以便能够安全地对其进行更改。

### 为何不直接与AI迭代？

那么，为什么要经历这一切呢？为什么不直接与AI迭代，直到它能工作为止？模型最终不会足够强大，以至于它就能直接工作吗？对我来说，“能工作”是不够的。能够通过测试的代码与能在生产环境中生存的代码之间存在差异；今天能运行的系统与未来能被他人修改的系统之间也存在差异。这里真正的核心问题是“**知识鸿沟**”（knowledge gap）。当AI能在几秒钟内生成数千行代码时，理解它可能需要你数小时，甚至数天（如果复杂的话），谁知道呢，如果它真的那么纠缠不清，可能永远也无法理解。

还有一点，我认为很多人甚至没有谈论过：每一次我们为了跟上生成速度而跳过思考，我们不仅在添加我们不理解的代码，我们还在失去识别问题的能力。那种“嘿，这变得太复杂了”的直觉，在你无法理解自己的系统时，就会萎缩。

模式识别来自于经验。当我发现一个危险的架构时，那是因为我曾凌晨三点还在处理它。当我推动更简单的解决方案时，那是因为我不得不维护别人留下的复杂替代方案。AI会生成你所要求的东西，但它不会编码过去的失败教训。

三阶段方法弥合了这一差距。它将理解压缩成我们可以以生成速度进行评审的产物。没有它，我们只会以比理解能力更快的速度积累复杂性。

### 结论

AI改变了我们编写代码的一切。但老实说，我认为它并没有改变软件本身失败的原因。每一代人都面临着自己的软件危机。Dijkstra那一代人通过创建软件工程这门学科来应对；而现在，我们则面临着无限代码生成带来的危机。

我认为解决方案不是另一个工具或方法论，而是要记住我们一直都知道的：软件是一项人类的事业。困难的部分从来不是输入代码，而是首先知道该输入什么。那些蓬勃发展的开发者，不仅仅是那些能生成最多代码的人，更是那些理解自己正在构建什么、能看到“缝隙”、能识别出自己是否在解决错误问题的人。这仍然是我们，而且只会是我们。

我想留下一个问题，我认为问题不在于我们是否会使用AI——这已是定局，船已经启航。对我来说，问题将是我们是否还能理解我们自己的系统，当AI编写我们的大部分代码时。

谢谢大家。
[掌声]