屎山源于不敢删,架构活于持续排熵。

架构腐朽
你大概见过这样的系统。
它跑在生产环境里,承载着真实业务。没有人说得清它的全貌,但每个人都记得几条祖训:那个模块不要动;那张表有几个字段没人知道用途,但删过一次,回滚了四个小时;那段看起来明显写错的逻辑,千万别修——下游已经依赖了这个错误。
新人问为什么,老人说不知道,反正别动。
后来,提出重构的人来了一茬又一茬。立项材料都很漂亮:边界更清晰、性能更好、风险更低、可维护性更强。可是两年后,大多数人选择了绕道:在旧系统旁边再起一个新服务,用适配层接上。旧系统没有被替换,只是被包裹;问题没有消失,只是多了一层壳。
这种系统有个不雅但精确的俗名:屎山。
本文想处理三个问题:腐朽是什么?为什么它几乎必然发生?既然无法避免,如何让它慢一点?
先说明立场:软件工程对这组问题没有唯一答案,本文也不假装有。下面是一组分析框架和工作假设,混着一些我在自己项目里的架构探索。它们的作用不是制造新的口号,而是把模糊的焦虑变成可以讨论、可以测量、可以被工具约束的对象。
何以成“屎”山
不谈理论,先给一个工程上能用的判别标准:屎山是这样一种系统:删除任何东西的风险,都大于保留它的成本。
注意,这个定义里没有“代码质量”。代码写得烂当然会加速腐朽,但它不是屎山的本质。屎山真正可怕的地方不是“难看”,而是“不敢删”。写得烂只是初始条件,不敢删才是增长引擎。
一个系统一旦进入“不敢删”的状态,通常会出现三个症状。
第一,局部性丧失。健康的系统里,“这次改动影响多大范围”是一个可以推理的问题:看依赖图、接口契约、类型签名,就能给出影响边界。屎山里,这个问题退化成占卜。没有人能给出影响范围的上界,于是所有改动都默认要全量回归。改一行,测全站;风险未知,发布窗口越拉越长。
第二,承重 bug。某个行为明明是错的,但已经有下游依赖了这个错误,于是 bug 转正成了契约。Hyrum 定律**[1]** 说得很直白:只要 API 的用户足够多,你在契约里承诺了什么并不重要,系统所有可观测行为都会被某个人依赖。一个典型例子:某接口返回的列表恰好按插入顺序排列,文档从未承诺过顺序;三年后你换了存储引擎,顺序变了,下游七个系统同时报警。屎山是 Hyrum 定律的晚期形态:不光接口被依赖,连 bug、时序、异常文案、分页边界都被依赖了。你维护的不是代码,而是一座违章建筑,每根歪柱子上都站着人。
第三,疤痕组织。没人删东西,所有人都绕着加东西。废弃字段旁边长出新字段,看不懂的分支外面再包一层新分支,v2、new、final、real_final 在命名里层层堆叠。后来的人不是在读代码,而是在做代码考古:每一层沉积物,都记录着某一任维护者的恐惧。
这三个症状指向同一个机制:系统只进不出。而只进不出会自我强化。

