关于慢下来的思考

5月21日15min

乌龟表情包

乌龟的表情就是我看着我们这个行业时的样子


距离真正能帮我们搭完整项目的编程代理登场,已经差不多一年了。Aider 和早期 Cursor 当然算先驱,但它们更像助手,不像现在意义上的代理。新一代工具太诱人了,很多人把大量业余时间砸进去,把那些一直想做却没有时间做的项目一口气搭了起来。

我不觉得这件事本身有什么问题。用业余时间做东西本来就是一件很快乐的事,尤其是那些没有用户、没有收入压力、也没有长期维护负担的小项目。它们更像实验,失败了也没什么关系。你甚至可以把它当成学习新技术栈的一种方式。

真正的问题是,很多人开始把这种心态带进生产代码库。代理写代码很快,于是我们很容易误以为速度就是进步。它能在一晚上生成几万行代码,于是我们很容易忘记,代码被写出来只是第一步,后面还有理解、维护、演化和承担后果。

过去这一年,Anthropic、OpenAI、Cursor 这些公司不断把代理式编程推到更显眼的位置。假期里的免费额度、各种活动、产品演示,都在让更多人第一次体验到这种魔力。那种感觉确实很上头:你描述一个想法,过一会儿,一个能跑的东西就出现在你面前。

但十二个月过去后,我们也开始看到另一面。不是所有速度都是真的效率。不是所有能跑起来的东西,都值得继续往上堆。

一切都开始变脆弱了

我很难说这些变化都直接由 AI 或编程代理造成。软件质量下降这件事,在代理出现之前就已经存在了。只是现在我越来越觉得,我们可能在加速这个过程。

很多服务变得更不稳定。很多用户界面会出现一些非常奇怪的问题,奇怪到你会以为它们根本不可能通过最基本的 QA。小故障、小回归、小的不一致,开始以一种更高的频率出现。单独看每一个都不致命,但放在一起看,就会让人觉得整个系统正在变得松散。

比如 AWS 那次据称由 AI 引发的故障。AWS 很快出来否认,但内部随后又有一场为期 90 天的重置。这当然不能直接证明什么,但它至少说明,大公司也开始重新审视代码变更的控制方式。

微软 CEO 萨提亚·纳德拉 也多次提到微软内部有相当比例的代码由 AI 生成。与此同时,Windows 的质量问题仍然频繁被讨论。微软自己也写过一篇关于 Windows 质量承诺 的文章。这里同样没有必要简单归因,但这种对比很有意思:一边是生成代码比例越来越高,另一边是用户对产品稳定性的信任越来越低。

还有一些声称产品几乎完全由 AI 构建的公司,它们输出的东西往往很难让人放心。内存泄漏、UI 抽风、残缺功能、莫名崩溃,这些问题都不是新问题。但当一个团队把“由 AI 构建”当成卖点时,这些问题就会变得更讽刺。因为它们没有证明代理能替代工程判断,只证明了代理可以更快地把问题堆出来。

我听到越来越多类似的故事。无论是小团队还是大公司,都有人说自己用代理编程把项目推进了死胡同。没有代码审查,没有清晰设计,设计决策交给代理,功能一层层堆上去。短期看很快,长期看很难收拾。

我们不应该这样和代理共事

我觉得现在最大的问题,不是代理不够强,而是我们太容易放弃自己的主体性。

我们把原本属于人的判断让了出去。架构怎么定,API 怎么设计,哪些功能该做,哪些功能该砍,哪些复杂度值得引入,哪些复杂度应该拒绝。这些事情本来就是软件工程里最重要的部分。但当代理表现得足够聪明时,人很容易退到一边,只负责说“继续”。

然后代码开始以一种不正常的速度增长。

你搭了一个编排层,用来指挥一堆代理并行工作。你安装了各种工具和任务系统,因为网上所有人都在说这是新范式。你看着 Anthropic 用一群代理做 C 编译器,看着 Cursor 用代理造浏览器,然后自然会觉得,也许所有软件问题都可以靠更多代理、更长上下文、更强模型解决。 这种想法很诱人。它也不是完全错的。对一些边界清晰、失败成本很低的任务来说,这套方法确实有效。一个几乎没人用的副业项目,一个临时脚本,一个内部工具,一个探索性 demo,都可以让代理放开手脚去做。 但把这套东西直接搬到真实产品里,就完全是另一回事了。

