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%
(吞吐指标全变好,稳定性没变,用户拿到的功能一个没多)
三句话读完:
- 报表口径 −12%、严重度口径 −23%,审计口径 0%。前两个数字都是“定义”的产物:降级 12 个、藏起 14 个,缺陷本身一个没修。
- 配对的三个指标同时恶化 50%–60%:逃逸、重开、线上事故。只要看板上有这三个数,造假就藏不住——这是“配对”比“换指标”有效的原因。
- 拆发布能把频率从 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),资源分配靠嗓门 |
关联
- 前置:59.1 DORA 四指标(指标本身的定义与配对)、53.10 回归测试与缺陷管理(缺陷数为什么不能当 KPI)
- 并列:56.4 告警治理(告警数量也是可被“优化”的指标)
- 下游:59.4 效能改进方法(在防线之内做改进)、56.6 无指责复盘(指标用来自省的正确场合)
自检
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 发现团队在“优化指标”之后,加更细的规则为什么通常更糟?
▸ 答案:因为规则越细,套利空间越隐蔽(从“降级”变成“提前私下沟通定级”),而信任成本上升、协作行为减少。正确顺序是:先把指标与奖惩解耦(防线①),再补配对指标与抽样(②④),最后才谈口径细化。规则只能约束可观测的行为,约束不了动机。