KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

03 · 质量门禁金字塔:什么东西该挡在主分支之前 — keel 龙骨

这一章回答:检查那么多,怎么排顺序、卡在哪一层、哪些必须卡死哪些只能提醒。核心判断标准是四个字——反馈性价比。

这一章回答:检查那么多,怎么排顺序、卡在哪一层、哪些必须卡死哪些只能提醒。核心判断标准是四个字——反馈性价比。

一个 PR 从提交到合并,中间能插的检查有几十种。全卡死 → 没人愿意等,团队会想办法绕过流水线;全不卡 → 流水线沦为装饰。金字塔的价值是给出"谁必须在哪一层"的排序依据。

一、金字塔本体:按成本升序、按拦截范围降序

                 ▲  昂贵的、慢的、但能抓到深层问题的
      ┌──────────┴──────────┐
      │  端到端 / 负载 / 契约 │  分钟~小时级,nightly 或发布前
      ├─────────────────────┤
      │  集成 / 数据库 / 外部服务 │  需要真实依赖,容器或 mock
      ├─────────────────────┤
      │  单元测试            │  秒级,全 PR 必跑
      ├─────────────────────┤
      │  类型检查 / 静态扫描(SAST) │  秒~十秒级,无需运行
      ├─────────────────────┤
      │  格式化 / Lint / 依赖审计 │  秒级,最便宜
      └─────────────────────┘

排这个顺序的依据不是"哪个更重要",而是两个变量的比值:

检查 单次成本 典型反馈时间 抓到的问题类型
格式化 / Lint 极低 秒 风格、明显的坏味道、未使用变量
类型检查 低 秒~十秒 接口契约不匹配、None 传播、遗漏分支
依赖 / 密钥扫描 低 十秒 已知 CVE、明文密钥进库
单元测试 中 秒~分钟 逻辑错误、边界条件、回归
集成测试 高 分钟 SQL 与迁移、缓存语义、外部协议
端到端 / 契约 高 分钟~小时 服务间协议漂移、真实链路不可用
压力 / 长稳 很高 小时 泄漏、抖动、容量问题

排序原则:便宜且快的检查排在最前,能消灭最多的低级失败,把人的时间留给高级问题。 这就是为什么"忘记 import"、"类型对不上"这类问题绝不该出现在人工评审阶段——它们最便宜地被机器抓住,却最贵地被人类讨论。

二、必卡 vs 仅通知:一个可操作的决策表

门禁 是否阻塞合并 理由
格式化 / Lint 阻塞(可自动修复) 机器能修的东西不该让人讨论
类型检查 阻塞 契约错误在运行时才发现代价太大
单元测试 阻塞 这是最后一道廉价的防线
覆盖率下降超过阈值 阻塞 覆盖率绝对值没意义,趋势有意义
依赖高危 CVE 分情况:有可用修复则阻塞;无修复则转 issue 一刀切会让团队习惯性 --no-verify
集成 / E2E 阻塞(合并前至少跑冒烟子集) 全量可放到夜跑
负载测试 不阻塞 PR,阻塞发布 分钟级的成本适配发布频率,不适配 PR 频率
AI Review 意见 永远只建议 LLM 会自信地给错建议;把它当评审员不给它否决权

最后一条是 AI 时代的新纪律:LLM 的意见是建议不是判决。原因见下。

三、不稳定的测试(flake)是最贵的债

一条看似结实的流水线,如果每周随机红三次,会发生三件事:人开始不看红了("再点一次重跑")、真错误被淹没、新人对流水线失去信任。三者叠加的后果是流水线事实上失效,却还在消耗 CI 分钟数。

治理做法:

手段 落地方式 副作用
自动重跑失败用例 1 次 pytest-rerunfailures / CI 层 retry 掩盖真 flake,必须配检测报告
隔离而不是删除 给 flaky 用例打 @pytest.mark.flaky,挪到独立 job 长期不修会变成"永久隔离区"
设修复预算 每周固定配额修复 flake,超期自动 disable 并建 issue 需要有人负责,否则照样失效
记录 flake 率 同一 commit 多次运行结果不一致的比例 指标本身要能查,否则无从判断是否在恶化

