3  承诺什么

章节 2 说清了不确定性住在哪。接下来的问题是: 在那个前提下,我到底能承诺什么。

这不是一个措辞问题。承诺什么决定了你去建什么 —— 一个承诺”输出稳定”的系统 和一个承诺”验收稳定”的系统,建出来的东西完全不一样,投入的方向也完全不一样。

3.1 不承诺同样的代码

一个功能有很多种正确实现。要求 Agent 每次给出同样的代码,等于要求它退化成 一个模板引擎 —— 而那你不如直接写模板,还更快更省。所以这不是妥协, 是把要求放在正确的层次上。

值得看一眼那个错误的做法长什么样,因为它很自然。有一种想法是给每类任务 准备一份”标准答案”,然后比对 Agent 的产出和标准答案的差距。这个做法的 失败方式可以精确预测:所有偏离标准答案的正确实现都会被拒绝(而正确实现 的空间远大于标准答案,所以拒绝率会很高),同时所有符合标准答案的错误实现 都会被接受(因为标准答案只覆盖了它自己那条路径上的正确性)。

于是这套东西同时具有高误报和高漏报。更糟的是它的动力学:被拒绝多了之后, Agent(和人)会学会朝标准答案收敛 —— 不是朝正确收敛。这是形状 A 的一个变种, 判定测的是相似度,而你以为它测的是正确性。

3.2 承诺同样的判定边界

那么承诺什么?承诺每一次产出都能用同一套证据来判断它算不算数。具体是五条:

边界 换来什么 在哪一章
行为能够被测试观察 运行时的事实可查 章节 12
结构符合仓库的架构约定 结构上的事实可查 章节 13
改动尊重 owner 与不变量 路径背后的约定不失效 章节 14
高风险动作保持显式 不可逆的事不会被悄悄做掉 小节 14.6
失败能定位、修复能复验 搜索空间能收缩 章节 11

这五条就是后面几部的目录。它们之间不是并列关系 —— 前四条是”拦住什么”, 第五条是”拦住之后怎么办”,而第五条决定了前四条的实际价值。

3.2.1 第五条最容易被漏掉

一个只返回”失败”的判定,Agent 只知道方向错了,它下一次的搜索空间和这一次 一样大。于是它会换一个写法再撞一次,撞到某次侥幸通过为止 —— 而侥幸通过的 那次,通常是因为它绕过了检查,不是因为它做对了。

一个返回了 owner、具体证据和最小重跑集合的判定则不同:Agent 的搜索空间 会实实在在地收缩。所以”失败信息的质量”不是用户体验问题,它决定这套系统 收不收敛。小节 17.6 会给出一个具体的数字:从红灯到转绿的 1.4 小时里,大部分时间不在等机器,而在 Agent 读失败、理解、修改这一段。

3.3 五条边界怎么判一次具体的改动

抽象的边界清单容易点头,所以走一次具体的。 假设一个 Agent 收到的任务是”给导出功能加一个失败重试”, 它交出了一份改动。五条边界各自问什么?

flowchart TB
  C["一次改动"]
  C --> B1["① 行为能被测试观察<br/>重试真的发生了吗"]
  C --> B2["② 结构符合架构约定<br/>重试逻辑放对地方了吗"]
  C --> B3["③ 尊重 owner 与不变量<br/>它碰了谁的状态"]
  C --> B4["④ 高风险动作显式<br/>重试会不会重复提交"]
  B1 & B2 & B3 & B4 --> V{"判定"}
  V -->|"不通过"| B5["⑤ 失败能定位、修复能复验<br/>告诉它哪条、哪行、怎么改"]
  B5 --> C
  V -->|"通过"| M["合入"]

第一条问的是”重试真的发生了吗”。 一个只加了 try/catch 和一个循环的实现,会让所有人(包括 Agent 自己)以为重试在工作 —— 而只有一条构造出失败、断言了重试次数的测试才能确认它。 注意这一条不问”代码看起来对不对”,它问的是有没有一个运行时的事实可查

第二条问的是”这段重试逻辑放在哪”。 如果它被写进了导出功能的 业务代码里,那么下一个需要重试的功能会再写一遍 —— 而这正是形状 C 的入口。结构检查看的是这个: 重试是一个跨功能的机制,它该在共享层,不在某个功能里。

