Day 51 Hard Alerting Base Rate Signal Detection Observability

低底率下的告警系统 — 报告级误报如何淹没真报Alerting Under a Low Base Rate: Why Report-Level False Positives Drown the Truth

问题场景 + 需求约束

你给一个数万实例的生产平台设计自动告警系统:一堆探针、断言、异常检测器、再加一个 LLM「判官」在每次判定上盖章「这是不是事故」。规模是关键——每天约 1000 万次检查,而真正值得 page 人的事故底率极低:假设一天只有约 5 起真事故,那么单次检查为「真」的先验概率约 5 × 10⁻⁷

直觉会说「我检测器很准,FP 才 0.1%」——但这正是基率谬误(base-rate fallacy):0.1% 乘以一千万次检查,一天就是 1 万条误报,把那 5 条真报彻底淹没,P(真│报) ≈ 5/10005 ≈ 0.05%。工程师收到的 2000 条里才 1 条真,几周后所有人学会同一动作——把告警全部划过去。此时召回再高也没意义:没人看的真报等于漏报。这与 Day 50「Code Review 当信号检测」同源,但这里的敌人不是噪声,而是底率

高层架构(底率漏斗 → 独立兜底 → 分诊 → page)

graph LR
    SRC["1000 万次检查/天
探针 · 断言 · 异常检测"] DET["检测器海
单次 FP≈0.1%"] ORA["独立 Oracle 兜底
差分/确定性复核"] TRI["分诊 · 降噪层
去重 · 聚合 · 精确率地板"] PAGE["Page 人类
<10 条/天"] ARCH[("归档 / 静默
可查不打扰")] SRC -->|海量| DET -->|1万候选| ORA ORA -->|独立判定| TRI -->|precision≥90%| PAGE TRI -.降级噪声.-> ARCH DET -.低置信.-> ARCH classDef src fill:#1a2530,stroke:#64c8ff,color:#e8eef5 classDef det fill:#1a1a30,stroke:#ffb450,color:#e8eef5 classDef ora fill:#0e2030,stroke:#5eead4,color:#e8eef5 classDef page fill:#2a1530,stroke:#ff7ab6,color:#e8eef5 class SRC src class DET,TRI det class ORA ora class PAGE page class ARCH src

底率漏斗每一级都在提升 precision:检测器保召回,独立 oracle 打破相关性,分诊层守精确率地板,最终只有 <10 条/天进人的视野

关键技术点

1. 基率谬误的贝叶斯数学:precision 被底率压垮

原理:告警系统的可用性由 P(真│告警)(贝叶斯检出率 / precision)决定,而它不只取决于检测器的召回率,更被底率 base rate 支配。贝叶斯公式:P(真│报) = TPR·B / (TPR·B + FPR·(1−B))。当底率 B 极小,分母被 FPR·(1−B) 主导——哪怕 TPR=99%,只要 FPR 没低到与 B 同量级,precision 就趋近于 0。这就是 Axelsson 在入侵检测里证明的结论:限制 IDS 实用性的从来不是检出率,而是误报率

Trade-off(三种应对方向,各有代价):
# 底率如何碾压 precision(先算清楚再设计)
def precision(tpr, fpr, base_rate):
    tp = tpr * base_rate
    fp = fpr * (1 - base_rate)
    return tp / (tp + fp)

# 检测器很"准":召回 99%,单次误报 0.1%
precision(0.99, 0.001, 5e-7)   # ≈ 0.000495 —— 2000 条报里 1 条真
# 把判定域缩到高危面,底率抬到 1e-4:
precision(0.99, 0.001, 1e-4)   # ≈ 0.090   —— 仍不够
# 再串一层独立 oracle,等效 FPR 降到 1e-5:
precision(0.99, 1e-5, 1e-4)    # ≈ 0.908   —— 过了精确率地板
现实案例:

2. 精确率地板:分诊层是可用性的守门人

原理:单检测器压不动底率,就把 precision 交给分诊 / 降噪层(triage)——它不产生新信号,只对候选做去重、聚合、置信打分、跨检测器投票,仅放行超过精确率地板(如 precision≥90%)的判定。低于地板的降级到「可查询但不打扰」的归档池。关键心态:宁可漏报进归档,也不让噪声进 pager——一条噪声的代价不是它自己,而是它侵蚀了对所有后续告警的信任。

Trade-off:
# 分诊:跨检测器投票 + 聚合 + 精确率地板
def triage(candidates, floor=0.9, window="5m"):
    groups = group_by_root_cause(candidates, window)   # 同因聚合,防刷屏
    for g in groups:
        # 独立检测器越多同时命中,置信越高(近似乘法降噪)
        conf = calibrated_confidence(g.detectors, g.signals)
        if conf >= floor:
            page(dedup_key=g.root_cause, evidence=g.top_signals)
        else:
            archive(g)        # 可查询、进指标,但绝不打扰 on-call
现实案例:

3. 独立 Oracle 兜底 vs LLM 自任裁判:相关性是隐形杀手

原理:低底率下最诱人的偷懒是——让生成告警的模型自己当裁判。但兜底降噪只在它与被兜底层失败模式独立时才成立。串联两层把 FPR 相乘(0.1%×1%)的前提是独立;若裁判和检测器共享盲区(同模型、同 prompt、同训练分布),它们会在同样输入上一起错——误报相关、漏报也相关,等效 FPR 从「相乘」退化成「几乎不降」。真正的兜底要用独立 oracle:确定性断言、差分复核(同输入跑参照路径比对,见 Day 52)、或不同架构/信号源的第二判定器。

Trade-off:
# 兜底的价值取决于独立性,不是「多加一层」
def confirm(alert):
    # ❌ 反模式:同模型自我确认,相关失败
    # return llm_judge(alert)                # 幻觉 → 自我盖章

    # ✅ 独立 oracle:确定性 + 差分,失败模式正交
    if deterministic_assert(alert) is FAIL:            # 硬规则先过
        return True
    ref = run_reference_path(alert.input)              # 独立参照路径
    return normalize(alert.observed) != normalize(ref) # 差分比对(Day 52)
现实案例:

4. 度量陷阱:报告级 vs 判定级误报率

原理:团队最爱说「我们检测器误报率只有 0.1%」——这是判定级(per-decision)FP 率,好看但骗人。工程师真正承受的是报告级(per-report)precision:打开 pager 的 N 条里有几条是真的。判定级 0.1% 在 10⁻⁷ 底率下对应报告级 precision 0.05%——同一系统,一个指标「优秀」,一个「不可用」。更深一层是活动指标 vs 结果指标(承 Day 50):告警数、覆盖率、检测器数量都是活动指标,涨了不代表变好;真正的结果指标是真事故被及时发现的比例on-call 对 pager 的信任度

指标类型陷阱该看什么
单次 FP 率 0.1%判定级乘以海量检查后报告级崩塌换算成 precision 再汇报
告警总数 / 覆盖率活动涨了像在进步,其实在制造噪声真报占比、漏报数
检测器数量活动越多越可能拉低整体 precision边际 precision 贡献
on-call 是否还看 pager结果没人量,却是系统死活的真相ack 率 / 忽略率
现实案例:

扩展与优化(增长后怎么办)

常见陷阱 + 面试追问

1. 「我检测器 99% 准」≠ 系统可用。 面试必追:底率多少?把 precision 算出来。答不出贝叶斯换算=没理解本质,最经典的失分点。
2. 用 LLM 给自己的输出当裁判。 相关失败——它对自己的幻觉一路盖章。追问:你的兜底 oracle 与生成器独立吗?独立性从何而来?
3. 把召回当唯一目标。 低底率下拉满召回=噪声淹没系统=人不看=实际召回归零。召回和 precision 在这里不是对立,而是「precision 不达标则召回作废」。
4. 用告警数/覆盖率证明进步。 活动指标。追问:ack 率、忽略率、真报占比多少?没人看的系统覆盖率 100% 也是死的。
5. 忽略聚合窗口的双刃。 聚合抬 precision,但窗口过大会把两起独立事故合并、或延迟首报。追问:一次雪崩式级联故障,你的聚合会不会把 root cause 藏起来?

深入资源

深入思考(点击展开答案)

1. 底率 10⁻⁷、想让报告级 precision 达到 90%,单检测器的 FPR 需要压到多少?这现实吗?给出数量级与替代路径。

令 TPR≈1。要 precision≥0.9,即 B / (B + FPR·(1−B)) ≥ 0.9,解出 FPR ≤ B/9 ≈ 1.1×10⁻⁸。也就是每一亿次检查最多错一次——单个统计/ML 检测器几乎不可能做到,校准误差本身就远大于这个数。

所以正确路径不是硬压单检测器 FPR,而是:

  • 抬底率:把判定域从「所有检查」缩到「SLO 违约/高危面」,B 从 10⁻⁷ 抬 3 个数量级到 10⁻⁴,要求的 FPR 立刻放宽到 ~10⁻⁵。
  • 串联独立层:两层独立 FPR 相乘,0.1%×0.1%=10⁻⁶,配合抬底率就能过 90%。
  • 聚合:一次事故的 N 个候选合成 1 条报告,等效把 FP 计数从「次」降到「事件」。

洞察:低底率问题几乎永远靠「抬 B + 独立层」解,而不是靠让某个检测器变神。

2. 为什么「再加一层 LLM 裁判」有时几乎不降误报?用相关性把它讲清楚。

串联降噪的数学前提是条件独立FPR_total = FPR₁ × FPR₂ 只在两层对同一输入的错误互不相关时成立。若两层是同一底座模型、同一 prompt 上下文,它们的错误高度相关——生成器幻觉出的「事故」往往正是裁判也会认可的那类输入(同样的分布盲区)。极端情况下相关系数趋近 1,FPR_total ≈ FPR₁,第二层白加。

这也是为什么真正的兜底要换失败模式:确定性断言的错误来自规则写漏(与模型幻觉正交);差分 oracle 的错误来自参照路径 bug(独立实现);不同架构的第二检测器盲区不同。独立性不是「有没有第二层」,而是「第二层会不会在同样的地方跟着错」。

3. 你把判定域从 1000 万次缩到 1 万次高危检查,precision 大幅改善——但代价藏在哪?如何量化你漏掉了什么?

代价是召回:判定域外的真事故你根本不判定,直接漏。缩域=用召回换 precision,只是这笔交易在低底率下往往划算(噪声淹没时召回本来就是虚的)。

怎么量化漏了什么:

  • 事后复盘反查:每次真事故复盘时,检查它当初落在判定域内吗?落在域外=你的缩域策略漏了它,记为一次「域外漏报」。
  • 混沌工程注入(Day 44):主动在域内域外都注入已知故障,直接测两侧召回。低底率下这是唯一能拿到足够真样本的办法。
  • 影子判定:对被排除的域仍在后台判定但不告警,离线统计「本会报中多少真事故」,评估漏报率。

决策标准:域外漏报里有没有「高严重度」的?precision 换召回可以,但不能把 Sev1 换出去——那类必须无论底率多低都保留判定。

4. 一次级联雪崩故障会同时触发成百上千个检测器。你的聚合/去重层是帮手还是帮凶?

两面性都有。 帮手:聚合把一场雪崩的上千条告警合成少数几条「事件」,避免刷屏、避免真正的 root cause 被淹在下游症状里——这正是分诊层的价值。

帮凶风险:

  • 过度聚合藏因:若按「时间窗口 + 相似性」粗暴合并,可能把触发雪崩的 root cause 告警下游连锁症状合成一条,on-call 只看到症状、找不到源头。
  • 窗口内混入独立事故:雪崩期间恰好发生的第二起无关故障被吸进同一聚合,被当噪声压掉。
  • 首报延迟:为了聚合等窗口攒够,首次告警反而晚了——雪崩最需要的是快。

对策:聚合要带因果/依赖拓扑(不是纯时间相似),保留并高亮「最上游」告警;对高严重度信号用「先报再聚合」而非「先聚合再报」,用拓扑而非时间窗定义同因。

5. on-call 已经不看 pager 了——这个「信任崩塌」状态在指标上长什么样?如何早于崩塌前发现?

崩塌的指标画像:ack 时延变长、ack 率下降、「已确认但无操作」比例上升、批量静默/snooze 飙升、同一告警反复触发无人处理。注意:告警量和覆盖率此时可能仍很好看——这正是活动指标骗人处。

如何早发现(在崩塌前):

  • 把「人的行为」当一等指标:监控 ack 率、忽略率、误报反馈率,而不只是监控系统。人开始忽略是最早的领先信号。
  • 误报预算 + 熔断:像 error budget 一样给「误报」设预算,某检测器一周烧超预算就自动降级下线,防止它侵蚀整体信任。
  • 抽样审计:定期人工标注一批 page 的真/假,直接测报告级 precision 的漂移。

核心:告警系统真正的 SLO 不在机器侧,在人侧——「人是否还相信它」。信任一旦烧穿,重建成本远高于当初省下的降噪工作。