软件行业:退火,但错了
摘要
本文批评了将大型变更拆分为许多小型拉取请求(类似于模拟退火)的常见做法,认为这可能会阻碍必要的大规模变更。文章还讨论了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个问题”,然后关闭拉取请求。这算粗鲁吗?如果建议质量高且回复快,其实不然。在一个审查代码困难、编写代码容易的世界里,应该对编写者提出更多要求。
相似文章
大型语言模型在浮点错误分类上的基准测试
本文介绍了InterFLOPBench,这是一个用于评估LLM在C代码中检测浮点错误的基准测试,发现最近的模型取得了较高的F1分数,但性能因错误类型而异。
超越图书馆:一种用于自动形式化研究数学的智能体框架
提出了一种智能体框架,利用通用编码大语言模型将研究级数学自动形式化为Lean 4代码,并在Putnam问题和STOC会议论文上进行了评估。
基于真实世界故障记录的自动驾驶系统测试场景生成
本文提出了一种模块化的LLM流水线,利用历史故障记录(例如NHTSA碰撞数据)生成多样化的自动驾驶系统测试场景,从而在有限的测试预算内实现有效的故障发现。
往昔即序章:面向序列进化LLM记忆的选择性更新插件控制器
介绍Janus,一种用于LLM的插件式记忆控制器,通过记忆动量触发器(Memory Momentum Trigger)和紧凑混合评估集,选择性接受或拒绝候选记忆更新,在多个数据集上平均准确率提升+2.7至+4.6个百分点。
DDIAgents:机制条件化上下文流用于药物-药物相互作用预测
提出DDIAgents,一种机制条件化的多智能体框架,用于药物-药物相互作用预测,该框架动态地将相关生物医学知识路由到专门的专家智能体,并聚合它们的分析,优于现有的基于特征、基于图和基于LLM的方法。