59.3 度量的陷阱(古德哈特定律)

一句话

古德哈特定律说的不是“指标不好”,而是指标一旦与利益绑定,被度量者就会去优化“指标的定义”而不是“指标想代表的东西”;解法不是找更聪明的指标,而是降低指标与利益的耦合、配对反向指标、保留独立抽样。

三条定律,一个机制

定律 通俗表述 在研发里的样子
古德哈特定律 一个指标一旦成为目标,就不再是好指标 考核缺陷数 → 降级、合并、不报
坎贝尔定律 量化指标一旦用于决策,就越容易被腐蚀,并反过来扭曲它本想监测的过程 覆盖率门禁 → 只给 getter 写断言
眼镜蛇效应 激励催生反向行为,越悬赏越糟 按代码行数计薪 → 拆行、复制粘贴

💡 古德哈特 1975 年讨论的是货币政策里的统计规律;今天流传的“当一个指标成为目标,它就不再是好指标”是后来的通俗改写(常被归给 Strathern)。眼镜蛇悬赏的故事版本很多、考据不清,只借它说明机制,不当史实用。

三条定律描述的是同一个结构:指标是代理变量(proxy),代理与目标之间永远有缝隙,而缝隙的两侧信息不对称——被度量者比度量者更清楚缝隙在哪。

条件 含义 例子
代理性 指标只能代表目标的一部分 “缺陷数”代表质量,但不含未发现的缺陷
定义缝隙 口径里有可解释空间 严重度、重复单、状态流转、是否算“一次部署”
信息优势 数据由被度量者生产 谁提单、谁定级、谁关单,谁就能影响数字
单点考核 一个数字决定奖惩 排名、奖金、晋升、末位淘汰

四个条件凑齐三个,套利几乎必然发生;凑齐四个,只是时间问题。

套利动作:指标是怎么被“做”出来的

动作 做法 指标变化 真实交付变化
降级 S2 改判 S3、P1 改 P2 严重缺陷数下降 无
拆分 一次发布拆成三次;一个大 PR 拆成十个 部署频率上升、前置时间下降 无
合并 重复单并入已有单、多个 bug 记一条 缺陷总数下降 无
延迟 卡到季度末再关单、卡到考核后再上线 本周期指标变好 变差(债务后移)
不报 口头修掉、只在内网群说 缺陷数下降 风险上升
改口径 分母去掉热修、前置时间从“合并后”起算 全线变好 无
刷量 空提交、重复部署同一 sha、为覆盖率写无断言测试 频率/覆盖率上升 无
转移 把脏活推给下游团队、外包、用户 本团队指标变好 系统整体变差

🔑 注意最后一行:指标考核的是局部,代价往往落在未被度量的地方。这就是为什么“每个团队都达标,公司整体没变快”。

案例地图:每个指标都有它的“玩法”

指标 想度量 常见玩法 配对/反向指标
代码行数 产出 拆行、复制粘贴、生成器灌水 变更失败率、评审时长、功能交付数
缺陷数 质量 降级、合并、不报 逃逸缺陷率、重开率、事故数
故事点 产能 估大、拆细、只挑好估的 可数完成项、周期分布
覆盖率 测试充分性 只测简单分支、无断言测试 变异测试得分、逃逸缺陷率
部署频率 交付速度 拆发布、空部署、重复部署 每次部署变更数、返工率、CFR
变更前置时间 管道效率 只算 commit 之后、只挑小需求 需求前置时间、P85
工时填报 投入 填满 8 小时、项目间平摊 交付结果、周期
评审数量 协作 走过场 LGTM、只改错别字 评审发现缺陷数、评审时长
响应时长 服务质量 秒回“已收到”、自动关单 解决时长、重开率、满意度

这张表的用法不是“别度量”,而是每个指标旁边必须站一个它管不住的反向指标:速度配稳定(59.1 DORA 四指标),缺陷数配逃逸率(缺陷侧的完整讨论见 53.10 回归测试与缺陷管理),覆盖率配变异测试。告警数量也是同一类可被“做低”的指标(56.4 告警治理)。

可复现的推演:报表降 12%,真实缺陷一个没少

模型:一个季度 120 个真实缺陷(S1 12 / S2 36 / S3 72)。考核“缺陷数”与“严重缺陷数”后出现三类行为:30% 的 S2 降级为 S3、20% 的 S3 不报、10% 的 S1 降级为 S2;被藏起来的问题有 25% 在后续版本以逃逸缺陷回来。

#!/usr/bin/env python3
"""goodhart.py —— 指标被优化成目标之后:报表变好,真实缺陷不变。"""
import random
import unicodedata

random.seed(59)


def w(s):
    """显示宽度:中文按 2 列算,让表格在终端里对齐。"""
    return sum(2 if unicodedata.east_asian_width(c) in "WF" else 1 for c in str(s))


def pad(s, n):
    return str(s) + " " * max(0, n - w(s))


S1, S2, S3 = 12, 36, 72
TRUE_TOTAL = S1 + S2 + S3

