本节摘要:7.1 让置信度变得可信,本节让它变得可用。《Jev 决策编程》9.3 节的命题是:决策门控的阈值不该拍脑袋,该由代价结构决定——本节把这套阈值代价分析推广到一切 LLM 应用的低置信路由场景(转人工、降级、缓存兜底)。核心观察:F1 最优 ≠ 代价最优。当"错放"(低置信却放行,错误直达用户)与"误拦"(高置信却转人工,多花一次人力)代价不对称时——客服场景错放一次可能赔一单,误拦一次只是几毛钱人力——以 F1 为目标的阈值会系统性偏"胆大"。工程做法是
sweep_threshold:对回归集上每条样本的 (置信度, 对错) 记录,扫描全部候选阈值,画出期望代价曲线,取代价最小处的阈值作为生产参数;同时对照 F1 最优点,量化两个最优的差距。阈值选定后还有两件事:敏感性检查(错放代价的估计翻倍,阈值该移动多少——代价本来就是估的)与版本化(阈值写进配置进仓库,改阈值走变更,第 9.1 节)。本节附完整可运行脚本(MOCK 数据,标准库)。
阅读完本节,你应当能够:
低置信路由的混淆矩阵只有两行两列,但两个错误的代价天差地别:
| 放行(自动回答) | 拦截(转人工) | |
|---|---|---|
| 模型其实会做(对) | 自动解决:赚下人力成本 | 误拦:白付一次人工(+用户体验小损) |
| 模型其实不会做(错) | 错放:错误直达用户 | 正确拦截:人工成本照付,但错误被接住 |
记:c_miss = 错放单价(赔偿、客诉、信任损失),c_human = 转人工单价(人力 + 延迟)。总代价:
Cost(t) = c_miss × #{对错=错 且 conf ≥ t} + c_human × #{conf < t}
示例量级(示意,按业务填):客服 c_miss = 10、c_human = 1;法务合同审查 c_miss = 100、c_human = 2;内容摘要 c_miss = 1、c_human = 1。代价表本身就是评测产物——它逼着业务方把"错得起吗"说清楚(观点:很多团队的阈值之争,本质是没人肯写下这张两行的表)。
# sweep_threshold.py(纯标准库,可直接运行) """阈值扫描:代价曲线 + 代价最优 vs F1 最优 + 敏感性检查。""" import random def load_records(n: int = 1000, seed: int = 7) -> list[tuple[float, int]]: """MOCK:双种群生成 (conf, correct)——85% 高置信主群极少出错, 15% 长尾散布在 0.40~0.97 且命中率随 conf 走(示意真实跑分形态)。""" rng = random.Random(seed) out = [] for _ in range(n): if rng.random() < 0.85: # 主群:高置信、极少错 conf = rng.uniform(0.85, 0.99) hit = rng.random() < 0.995 else: # 长尾:置信散、错误密 conf = rng.uniform(0.40, 0.97) hit = rng.random() < max(0.0, (conf - 0.35) * 1.1) out.append((conf, 1 if hit else 0)) return out def sweep(records: list[tuple[float, int]], c_miss: float, c_human: float): """扫描阈值,返回按 t 排序的行:t, 拦截率, 错放数, 总代价, F1。""" n = len(records) rows = [] for step in range(21): # t = 0.00 ~ 1.00 步长 0.05 t = step / 20 auto = [(c, ok) for c, ok in records if c >= t] held = n - len(auto) missed = sum(1 for _, ok in auto if not ok) # 错放 caught = sum(1 for c, ok in records if c < t and not ok) # 正确拦截 cost = c_miss * missed + c_human * held tp = sum(1 for _, ok in auto if ok) prec = tp / len(auto) if auto else 0.0 recl = tp / sum(1 for _, ok in records if ok) f1 = 2 * prec * recl / (prec + recl) if prec + recl else 0.0 rows.append({"t": t, "hold": held / n, "missed": missed, "caught": caught, "cost": cost, "f1": f1}) return rows if __name__ == "__main__": records = load_records() for c_miss in [5, 10, 20]: # 敏感性:错放代价三档 rows = sweep(records, c_miss=c_miss, c_human=1) best_cost = min(rows, key=lambda r: r["cost"]) best_f1 = max(rows, key=lambda r: r["f1"]) print(f"c_miss={c_miss:>2} | 代价最优 t={best_cost['t']:.2f}" f" 代价={best_cost['cost']:.0f} 拦截率={best_cost['hold']:.0%}" f" 错放={best_cost['missed']}" f" || F1 最优 t={best_f1['t']:.2f} 其代价={best_f1['cost']:.0f}") print("\n阈值表节选(c_miss=10):") rows = sweep(records, c_miss=10, c_human=1) for r in rows[::4]: print(f" t={r['t']:.2f} 拦截率={r['hold']:.0%} 错放={r['missed']:>3}" f" 接住={r['caught']:>3} 代价={r['cost']:>6.0f} F1={r['f1']:.2f}")
运行输出(实测,本机 Python 3.12):c_miss=10 时代价最优 t*=0.85(拦截率 12%、错放 14 条、总代价 262);F1 最优点在 t=0.70(F1=0.97 更高),但代价 378——照 F1 选阈值要多付约 44% 的代价。三档 c_miss(5/10/20)下 t* 都稳在 0.85、代价 192/262/402:错误集中在 0.85 以下的"悬崖"结构让阈值对代价估计不敏感——按 7.2 节敏感性口径属"稳健"档(不同数据的结论会不同:错误若平铺在高置信区,c_miss 翻倍会把 t* 明显推高)。
t* → 写进配置(如 confidence_gate: 0.65),标注来源(哪次跑分、哪个数据版本,第 3.4 节);t ∈ [t*−0.1, t*+0.1] 的代价带——如果曲线在 t* 附近很平,说明阈值不敏感、运维压力小;如果很尖,说明代价假设稍有偏差最优解就跑远,要回头把代价表估准;c_miss 高、闲聊类低)各配各的 t*,与第 4.4 节分级路由、第 4.2 节置信路由三岗位衔接。⚠️ 两个前置条件:(1) 置信度必须先过 7.1 的校准关(ECE > 0.15 的信号扫出来的阈值是空中楼阁);(2) 代价表里的单价是估计值——
c_miss = 10意为"错放一次约等于十次转人工",量级对了就够用,敏感性检查就是为量级误差准备的保险。
脚本里 c_miss ∈ {5, 10, 20} 的三档就是一次敏感性检查。判读(示意口径):
| 现象 | 判读 | 动作 |
|---|---|---|
三档 t* 变动 ≤ 0.05 |
阈值稳健 | 直接上线 |
t* 随档位线性平移但代价曲线平缓 |
可控 | 按保守档(高 c_miss)取值 |
t* 大幅跳动或曲线尖峰 |
代价假设主导结论 | 先做几周线上小流量实测错放代价,再定阈值 |
阈值会随三件事过期,对应三个重算触发器:代价变化(客服扩编让 c_human 下降;政策变化让 c_miss 上升)、模型变化(换模型快照后 conf 分布变了,7.1 要重测校准、7.2 重扫阈值)、分布漂移(PSI 报警触发后,第 7.3 节——线上 conf 分布变了,同一阈值的拦截率会漂)。重算出来的新阈值走变更流程进配置仓库,老阈值留档可回滚(第 8.3 节)。
c_miss 三档扫描,阈值稳健才上线;尖峰曲线说明先修代价估计。阈值把"模型多自信才敢放行"变成了配置项。但它建立在"分布不变"的假设上——而第 1.2 节说过,分布一定会变。下一节实现 PSI,让漂移在业务指标恶化之前现形。