第三条问的是”它碰了谁的状态”。 重试意味着同一个操作会执行多次, 所以它必须回答一个问题 —— 那个操作幂等吗? 如果导出会写一条记录,重试三次就是三条记录, 而这条不变量的 owner 是那张表,不是导出功能。

第四条问的是”这里有没有不可逆的动作”。 如果导出的最后一步是发一封邮件给用户,那么重试就是重复发送 —— 而这一步必须是显式的,不能被一个泛泛的”失败就重试”覆盖掉。

第五条决定了前四条的实际价值。 假设第三条不通过 —— 一个只说”改动被拒绝”的判定,会让 Agent 换个写法再试; 而一个说”这张表要求单次写入,见 docs/export-invariants.md, 你的重试会产生重复记录,修法是先做幂等键”的判定, 会让它一次改对。同一次拒绝,两种信息量, 而它们之间的差别就是这套系统收不收敛。

这个例子里值得注意的一点是:五条边界没有一条在问 “这个重试策略设计得好不好”。指数退避还是固定间隔、 重试三次还是五次 —— 这些属于边界之内的自由, Agent 怎么选都算数。这正是下一节要讲的。

3.4 边界之内,自由探索

有一个反直觉的效果值得说清楚:边界越明确,可探索的空间越大,而不是越小。

模糊的边界会让 Agent(和人)保守。因为不知道哪里会踩雷,最安全的策略就是 只做最像已有代码的事情 —— 抄一段相邻的实现,改几个名字。这恰恰是创新的反面, 而且它有一个很隐蔽的代价:它让技术债以”和周围一致”的名义扩散。周围有一段 重复的机制,新代码就再重复一次,因为那样看起来最安全。

一套明确的边界传递的信息正相反:这些线之外的地方,你随便走。这是我的核心 主张之一,也是”不承诺同样的代码”那句话的正面表述 —— 判定边界不是笼子, 是许可。

3.5 三个明确的不承诺

一套方法的可信度,很大程度上由它明确拒绝承诺的东西决定。我拒绝三条。

第一,不承诺 Agent 不犯错,只承诺错误在副作用发生之前被发现。

“副作用发生之前”这个限定很重要,它划出了三个代价差几个数量级的阶段: 一个错误的实现被写出来了是可接受的,那是探索的一部分;一个错误的实现被 合并了,说明判定失效了,这正是我要解决的;而一个错误的迁移在生产库上跑了, 那是不可挽回的 —— 所以那条路径上有最高等级的不变量守着。整套设计 就是把错误尽量挡在第一个阶段。

第二,不承诺需求是对的。

章节 20 会详细讲:这是范畴上的事,不是能力上的事。一个闭环系统 不能生成自己的设定点。这套东西能保证你高效地、可靠地、可验证地把一件事做完, 它完全不能保证那件事值得做。而且它有一个放大效应 —— 执行效率越高, 选错方向的代价越大。

第三,不承诺覆盖到工具链自己。

这是目前最大的缺口,而且它是自曝的。三层判定全都作用在代码上, 而生产这些判定的工具链 —— 那 28 条定时任务、近四个月 18 万次执行 —— 没有任何一层判定在管它们。具体的后果是一个空指针崩溃挂了四个月没有人发现, 因为它坏了不产生任何可见后果。

章节 19 会讲这个缺口该怎么补。这里只需要记住那句话,因为它是全书的枢纽: 判定覆盖到哪里,确定性就只到哪里。

3.6 承诺给谁

同一套判定边界,有三类消费者,而它们需要的形态不一样 —— 搞混了会导致一份对谁都不够用的承诺。

Agent 是最主要的消费者,而它需要的是可执行的形态。 一条”改动尊重 owner 与不变量”的承诺,对 Agent 来说 必须落成一次具体的检查、一份具体的路径清单、一句具体的修复提示。 Agent 不会去推断一句抽象承诺的含义,它只会消费摆在它面前的东西 —— 这就是第二部整部在讲的事。

人是第二类消费者,而人需要的是边界的理由。 一条规则拦住了一个人,他会问”为什么”, 而如果答案只是”规则这么写的”,那么下一次他会试着绕过去。 每条规则要带 incident 字段,正是出于这个(小节 13.4.4)—— 它服务的不是执行,是接受

