1  两堵墙

我在这个仓库上把 Agent 的并行度推上去之后,连着撞了两堵墙。第一堵是人审 不过来,第二堵是机器也审不过来。两次我都想过同一个省事的办法 —— 让 Agent 少产出一点 —— 而两次我都没有那么做,因为那是在拿一个可以被工程化的问题, 换一笔要一直付下去的税。真正管用的解法都在另一侧:把「判定」这件事扩容、 机械化,最后再让它只做必要的那部分。

先把两堵墙各自长什么样说清楚。它们的形状其实是一样的,只是撞上去的那一刻 看起来完全不像 —— 第一次你看到的是人在抱怨,第二次你看到的是流水线在排队。

1.1 墙一:人审不过来了

最先撑不住的是人。取样的那个月里 Agent 合并了两百九十一次,平均每天有十份改动 排队等人看。这个数字本身不算离谱,离谱的是它的另一半:人的逐行阅读速度不会 因为 Agent 的出现而变快,注意力也不会随着队列变长而增加。产出这一侧翻了几倍, 消化这一侧一点没动。

审查队列一旦变长,结果只有两种。要么让代码在队列里等着,于是 Agent 的产出 变成了库存;要么在疲惫的时候快速点通过,于是审查变成了一道手续。两种都不是 质量,但它们在指标上的表现截然不同 —— 这才是麻烦的地方。

第二种值得多看一眼。它不会表现为质量下降,恰恰相反,它表现为审查通过率 上升、平均审查时长下降、队列长度回落。所有你会盯的数字都在变好,唯独没有 人真的看过代码。我在后面会反复回到这个形状:一个失效的检查,看起来和一个 通过的检查一模一样,而你从输出上区分不出来。

1.1.1 那个当时看起来很激进的决定

想清楚这一点之后,我做了一个当时看起来挺激进的决定:不再用人做机械性的代码审查。

格式、编译、测试、依赖方向、生成物来源、危险配置 —— 这些事情都有明确答案, 交给机器判定不但更快,标准还更一致。人腾出来的注意力,留给那些机器判断不了 的问题。

这个决定的关键不在”不用人”,在”有明确答案的”这五个字。它划的是这样一条线:

flowchart TB
    Q["一次改动<br/>需要被判断的事情"]
    Q --> A["答案唯一,可被程序确认"]
    Q --> B["答案取决于判断"]
    A --> A1["这个文件放对位置了吗?<br/>依赖方向合法吗?<br/>这个字符串该硬编码吗?"]
    B --> B1["这个需求要解决什么问题?<br/>两个方案怎么取舍?<br/>这个不可逆的动作该发生吗?"]
    A1 --> M["交给机器<br/>快、一致、不疲劳"]
    B1 --> H["留给人<br/>注意力是稀缺资源"]
    style M fill:#e8f4e8
    style H fill:#fdf3e3

把第一类交给机器不是妥协,是升级。因为人在这类判断上本来就不可靠: 一个人连续看三十个 diff 之后,他对”这条依赖方向对不对”的判断准确率会掉到 什么程度,没有人愿意去测量 —— 而正因为没人测量,这个下降不会出现在任何指标上。

1.1.2 机器判定要吃算力

判定交给机器之后,紧接着的问题是机器从哪来。我开始买机器。

现在机柜里有三台虚拟化服务器、两台 NAS、九台 Mac Mini,外加一个硬盘柜。 九台 Mac Mini 里有八台组成了 CI 的执行池,而这个池子的并发数不是统一的, 是按每台机器的内存配额分档的:五台小内存的各跑三个并发,三台大内存的各跑九个, 加起来四十二路并发的 macOS CI。

这里已经埋了一个后面要用到的观察:四十二是配额之和,不是实测吞吐。这两个 数字之间的差距,就是第二堵墙所在的位置。

1.2 墙二:机器也审不过来了

买完机器没过多久,第二堵墙就来了。产出速率又一次越过了判定速率, 每条改动都跑全量检查时,流水线开始排队,改动在队列里堆积。瓶颈只是从 人的注意力换成了机时,问题的形状一点没变。

