软件行业:退火,但错了

Lobsters Hottest 新闻

摘要

本文批评了将大型变更拆分为许多小型拉取请求(类似于模拟退火)的常见做法,认为这可能会阻碍必要的大规模变更。文章还讨论了AI驱动的编码工具如何实现快速探索,但也带来了不连贯和失败的风险。

<p><a href="https://lobste.rs/s/nv2cnf/software_industry_annealing_wrong">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/01 04:27

# 软件行业:退火,但方向错了 来源:https://apenwarr.ca/log/20260531 **软件行业:退火,但方向错了** 最近几个月,我听说了几个团队采用的有趣策略:每个拉取请求(PR)不应超过几个文件,且行数不超过某个数值(例如500行)。每个PR只做一件事,并做好它。要便于人工审查。必须通过测试套件的全面测试。 这些要求听起来都很好,对吧?这肯定是高质量的软件工程实践。 而且,结果往往也不错。当然,将一个6000行的功能或修复拆分成十二个500行的PR会增加工作量,但每个PR审查起来确实更容易。遇到bug时,你还可以用`git bisect`定位问题,甚至可能单独回滚出问题的那个改动。 ……但这也给审查者带来了12倍的情境切换次数( [来源](https://apenwarr.ca/log/20260316) ),因为他们需要逐个顺序审查每个PR。但这只是软件质量的成本,对吧? 大体上,是的。我在这里用“模拟退火”来类比( [维基百科](https://en.wikipedia.org/wiki/Simulated_annealing) )。在这个过程中,你以高能量开始解决问题——进行大改动,快速穿越问题空间——然后逐渐降低能量水平,使“跳跃”越来越小。在真实的物理退火(例如用于冶金)中,结果是更坚固、更稳定、更结晶的结构。在模拟退火中,你用它来发现不明显的解决方案,通过快速探索解空间,然后放大到最有希望的领域。 在软件中,类比很明显:当然,你可能会从大跳跃开始,但一旦系统趋于成熟,就应该做更小的跳跃。大跳跃会破坏晶体结构,引发bug。 **“害怕破坏晶体结构”比“害怕变化”听起来更酷** 退火驱动的直觉主要问题在于,当事情*确实*需要快速变化时,它并不适用。你通常不会造好一把锤子,然后某天决定把它改成另一种形状。但每天都有各种貌似合理的原因,让你想把软件改成另一种形状。退火是变化的敌人。 现代AI驱动的编码(讽刺的是,LLM的训练过程与退火非常相似)并不关心你的退火、你的风险管理以及你对变化的恐惧。它会生成你所期望的任何规模和关联度的改动,以你提示的速度在解空间中跳跃。其结果也符合数学预测:输出更弱、更不连贯、更容易失败。LLM没有对变化的恐惧,因为当后果显现时,LLM实例早已不存在了。 但是,突然能够对一个庞大而成熟的代码库进行任何你想要的重大改动,这是一种新奇而独特的体验。大多数这样的改动最终都会是坏主意……但能快速丢弃坏主意也是好事。而有些则会成为好主意。然后呢? 那就遵循你的开发流程吧。把大改动拆成500行的补丁,逐一审查。你已经做过研究了!你知道这是值得的。 **并非所有的大步都由小步组成** 但问题不在于“值不值得”——有些改动本身就不适合拆成小步骤。 在开发 [Aperture](https://aperture.tailscale.com/) 的早期,我想实现基于美元的消费配额:跨所有LLM后端,允许某个团队、个人或节点在单位时间内消费最多x美元。但要做到这一点,我们首先必须添加定价信息(LLM供应商不告诉你查询成本,这很神秘),这意味着要为基础模型定义分配价格,然后还要为特定的“身份+模型+会话”组合分配配额。而配额正是Aperture的关键价值主张之一。我们必须实现它,但必须先有所有这些基础设施。 于是,我做了一个庞大的改动,包含三个主要部分:第一,将属性应用于会话的Grant语法;第二,一个结合了多种来源和混乱启发式的查询成本估算器;第三,实际的配额执行系统。每个部分都不完美,但在我们改进它们之前,必须让这三个部分协同工作才行。这就是高能量、大跳跃的阶段。最终大约是12000行代码。 当然,我不是怪物。在让一切工作起来之后,我把它拆成了三个部分:Grant、定价、配额。^2^否则它真的会成为一个不可审查的混乱。但同样,在现实中,我无法按照那种人为的顺序来开发配额功能。Grant结构随着我对定价和配额执行的理解而演变。最初的配额语义很糟糕,所以我回溯到数据结构,这影响了定价的导入方式,又改变了配额的存储方式。代码审查者不必担心这些,但我必须操心。 幸运的是,由于Aperture是新产品,团队中的每个人都明白,在实现这一系列功能时,三个4000行的补丁比二十四个500行的补丁更好。甚至后来不可避免地发现每个部分都不太正确、需要更多bug修复时,也得到了谅解。新软件就是这样被创造出来的。这就是退火阶段。 但困难在于,这种做法与核心Tailscale的做法之间的哲学差异。Tailscale已经发展了7年以上,已经退火了很长时间,并以极高的质量、加固、耐用性(随便你怎么称呼)而闻名。如果你开始在核心Tailscale里搞这种操作,东西绝对会坏,其数百万用户绝不会买账。这也是为什么大多数情况下,我们不会这么做。 但再次快速前进的感觉真是太棒了。有些人把这种分析降级为“创始人模式”,称之为性格问题,但并非如此。这是在正确的时间为正确的工作使用正确的工具。有时你需要快,有时你需要慢。 **痛苦并不会带来收益,它只是经常与收益相关** 那种快速前进的感觉让我的大脑稍微重置了一下。它提醒我,对成熟产品的某些改动可能变得不可能,因为我们过于执着于退火的数学原理,从而永远陷入局部最优。有时,当井太深时,你不做更大的跳跃就无法脱身。 我们正在进入一个*产生*更大改动变得廉价的世界,但这并不会让它更安全。或者,你可以让LLM将改动人为地拆成十几个符合规则的PR,但那样你就会陷入无休止的繁琐代码审查中( [来源](https://apenwarr.ca/log/20260316) )。 另一方面,你也可以将自己的项目分叉出十几个不同版本,添加以前你根本负担不起的庞大合规测试套件,或者在 [一周内用Rust重写你的项目](https://news.ycombinator.com/item?id=48132488) ,只是为了看看会发生什么。 斯塔金定律指出,你的大改动中90%会是垃圾,因为90%的东西都是垃圾。当你的改动是500行时,你必须拒绝它们,这感觉并不像巨大的沉没成本。但现在,如果你的12000行改动是垃圾而必须拒绝,那也没关系;因为编写这些改动的成本^3^和过去500行的改动是一样的。 你仍然需要弄清楚如何有效地审查、拒绝和完善这些大跳跃。你肯定需要在CI/CD自动化、规格说明、UX测试等各方面进行更重的投入。但同样,所有这些事情也都变便宜了。 我不建议过度使用。另一件事是,客户不喜欢你频繁地在他们的眼皮底下改变产品。但有时,你只是陷入了困境。有时你必须用更高能量的跳跃来摆脱困境。这并不意味着你要放弃小步前进。为正确的工作使用正确的工具。 **脚注** 1. 审查之所以要顺序进行,是因为GitHub的代码审查系统在18年多之后仍然不支持栈式diff,这首先就让我们陷入了这种虚假的二分法。 2. 这稍微有些简化,因为还有另外几个部分在前面。在添加配额系统之前,我必须先定义配额的数据结构,以便在Grant语法中使用这些数据结构,如此循环往复。 3. 一个12000行的AI驱动补丁可能和一个人工编写的500行补丁耗时相同,但默认情况下审查工作量大得多。事实上,大到人们会放弃尝试,这是有道理的。与其放弃希望,我仍然认为我们应该更多地投资于(并将从中获益)不令人厌烦的AI辅助审查工作流,而非AI辅助开发工作流。例如,想象一个自动化的预人工审查步骤,它会说“不,这不行,先修复这25个问题”,然后关闭拉取请求。这算粗鲁吗?如果建议质量高且回复快,其实不然。在一个审查代码困难、编写代码容易的世界里,应该对编写者提出更多要求。

相似文章