第三类是仓库之外的人:一个新加入的同事、一个评估这套做法的团队、 一个需要判断”你们的代码能不能信”的下游。 这类消费者需要的是承诺的边界,也就是那两张表里的第二张 —— 你明确不承诺什么(小节 3.10)。

三类消费者里,第三类最常被忽略,而忽略它的代价很具体: 别人会按他自己的假设来理解你的承诺, 而他的假设通常比你实际做到的更宽。 等到某天出了一个你从来没承诺过要防住的问题, 分歧不会停在技术层面,它会变成”你们不是说有一整套检查吗”。 一份写清楚了”不承诺什么”的清单, 它防的正是这一类分歧 —— 而它的成本是二十分钟。

3.7 承诺的粒度

有一个地方容易搞错:承诺应该在什么粒度上给出。太粗的承诺(“代码质量有保证”) 无法证伪,也就无法验收;太细的承诺(“每个函数覆盖率高于 95%”)则把手段 当成了承诺,而这个错误更隐蔽。

它隐蔽在两处。第一,手段会被优化而不是被满足 —— 小节 12.12 会 说明,提高覆盖率有一条不经过”验证行为”的捷径,而一旦覆盖率本身成了承诺, 走捷径就成了合理行为。第二,手段变了之后承诺就没了 —— 如果哪天你换了一种 更好的验证方式(比如以变异验证为主,小节 12.2.1), 那个以覆盖率表述的承诺无法迁移。

正确的粒度是”要达到什么”,不是”用什么达到”。这条有一个可以直接执行的测试: 把你的承诺里所有的工具名、指标名、阈值都删掉,看它还剩下什么 —— 剩下的那部分才是真正的承诺。

3.8 承诺必须比它守的东西活得久

一条承诺如果依赖某个具体的工具、某个具体的人、 或者某个具体的流程步骤,那么它的寿命就是那个东西的寿命。 这一点在写承诺的时候几乎不会被想到, 因为那个工具当下正在正常工作。

具体的失效路径是这样的:你承诺”每次改动都会被三层判定检查”, 而这条承诺的实际载体是一份 CI 配置。半年后有人为了加速 把其中一层拆成了可选的,那条承诺就悄悄变窄了 —— 而没有任何东西会告诉你承诺已经不成立了, 因为承诺住在一份文档里,而它守的东西住在一份配置里。

正确的做法是让承诺和它的证据住在一起, 或者至少让它们互相引用。这套系统里的形态是 每条规则带 incident 字段 —— 那行字既是承诺 (这里守着什么),也在规则文件里, 所以规则被删掉时那句承诺跟着一起消失, 不会留下一个失效的声明。

这条推论可以说成一句更一般的话:一条不会随着它守的机制 一起消失的承诺,迟早会变成一句谎话。 检查它的方法只有一个问题 —— 如果明天有人把那个机制删了, 这条承诺会跟着不见吗?不会,那你就有一条会腐烂的承诺。

这也回答了 小节 3.10 那两张表为什么 不该被写进一份独立的文档:它们应该住在 那些证据实际产生的地方旁边, 或者至少有一条检查在验证”每条承诺都还有它的证据”。 这条检查本身也是承诺的一部分—— 它回答的是”你怎么知道你还在兑现你的承诺”。

3.9 承诺和信任的关系

一个承诺的实际价值,不只取决于它有多严格,还取决于别人相不相信它。 这里有一个很陡的不对称:建立信任需要长时间的一致,摧毁它只需要一次不一致。

具体到这套系统:一次假绿会让所有绿灯的可信度下降。不是下降一点 —— 因为人(和 Agent)无法区分”这次是真绿”和”这次是假绿”,所以一次假绿会让 所有绿灯的可信度按一个未知的比例打折,而你不知道那个比例是多少。

这解释了后面几件看起来投入过大的事。为什么我对假绿如此在意 (小节 12.3)—— 它损害的不是那一条测试,是整个体系的可信度。 为什么退出码三分值得那半天成本(小节 10.2.3)—— 它防的是 Agent 因为一个不该信的红灯而改坏代码,而那类事件同样在消耗信任。 为什么规则的误报要被认真对待(小节 5.8)—— 它们消耗的是同一个池子。