后文会反复回到这个环:所有延缓腐朽的机制,本质上都是在这个正反馈环的某一段上做切断。
系统终将变旧
“腐朽不可避免”听起来像丧气话,但它背后有扎实的经验基础。
从上世纪七十年代起,Meir Lehman 等人持续研究大型软件系统的演化,后来总结出一组“软件演化定律**[2]**”。这些定律主要描述的是 E-type 系统,也就是嵌在真实世界中、必须不断适应环境变化的软件系统。用今天的话转述,其中三条与本文最相关:
- 持续变化律(Continuing Change):一个被真实使用的系统必须持续变化,否则它会逐渐不再令人满意——因为业务、用户、运行环境都在变。
- 复杂度递增律(Increasing Complexity):系统一边演化,一边倾向于变复杂;除非有人专门投入工作量去维护或降低复杂度。
- 质量下降律(Declining Quality):如果系统没有被严格维护,并持续适配运行环境的变化,它表现出来的质量会逐渐下降。
这组定律最重要的地方在于主语。它说的不是“烂团队会把系统做烂”,而是:只要系统还活着,它就会持续变化;只要变化没有配套的减法和整理,复杂度就会累积;复杂度的无序累积,就是腐朽。
很多人喜欢用热力学打比方:架构是负熵投资,腐朽是熵增。这个类比要诚实标注边界——软件系统不是封闭物理系统,“熵”在这里也没有严格的物理定义。但这个类比有一处真正有用:它提醒我们,活的系统对抗无序积累的方式,不是阻止变化,而是建立排出无序的通道。
生物体靠新陈代谢,城市靠拆迁、清运和更新。代码系统的排熵通道是什么?是删除、退役、归档、迁移、收敛概念、关闭旧路径。
一个只加不减的系统,等于自己焊死了排熵管道。后文的全部机制,都可以理解为对这条管道的疏通和制度化。
理解已死
这里有一个值得停十秒的悖论:代码是比特,比特不会变质。十年前的代码,今天逐字节地等于它自己。那“腐烂”的到底是什么?
Peter Naur 在 1985 年的 Programming as Theory Building**[3]** 中给了一个很有解释力的答案:程序不只是代码,程序还包括维护者头脑中关于这份代码的理论——为什么是这个结构,哪些地方能动,哪些地方不能动,哪些约束没有写在接口里但实际上在承重。
代码只是这个理论的不完整投影,就像乐谱不是音乐本身,地图不是城市本身。
接受这个视角,很多老问题会突然变得通顺。
为什么原作者五分钟能修好的 bug,接手的人要折腾三天?因为这不是两个人在读同一份代码,而是一个人带着理论在场,另一个人在理论缺席的情况下做逆向考古。
为什么“把文档写齐”仍然救不了屎山?因为文档能记录理论的结论,却很难完整记录理论的判断力。那些“为什么不那样做”的否决理由,往往从未被写下;不是因为作者故意偷懒,而是因为他意识不到这些判断在后来者眼里也是知识。
为什么重写永远那么诱人?因为从遗留代码里逆向重建理论,比从零建立一个新理论更痛苦。重写冲动里有相当一部分是认知上的避难。可是重写的代价,人月神话**[4]** 早就用“第二系统效应”提醒过:人在做第二个系统时最危险,容易把第一次积压的所有想法一次性塞进去。
所以,重写不是天然更高级的技术决策,它更像一次知识破产清算。你清算掉的不只是烂代码,还有那些用生产事故换来的、你不知道自己不知道的判断。
理论失传还有一个日常形态:规则比理由活得久。一条约束的理由死掉之后,它只剩两种下场:要么被不知情者当作累赘拆掉;要么被恐惧者供奉成不可触碰的图腾。前者是鲁莽,后者是迷信。两种都是腐朽,只是方向相反。
暗处蔓延
如果腐烂的是“理论”,那它是怎么传染的?下面归成三族七条。它们不是完备分类,而是一张工作地图。
1. 经济族:腐朽沿着价格梯度流动
路径一:定价倒挂。如果走正门比翻墙贵——规范流程比直接改线上慢,按分层调用比跨层直调多绕三个文件——那么腐败不需要任何人主观为恶。每一个 deadline 都会替翻墙投票,自然选择会完成剩下的事。
可以做一次具体核算:假设合规路径是“提变更单 → 评审 → 走发布流水线”,端到端两天;绕行路径是 “SSH 上去改配置”,五分钟。价差是两个工作日。只要这个价差存在,纪律文件发多少遍都没用。
推论很直接:绕行首先是定价问题,其次才是纪律问题。当团队系统性绕过某个规范时,更有效的第一反应不是重申纪律,而是算账:守规矩比不守规矩贵多少?那个差值就是腐败压力的近似读数。
如果整篇文章只允许带走一句话,则是:治理路径必须是最便宜的路径,否则你建的每一道墙,都在为翻墙者训练肌肉。
路径二:例外累积。“就这一次”从来不是一次,它是判例。事故研究里这个机制有个准确的名字:偏差正常化。每次小越界没出事,越界就被重新定义为正常,下一次的基线悄悄外移,直到某次越界落在灾难一侧。
代码评审里的机制一模一样:你批准的不是这一个特例,而是此后所有引用它的特例。
2. 认识族:腐朽从“何为真”的含混处渗入
路径三:真相分裂。缓存悄悄变成权威数据源;同一条业务规则在两个服务里各写一份,然后各自演化;某个导出表原本只是报表中间产物,几年后却被下游当作事实来源。
最隐蔽的版本是“防御性重复”:三层都校验同一件事,听起来像纵深防御。问题在于,纵深防御和重复真相只有一线之隔,分界线是:每一层的职责是否被明确写下。哪一层是权威校验,哪几层只是快速失败的副本?如果没写,几个月后必然有人在错误的层修改规则,然后三层互相矛盾,且每一层都认为自己是对的。
路径四:概念通胀。每个功能引入一个新名词,却没有机制退役旧名词。名词总量一旦超过单个维护者的认知容量,所有人就只能局部理解、局部修补;局部修补又制造新名词,正反馈闭合。
我在自己的项目里做过一次概念普查:一轮两天的设计冲刺,净增约 10 个概念,退役 0 个,而且每个新增概念单独看都有充分理由。问题正在这里:加的每一个都有道理,加起来就是没道理。
3. 组织族:腐朽镜像人的结构
路径五:治理自腐。这是讨论最少、杀伤最大的一条——守卫先于被守卫者腐败。永远通过的检查会变成墙纸,没人再读它的输出;经常误报的检查会被加上跳过标记;必填的申报字段会被复制粘贴填充,从此不再携带信息。
治理体系和它治理的对象遵循同样的腐朽规律。区别是:业务代码腐朽时,生产环境迟早报警;治理体系腐朽时,最常见的表现恰恰是“看起来一切正常”。
路径六:组织断层。康威定律**[5]** 说系统结构会镜像组织的沟通结构。它还有一个少被提起的推论:当组织变化快于系统变化时,系统就会失去真正的负责人。每一次交接、离职、重组,都会带走一部分隐性知识;留下来的代码还在运行,但解释它的人正在消失。
路径七:重写引力。腐朽的终态通常不是系统烂死,而是它被重写——然后新系统以更快的速度积累同样的腐朽。因为重写往往清算掉旧系统里所有用事故换来的判断,却没有同步建立新的排熵机制。
腐朽 → 重写 → 更快的腐朽,这就是重写引力。能逃出这个引力的系统有一个共同点:它们不把“重写”当成一次性革命,而是把“局部替换”变成日常代谢。
顺带评估一下“技术债”这个流行比喻。它有用,但也容易误导。债务有账面、有利率、有还款日,而腐朽常常没有账面。更准确地说,腐朽是经济学意义上的外部性:每次走捷径的人不需要支付全部成本,那些成本悄无声息地记在未来所有维护者头上。
没有账面的债,是不会被偿还的。后面的“棘轮”和“熵增仪表”,本质上就是给这笔外部性强行建账。
约束 & 遗忘
铺垫到这里,可以回头审视“架构是什么”这个老问题。把常见回答排成一个阶梯,一层比一层接近可操作。
第一层:架构是那张图。这离工程现实最远。图是快照,画完那一刻就开始和现实脱钩。
第二层:架构是那些重要且难以改变的决策。Martin Fowler**[6]** 在讨论软件架构时,曾转述过一种常见看法:架构关心的是“重要的东西”,以及人们认为“难以改变”的东西。这个定义抓住了架构的经济本质:架构不关心所有决策,而关心那些改变代价极高的决策。但它仍然偏静态,还没有解释架构如何在时间中存活。
第三层:架构是约束,是“你不许做什么”。这更近了。能做的事无穷多,好架构的贡献常常不是增加自由,而是把危险的自由收掉。
第四层:架构是 Naur 意义上的理论。这是本体。真正决定系统能不能被修改的,是维护者是否拥有足够完整的系统理论。但理论活在头脑里,而头脑会失忆、会离职、会换人。
第五层是我目前更愿意采用的工作定义:
架构 = 被持续执行的约束 × 能被追溯的理由链
这不是加法关系,而是乘法关系:任意一项缺失,结果都接近归零。
只有约束、没有执行,它就只是写在文档里的愿望清单;只有执行、没有理由,它就会变成一条失去上下文的旧规矩。前者挡不住现实,后者经不起追问。真正能长期生效的架构,必须同时回答两个问题:什么不能做?为什么不能做?
这个定义自带一把尺子:你的架构不是你画的图,而是你的 CI 实际拒绝的东西。
拿它量一下自己的系统:“核心模块不许依赖 UI”,是文档里的一句话,还是一条会挂掉流水线的检查?这个问题在工具层面早已不是难题:JVM 系有 ArchUnit**[7],前端有 dependency-cruiser[8]** 和 eslint-plugin-boundaries**[9],Python 有 import-linter[10]**,它们都能把“依赖只许向下”“禁止跨层调用”写成可执行断言。
Building Evolutionary Architectures**[11]** 把这类断言叫适应度函数:对某个架构特性进行客观、可自动化评估的机制。这个词有一个重要含义:架构不是一次设计会的产物,而是每次提交都要重新接受环境选择的东西。
沿这条线再走一步,是一个我近年才真正理解其分量的实践:把认识论纪律写进类型系统。
举两个例子。策略闸门的返回值类型中,硬编码一个字面量类型为 false 的字段:
interface GateDecision {
action: 'pass' | 'reject';
// 字面量 false:闸门放行 ≠ 上游授权
// 类型层面禁止任何代码声称“已经获得授权”
permissionGranted: false;
reason: string;
}
类似地,物化视图的记录类型里硬编码 projectionIsTruth: false:投影不等于权威数据源,任何想把视图当真相用的代码,在类型检查阶段就会撞墙。每个交付切片还必须显式声明两张并排清单:proves(我证明了什么)和 doesNotProve(我不证明什么)。
这些字段对业务流程本身可能毫无用处。它们存在的目的,是让“夸大声明”在编译期就不可能成立。如果架构的头号敌人是理论失传,那么把理论里最容易被误读的部分——“这个东西到底保证了什么”——直接铸进类型,是我知道的最不依赖人类记性的办法。
同一类思想也能在数据模型中看到。Git 的对象模型之所以强,不只是因为代码写得好,而是因为它把“内容决定身份”这个不变量放进了内容寻址存储里:blob、tree、commit 等对象由内容哈希命名和连接,许多完整性检查因此变成了结构性质,而不是靠开发者每次记得补一段校验逻辑。
时间裂缝
设想一个系统,它非常认真地想保证一件事:同一个请求产生的外部副作用,只能发生一次。
于是它给自己加了三道防线:
- 内存去重表:同一个请求 ID,处理过就不再处理;
- 追加式账本:同一个事件 ID,只允许落一笔记录;
- 执行令牌:执行前先拿到令牌,避免并发重复执行。
每道防线单独看都合理,单元测试也全绿。
问题出现在时间轴上。
一次请求到达后,系统通过去重,拿到令牌,开始执行副作用。副作用已经真实发生:钱扣了,消息发了,外部接口调用了。就在这时,进程崩溃。
关键在于:副作用已经发生,但“执行完成”这个事实还没来得及可靠落盘。
重启后,同一个请求被原样重放。系统看到的是一个残缺的世界:内存去重表没了,令牌状态没了,账本里也没有足够可靠的完成记录。于是它再次放行请求,副作用发生了第二次。