真实产品有用户,有数据,有历史包袱,有无法轻易改动的接口,有隐含的业务规则。它不是一块空地。代理最擅长的是在局部上下文里完成局部任务,而真实软件最麻烦的地方,恰好是所有局部决策最终会互相影响。 也许有人能把大规模代理编程用在严肃软件上,同时不制造出一堆难以维护的垃圾。如果你能做到,我很佩服。但至少在我看到的很多场景里,这件事远没有演示里那么轻松。

小问题是如何滚成大问题的

代理会犯错。这很正常。人也会犯错。

有些错误很明显,比如类型不对、测试失败、逻辑分支漏了。这类错误反而不太可怕,因为它们容易被发现,也容易通过回归测试固定下来。

更麻烦的是那些没有立刻爆炸的小问题。这里多一个没必要的方法,那里多一层说不清楚的抽象,远处又重复实现了一段现有逻辑。单独看,它们都不像事故。代码能跑,测试也许也能过。但系统的复杂度就是这样一点点涨上去的。

人也会写出这种代码。区别在于,人有瓶颈。

一个人一天能写的代码有限。即便这个人总是做出糟糕选择,他每天往代码库里塞进去的问题也有限。更重要的是,人会感受到痛苦。当同一个问题反复出现,当维护成本变高,当自己被过去的代码绊倒,人通常会停下来修一修。要么是自己受不了,要么是被别人提醒,要么是团队流程把问题拦住。

代理没有这种痛感。

开箱即用的代理不会因为昨天犯过同样的错,今天就真正长记性。你当然可以在 AGENTS.md 里写规则,可以维护记忆系统,可以把过去的错误和最佳实践喂给它。这些方法有用,但前提是你已经发现了那个错误。你没有发现的错误,代理还会继续制造。

这就是问题变得危险的地方。代理不只是会犯错,它还能以很高的速度重复犯错。一个人几周才能慢慢积累出来的复杂度,一组代理可能几天就能堆出来。

等你发现问题时,代码库已经变成了另一种东西。你想加一个新功能,却发现现有架构到处都是临时决策的痕迹。你想重构,却发现测试也不可信,因为很多测试也是代理在同一套错误假设下写出来的。你想确认产品能不能跑,最后只能回到最原始的方式:手动点一遍。

这时候你才意识到,真正丢掉的不是代码质量,而是你对系统的理解。

复杂度才是真正的问题

我一直觉得,写代码最难的部分不是把功能做出来,而是控制复杂度。 代理恰好很容易制造复杂度。它们读过大量代码,也读过大量看起来像“最佳实践”的东西。分层、抽象、工厂、适配器、事件系统、插件系统,这些东西本身没有错。但如果你在不需要它们的时候引入它们,它们就会变成负担。

代理很擅长生成一种看起来很专业的复杂方案。它会给你完整的目录结构、抽象层、接口定义、测试框架和扩展点。第一眼看上去很工程化,甚至会让人觉得“这才像一个成熟项目”。但真正的问题是:你的项目真的需 要这些东西吗?

很多时候并不需要。

更麻烦的是,代理的判断通常是局部的。它看不到完整代码库,看不到所有历史决策,也看不到另一个代理刚刚做过什么。即使用上索引、LSP、向量搜索和长上下文,它的理解仍然和一个长期泡在项目里的工程师不一样。

这会导致一类很常见的问题:它不知道已有模式,于是重复造轮子;它不知道某个设计为什么存在,于是绕开它;它不知道某个接口为什么不能改,于是给出一个看起来更“干净”的破坏性方案。

企业代码库之所以会变复杂,往往不是因为某个人突然写了一坨很烂的代码,而是因为很多人在不同时间做了很多局部合理的决定。这些决定彼此叠加,最后形成了一团难以理解的复杂性。

代理会把这个过程加速。

过去一个组织可能要几年才会把代码库演化成那种状态。现在,如果缺少约束和审查,几个代理加上两个活人,几周就能走到那里。

代理的搜索能力没有我们想象得那么可靠

有人会说,既然代理能写代码,那也可以让代理重构,把烂摊子收拾干净。

这当然可以尝试,但前提是代理得先找到所有相关代码。这里的问题比上下文窗口更麻烦。