downgrade_s2 = round(S2 * 0.30)     # S2 -> S3
hide_s3 = round(S3 * 0.20)          # 不报 / 合并
downgrade_s1 = round(S1 * 0.10)     # S1 -> S2

after_s1 = S1 - downgrade_s1
after_s2 = S2 - downgrade_s2 + downgrade_s1
after_s3 = S3 - hide_s3 + downgrade_s2
reported = after_s1 + after_s2 + after_s3
severe = after_s1 + after_s2

# 被隐藏的问题不会消失:25% 以逃逸缺陷回来,其中 1/3 变成线上事故
escape = round(hide_s3 * 0.25)
incidents = round(escape / 3)
reopen = round(escape * 0.8)
BASE_ESCAPE, BASE_INCIDENT, BASE_REOPEN = 8, 2, 5

print("=== 缺陷数考核:一个季度的前后对比 ===")
rows = [
    ("缺陷总数", TRUE_TOTAL, reported, "报表口径"),
    ("严重缺陷 (S1+S2)", S1 + S2, severe, "严重度口径"),
    ("逃逸缺陷", BASE_ESCAPE, BASE_ESCAPE + escape, "发布后才发现"),
    ("重开缺陷", BASE_REOPEN, BASE_REOPEN + reopen, "没修干净就关单"),
    ("线上 P2 事故", BASE_INCIDENT, BASE_INCIDENT + incidents, "被藏问题的代价"),
    ("审计抽样真实缺陷", TRUE_TOTAL, TRUE_TOTAL, "独立抽样,不参与考核"),
]
print(pad("指标", 18) + pad("考核前", 8) + pad("考核后", 8) + pad("变化", 8) + "说明")
for name, before, after, note in rows:
    print(pad(name, 18) + pad(before, 8) + pad(after, 8)
          + pad(f"{after/before-1:+.0%}", 8) + note)
print(f"降级 {downgrade_s2} 个 S2、{downgrade_s1} 个 S1;{hide_s3} 个 S3 没进系统;"
      f"其中 {escape} 个以逃逸缺陷回来")

print()
print("=== 部署频率考核:把一次发布拆成三次 ===")
print((pad("指标", 16) + pad("拆分前", 8) + pad("拆分后", 8)).rstrip())
for name, a, b in [("部署次数/周", 12, 34), ("每次部署变更数", "3.0", "1.1"),
                   ("前置时间中位(h)", 22, 9), ("变更失败率", "9%", "9%")]:
    print((pad(name, 16) + pad(a, 8) + pad(b, 8)).rstrip())
print("(吞吐指标全变好,稳定性没变,用户拿到的功能一个没多)")

本机实测输出(Python 3.14.8):

=== 缺陷数考核:一个季度的前后对比 ===
指标              考核前  考核后  变化    说明
缺陷总数          120     106     -12%    报表口径
严重缺陷 (S1+S2)  48      37      -23%    严重度口径
逃逸缺陷          8       12      +50%    发布后才发现
重开缺陷          5       8       +60%    没修干净就关单
线上 P2 事故      2       3       +50%    被藏问题的代价
审计抽样真实缺陷  120     120     +0%     独立抽样,不参与考核
降级 11 个 S2、1 个 S1;14 个 S3 没进系统;其中 4 个以逃逸缺陷回来

=== 部署频率考核:把一次发布拆成三次 ===
指标            拆分前  拆分后
部署次数/周     12      34
每次部署变更数  3.0     1.1
前置时间中位(h) 22      9
变更失败率      9%      9%
(吞吐指标全变好,稳定性没变,用户拿到的功能一个没多)

三句话读完:

  1. 报表口径 −12%、严重度口径 −23%,审计口径 0%。前两个数字都是“定义”的产物:降级 12 个、藏起 14 个,缺陷本身一个没修。
  2. 配对的三个指标同时恶化 50%–60%:逃逸、重开、线上事故。只要看板上有这三个数,造假就藏不住——这是“配对”比“换指标”有效的原因。
  3. 拆发布能把频率从 12 拉到 34(×2.8)、前置时间从 22h 压到 9h,而 CFR 与交付价值完全不变。所以频率必须和“每次部署的变更数”一起看。

防线:五条,按成本从低到高

防线 做法 反例(只做这一条会怎样)
① 用途分层 指标用于团队自省与改进,不用于个人排名、奖金、末位 只喊“不排名”,但季度评优还在看数
② 配对张力 每个速度指标配一个稳定指标,每个数量指标配一个质量指标 配了但权重失衡(速度 90%、质量 10%)
③ 结果 + 过程 结果指标(CFR)解释“发生了什么”,过程指标(评审等待、批量大小)解释“为什么” 只看结果,团队不知道从哪改,只能改数字
④ 独立抽样 随机抽 5%–10% 的工单/缺陷,由不参与考核的人复核 抽样也由被考核团队自己报,等于没抽
⑤ 看趋势与分布 同比/环比、P50/P85,不用绝对值排名 用某个月的绝对值定档,噪声被当成绩