这一次我没有继续加机器。我让机器自己决定该审什么:拿到一次改动之后, 构建系统先去查反向依赖图,只为真正受影响的目标生成这一次的流水线, 测试再按每片若干个目标并行下发。没被波及的部分根本不会跑。

从表面看这只是”省算力”,但它真正改变的东西不在那里。加机器是在放大分子, 而查依赖图是在缩小分母 —— 它把测量从”整个仓库的状态”收缩到了”本次真正 变化的那个子空间”。章节 17 会说明这两者为什么不是程度差异, 而是性质差异:一个在扩大带宽,一个在减少需要被带宽承载的东西。

flowchart LR
    subgraph W1["墙一:判定的执行者不够"]
        direction TB
        A1[Agent 产出] --> R1[人逐行审]
        R1 --> Q1[(队列爆炸)]
    end
    subgraph W2["墙二:判定的范围没缩小"]
        direction TB
        A2[Agent 产出] --> R2[机器跑全量检查]
        R2 --> Q2[(队列爆炸)]
    end
    subgraph OK["解:稀疏测量"]
        direction TB
        A3[Agent 产出] --> D[查反向依赖图]
        D --> R3[只跑受影响的目标]
        R3 --> M[合并]
    end
    W1 --> W2 --> OK
    style Q1 fill:#f8d7da
    style Q2 fill:#f8d7da
    style M fill:#e8f4e8

1.3 两堵墙的位置可以粗算出来

这两堵墙不是模糊的感受,它们各自的位置能被算个大概, 而算一遍会发现它们比想象中近得多。

墙一的位置就是一个人的阅读速度。 一份不平凡的改动, 真正逐行读懂并给出有意义的意见,通常要几十分钟 —— 不是扫一眼点通过的那种读,是能发现”这条依赖方向不对”的那种读。 按每份三四十分钟算,一个专职的审查者一天能处理十份左右, 而这已经假设他一整天不做别的、不开会、不写代码。

而那个月的实际队列是每天十份。 也就是说,墙一恰好撞在”一个人全职做审查”这条线上—— 不是接近,是刚好。这个巧合并不神秘: 它说明在此之前,这个仓库的产出速率一直被审查能力隐式地限制着, 而 Agent 只是把这条限制第一次顶到了明面上。

墙二的位置更微妙,因为它不是一个吞吐数字。 机柜里那四十二路并发看起来很多,但那是配额之和,不是实测吞吐 —— 一条流水线要跑多少个目标,取决于这次改动波及了多少东西, 而在”每条改动都跑全量”的模式下,这个数和改动大小无关, 它是一个常数:整个仓库。

于是墙二的算式变成了:产出速率乘以全量检查的成本, 除以并发数。分子里有一项在涨(产出速率), 另一项是常数且很大(全量检查),而分母是固定的。 买机器改的是分母,而它的增长是线性的、有成本的、有上限的; 查依赖图改的是分子里那个常数,而它能改几个数量级。

两堵墙之所以看起来像同一个问题的两次出现 —— 它们是同一个不等式的两侧被顶到极限, 而 章节 17 会给这个不等式一个正式的名字。

1.4 两堵墙之后

这两个决定的效果在流水线数据里能直接看到。取样的三十个合入改动里, 有八个在合并前至少被拦下过一次 —— 27%。它们平均每个跑 1.7 条流水线, 从红灯到最终转绿的中位时间是 1.4 小时,最快的一次六分钟。整个过程里 没有人做逐行审查。

被拦得最狠的两个都是大改动。一个是把远端文件系统与传输队列并入基础层, 红了四次,四个多小时后才转绿;另一个是把输入法收敛到自研引擎,同样红了四次, 跨了将近二十个小时。这两条如果排队等人审,光把 diff 读一遍的时间就不止于此 —— 而且读完之后,人能发现的问题大概率还没有那四轮流水线发现的多。

1.5 为什么”让 Agent 慢下来”是错的解法

面对排队,有一个显而易见的选项我没有选:少开几个 Agent。它确实能解决排队, 而且它是唯一一个不需要任何投入就能立刻见效的办法,所以我得说清楚为什么不选它。

