第 1 章留了一个问题没有回答:重构后的版本到项目接近半年时已经涨回七百多行,而期间没有人察觉——因为大家看的是通过率,不是规模。
这一章处理版本上线之后的事:凭什么继续放量、凭什么回滚、以及那个几乎没人做的动作——凭什么让一条规则退休。
一、每一条都有理由,加起来还是出了问题
第 1 章那两次膨胀,性质完全不同。
第一次增长的是散布的禁令和重复的口径,它们互相冲突、互相稀释。这种膨胀好办——它经不起一次认真的审视,去重、统一口径、删掉无效补丁,规模就下来了。
第二次不一样。涨上来的是结构化的机制:状态一致性断言、字段真值表、发送前的检查清单。它们彼此不冲突,而且每一条都能说出自己防的是哪一次真实事故。
这就麻烦了。你没法用“这条没用”去反驳任何一条。
于是问题变成了:如果每一条规则单独看都正当,凭什么删掉它?
这个问题不解决,系统就会持续单调增长。原因很简单,而且是结构性的:
新增有具体的理由——某一次真实事故;删除只有抽象的理由——复杂度。 具体总是赢过抽象,所以没有退出机制的系统只会往一个方向走。
版本治理要管的就是这件事。它不判断单条规则对不对,它管的是整体的增长与退出。
二、发布是一个闭环,不是一个动作
图 22-1 给出完整闭环。它特意保留了“长期观察、决策与退役”,因为治理最容易在发布之后断掉。
图 22-1 版本治理的证据闭环
下面是同一闭环的文字版:
验收指标
↓
灰度 → 发布
↓
线上失败
↓
登记来源,补一条能复现失败的用例 ← 顺序不能反
↓
修复:新增或调整规则
↓
回归 + 相关的冻结评测
↓
重新灰度 → 长期观察
↓
退役候选 ────→ 回到起点,开始下一轮
有一段顺序值得先点出来:失败之后先补用例,再动手修(第 19 章)。反过来做——先打补丁,再补一条证明补丁有效的测试——是很典型的旧模式,它拿不到“修复前这条用例确实是红的”这个证据。
前半段通常都有人做:定指标、灰度、发布、出问题、补用例、修复。后半段——长期观察和退役——往往没有明确的负责人。
这一章按这个环走一遍。
三、验收指标:只看目标指标,容易慢性劣化
发布决策至少需要三类指标:
表 22-1 三类发布验收指标
| 类型 | 是什么 | 例子 |
|---|---|---|
| 目标指标 | 这次改动想改善什么 | 一次通过率、信息收集完成率 |
| 护栏指标 | 不允许变差的东西 | 已拒绝字段被再次索取的次数、完成语误报、P95 延迟、单次成本 |
| 诊断指标 | 解释为什么会这样 | 闸门拦截分布、原因码分布、转人工率、验证否决率 |
第二类最容易被跳过,却直接决定能不能安全放量:
一次改动往往能让某个目标数字变好,而代价可能藏在别的地方。 只盯目标指标的发布决策,会增加系统在一次次“看起来是进步”的发布里慢性劣化的风险。
有两条实践值得固定下来。
第一,目标和护栏必须放在同一张报表上。 分开看,人天然会去找对自己有利的那一张——这不是谁不诚实,是注意力的自然走向。
第二,回滚条件要在上线之前预先定义。 哪个护栏指标越过哪条线就回滚,写清楚、写在发布单上。等出了事再讨论“这算不算严重”,讨论的往往不是数据,而是刚熬夜发完版的心情。
同时要留一条不依赖统计阈值的紧急通道:单次严重事故本身就足以触发停止。错误扣款、发错对象的通知、泄露了另一个用户的信息——这类事情不该等到“错误率越过百分之几”才动手。
第 1 章那句话在这里成立得很直接:指标决定了你能看见什么。 规模没被纳入指标,所以涨了五百行也没人看见。
四、灰度:放量既不能只看时间,也不能只看流量比例
常见的放量方式是按时间走:先放百分之五,观察一天,没问题就放到一半。
问题在于“一天没问题”可能只是没遇到。低频但严重的场景——用户中途改口、拒绝之后又反悔、工具超时——在小流量下一天可能一次都不出现。
但反过来只看样本量也不对。有些问题必须经过时间才会暴露:延迟回调、次日的重复触达、跨会话的状态、周期性任务、外部系统的滞后结算。两小时内攒够一万个样本,也发现不了一个“二十四小时后重复通知”的缺陷。
所以放量要同时满足三个条件:
关键场景是否得到了足够暴露
护栏指标是否稳定
需要时间才能暴露的问题,是否跨过了必要的观察窗口
时间不是放量的充分依据,样本量也不是。
分层的顺序也有讲究,和第 21 章那条一致:无外部副作用的场景先行,涉及提交、通知、扣款的最后。 用户维度上则是内部、低风险人群、全量。
还有一条前提,它决定了前面所有事情的节奏:
能不能快速回滚,决定了你敢放多快。
回滚要多快,取决于业务能承受多长的错误扩散窗口——高风险交易可能要求秒级,低流量的内部助手十几分钟也够。原则是:回滚速度必须小于业务可接受的影响窗口,并且能用配置开关或版本指针完成的,就不该依赖一次完整重新发布。
还有一件事必须说清楚,否则“回滚”这个词会给人虚假的安全感:
版本回滚只能阻止新的错误继续发生,不能撤销已经发生的事。 已经创建的订单、已经发出的通知、已经提交的服务请求,不会因为切回旧版本就消失——它们仍然要按第 18 章那套补偿机制处理。
五、线上失败之后:先补用例,还要登记来源
第 19 章那条顺序在这里继续有效:先补用例,再改系统。
但治理层面还要多做一件事——登记这条规则为什么存在。这就是那条可追溯链:
来源(origin) ── 一次失败、一项需求、一条不变量、
一个风险或合规要求、一类边界或对抗场景
↓
规则或控制要求
↓
用例
↓
实现位置
↓
版本
↓
退役候选
第一环沿用第 19 章那个 origin:规则和用例一样,来源不止“出过事”这一种。 需求、不变量、合规要求、边界与对抗场景,都是完全正当的存在理由,而且其中一部分从上线第一天起就没有触发过任何失败。
每一环都有用,其中两环最容易被省掉。
来源回答“这条规则为什么存在”。省掉它,过一段时间就没人能判断它还有没有必要——退役常常就卡在这里:不是不想删,是不知道删了会失去什么。
实现位置回答“它到底写在哪一层”。这一环连着第 16 章那条分工:这条规则是写进了提示词、代码、闸门条件、验证器,还是决策表?
由此得到一条本章最实用的纪律:
新增一条规则时,最该问的不是“要不要加”,而是“加在哪一层”。
机制膨胀有一个很常见的成因:所有新规则都默认加进提示词。因为那里改起来最快,不用发版、不用写测试。但那也是最容易被稀释、最难被验证的一层(第 1 章)。默认加在那里,等于把每一次修复都变成一次容量消耗。
六、两种增长,两种处理方式
第 1 章那两次膨胀需要不同的动作:
表 22-2 两次复杂度增长的对比
| 第一次 | 第二次 | |
|---|---|---|
| 涨的是什么 | 散布的禁令、重复的口径 | 结构化的机制 |
| 彼此关系 | 互相冲突、互相稀释 | 不冲突,各有所防 |
| 该怎么处理 | 合并、删除、统一口径 | 外置、下沉,然后才是退役 |
第二种的下一步通常不是继续删规则,而是先把职责分层:暂时不能改应用层时,先在提示词内部把状态、闸门和检查集中到唯一位置;具备条件后,再把能写成代码、状态、闸门条件和验证器的部分逐步搬出去(第 16 章)。真正完成外置后,提示词会变短,但业务规则并没有减少。
这是“减法”的目标形态:不是砍掉功能,而是把功能挪到更适合承担它的地方。贯穿案例完成了提示词内部的分层,外部迁移由本书的参考实现展示。
第 1 章那条教训也在这里落地:规模红线必须绑定一个超线动作,而这个动作不能永远是“再压缩一次”。 第一次大扫除的收益是白送的,第二次面对的是一堆各有理由的机制,靠压缩拿不回同样的幅度。超线之后该做的是外置。
但外置也有上限——不是所有东西都能搬走,搬完之后总量仍然在涨。所以最后还需要退役。
七、退役:两种不同的减法
在问“该不该退役”之前,要先分清退的是什么。
一种是规则本身已经失去业务意义;另一种是规则依然有效,只是它的某个旧实现已经被更权威的实现完整覆盖了。
第二种就是上一节那个外置动作的收尾。假设“已拒绝的字段不得再问”已经下沉为代码里的动作不变量,业务规则一条没少,但提示词里那句承担权威计算的旧实现就成了多余副本——继续保留不仅占用执行容量,还会制造“同一条规则有两个来源”的隐患。提示词仍可用一句话告诉模型表达边界,但不能再保留一套与代码并行的判定逻辑。
这个区分很重要,因为两者的判据不同:规则退役要问“这件事还需不需要做”,实现退役只需要问“这一份副本还有没有作用”。 后者门槛低得多,也是外置之后最常被漏掉的一步——搬完新的,忘了删旧的。
判断一条规则该不该退役,问四件事:
它防的那次失败,现在还可能发生吗?(业务变了?上游修了?)
它是不是已经被更下层的机制覆盖了?(外置之后,提示词里那条还有用吗)
去掉它之后,有没有任何用例会变红?
它每次改动要牵动多少地方?
第三条最关键,也最容易被误读。
“删掉之后没有任何用例变红”有两种截然不同的含义:一种是它确实已经没有区分度了,另一种是你的用例根本测不到它。把后者当成前者,就是在拆一条自己看不见的防线。
分辨的方法正是 origin:这条规则防的那次失败,回归集里有没有对应的用例?没有,就先补用例,而不是直接删。
这条纪律和第 19 章那条是同一个道理的两面:
一条事故回归用例如果在修复前无法复现原故障,它就证明不了这次修复覆盖了那个问题;同样,删除之后全绿也不能单独证明删除安全——还要确认测试确实覆盖了这条规则存在的理由。
这里说的是事故回归用例。来自需求、不变量、风险与对抗场景的那些用例,从第一天起就是绿的,这不影响它们的价值(第 19 章)。
退役本身是一次改动,必须走完整流程,不能“顺手删了”:
- 确认它对应的用例在回归集里,且确实覆盖了那个场景;
- 移除规则;
- 按影响范围跑回归,以及相关的冻结评测子集——只跑回归,最多证明“已知的坑没有回来”,证明不了整体能力没被破坏(第 19 章)。变红说明它仍有区分度,退役中止;全绿则进入下一步;
- 灰度发布,重点盯与它相关的护栏指标;
- 观察窗口内无异常,退役完成,并在追溯链上标记退役版本。
第 5 步的记录不是形式主义。退役失败是会发生的——某条规则删掉三个月后问题复现。那时你需要知道它是什么时候、以什么理由被删的,否则只会重新加一条,然后陷入循环。
八、把规模本身变成一个指标
第 1 章那次审计发现的三个信号,值得作为常规观察项:
规模 提示词行数、规则条数、条件分支数
重复度 同一个约束在几个地方出现
方差 同一版本重复运行的稳定性(第 19 章)
生产观察 这三项在第 1 章那个系统上都留下了可比的数字。
规模:同一条演化线上的两个版本,前一版 1925 行、后一版 2196 行,涨了两百七十一行。
而同期“最高优先级”标记从 18 处降到了 15 处。 也就是说,局部确实发生了收敛——三处冗余的优先级声明被删掉——但整体规模仍然在涨。这是一个很典型的信号:局部优化跟不上整体增长时,看单点改进会得出乐观结论。
重复度:同期“单位”从 284 次涨到 299 次,“手机号”从 291 次涨到 309 次。同一批概念反复出现,而它们本该在重构中被收敛到唯一位置。
回归集规模:从重构时的五十三条长到八十三条。这一项不是坏消息——用例增长通常意味着覆盖变好,但它必须和前两项一起看:如果规则在涨、重复度在涨,而用例增长只是跟着补,那么系统是在用测试追赶复杂度,而不是在降低复杂度。
第二项是第 1 章那次膨胀最早的征兆——同一个约束在断言里写一遍、真值表里写一遍、发送前检查里再写一遍。那次重构时,“一条规则只出现一次”要禁止的正是这件事,只不过这次它穿了更体面的衣服。
但这一项要统计对东西,否则会误伤纵深防御。该数的不是“同一个安全目标在几层出现过”,而是“同一个业务决定有没有被多个地方各自独立重算”。
不健康的重复
提示词自己判断能不能提交
闸门再判断一次
验证器又用自己的逻辑判断一次
——三个地方各有一套业务规则
健康的纵深防御
闸门给出唯一的权威判定与可用动作
动作不变量检查:这个动作在授权集合里吗
验证器检查:说出来的话有没有超出已批准的动作
——一个权威结论,加两层一致性检查
纵深防御可以重复检查同一个结论,但不应该重复计算同一个决定。 这条边界第 13、15 章已经立过,在治理指标里同样要守住。
三项一起看的时候,有一个组合特别值得警惕:
生产观察 在本书这个项目里,规模上升与跨通过/不通过边界的方差上升同时出现,曾经是一次有效的重构预警——第 1 章那次就是。这是一次项目内的经验,不是普适规律;你的系统适不适用这条信号,要看自己的历史数据。
适用边界 规模变大本身不是坏事——一个成熟系统处理的边界情况本来就比新系统多。它只在规模涨了、可测性和稳定性却没跟上的时候才是问题。不要把行数当成 KPI 去砍,那会退化成另一种形式的指标游戏。
第 1 章还有一条教训在这里收口:成功的叙事会掩盖问题。 从重构完成到项目接近半年这段时间里,“一百七十五行”一直是一个很有说服力的成功故事,而系统已经在这段时间里涨回了七百行。定期审计的价值,很大一部分就是打断这种叙事惯性。
九、治理之后
版本治理落到执行上,就是四条纪律:
新增有具体理由,删除只有抽象理由——没有退出机制的系统只会单调增长。
先确定一条新规则属于哪项责任,再决定在哪一层实现它。
规则仍然需要,不代表它的每一个实现副本都还需要存在。
删掉之后没有用例变红,可能是它没用了,也可能是你测不到它——先看它的来源。
评测与治理这条闭环由四项能力合上:能证伪(第 19 章)、能定位(第 20 章)、能扛住输入对抗和模型更换(第 21 章)、能管住增长与退出(本章)。
但还有一件事一直是散着的:安全约束从第 7 章就开始出现——材料分区、注入治理、权限最小化、不变量、发出前验证、探针边界和对抗用例。它们分散在多个章节里,每一条都对,却从来没有被横着排开:攻击者会从哪里下手?这些防线连起来,中间有没有缝?下一章从攻击者视角回答。