3.10 写下你自己的那份

这一章唯一的动作项:把你自己系统的判定边界列出来。

一张两列的表:

我承诺 靠什么证据

右边一列填不出来的那行,你其实没在承诺它,你只是希望它。

然后再写第二张表,这张更重要:

我明确不承诺 为什么

大部分团队从来没写过第二张表,于是发生两件事: 外部的人以为你承诺了你没承诺的东西, 而内部的人会花力气去优化一个本来就不在承诺范围内的指标。

3.11 反面教材:常见的承诺清单

给一份常见但有问题的承诺清单,逐条指出问题所在:

常见的承诺 问题
“代码质量有保证” 无法证伪(小节 3.7
“所有代码都经过审查” 审查了不等于审出问题了
“覆盖率高于 80%” 把手段当承诺
“CI 通过才能合并” 没说 CI 检查了什么
“遵循团队规范” 规范如果没有可执行的部分,这句话没有内容

第二行和第四行的问题是同一个:它们承诺的是一个流程发生了, 而不是一个性质成立了。

流程发生了很容易验证(有审查记录、CI 是绿的), 而这正是它们受欢迎的原因 —— 但它们和质量之间的联系没有被建立。

改写成有内容的版本:

  • “所有代码都经过审查” → “每次改动的结构、依赖方向和路径不变量 都被机器验证过,需要判断的部分被人看过”
  • “CI 通过才能合并” → “CI 验证了行为、结构和路径不变量三类事实, 而且它自己有自检”

改写之后的版本更长、更笨拙,但它们可以被检验 —— 而一个可以被检验的承诺,才是承诺。

3.12 承诺怎么随时间变

承诺不是一次写定的。它会变,而它变的方式本身是一个健康指标。

健康的变化有一个固定的方向:从”不承诺”移向”承诺”, 而且每一次移动都伴随着一个新建的判定。 这套系统的第三条不承诺(不覆盖工具链自己)就是一个候选 —— 等 章节 19 那几个观测器建起来之后, 它可以从第二张表移到第一张表,而移动的依据是 “现在有证据了”,不是”现在感觉还行”。

不健康的变化有两种。一种是承诺悄悄变宽而没有新的证据: 没有人写下”我们现在也承诺 X 了”,但对外的说法里开始包含 X —— 这通常发生在一次成功之后,而它的代价会在下一次失败时一起付。 另一种是承诺变窄但没有人说:某条检查被关掉了、 某份清单失效了,而第一张表没有跟着改 —— 这一种更常见,因为收缩承诺是一件没有人愿意主动做的事。

所以这两张表值得有一个复核节奏,而节奏用 小节 17.4 那条规则来定:取决于你的系统变化有多快。 一个每周都在改基建的团队,每季度看一次; 一个已经稳定的系统,一年一次就够。

复核时只需要问两个问题:第一张表里的每一条, 今天还有证据支撑吗?第二张表里的每一条, 是不是已经有证据了、可以移过去了? 两个问题走完通常十分钟, 而它的产出是这套系统”实际承诺”和”声称承诺”之间的差值 —— 而那个差值就是你现在最该补的东西。

3.13 为什么这一章排在第一部

“承诺什么”看起来像一个总结性的话题, 但它排在第一部的最后,理由是:

承诺清单决定了后面三部要建什么。

具体地说,小节 3.2 那五条边界, 一一对应后面的章节

边界 它在哪一章被兑现
行为能被测试观察 章节 12
结构符合架构约定 章节 13
改动尊重 owner 与不变量 章节 14
高风险动作保持显式 小节 14.6
失败能定位、修复能复验 章节 11

所以这一章不是总结,是提纲。

它排在第一部还有第二个理由:先写下承诺, 再建系统,顺序不能反。

一个先建系统再总结承诺的团队, 它的承诺会是它已经做到的那些 —— 而那意味着承诺清单失去了它的主要功能:指出还缺什么。

小节 3.5 那三条明确的”不承诺” 就是这个功能的产物 —— 其中第三条 (不承诺覆盖到工具链自己)指向的正是这套系统 现在最大的缺口章节 19)。

如果承诺是事后总结的,那一条不会出现在清单里 —— 因为总结的人不会主动写下自己没做到的东西。