本节摘要:第 4.2 节给了口径(ECE = Σ(桶样本占比 × |预测均值 − 实际命中率|),<0.05 优 / >0.15 慎用),《Jev 决策编程》9.2 节给了决策场景的度量脚本;本节把它扩展成通用置信度实验室:任何 LLM 应用,只要能产出 (置信度, 是否命中) 对,就能套进同一套流程——三步走:可靠性曲线(分桶对比预测与命中,文本直方图直接可读)、ECE 判定(按全书统一口径)、线性缩放修复(网格搜索斜率与截距,把过自信/欠自信的置信度拉回对角线)。脚本内置 MOCK 模式:用确定性数据生成器模拟第 4.2 节的三个置信度来源(logprobs 聚合、裁判映射、自报置信),无需任何 API Key 即可完整跑通;接入真实数据时只替换
load_pairs一个函数。示例结论(MOCK,示意):自报置信系统性过自信约 0.15~0.25(与研究共识一致),修复后 ECE 可显著下降——但修复必须在留出数据上复验,防止把校准"调"成过拟合。
阅读完本节,你应当能够:
《Jev 决策编程》9.2 节的脚本度量决策系统的 confidence;本节的推广只有一个约定:把一切置信度统一为 (conf ∈ [0,1], hit ∈ {0,1}) 对:
| 来源 | 怎么变成 (conf, hit) | 典型病(研究/社区共识) |
|---|---|---|
| logprobs 聚合 | 输出 token 概率聚合为分数;hit 由回归集金标准判 | 常见欠自信(低桶命中率反高于自报) |
| 裁判映射 | 裁判分/投票比例线性映射到 [0,1] | 继承裁判偏见(第 5 章),档位挤压在中段 |
| 自报置信 | 问模型"你有多确定" | 普遍过自信 0.1~0.3(研究共识,示意量级) |
MOCK 模式按这三种病理各生成一份 200 条的模拟数据;真实接入时替换 load_pairs(读第 6 章工具或自家日志导出的跑分记录)。
# reliability_lab.py(纯标准库,可直接运行) """置信度实验室:可靠性曲线 + ECE + 线性缩放修复。 MOCK 模式无 API 可跑;真实数据只替换 load_pairs。""" import random def load_pairs(source: str, n: int = 200, seed: int = 7) -> list[tuple[float, int]]: """MOCK:按三种来源的典型病理生成 (conf, hit)。""" rng = random.Random(seed) def one(c: float, hit_rate: float) -> tuple[float, int]: return (c, 1 if rng.random() < hit_rate else 0) pairs = [] for _ in range(n): c = rng.uniform(0.3, 0.99) if source == "logprobs": # 欠自信:实际命中比自报高 0.1 pairs.append(one(c, min(1.0, c + 0.1))) elif source == "judge": # 中段挤压 + 轻度过自信 0.05 cc = 0.5 + (c - 0.5) * 0.7 pairs.append(one(cc, min(1.0, cc - 0.05))) else: # verbalized:过自信 0.2 pairs.append(one(c, max(0.0, c - 0.2))) return pairs def reliability(pairs: list[tuple[float, int]], bins: int = 10): """返回每桶 (区间, n, 预测均值, 实际命中率)。ECE 与 4.2 节同式。""" n, rows, ece = len(pairs), [], 0.0 for b in range(bins): lo, hi = b / bins, (b + 1) / bins in_bin = [(c, h) for c, h in pairs if (lo < c <= hi) or (b == 0 and c == 0)] if not in_bin: continue conf = sum(c for c, _ in in_bin) / len(in_bin) acc = sum(h for _, h in in_bin) / len(in_bin) ece += len(in_bin) / n * abs(conf - acc) rows.append((lo, hi, len(in_bin), conf, acc)) return rows, ece def plot_text(rows: list[tuple[float, float, int, float, float]]) -> None: """文本可靠性曲线:# 为预测均值,= 为实际命中率(每格 0.05)。""" print(" 桶 n 预测 实际 曲线(#=预测 /=命中)") for lo, hi, n, conf, acc in rows: bar_c = "#" * round(conf * 20) bar_a = "/" * round(acc * 20) print(f"{lo:.1f}-{hi:.1f} {n:>4} {conf:.2f} {acc:.2f} {bar_c}") print(f"{'':>32}{bar_a}") def linear_fix(pairs: list[tuple[float, int]]) -> tuple[float, float, float]: """网格搜索 cal = clip(slope*conf + intercept, 0, 1) 使 ECE 最小。 返回 (slope, intercept, 修复后 ECE)。""" best = (1.0, 0.0, reliability(pairs)[1]) for slope in [x / 20 for x in range(10, 41)]: # 0.5 ~ 2.0 for intercept in [x / 100 for x in range(-35, 36)]:# -0.35 ~ +0.35 fixed = [(min(1.0, max(0.0, slope * c + intercept)), h) for c, h in pairs] e = reliability(fixed)[1] if e < best[2]: best = (slope, intercept, e) return best if __name__ == "__main__": for source in ["logprobs", "judge", "verbalized"]: train = load_pairs(source, n=1000, seed=7) heldout = load_pairs(source, n=1000, seed=99) # 留出集:验收用 rows, ece = reliability(train) s, i, e_after = linear_fix(train) fixed_heldout = [(min(1.0, max(0.0, s * c + i)), h) for c, h in heldout] e_held_before = reliability(heldout)[1] e_held = reliability(fixed_heldout)[1] print(f"== {source} == 训练集 ECE={ece:.3f} -> 修复后 {e_after:.3f}" f"(slope={s:.2f}, intercept={i:+.2f})" f";留出集 {e_held_before:.3f} -> 修复后 {e_held:.3f}") if source == "verbalized": plot_text(rows)
运行输出(实测,本机 Python 3.12):logprobs(欠自信)训练集 0.096 → 0.012,留出集 0.103 → 0.041——网格搜出 slope≈0.95、intercept≈+0.12,与"+0.1 平移"的生成病理几乎一致;verbalized(过自信)0.197(>0.15,慎用档)→ 留出集 0.042,全线压低约 0.13;judge 最有意思——留出集 0.046 本就在可用档,修复后 0.051 反而略差:本不病,修复无益(网格搜索在分桶噪声上"赚"到的训练分提升,到留出集就吐了回去)。是否启用修复参数,由留出集 ECE 说了算。
⚠️ 修复只允许在训练分片上搜索参数,在留出分片上验收——在全部数据上调缩放参数再报全量 ECE,是把校准"调"成过拟合(第 9.3 节反模式在此已有苗头)。
替换点只有一处:
# load_pairs_real.py(写法示意,以官方文档为准) import json def load_pairs_real(path: str, conf_field: str = "conf", hit_field: str = "hit") -> list[tuple[float, int]]: """从跑分记录 JSONL 读 (conf, hit):每行一条回归集样本的判决。""" pairs = [] for line in open(path, encoding="utf-8"): r = json.loads(line) pairs.append((float(r[conf_field]), int(r[hit_field]))) return pairs
数据从哪来:Promptfoo 跑分结果导出(6.1)、Langfuse 裁判评分器写回的 trace 分数(6.2)、或自家回归脚本的落盘日志。每次换 prompt 或模型快照,ECE 重测一遍——回归集现成,成本是几分钟(第 4.2 节纪律在此落地为脚本)。
load_pairs。ECE 达标只说明"置信度可信";可信之后下一步是"用它做决策"——低于多少转人工?阈值不该拍脑袋,该算一笔代价账。下一节扫出期望代价曲线。