不是因为它无效,是因为它把一个可以被解决的问题变成了一笔永久的税。判定这一侧 的产能是可以被工程化的:机器可以买,检查可以只跑受影响的部分,判定可以下沉 到便宜的那一层。这些投入都是一次性的,而收益是持续的。降低产出速率则相反 —— 它是一笔要一直付的成本,今天少开五个 Agent,明天还得少开五个,而且这个动作 不产生任何积累。两者的区别不在于哪个当下更快,在于哪个会随时间变好。

这个判断依赖一个前提,而这个前提必须被检验:判定这一侧的产能确实是可以被 工程化的。第二堵墙的存在说明这个前提不是无条件成立的 —— 买机器到某个点之后 就不再有效了,那时候需要换一种做法而不是加大剂量。小节 17.8 会说明, 在增益(Agent 的吞吐)与延迟(判定回到手里的时间)这个框架下 (小节 17.1),“降低产出速率”其实是三条正当出路之一, 而且当另外两条都走不通时,它比失稳要好。

1.6 这两堵墙其实是同一堵

到这里可以把话说破了。墙一是修正速率上去了而判定速率没有,于是队列爆炸; 墙二是修正速率又上去了、判定速率也上去了,但判定的对象没有缩小,于是队列 又爆炸。两次的形状完全一样:一侧的速率提高,另一侧的带宽没跟上。

两次的解法之所以不同,是因为瓶颈的性质不同。第一次是带宽绝对不足 —— 一个人一天读不了十份 diff,而且第二个审查者也帮不上忙,因为他们要看的是同一批 代码。第二次是测量冗余 —— 带宽已经够了,但大部分检查测的是没有变化的东西。 第一种要换执行者,第二种要缩范围,而把这两种搞混的代价是你会在错误的地方 一直加钱。

章节 17 会给这两件事一个统一的名字。在那之前,先记住这个形状: 每当你想通过”加更多”来解决一个排队问题,先问一句排队的是修正还是判定。 如果排队的是判定,那么加修正端的产能只会让队列更长 —— 而这恰恰是引入 Agent 时最自然的那个动作。

1.7 判定这一侧到底在做什么

在进入后面的章节之前,得把”判定”这个词拆开,因为它容易被读成”跑测试”。 它至少包含四类活动,而这四类的成本、时机和失效方式都不一样。

结构判定问的是这段代码放得对不对 —— 它在正确的目录里吗?依赖方向合法吗? 这个符号出现在了不该出现的地方吗?这类判定最便宜,因为它不需要把程序跑起来。

行为判定问的是跑起来之后发生了什么。它最贵,需要起环境、起数据库、 起模拟器,而且它是唯一能发现”逻辑错了”的一类。

不变量判定问的是这条路径背后的约定守住了吗。它也很便宜,因为它只扫本次 改动新增的那些行,而不是整个仓库。

元判定问的是上面三类本身可不可信 —— 这次测试真的跑了用例吗?这条规则 自己有没有坏?这个红灯是代码的错还是环境的错?

第四类是我的重点,也是最少被讨论的一类。原因在于前三类的失败会主动表现出来: 测试红了、检查报错了、构建挂了,总有一个信号找上门。第四类的失败是静默的 —— 一个坏掉的检查器,输出的是一片绿色。

flowchart TB
    C[一次改动] --> S["结构判定<br/>放得对吗"]
    C --> B["行为判定<br/>跑起来对吗"]
    C --> I["不变量判定<br/>约定守住了吗"]
    S --> M["元判定<br/>上面三类可信吗"]
    B --> M
    I --> M
    M --> V{{"最终结论"}}
    style M fill:#fdf3e3
    style V fill:#e8f4e8

1.8 第三堵墙在哪

两堵墙都是”判定跟不上产出”。按 小节 1.6 那个框架, 可以预测第三堵墙的位置。

增益还在涨(Agent 数量、并行度)。这一次, 判定这一侧还能怎么扩容?

  • 换执行者:已经做了(人 → 机器)
  • 缩小测量范围:已经做了(全量 → 只测受影响的)
  • 加带宽:一直在做,但有物理下界

三条路都走过了之后,剩下的是什么?

小节 17.6 会给出一个答案:回路延迟里最大的一块, 可能已经不在基础设施上了,而在”Agent 读懂失败并修好”这一段。