面试里说"我们治理过 flake"比"我们加了很多测试"更能体现工程成熟度——前者说明你真的长期运营过一条流水线。

四、覆盖率的含义与四个陷阱

覆盖率回答的是"哪些代码被执行过",不是"哪些代码被验证过"。

陷阱 表现 对策
只为行覆盖写断言 assert result is not None 蹭到的行,断言无意义 评审时看断言强度,不看百分比
生产代码反向适配测试 为了好 mock 把设计改丑 契约测试优于大量 unit mock
整体覆盖掩盖热点 60% 但核心支付模块 0% 关键模块设更高阈值,全局阈值没意义
忽略分支覆盖 if 只有一条分支被跑过仍算覆盖 看 branch coverage 指标

进阶手段是变异测试(mutation testing):工具(如 mutmut / cosmic-ray)自动往源码注入小变异(把 < 改成 <=),如果测试仍然全绿,说明这个分支没被真正断言。它是唯一能直接度量"测试有没有用"的手段,代价是耗时,适合夜跑或对核心模块做。

五、PR 体积:AI 时代被严重低估的一个门禁

DORA 的 2025 研究反复强调小批量是释放 AI 收益的关键能力之一;反面也很清楚——LLM 乐于一次性输出跨几十个文件的大改动,而人的评审能力在超过 ~400 行后急剧下降。

可行的轻量门禁:

门禁 阈值(可自定义) 处理
单次 PR 改动行数 > 500 行 warn,> 1000 行请求拆分说明 用 label 标记 needs-split
单次 PR 涉及文件数 > 30 提示 往往意味着"顺手重构"混进了功能改动
是否同时改了 *.lock/依赖清单与业务代码 是 → 要求拆成两个 PR 依赖升级有独立回滚需求
是否包含生成物 是 → 拒绝 生成物不该进版本库

这条门禁的本质是把"评审可行性"变成可执行的规则。 AI 让写大改动的成本趋近于零,所以"人不至于被撑死"必须由流水线来保护。

六、分支保护与合并队列

主分支 ── 要求:所需检查全部通过 + 至少 1 人评审 + 分支最新 + 线性历史
            ↑
            └── required checks:把 03 章的阻塞级门禁列进来

动手:可观察结果

动作 产出物 判断标准
给项目排一张金字塔 各检查的耗时表 能算出"从 push 到拿到第一条错误"的中位时间
给 coverage 分组设两套阈值 fail_under + 关键模块单独阈值 故意删一个核心用例,看到 CI 红而非整体
连续 10 次跑同一 commit 的测试 flake 率 数值可查;> 5% 就该启动治理
对一个 300 行模块做变异测试 存活变异体清单 存活率低说明测试有效;存活高说明只是跑过代码
加 PR 体积门禁 label 自动打 造一个 800 行的 PR,看是否被标记

故障注入

注入方式 观察什么 说明的现象
提交一个依赖里有高危 CVE 的 PR 流水线是红还是只是提示 只有"红了又被迫 --no-verify"或"转 issue"两种合理结果
让一个测试带随机性(random.random()) 多次运行结果 flake 如何被"重跑一次就好了"掩盖
把集成测试挪到 require 列表 合并等待时间 分钟级涨价会立刻体现在团队行为上(开始批量攒 PR)
制造两个各自绿、合并后红的 PR 是否出现 出现 → 缺合并队列或不要求"分支必须最新"

自测题

  1. 金字塔的排序依据是什么?为什么不是"按重要性排序"?
  2. 覆盖率 80% 但测试无效,可能是哪三个原因?
  3. 变异测试能回答哪个覆盖率回答不了的问题?
  4. AI Review 的意见为什么不建议设为阻塞门禁?
  5. 两个 PR 各自通过、合起来失败,有哪两种流水线层面的解法?

进入 keel 阅读