🔑 最有效的一条是 ①:指标与被度量者的利益解耦。同样的四个数,用于复盘会(56.6 无指责复盘)就是诊断工具,用于排行榜就是造假指南。工具没变,用途变了。

边界:什么时候必须用硬指标

不是所有场景都能“只用于学习”:

  • 合规与安全:审计日志完整性、漏洞修复时限,必须硬性达标,做法是门槛 + 例外审批 + 抽样,不是排名。
  • 对外 SLO:错误预算烧完就冻结发布,这是契约,不能“自省”。
  • 小团队(< 10 人):度量成本可能高于收益,直接看板对话、每周复盘更有效;此时上仪表盘主要是给上级看,容易催生第一种套利。

判断标准:指标是否直接决定某个人的得失。决定,就必须配审计与申诉;不决定,才可以只用于改进(改进动作本身见 59.4 效能改进方法)。

反模式

反模式 后果
用单一指标做团队排名 三个月内数字变好、交付没变,之后数据全面失真
“多指标”但都指向同一方向 组合分被整体优化,和单指标一样(如速度 90% + 质量 10%)
指标进个人绩效 协作行为消失:不报 bug、不接脏活、不评审别人的代码
只加指标不减指标 看板 40 个图,没人看;采集成本压垮团队
抽样审计由被考核方自己做 审计变成第二层报表,问题被二次掩盖
用月度绝对值判断改进 需求构成、假期、版本节奏都会让数字波动,把噪声当信号
发现造假就加更严的规则 军备竞赛:规则越细,套利越隐蔽,信任成本越高
因为怕被玩坏就完全不度量 退回拍脑袋(HiPPO),资源分配靠嗓门

关联

自检

Q1 季度报表显示缺陷数下降 12%、严重缺陷下降 23%。为什么不能据此判断质量改善?

▸ 答案:这两个数都可以由“定义”产生:降级(S2→S3)、合并不报、季度末延迟关单。要证明改善,必须看不受报表口径控制的配对指标:逃逸缺陷率、重开率、线上事故数,以及独立抽样的真实缺陷数。本节模型里报表 −12%,配对指标 +50%~+60%,审计口径 0%。

Q2 覆盖率从 60% 提到 85%,为什么可能是假的?

▸ 答案:覆盖率只证明“这些行被执行过”,不证明“断言验对了”。常见玩法是给 getter/setter、异常分支写无断言测试,或者把难测的代码用 // coverage:ignore 排除。配对指标是变异测试得分(故意注入 bug,看测试能否发现)与逃逸缺陷率;只加覆盖率门禁,等于奖励“跑过”。

Q3 古德哈特定律和坎贝尔定律的区别是什么?

▸ 答案:古德哈特讲的是指标作为目标后失去测量效力(指标不再是好指标);坎贝尔讲的是指标用于社会决策后会被腐蚀,并反过来扭曲它所监测的过程(不只是失效,而是主动破坏)。前者是测量问题,后者是治理问题;研发里的对应分别是“数字不再可信”和“为了数字改变行为、伤害系统”。

Q4 团队已经把“速度 + 质量”做成 90/10 的加权分,为什么还是被玩坏?

▸ 答案:加权分仍然是单目标:只要把质量分压到 10%,团队用 5% 的质量损失换 15% 的速度提升,总分就上升。有效做法不是调权重,而是硬约束 + 张力:质量不达门槛(如 CFR > 15%)时速度分不计,或者两个指标分开汇报、分开决策。

Q5 部署频率从 12 次/周涨到 34 次/周,怎么用已有数据判断是不是拆发布?

▸ 答案:看 每次部署的变更数 与 返工率。拆发布时,部署数涨、每次部署变更数从 3.0 掉到 1.1,而 CFR、返工率、交付的功能数不变;真提速时,每次部署变更数大体稳定,前置时间与 CFR 一起改善。若没有变更数,可用“部署 sha 的去重数/部署次数”近似。

Q6 独立抽样审计怎么做才算独立?

▸ 答案:三条:① 抽样随机(按工单号哈希取模,不让人挑);② 复核人不向被考核团队汇报,且不参与其绩效;③ 抽样结果不直接用于奖惩,只用于校准报表口径与发现系统性偏差。样本量按 5%–10% 或每月 ≥ 30 条,低于 30 条只能看个案,不能估比率。

Q7 什么情况下“不度量”反而是对的?

▸ 答案:三个条件同时成立时:团队小(< 10 人)、问题靠对话就能定位、度量成本高于收益。此时上仪表盘往往是为了向上一层汇报,而汇报压力正是套利的起点。替代方案是每周看板复盘 + 每月一次 DORA Quick Check 式自评,够用且便宜。

Q8 发现团队在“优化指标”之后,加更细的规则为什么通常更糟?

▸ 答案:因为规则越细,套利空间越隐蔽(从“降级”变成“提前私下沟通定级”),而信任成本上升、协作行为减少。正确顺序是:先把指标与奖惩解耦(防线①),再补配对指标与抽样(②④),最后才谈口径细化。规则只能约束可观测的行为,约束不了动机。