如果这个推测成立,那么第三堵墙的形态会是:

流水线不排队了,机器也够,但每个改动要跑更多轮才能合入。

它的解法不在基础设施上,在失败信息的质量上 —— 一个带着 owner、证据和下一步的失败, 和一个只说”不允许”的失败,中间差的是好几轮迭代。

这些后面的很多内容,回头看都是在为这堵墙做准备: 小节 13.4.4 那两个字段、小节 14.7 那四步、 小节 14.3.3 那个最小重跑集合 —— 它们全都在压缩同一段延迟。

1.9 常见的误解:这两堵墙不是”规模问题”

容易被读成”仓库大了才会有这些问题”。不是的。

两堵墙的触发条件是产出速率与判定速率的比值, 和仓库大小、团队人数都没有直接关系。

一个两个人的团队,如果两个人各开三个 Agent, 每天产出十几个改动,而判定仍然靠两个人互相审 —— 它已经撞上了第一堵墙,只是墙比较近,撞得比较轻。

这个观察有一个实际推论:这两堵墙会比大部分人预期的更早出现。

它们不是”等我们做大了再考虑”的问题, 而是在引入 Agent 的那一刻,比值就已经开始变了。

区别只在于,小团队撞墙时的表现比较温和: 不是流水线排队,而是”最近合并的东西好像没人仔细看过”。

这两种表现,本质上是同一件事。

一个提醒

书里会反复出现”三十个改动的样本”“五百次失败的样本”这类窗口。 它们都是真实的,但样本量不大,当趋势看没问题,当结论用会偏薄。 凡是我知道样本不够的地方,都会说明。

1.10 这三个数一年后该变成什么

这一章给的三个数 —— 27% 的拦截率、1.7 条流水线、1.4 小时转绿 —— 它们的目标值是什么?

这是一个值得问的问题,因为没有目标值的指标会被当成”越高越好”或”越低越好”, 而这三个都不是。

拦截率:不该趋近于零。趋近于零意味着检查太松, 或者 Agent 已经学会了绕过。一个健康的值是稳定在某个区间, 而它的变化比它的值更有信息量。

流水线次数:应该缓慢下降 —— 随着规则的失败信息变好、 随着更多规则被前馈化 —— 也就是从「事后检查」变成 「事前就走不到错的路上去」(小节 5.4 · 小节 15.8)。 如果它在上升,那是第三堵墙的信号小节 1.8)。

转绿时间:这个数由两部分组成,而它们该往不同方向走。 流水线耗时应该随稀疏测量的改进而下降; 而”Agent 理解并修正”的那部分取决于失败信息的质量

三个数里,只有第二个有明确的方向。 另外两个是区间指标, 而区间指标最容易被误用 —— 因为它们没法被”优化”, 只能被观察。

这本身也是一条一般的经验:一个不知道目标值的指标, 不要放进任何人的考核里。它会被优化到一个你没想要的方向。

1.11 两堵墙之前发生了什么

有一件事容易被跳过,但它决定了你该不该读下去:在撞第一堵墙之前, 有相当长一段时间是没有这些问题的。

那时候 Agent 的产出还不多,我一个人审得过来,所有的规矩都在我脑子里 和评审的对话里。那段时间的做法是完全正确的 —— 在那个产出速率下, 建一套判定系统是不折不扣的过度工程。你会花几周建一个当时用不上的东西, 然后一直维护它。

我后面写的所有东西,都是被产出速率逼出来的,而不是因为它们在抽象意义上 “更好”。这个区别有一个很实际的推论:如果你现在没有撞墙,那么这些内容 对你的价值主要是”知道墙在哪”,而不是”现在就把墙拆了”。

小节 1.9 会说明这两堵墙会比大部分人预期的更早出现。但它还没到的时候, 提前建墙的成本是真实的,而收益是零。唯一值得提前做的是那种成本极低的 —— 比如给每条规则写下它的失败形态、比如把退出码分成三种。这些即使在撞墙之前 也不亏,因为它们不需要维护。

1.12 两堵墙的另一个用处:当筛子

我没有从理念开始讲,而是从两个具体的排队现象开始,理由有两条。