真正的漏洞不在某个模块内部,而在两个动作之间:副作用已经发生,但系统没有留下一个可恢复、可判定、可追溯的完成事实。

这个例子想说明的不是“内存表不好”、“账本没用”或“令牌设计错了”,而是:很多工程问题不发生在空间边界上,而发生在时间边界上。
空间边界回答的是:A 能不能调用 B?上层能不能依赖下层?某个模块能不能访问某个目录?类型系统、依赖检查、模块边界、import 规则,大多都在守这类边界。
时间边界问的是另一组问题:重启之后,系统还记得什么?请求重放时,系统如何判断自己已经做过什么?副作用发生了但完成状态尚未落盘时,系统应该重试、跳过、查询、补偿,还是交给人工确认?
空间边界有类型系统站岗,时间边界常常裸奔。
修复这类问题,不能轻易承诺“恰好一次执行”。在存在网络、崩溃、重试和外部副作用的系统里,更稳妥的表达是:至少一次投递 + 持久化幂等检查 + 可恢复的副作用状态机。
所谓幂等检查,不能只是“账本里有没有这笔记录”。系统必须把请求状态写进持久介质:准备执行、执行中、已完成、失败待确认、失败待补偿。这样,重启后的系统才知道自己站在时间轴的哪一段。
更稳妥的流程通常是:先落盘“执行意图”,再执行副作用,完成后落盘“执行完成”。但这仍然不是魔法。若系统在副作用之后、完成落盘之前崩溃,重启后看到的状态只能是“有意图、无完成”。这时不能默认重跑,也不能默认成功,而应该进入查询确认、幂等重试、补偿流程或人工处理。
消息系统里的 transactional outbox**[12]** 和数据库里的 transactional inbox,本质上都在做同一件事:把时间边界变成可恢复的状态边界。但它们只解决自己覆盖范围内的问题。对于支付、短信、第三方 API 这类外部副作用,系统还需要下游提供幂等键、业务唯一约束,或者可查询、可撤销、可补偿的外部契约。
被时间反复教训过的系统,最后往往会长出几类相似机制:
- 幂等键:同一件事到达两次,必须被识别为同一件事,且判定依据必须落盘;
- 状态机:把“准备执行、执行中、已完成、待确认、待补偿”显式建模;
- 业务唯一约束:用订单号、请求号、交易号等业务主键阻止重复落地;
- 租约与过期:任何授权都自带时间边界,没有永久有效的允许;
- 追加与取代:历史事实不被覆盖,修正以新事实追加,旧事实保持可考;
- 隔离区:解释不了、恢复不了、状态不明的数据,不拖垮主流程。
所以设计评审时,最好对每条关键路径多问三句:
- 重启之后呢?
- 重放之时呢?
- 状态不明的时候呢?
很多系统不是因为某个模块写错了才腐朽,而是因为它们没有认真回答这些时间问题。
长寿秘诀
讨论长寿系统时,最容易犯的错误是把它们神化:好像 Linux、PostgreSQL、SQLite 之所以活得久,是因为早期设计者一次性做对了所有事情。真实情况恰好相反。长寿系统当然有优秀设计,但它们真正值得研究的地方不是“从不腐朽”,而是:它们把变化、兼容、迁移和删除变成了制度。
换句话说,长寿系统不是没有腐朽,而是代谢率长期高于腐朽率。
下面三个例子不是为了证明“某软件完美”,而是为了抽取三条更一般的架构原则。
1. Linux:稳定性预算必须花在正确的边界上
Linux 内核最值得学习的地方,不是“代码量巨大还能运行”,而是它对稳定性的分配极其不对称。
一方面,内核对用户空间接口非常谨慎。应用程序依赖的 syscall 语义、用户可见行为、已有工作负载,一旦破坏,成本会外溢到整个生态。另一方面,Linux 对内核内部接口明确拒绝稳定性承诺。内核文档里的 stable-api-nonsense.rst**[13]** 说得很直接:文档讨论的是 in-kernel interfaces,不是 kernel-to-userspace interface;后者才是应用程序可以依赖的稳定边界。
这个不对称非常关键:对外承诺越昂贵,内部变化就越不能被轻易冻结。如果内部接口也被无差别稳定化,每一次修正都会变成兼容包袱;旧接口不能删,新接口只能叠加,腐朽就会获得制度保护。
Linux 的另一个启发是所有权可见。MAINTAINERS**[14]** 文件和维护者体系把大量路径、子系统、邮件列表、review 路径显式化。它不能消灭所有无主代码,但它至少让“谁有资格解释这块理论”、“补丁应该送到哪里”、“长期没人维护时谁来处理”这些问题不至于完全悬空。Linux 维护者文档甚至明确提到,长期不活跃的维护者可以从 MAINTAINERS 中移除,缺乏维护的代码也可能被拒绝或移除。
从 Linux 学到的不是“内部 API 一律不稳定”,而是更精确的一条:稳定性不是越多越好。真正的架构能力,是知道哪些边界必须稳定,哪些边界必须保持可替换。
2. PostgreSQL:把变化写进版本制度,而不是藏进口头承诺
数据库系统比多数业务系统更怕腐朽,因为它承载的不只是代码,还有用户的数据、查询习惯、扩展生态和运维流程。PostgreSQL 的长期生命力,很大一部分来自它把变化制度化,而不是假装变化不存在。
PostgreSQL 官方版本策略很清楚:大版本大约每年发布一次;每个大版本支持五年;小版本主要承载 bug fix、安全修复和低风险修复;大版本升级可能需要 pg_upgrade、dump/reload 或逻辑复制,不能假装数据目录永远向后兼容。
这套策略背后的架构思想是:不兼容不是不能发生,但必须被命名、分层、记录,并给出迁移路径。
这比一句“我们保证兼容”专业得多。成熟系统不会把所有变化都塞进同一个“发布”概念里,而会区分:
- 哪些是小版本内可以安全吸收的修复;
- 哪些是大版本才允许发生的结构变化;
- 哪些变化需要迁移工具;
- 哪些行为需要 release notes 明确提醒;
- 哪些扩展点需要承诺,哪些内部实现不承诺。
PostgreSQL 还展示了另一条长寿系统的常识:长期演化不等于把所有需求都塞进核心。扩展、外部数据包装器、自定义类型、索引方法、过程语言等机制,把一部分变化导向受控扩展点,而不是要求核心持续膨胀。
从 PostgreSQL 学到的是:长寿系统不会消灭破坏性变化;它会把破坏性变化放进明确的版本边界和迁移流程里。
3. SQLite:小边界、硬契约、重测试
SQLite 的启发和 Linux、PostgreSQL 不同。它不是靠庞大的子系统治理取胜,而是靠极其清楚的边界取胜:嵌入式、无服务器、零配置、单文件数据库。这个边界看似限制,实际上是它长期稳定的来源。
SQLite 官方文档反复强调几个事实:数据库是普通磁盘文件,文件格式跨平台;事务在崩溃或断电后仍要求保持 ACID;项目重视稳定文件格式、详细文档、长期支持和大量自动化测试。它还明确说,SQLite 项目始于 2000 年,并以支持到 2050 年为设计目标。
📌 ACID
ACID 是数据库事务常用的一组可靠性性质,分别代表原子性、一致性、隔离性和持久性。它描述的不是“数据库一定不会出错”,而是在数据库承诺的故障模型和配置边界内,一个事务系统应该如何处理部分失败、并发访问和崩溃恢复。
原子性(Atomicity)强调事务是一个不可分割的工作单元。事务内的数据库变更要么作为整体提交,要么在失败时整体撤销;系统不应在恢复后暴露“只完成了一半”的事务结果。
一致性(Consistency)强调事务执行前后,数据库都应满足已定义的不变量和约束,例如数据类型、唯一性约束、主外键关系、检查约束,以及被正确编码进系统的业务规则。需要注意的是,数据库只能自动维护它知道的规则;没有被写成约束、事务逻辑或应用逻辑的业务规则,并不会被 ACID 自动保证。
隔离性(Isolation)处理的是并发事务之间的相互影响。它要求数据库在所选择的隔离级别下,限制一个事务看到另一个事务中间状态的方式,从而避免脏读、不可重复读、幻读或序列化异常等问题。最强的可串行化隔离可以让并发事务的结果等价于某种顺序执行;较低隔离级别则是在一致性和性能之间做权衡。
持久性(Durability)是指事务一旦成功提交,在数据库承诺的存储和故障模型内,其结果应能在进程崩溃、操作系统崩溃或断电后恢复。它通常依赖预写日志、刷盘、校验、恢复流程等机制。不过,持久性并不等于“数据永远不会丢”:异步提交、关闭刷盘、硬件撒谎、磁盘损坏、运维误删等情况,都可能超出单机数据库事务本身的保证范围,需要通过备份、复制、校验和灾备机制继续补足。
这类承诺之所以可信,不是因为开发者“更认真”,而是因为系统边界足够清楚。SQLite 不试图成为一个分布式数据库、不试图管理集群、不试图把所有数据库职责揽进来。边界越窄,可测试空间越可控;可测试空间越可控,长期契约越有可能被守住。
从 SQLite 学到的是:范围控制不是保守,而是稳定性的前提。你承诺的表面积越大,长期维护成本越高。
4. 三个例子共同点
Linux、PostgreSQL、SQLite 的差异很大:一个是操作系统内核,一个是客户端/服务器数据库,一个是嵌入式数据库。但它们的长寿机制有共同结构:
- 边界分级:不是所有接口都同等稳定。对外契约慎重承诺,内部实现保持可替换。
- 所有权显式:代码、子系统、版本、扩展点都有明确责任人或责任流程。
- 变化制度化:发布、迁移、弃用、兼容性说明不是临时补丁,而是系统代谢的一部分。
- 不变量下沉:关键约束尽量进入数据格式、状态机、测试、工具和发布流程,而不是只写在文档里。
- 删除合法化:内部旧接口、失效规则、缺乏维护的路径,必须有体面退出机制。
所以,长寿系统的秘密不是“设计得足够完美”,而是它们从一开始或在长期演化中承认了一件事:变化会一直来,腐朽会一直长,系统必须有持续排出复杂度的能力。
这也呼应了 Gall 定律**[15]**:能工作的复杂系统,通常是从能工作的简单系统演化来的;从零设计的复杂系统,很少能直接工作,也很难靠事后补丁修成一个健康系统。长寿系统不是一次性设计出来的,而是在高代谢中演化出来的。
系统排熵
把前面的线索收拢。下面七个机制是我正在实践、初步验证有效的工作笔记,不是处方。参数请按自己系统的疼痛程度调整。
它们与第一节那个正反馈环的对应关系是:1、2、5 切“只加不减”;3、4 切“治理自腐”;6 切“重写引力”;7 切“理论失传”。

