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["合入"]
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 收到的任务是”给导出功能加一个失败重试”, 它交出了一份改动。五条边界各自问什么?
第一条问的是”重试真的发生了吗”。 一个只加了 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)。
如果承诺是事后总结的,那一条不会出现在清单里 —— 因为总结的人不会主动写下自己没做到的东西。