第一条是这两堵墙可以被识别,而理念不能。“我们需要确定性的基建”这句话, 没有任何一个团队能判断它现在适不适用;而”我们的审查队列在变长”可以被观察, “流水线在排队”可以被观察。所以入口是两个症状,不是一个主张。

第二条是这两堵墙决定了后面所有内容的必要性。如果你没有撞上它们,那么后面的 大部分内容对你来说都是过度工程(小节 1.11),而我宁可你现在不建, 也不希望你建一套还不需要的东西然后一直维护它。这两堵墙的实际功能是一个筛子, 它让你判断自己在不在后面那些东西的适用范围里。

1.13 两堵墙之外的第三种可能

有一种情况这一章没讲,但它在实践中很常见:

产出速率没有变,但质量下降了。

这不是这两堵墙里的任何一堵 —— 队列没有变长, 流水线没有排队,只是合并进去的东西变差了。

它的成因通常是:判定的形式还在,实质没了。

具体表现:评审仍然在做,但评审的人已经不逐行看了; 测试仍然在跑,但新加的测试没有断言; 检查仍然是绿的,但没有人问它检查了什么。

这是形状 A 在流程上的形态,而它比排队更危险, 因为排队是可见的,而这个不是。

识别它的方式:不看队列长度, 看”从提交到合并”这个过程里,有多少个环节 会因为提交者的态度不同而给出不同的结果。 那些环节就是实质正在流失的地方。

我讲的所有机制,共同的性质是: 它们的结果不取决于任何人的态度。

1.14 两堵墙都不是被”预见”到的

补一句:这两堵墙,我一堵都没有提前想到。

墙一是在队列已经堆起来、我连续几天在下班前赶着看 diff 的时候 才意识到的。意识到它的那一刻并不是一个分析的结果, 是一个体感 —— “这样下去不行”比”审查吞吐是 O(1) 而产出是 O(n)” 早到了大概两周。那两周里我做的是加班,不是设计。

墙二更隐蔽。它的表现是流水线开始变慢, 而变慢的第一反应永远是”机器不够”—— 而且这个反应是对的, 加机器确实缓解了一段时间。它错在它是一个会耗尽的解法, 而这一点在缓解生效的那几周里完全看不出来。

我把这一点写出来,是因为它对读者有一个直接的用处: 如果你现在没有撞上这两堵墙,你大概率也预见不到它们。 这不是能力问题 —— 一个系统的瓶颈在被顶到之前, 它和别的环节看起来没有任何区别。

所以这一章的实际建议不是”提前避开这两堵墙”, 是提前把那三个数量起来小节 17.7)。 量了之后,墙一和墙二会在撞上之前几周表现为一条上升的曲线, 而那条曲线是你唯一可能提前看到的东西。

这也是我唯一能说”预见”是可能的地方: 不是预见问题的形状,是预见某个量正在接近它的极限。

1.15 压成三句话

一、两堵墙的形状是一样的:一侧的速率提高了,另一侧的带宽没跟上。 两次的解法不同 —— 一次是换判定的执行者, 一次是缩小判定的范围 —— 是因为两次的瓶颈性质不同: 一次是带宽绝对不足,一次是测量冗余(小节 1.6)。

二、“加更多”解决不了排队,先问排队的是修正还是判定。 如果排队的是判定,加修正端的产能只会让队列更长 —— 而这恰恰是引入 Agent 时最自然的动作。

三、判定的第四类(元判定)最容易被漏掉。 前三类(结构、行为、不变量)的失败会主动表现出来, 而”这些判定本身可不可信”的失败是静默的 (小节 1.7)。

1.16 先说明一件事

这些后面会反复回到这一章的三个数 (27% 拦截率、1.7 条流水线、1.4 小时转绿), 而每一次都会从一个新的角度重读它们。

这不是重复,是因为同一组数在不同的框架下说的不是同一件事:

在哪章 这三个数说明什么
本章 判定在工作
小节 21.1.1 它们全是过程指标,没有一个是结果指标
小节 17.6 1.4 小时里大部分不是在等机器
小节 17.7 它们的趋势比值更有信息量

四种读法,而第二种是最重要的那个 —— 因为它指出了我这条证据链上最大的一处缺失。