你给 5000 名工程师的 monorepo 接入自动 code review:一堆 linter、静态分析器、加上一个 LLM reviewer,都作为 bot 在每个 diff 上贴评论。每天约 2 万个 diff,bot 平均每个 diff 产 8 条评论——每天 16 万条。第一周大家还看,第三周所有人学会了一个动作:把 bot 评论全部划过去。工具还在跑、指标(评论数、覆盖率)还很好看,但它已经死了——因为它没有信号,只有噪声。
这不是「加更多规则」能解的问题。这是一个信号检测(signal detection)问题:在真 bug(信号)和无关噪声中做二分类,而接收方(工程师)的注意力是全系统共享的稀缺资源。一旦误报率高到让人不信任,整个工具的召回全部作废——没人看的真报等于漏报。
graph TD
D["Diff / PR
本次改动"]
subgraph GEN["① 生成层 · 多来源信号"]
L["Linter / Formatter
确定性 · 廉价"]
SA["静态分析器
Infer 类 · 跨过程"]
LLM["LLM Reviewer
语义 · 贵 · 会幻觉"]
end
T["② 分诊层
severity × confidence 门控
nitpick 过滤 · 去重"]
R["③ 展示于 Review
blocking / 折叠 / 丢弃"]
H["工程师
NOT USEFUL 按钮"]
FB["④ 反馈回路
每 analyzer 的有效误报率
超预算→自动降级/禁用"]
D --> L & SA & LLM
L & SA & LLM --> T --> R --> H
H -.打分.-> FB
FB -.治理阈值.-> T
FB -.禁用.-> GEN
classDef src fill:#1a2530,stroke:#64c8ff,color:#e8eef5
classDef mid fill:#1a1a30,stroke:#ffb450,color:#e8eef5
classDef out fill:#0e2030,stroke:#5eead4,color:#e8eef5
class L,SA,LLM src
class T,FB mid
class R,H out
生成层尽管多产;价值全在分诊层和反馈回路——它们把召回换成 precision,并让「谁该存在」变成数据驱动
原理:把每条评论看成一次二分类判定,落入混淆矩阵四格:真报(TP)、误报(FP)、漏报(FN)、真阴(TN)。工具作者爱用技术定义的误报(「规则逻辑上确实成立」);但对用户,误报是任何他不想看到的报告——哪怕逻辑正确,只要它无关紧要、不值得改,就是噪声。Google 因此定义「有效误报」(effective false positive):开发者看到后没有采取任何正向动作。这个视角的转换是全篇的地基。
# 有效误报率:从用户信号反推,不是从规则逻辑
def effective_fp_rate(analyzer):
shown = analyzer.comments_shown # 展示的评论
# 「正向动作」= 改了代码 / 点了 useful / 标了 will-fix
acted = count(c for c in shown if c.got_positive_action)
return 1 - acted / max(shown, 1) # 没人行动的占比 = 有效误报
# 误报预算(Google 经验阈值)
FP_BUDGET = 0.10
def healthy(analyzer):
return effective_fp_rate(analyzer) < FP_BUDGET
原理:生成层可以尽管多产,不是所有真报都配打断人类。分诊层给每条评论打两个维度——严重度(severity)(空指针 vs 变量命名)和置信度(confidence)(分析器有多确定)——再决定它的命运:高 severity × 高 confidence → blocking;中等 → 折叠为可展开建议;低 → 直接丢弃。风格类 nitpick(格式、import 顺序)根本不该进人类视野——交给 formatter 自动改,人类注意力只留给正确性与设计。
# 分诊:把「是否真」与「是否值得打断人」解耦
def triage(c):
if c.category == "style" and c.autofixable:
apply_autofix(c); return DROP # nitpick → 机器改,不问人
score = c.severity * c.confidence
if score >= BLOCK_TH: return BLOCKING # 必须解决才能合并
if score >= SHOW_TH: return COLLAPSED # 可展开的建议
return DROP # 宁可漏,不制造噪声
# 同一问题多来源命中 → 去重,只留最可行动的一条
comments = dedup(comments, key=lambda c:(c.file, c.line, c.bug_class))
原理:一条报告的价值,一半在内容,一半在出现的时机。同样的 bug 报告,在 diff 评审时展示(作者刚写完、上下文全在脑子里),修复率极高;换成离线批扫描出一张几千条的清单丢给团队,修复率接近 0——没人愿意去动三个月前别人写的、还在跑的代码。推论还有一条:只报本次改动新引入的问题,不报存量债务,否则第一次接入就用几千条历史报告把人淹死。
原理:分诊阈值不能靠人拍。给每个 analyzer / 规则维护一条自己的有效误报率曲线(由 NOT USEFUL 点击、代码是否真被改推导),像 SLO error budget 一样管理:连续超预算的规则自动降级(从 blocking 降到折叠)乃至禁用。这把「哪些规则配存在」从政治辩论变成数据裁决,也倒逼规则作者对自己的信号质量负责。
# 误报预算 gate:像 SLO 一样治理规则生命周期
def govern(rule):
if rule.shown < MIN_SAMPLES: return "canary" # 样本不足,别急着判死刑
fp = effective_fp_rate(rule)
budget = BUDGET_BY_SEVERITY[rule.severity] # 安全类更宽容
if fp > budget * 1.5: return "disable" # 严重超预算 → 下线
if fp > budget: return "downgrade" # 超预算 → 降为非阻塞
return "active"
在信号检测里,precision 和 recall 是一条权衡曲线上的两端。把判定阈值拉到极高,确实能让展示出的每条都是真报(precision→100%、FP→0),但代价是大量真 bug 达不到阈值被丢弃——漏报(FN)飙升。
零误报通常意味着系统极度保守,只报那些闭眼都对的东西,而这些往往也是 formatter/编译器早就能抓的低价值问题。真正有价值的报告——需要跨过程推理、有一定不确定性的——恰恰带着非零误报率。所以健康系统追求的是「有效误报稳定在预算内(如 <10%)」,不是压到 0。看到某规则误报 0% 但触发量也极低,该问的是:它到底拦住过什么真问题?
90% 单条准确率 = 每 10 条评论有 1 条是误报。若 bot 每天贴 16 万条评论,那就是每天 1.6 万条误报砸在工程师脸上。人类不是按「单条概率」体验系统的,是按绝对噪声量体验的。
更狠的是基率:真正的严重 bug 在所有 diff 里本就稀有(可能千分之几)。当阳性事件的底率极低,即便单条判定很准,报告池里真报占比仍可能被误报稀释到很低——你翻 20 条 bot 评论才碰到 1 条值得改的,很快就学会全部跳过。这正是 Day 51 要展开的基率谬误:低底率下,度量必须看报告级信噪比,而不是单条准确率。破解靠分诊层大幅压低展示量、只上浮高置信高危。
Goodhart 定律:当一个度量变成目标,它就不再是好度量。给「评论数」定增长目标,工程师/规则作者会降低门槛多贴来冲量——直接制造噪声,与系统真正目的(保护注意力、拦真 bug)背道而驰。「有用评论数」稍好但仍是活动指标,且"有用"的判定容易被操纵。
该盯结果指标:① 评论采纳率(作者据此真的改了代码的比例);② 逃逸缺陷——本可被拦却漏到生产的 bug 数(衡量 recall);③ 有效误报率是否守在预算内(衡量 precision);④ 更上层的:评审周期时间、缺陷密度趋势。理想是一对拮抗指标同时约束(采纳率↑ 且 逃逸缺陷↓),单独优化任一个都会被钻空子。
blocking(不解决就不让合并)是分诊层能开的最强火力,也最危险。它是净收益的条件很苛刻:severity 高(真出事代价大,如安全/数据损坏)× confidence 极高(几乎不可能误判)× 有明确 autofix 或修复路径。三者同时满足时,block 挡住的是真事故。
它变灾难的场景:置信度不够却硬 block——一次把正确代码判死、卡住紧急发布,就足以让团队要求给 bot 开全局 override 后门,从此 blocking 形同虚设。经验做法:绝大多数报告走「非阻塞建议」,只有极少数经过长期灰度、误报率被证明趋零的规则才升级到 blocking;且永远保留一个需审计的人工 override 通道,让紧急情况有出口,而不是逼人整体绕过。
同构处:两者都是「在稀有信号 + 大量噪声里做二分类,且接收者注意力有限」。可迁移的原则——① 按 severity 分级路由(page vs ticket ↔ block vs 折叠);② 误报预算即 alert fatigue 治理,超预算的告警/规则要下线;③ 抑制与去重(同一根因多次触发合并成一条);④ 用结果指标(MTTR、逃逸缺陷)而非活动指标(告警数、评论数)考核。
断裂处:① 时效——告警要求秒级响应、把人从快回路移出(Day 41);code review 是异步的、人在回路里,可以慢。② 接收者——告警面向 on-call 个体的疲劳;review 面向整个工程组织的共享信任,一次误报的传播面更广、恢复更慢。③ 可回滚性——告警误报是瞬时噪声;review 误报若 block 了合并会直接拖慢交付,负外部性更实。所以 code review 系统对 precision 的要求,往往比一般监控更严苛。