1. 定价对齐:让正门最便宜。定期审计合规路径与绕行路径的成本差。投资优先花在降低正门价格上——脚手架、模板、代码生成、一键流水线、默认安全配置——而不是加高墙。
一个可操作检查:找出过去一个季度被绕过最多的三条规范,分别测量“守规矩”与“不守规矩”的耗时差。差值最大的那条,优先修正流程,而不是优先处分翻墙的人。
2. 棘轮:把坏现状做成只许变短的清单。把所有“暂时容忍”显式化为一份基线文件:现存违规、超标文件、批准过的例外,全部登记,标注日期、责任人和复查时间。然后立一条机器规则:这份清单只许变短。
实现很朴素:把基线存成 JSON/YAML 提交进仓库,CI 里跑当前扫描并与基线 diff。出现基线之外的新违规,构建失败;基线里的条目已经不再违规,也构建失败,强制删除该条目,防止基线本身变成垃圾场。lint 工具的 baseline 或 suppress 清单,本质上就是这个思路。
棘轮的价值在于:单调递减是极少数能被机器验证的“正在变好”。
3. 守卫自检:让检查器先证明自己还能失败。一个永远通过的检查,对判断当前代码是否破坏约束几乎没有信息量。它可能表示系统完美,也可能表示检查器早已失效。
对策是把可证伪性变成工程要求:检查器每次运行时,先在内存里注入一个已知违规样本,断言自己能抓到它,再去检查真实代码。
runCheck():
plantKnownViolation() # 构造一个必然违规的样本
assert detect(sample) == FAIL # 抓不到 → 检查器自身报废,流水线红
return detect(realCodebase) # 自检通过,才有资格检查真实代码
守卫必须周期性地证明自己还有能力失败,才算活着。这和测试领域的变异测试是同一个精神:用已知坏样本校准探测器。
4. 熵增仪表:只看趋势,不迷信阈值。无法阻止复杂度上升,但可以测量它上升的速度。挑一组先行指标:例外基线条目数、概念总数、平均文件行数、超预算文件数、循环依赖数、跨层调用数、废弃 API 调用数、状态不明工单数。
定期快照,只看趋势的单调性,不急着争论绝对值。绝对值会引发关于阈值的无穷争论,单调性不会。任何指标连续多期上行,触发一次强制减法批次。
5. 生命周期:让新增概念自带退场方式。“减法配额”不应该只是口号。每引入一个新概念、新字段、新接口、新服务,都至少回答三个问题:谁拥有它?它替代了什么?它在什么条件下可以退役?
做不到立即退役也没关系,但要显式写下“本次净增 +1”,并给出复查日期。不必一开始就硬性禁止——硬禁会被绕过。让通胀可见,就已经赢了一半。
6. 不对称稳定性:接口慎重承诺,内部保持可替换。学 Linux,但不要机械模仿 Linux。稳定性预算优先花在接口与契约上,内部实现明确声明较低稳定承诺。这样所有重写都被限制为局部重写——像忒修斯之船,一块一块换板,船始终在航行。
增量替换有现成模式可循,比如绞杀者模式(Strangler Fig Pattern):新实现先在旧系统旁路上接管一小片流量,验证后逐步扩大;旧路径流量归零后,删除旧路径。配套一条流程规则:任何全局重写提案,自动降级为“请指出最小可重写切片”。这是我知道的、最能对抗重写引力的办法。
7. 理由保鲜:每条规则都要指回它解决过的真实压力。每条规则旁边放一个指针,指回它诞生时的压力来源:那次事故的复盘链接、那篇分析、那个被它消灭的 bug、那次被阻止的设计偏差。
ADR(架构决策记录)是这个实践的标准载体,但还要加一步很多团队没做的:定期检查指针是否还活着。理由还活着的规则继续受保护;理由已成死链的规则,进入退役评审。让每条规则要么仍能解释自己为什么存在,要么体面退场。最糟糕的状态,是谁也说不清它为什么还在,却没人敢碰它。
模型失忆
最后是一个正在发生、且会把以上一切放大的变化:越来越多代码由大语言模型生成、修改、解释。它们未必是唯一维护者,但会越来越多地参与维护。
回到 Naur 的框架:程序是维护者头脑中的理论。那么模型的“头脑”是什么?在实际协作里,它常常是一个会话窗口。会话结束,理论清零;上下文裁剪,理论残缺;模型升级,行为习性变化。会话失忆是百倍速的组织失忆。
这并不是说模型不能维护代码,而是说:如果理论只存在于人的记忆或模型上下文里,系统会更快进入失传状态。
康威定律也出现了新形态:架构开始镜像上下文窗口的形状。什么放得进注意力,什么就更容易被维护好;放不进的,就在沉默中腐朽。模块大小、文档组织、命名自解释程度、测试失败信息的可读性,都开始受到一个新的选择压力:能否被一个有限窗口的维护者一次性装下。
专业团队对这件事的处理,不应该停留在“让模型多读点文档”。更可靠的方向有三条。
第一,理论外置。把架构约束、ADR、依赖规则、数据契约、运行手册、迁移策略、危险边界,整理成可检索、可链接、可验证的工件。项目级指令文件、AGENTS.md、CLAUDE.md、ADR 索引、schema 迁移测试、架构适应度函数,都是理论外置的不同形态。
第二,责任不外包。模型可以生成内容,但不能替系统承担责任。Linux 内核关于 AI coding assistants**[16]** 和 tool-generated content**[17]** 的文档已经给出一个很好的方向:AI 工具应遵循标准开发流程;人类提交者需要审查、测试并对提交负责;工具生成内容需要透明说明。这个原则放在任何严肃工程系统里都成立。
第三,检查可执行。不要指望模型“记住”所有规则。让规则进入 CI、类型系统、测试、lint、迁移检查和发布门禁。模型可以忘,流水线不能忘。
我最近一直在用 Codex 5.5 Extra High 重写 Noi 架构(Claude Fable 5:最强 AI 正在变成“特权资源”、浅谈 AI 编程、AI 时代下的“认知投降”、深度解析:Harness Engineering),往 Harness 工程化方向走。在 Noi 架构中写过这样一条规则:治理区内的文档不得链接到治理区之外。目的很简单:让治理规则保持自包含,避免关键约束散落在系统外部。
这条规则很快被写进了链接检查器(检查器被 fable max 优化过)。当天晚些时候,我让 Claude Fable 生成了一篇新文档,结果它恰好违反了这条规则。几个小时前由我设计、AI 协助实现的检查器,把这次提交拦在了流水线上。
几乎同一时间,熵增仪表完成了第二次快照,报告中有指标上行。原因之一,正是仪表自己的说明文档刚刚加入系统。
守卫拦下了守卫的建造者,仪表测量到了它自己的出生。这不是系统出了问题,恰恰是系统在工作的证据:规则面前没有作者特权。一个连建造者本人都约束不了的治理体系,才真正值得怀疑。
以下截图就是 Fable 自己的爆金句:

Loop Engineering
之前写过 Harness Engineering,最近又出来个 Loop Engineering(Loop Engineering.[18],Loops: What Every AI Engineer Needs to Know in 2026**[19]**),放在这里也很适合,可以聊聊。限于篇幅,这里就不具体展开实践细节了,有机会再新开一篇。


写到这里,Loop Engineering(把排熵做成循环)已经不是一个额外话题,而是前文逻辑的自然延伸。
前文一直在讲一件事:系统腐朽很少来自某一次孤立错误,更多来自失控的反馈环路。只加不删,会让删除越来越贵;例外累积,会把越界重新定义为正常;治理自腐,会让检查器变成墙纸;理论失传,会让规则变成图腾;重写引力,则会把一次知识破产伪装成技术升级。它们本质上都是循环:每转一圈,系统更难理解,更难删除,也更难修正。
所以,在这篇文章的语境里,Loop Engineering 的价值不只是“让 AI 自动干活”,而是把反馈环路重新设计成可观察、可约束、可验证的工程机制。
过去,工程师往往就是循环本身:你提示模型,阅读输出,发现问题,补充上下文,再提示下一轮。现在,人的位置开始前移。你不再只是写 prompt,而是在设计一个系统:它知道目标是什么,知道可以执行哪些动作,知道从哪里获取上下文,知道如何验证结果,也知道什么时候应该停下来,把问题交还给人。
一个 prompt 给 Agent 一条指令;一个 loop 给 Agent 一套工作机制。
最小的 Loop Engineering,可以压缩成五个问题:目标是什么?上下文从哪里来?允许执行哪些动作?反馈如何产生?什么时候必须停止?这五个问题回答不清楚,循环越自动,风险越大。因为它不是一次性输出,而是一个会反复放大自身判断的机器。好的循环会把工程判断制度化;坏的循环只会把混乱自动化。
这正好对应前文的架构定义:架构 = 被持续执行的约束 × 能被追溯的理由链。Loop Engineering 是这个定义在 AI Agent 时代的一种展开。它不是让模型“记住”所有规则,而是把规则放到模型之外:放进文档、技能、状态文件、CI、测试、检查器、工作流和停止条件里。模型可以失忆,但循环不能失忆;模型可以犯错,但反馈系统必须能发现错误。
一个能长期运行的 loop,通常要回答六类工程问题:
-
谁来触发它?这对应 automation。 没有自动触发,所谓循环只是你手动跑过的一次任务;有了定期巡检,它才开始拥有持续性。
-
它在哪里执行?这对应 worktree。 多个 Agent 同时修改同一个仓库,如果没有隔离,很快就会互相踩文件。隔离不是为了让 Agent 更聪明,而是防止并行执行先把工作现场弄坏。
-
它凭什么理解项目?这对应 skills。 Agent 每次启动都是冷的,如果架构约束、构建方式、禁忌边界、历史事故只存在于人的记忆里,循环每次都会重新猜一遍。skills 的价值,是把这些隐性知识写成可复用的项目理论。
-
它如何接入真实环境?这对应 connectors。 只会读写本地文件的循环很小。真正有用的循环需要进入 issue、CI、代码仓库、日志、监控和发布系统;否则它最多只能提出建议,不能进入真实工作流。
-
谁来检查它?这对应 sub-agents。 让同一个模型写代码,再判断自己是否完成,是危险的结构。模型很容易相信自己的解释,也容易把“看起来合理”当成“已经正确”。循环越自动,越需要把 maker 和 checker 分开。
-
它如何记住历史?这对应 memory。 长期循环最怕的不是不会执行,而是不会记住。会话会结束,上下文会裁剪,模型会升级;真正能跨运行保存状态的,必须在模型之外。状态文件、任务板、issue、变更日志,本质上都是循环的外部记忆。模型会忘,仓库不能忘;会话会断,状态不能断。
把这些部件连起来,一个面向架构腐朽的 loop 就很容易想象出来。
每天早上,它读取项目规则、架构文档和上一次状态;扫描最近的 CI 失败、循环依赖、废弃 API 调用、超预算文件、新增长的概念和状态不明的任务;把发现写入状态文件;对低风险问题开独立 worktree 尝试修复;让另一个 Agent 根据项目规则、测试结果和架构约束复核;能自动处理的进入 PR,不能自动判断的进入人工 triage。
这不是“让 AI 自己维护架构”,而是把前文讨论的排熵机制自动化。棘轮由 CI 执行,熵增仪表定期快照,守卫自检防止检查器失效,skills 保存理由链,memory 记录历史尝试,sub-agent 把写作者和检查者分开。Loop Engineering 最适合做的,不是替你拍脑袋设计系统,而是替你持续执行那些你已经想清楚、但人类很难每天坚持的工程纪律。
因此,真正有用的 loop 往往不是最开放的,而是最有边界的。
开放循环很诱人:给 Agent 一个大目标,让它自由探索、自由拆解、自由修正,直到它认为完成。它的上限确实高,但代价也高:成本不可控,路径不可预测,质量门槛如果不够硬,很容易变成昂贵的垃圾生产线。对大多数真实项目来说,更现实的起点是闭环循环:目标明确,动作有限,反馈可验证,停止条件清楚,状态可追溯。
闭环循环听起来没那么酷,但它更像工程。比如:每天检查新增循环依赖,有则隔离或生成修复 PR;无法自动修复,则写入 triage。又比如:每次新增架构规则,必须生成对应检查器和理由链接;检查器无法自证时,流水线失败。这些循环的自由度不高,但它们能稳定降低腐朽率。
这里也必须补一层冷水:Loop Engineering 不能替你承担责任。
无人值守的循环,也可能无人值守地犯错。Verifier 的结论只是声明,不是证明。测试通过、lint 通过、类型检查通过,也不等于系统行为已经被你理解。循环越顺滑,越容易产出你没有亲自写、也没有真正理解的代码。短期看,速度变快;长期看,代码库与你的认知之间会出现新的裂缝。这就是 AI 时代的理解债。
所以,同一个 loop,在两个人手里会产生相反结果。一个人用它放大自己已经理解的工程判断;另一个人用它逃避理解。循环不知道区别,人知道。
这也是为什么 Loop Engineering 并不比 Prompt Engineering 更轻松。它只是把杠杆点换了位置。以前的关键是“如何把需求说清楚”,现在的关键是“如何设计目标、上下文、动作、反馈、停止条件和责任边界”。你不再只是提示 Agent,而是在设计一个会不断提示 Agent 的系统。系统一旦开始循环,就会放大你的判断,也会放大你的偷懒。
因此,这一节真正想落到的结论不是“停止写 prompt”,而是:构建循环,但不要退出工程。
让 Agent 执行重复劳动,让检查器执行规则,让 memory 保存状态,让 automation 触发例行巡检,让 sub-agent 提供第二视角。但目标、边界、取舍和责任,仍然必须留在人这里。
Loop Engineering 最好的用法,不是让 AI 替你思考,而是把你已经想清楚的工程判断,变成系统可以反复执行的循环。
结语
本文没有给出“如何避免腐朽”的答案,因为这个问题本身就问错了。
从 Lehman 的软件演化定律,到那些跨越十年、二十年仍在演化的长寿系统,再到 AI Agent 时代正在出现的 Loop Engineering,一个越来越清晰的事实正在浮现:软件不是一座建成后就可以静止保存的建筑,而是一个持续变化的生命体。它会适应环境,也会积累历史;它会长出新能力,也会沉积旧负担。复杂性会增长,熵会增加,腐朽会发生——这不是失败,而是演化的代价。
因此,架构治理真正面对的,从来不是如何彻底阻止腐朽,而是如何在不可避免的腐朽中维持秩序。
换句话说,架构的本质不是结构,而是代谢。
一个系统是否健康,不取决于它今天看起来有多整洁,而取决于它是否仍然具备持续更新自己的能力;不取决于它积累了多少规则,而取决于它是否还能删除过时的规则;不取决于它拥有多少自动化,而取决于它是否还能识别自动化失效的时刻。
前文讨论的删除机制、架构棘轮、熵增仪表、守卫自检、理论保鲜,看似是不同层面的实践,实际上都服务于同一个目标:建立稳定的排熵能力。让问题能够被发现,让偏离能够被暴露,让修复能够被执行,让经验能够沉淀为新的约束。它们共同构成的,不是一套治理工具,而是一套维持系统生命力的代谢系统。
Loop Engineering 则是这一逻辑在 AI Agent 时代的自然延伸。过去,发现问题、理解问题、执行修复、验证结果,主要依赖工程师亲自完成;今天,我们开始把越来越多的反馈环路外置给系统。Automation 负责巡检,CI 负责执法,memory 负责保存状态,verifier 负责质疑结果,Agent 负责承担重复劳动。工程师的价值不再主要体现在推动每一步执行,而体现在设计这些循环、定义这些边界、维护这些反馈机制。
技术形态在变化,但三条原则不会变化:
-
屎山从来不是因为代码写得太多,而是因为系统失去了删除能力。 任何不能持续淘汰历史负担的系统,最终都会被历史本身压垮。
-
架构从来不是文档中的蓝图,而是现实中的约束网络。 只有能够被验证、被执行、被追溯的规则,才是真正存在的架构;无法落地的原则,最终都会退化成组织记忆里的传说。
-
循环不会替代判断,只会放大判断。 无论执行者是工程师、脚本还是 Agent,反馈环路最终放大的,永远是设计者的认知质量。好的循环会持续积累秩序,坏的循环也会持续放大混乱。
一个成熟系统最值得珍视的能力,不是自动化,不是规模化,甚至不是高效率,而是持续自我修正。
当规则的制定者被自己制定的规则拦下;当自动化流程在边界不清时主动停止;当 Agent 选择交还控制权,而不是继续生成看似合理的答案;当系统能够承认“不知道”,并把问题重新暴露给人类判断——这些都不是治理失败的迹象。
恰恰相反,它们说明反馈环路仍然存在,约束仍然有效,系统仍然保有对自身状态的感知能力。
而这或许就是架构治理最终追求的东西:不是建立一个永远正确的系统,而是建立一个能够持续发现自己正在变错的系统。
只要这种能力仍然存在,腐朽就不会无声完成。
References
[1]
Hyrum 定律:https://www.hyrumslaw.com/
[2]
软件演化定律:https://en.wikipedia.org/wiki/Lehman%27s_laws_of_software_evolution
[3]
Programming as Theory Building:https://pages.cs.wisc.edu/~remzi/Naur.pdf
[4]
人月神话:https://en.wikipedia.org/wiki/The_Mythical_Man-Month
[5]
康威定律:https://en.wikipedia.org/wiki/Conway%27s_law
[6]
Martin Fowler:https://en.wikipedia.org/wiki/Martin_Fowler_(software_engineer)
[7]
ArchUnit:https://www.archunit.org/
[8]
dependency-cruiser:https://github.com/sverweij/dependency-cruiser
[9]
eslint-plugin-boundaries:https://github.com/javierbrea/eslint-plugin-boundaries
[10]
import-linter:https://import-linter.readthedocs.io/
[11]
Building Evolutionary Architectures:https://www.oreilly.com/library/view/building-evolutionary-architectures/9781491986356/
[12]
transactional outbox:https://microservices.io/patterns/data/transactional-outbox.html
[13]
stable-api-nonsense.rst:https://www.kernel.org/doc/Documentation/process/stable-api-nonsense.rst
[14]
MAINTAINERS:https://www.kernel.org/doc/linux/MAINTAINERS
[15]
Gall 定律:https://en.wikipedia.org/wiki/John_Gall_(author)[#Gall](javascript:;)'s_law
[16]
AI coding assistants:https://docs.kernel.org/process/coding-assistants.html
[17]
tool-generated content:https://docs.kernel.org/process/generated-content.html
[18]
Loop Engineering.:https://x.com/addyosmani/status/2064127981161959567
[19]
Loops: What Every AI Engineer Needs to Know in 2026:https://x.com/sairahul1/status/2064277888216555684