在代理真正开始改代码之前,它需要知道哪些地方要改,哪些现有实现可以复用,哪些约束不能碰。你可以给它 Bash,让它用 ripgrep 搜索;可以给它代码索引;可以接 LSP;也可以给它向量数据库。但无论工具多强,代码库越大,召回率越难保证。

低召回率意味着,它找不到完成任务所需的全部信息。

这正是很多代码异味最开始出现的原因。代理没有找到已有实现,于是重复写了一份。代理没有找到某个隐含约束,于是写了一个局部正确、全局错误的实现。代理没有找到相关测试,于是以为自己已经验证过了。

搜索问题听起来很技术,但最后会变成工程管理问题。因为当系统变得足够复杂时,连人都需要靠经验和历史上下文来导航。代理只能通过工具搜索,而工具搜索很难替代一个人长期积累下来的系统理解。

这也是为什么我越来越不相信“让代理自己收拾代理制造的问题”这种说法。它可以帮忙,但不能成为唯一答案。

我们应该如何和代理共事

我并不反对编程代理。相反,我非常依赖它们。

但越是依赖,我越觉得边界很重要。代理最适合的任务通常有几个共同特征:范围清晰,不需要理解整个系统;有明确的反馈闭环,比如测试、类型检查、性能指标;失败成本可控,不会直接影响用户数据和收入;或者它只是用来帮你探索方案、整理思路、做一些枯燥重复的工作。

这类任务非常适合交给代理。它能节省大量时间,也能帮你试出一些你原本不会尝试的路径。

但有些事情不应该直接交出去。架构、核心 API、数据模型、权限边界、长期维护策略,这些定义系统形状的东西,最好仍然由人来主导。你可以让代理参与讨论,让它列方案,让它写草稿,让它指出风险。但最后的判断应该由你来做。

我之前写过一篇《我如何使用 Claude Code》。那篇文章里,我最强调的一点就是:在 Claude 写代码之前,先让它研究,再写计划,然后由人审阅计划。这个流程看起来慢,但它保留了人的判断力。

研究阶段让代理理解系统。计划阶段把代理的假设暴露出来。注释阶段让我把自己的产品判断、工程偏好和历史上下文写进去。只有当这些东西都对齐后,我才让它开始实现。

这不是为了显得流程复杂。恰恰相反,这是为了让实现阶段变简单。真正有创造性的部分应该发生在规划和判断阶段,而不是让代理在写代码时临场发挥。

慢下来是正路

我现在越来越相信,慢下来本身就是一种工程能力。

慢下来不是不用代理,也不是回到手写所有代码的时代。慢下来是给自己一点摩擦力,让自己有时间判断:这个功能真的要做吗?这个抽象真的需要吗?这个 API 以后会不会后悔?这个方案是在解决问题,还是只是看起来很专业?

我们需要给代理生成代码的速度设一个上限。这个上限不应该由模型能力决定,而应该由我们能审查多少代码决定。你一天只能认真审 500 行代码,就不要让代理一天往主分支里塞 5000 行你根本没看懂的代码。

任何定义系统整体形态的东西,都应该慎重。架构、数据模型、公共 API、核心交互流程,这些地方要么手写,要么和代理结对写。你需要泡在代码里,感受系统的手感。很多判断不是靠 prompt 写出来的,而是靠长期经验、产品直觉和维护痛苦积累出来的。

这种摩擦并不落后。它是学习和成长的一部分。

如果一切都太顺滑,你很容易错过自己本该思考的问题。代理越强,人越需要清楚自己在做什么。否则你得到的不是更高效的软件工程,而是更快的失控。

最终,好的结果应该是这样的:代码库仍然可以维护,产品仍然可靠,用户感受到的是稳定和愉悦,而不是一堆半成品功能。你可能会做更少的功能,但它们更正确。你会更经常说“不”,但这本来就是工程判断的一部分。

最重要的是,你仍然知道系统为什么长成现在这样。出了问题时,你能亲自进去修。设计不够好时,你知道哪里次优,也知道应该怎么重构。至于实现过程中有没有用代理,其实没有那么重要。

重要的是,你还拥有主体性。

这一切都需要纪律。

这一切都需要判断。

这一切都需要人。

CC BY-NC-SA 4.02022-PRESENT © Elone Hoo