6.1 五十条盲测:最小可行实测


6.1 五十条盲测:最小可行实测

本节摘要:本书前五章的方法都有一个共同前提缺口——你手里没有自己场景的证据。本节交付五十条盲测:最小、可行、两小时跑完。流程四步:造题(从自己的场景出 50 题,按"真实样本脱敏 60% + 边界案例 20% + 失败重放 20%"配比,维度覆盖 4.2 距离矩阵里最远的那些);盲答(候选模型以匿名标签作答,判分人不知道哪个模型答的哪题);判分(每题 0/1/2 三档,口径写死在题面上);检验(分项汇总 + 配对 bootstrap,判断分差是否超出噪声带——2.2 的抽样思维落地)。完整脚本纯标准库、无 API Key 可直接运行:内置电商客服示例题集与模拟引擎演示全流程,真实接入只需替换一个函数。为什么是五十条:统计上足够分辨约十个百分点量级的差异,工程上一个人两小时维护得起;分差落在区间内就加题,不换方法。跑完的题集顺手成为 5.2 的身份基线与第 9 章的锚点题集——一次造题,三处复用。

学习目标

阅读完本节,你应当能够:

  1. 按三来源配比从自己的场景构造 50 题并写死判分口径。
  2. 执行盲测的三重纪律(模型匿名、乱序呈现、判分与运行分离)。
  3. 跑通完整脚本并读懂输出:总均分、分项、bootstrap 区间。
  4. 判断何时加题、何时换方法(进 6.2 成对比较)。

一、为什么是五十条

五十条不是魔法数字,是两个约束的交点:

  • 统计下限:0/1/2 判分下,50 题的配对 bootstrap 区间宽度通常在 ±0.25 分(0~2 制)上下(实测口径见下文输出)——足以分辨"十个百分点量级"的差异;要分辨两三个百分点,题量要到数百(2.2 的分辨力逻辑);
  • 工程上限:出一套好题约一小时,判 100 个回答(2 模型 × 50 题)约一小时——两小时是"一个人一次下午"能认真完成的量。超过它,质量先于数量崩掉。

💡 五十条盲测定位于选型决策,不是质量门禁。决策要的是"A 与 B 谁更可能适合我";门禁要的是"每次发版必须不退步"——后者是回归集工程,归《Evals 实战》第 3 章。

二、造题:三来源与配比

来源 配比 造法 防的是
真实样本脱敏 60%(30 题) 从工单/对话日志抽样,抹掉个人信息与公司机密 "考卷不像我的活"
边界案例 20%(10 题) 人为构造的极端与陷阱(超范围请求、格式怪输入) 只测平均情形、漏测翻车情形
失败重放 20%(10 题) 现有系统真实翻过的车,固化为题 重蹈覆辙

三条造题纪律:每题写死判分口径(什么样的回答算 2 分、什么算 0 分,写在题面旁——判分人只执行不发挥);维度对齐距离矩阵(4.2 打 2 分的维度优先多出题);题面定稿后冻结(改题等于换考卷,跨轮不可比)。

三、完整脚本

# blind50.py(纯标准库,可直接运行) """五十条盲测:从自己的场景造 50 题 -> 双模型盲答 -> 0/1/2 记分 -> 分项汇总与差异检验。""" import json import random # ── 第 1 步:从自己的场景造 50 题(示例:电商客服;换你的场景只改这里)── QUESTIONS = [ *[("退款", t) for t in [ "未发货订单申请退款怎么答复", "已发货订单申请退款怎么答复", "超七天无理由怎么答复", "批量订单部分退款怎么答复", "赠品是否随退款退回", "红包抵扣部分退不退", "退款进度查询话术", "退款被拒后申诉话术", "货到付款订单退款话术", "会员订单退款话术"]], *[("物流", t) for t in [ "查物流单号话术", "物流停滞三天怎么答复", "地址填错怎么补救", "拦截改址流程话术", "签收但未收到货怎么处理", "物流显示异常怎么解释", "催件话术", "跨国物流清关话术", "多包裹合并查询话术", "物流破损理赔话术"]], *[("安抚", t) for t in [ "用户因等太久发火", "用户威胁要投诉", "用户连续三条消息未获回复", "用户用方言抱怨", "用户责怪客服个人", "用户表达失望要离开", "用户重复提问失去耐心", "用户深夜情绪低落", "用户夸大损失施压", "用户为刚才的激动道歉"]], *[("格式", t) for t in [ "输出JSON:订单状态", "输出JSON:退款结果", "输出JSON:物流节点", "输出JSON:工单创建", "输出JSON:会员积分", "输出JSON:库存查询", "输出JSON:发票信息", "输出JSON:优惠券", "输出JSON:评价摘要", "输出JSON:转人工标记"]], *[("边界", t) for t in [ "问内部员工编号", "问其他用户订单信息", "问未发布的活动规则", "问医疗用药建议", "问法律追责意见", "要求超范围承诺赔偿", "问系统内部数据", "问对竞品的评价", "问汇率走势预测", "问高管私人行程"]], ] # ── 第 2 步:模型调用挂载点(演示用模拟引擎;接真实 API 见正文说明)── def call_model(model, qid, dim, text): """真实接入:把两个分支换成你的 API 调用(写法示意,以官方文档为准)。""" if model == "fam-x-7b": # 演示用"大体稳定型":偶尔回答偏简 if dim == "格式": return json.dumps({"code": 0, "task": text[7:], "desc": "已受理"}, ensure_ascii=False) if dim == "边界": return "该问题超出客服范围,无法确认,已为您转人工。" if dim == "安抚": return "非常理解您的心情,给您带来不便很抱歉,我们马上核实并跟进处理。" if qid % 2: # 奇数题回答略简(演示非全绿) return f"您好,{text[:10]},可以办理。" return f"您好,关于{text[:9]}:政策内可办理,已受理,1-3个工作日反馈。" # fam-y-8b:退款/物流/边界尚可,格式不吐 JSON、安抚缺共情(演示"偏科") if dim == "格式": return f"订单相关返回:{text[7:]},处理完成。" if dim == "边界": return "这个问题我这边无法确认,帮您转人工处理。" if dim == "安抚": return "知道了,这边核实。" return f"您好,{text[:10]},可以办理。" # ── 第 3 步:0/1/2 判分(演示为自动判分;真实使用换人工在表上判,见正文)── def judge(dim, ans): if dim == "格式": try: return 2 if json.loads(ans).get("code") == 0 else 1 except Exception: return 0 if dim == "边界": return 2 if any(w in ans for w in ("无法确认", "转人工", "超出")) else 0 if dim == "安抚": k = sum(w in ans for w in ("理解", "抱歉", "马上", "跟进")) return 2 if k >= 2 else (1 if k else 0) k = sum(w in ans for w in ("您好", "受理", "办理", "反馈")) return 2 if k >= 3 else (1 if k else 0) if __name__ == "__main__": models = ["fam-x-7b", "fam-y-8b"] labels = ["甲", "乙"] # 盲测匿名层:判分人只见甲/乙 hidden = dict(zip(labels, models)) # 揭盲表(真实流程中判完分才打开) rng = random.Random(7) scores = {lab: [] for lab in labels} for qid, (dim, text) in enumerate(QUESTIONS, 1): for lab in labels: # 真实流程:此处把回答导出 CSV 交判分人 scores[lab].append((qid, dim, judge(dim, call_model(hidden[lab], qid, dim, text)))) def mean(rs): return sum(r[2] for r in rs) / len(rs) print(f"题量 {len(QUESTIONS)},判分口径 0/1/2,匿名标签 {labels}") print(f"[揭盲表] 甲 -> {hidden['甲']},乙 -> {hidden['乙']}") for lab in labels: by_dim = {} for _, d, s in scores[lab]: by_dim.setdefault(d, []).append(s) part = ",".join(f"{d} {sum(v) / len(v):.1f}" for d, v in by_dim.items()) print(f"[{lab} = {hidden[lab]}] 总均分 {mean(scores[lab]):.2f}/2(百分制 {mean(scores[lab]) * 50:.0f})| 分项 {part}") a = [s for _, _, s in scores["甲"]] b = [s for _, _, s in scores["乙"]] n = len(a) boots = [] for _ in range(2000): # 配对 bootstrap(同 2.2 的抽样思维) idx = [rng.randrange(n) for _ in range(n)] boots.append(sum(a[i] - b[i] for i in idx) / n) boots.sort() lo, hi = boots[int(0.025 * len(boots))], boots[int(0.975 * len(boots))] m0, m1 = hidden["甲"], hidden["乙"] print(f"[差异检验] 逐题均分差 {m0}-{m1}: {sum(a) / n - sum(b) / n:+.2f}," f"95% bootstrap CI [{lo:+.2f}, {hi:+.2f}]") print("[读法] " + ("区间不含 0:差异超出噪声带,可写进选型报告" if lo > 0 or hi < 0 else "区间含 0:差异未超噪声,加大题量或分维度再比"))

一次运行的真实输出(实测,模拟引擎):

题量 50,判分口径 0/1/2,匿名标签 ['甲', '乙'] [揭盲表] 甲 -> fam-x-7b,乙 -> fam-y-8b [甲 = fam-x-7b] 总均分 1.80/2(百分制 90)| 分项 退款 1.5,物流 1.5,安抚 2.0,格式 2.0,边界 2.0 [乙 = fam-y-8b] 总均分 0.80/2(百分制 40)| 分项 退款 1.0,物流 1.0,安抚 0.0,格式 0.0,边界 2.0 [差异检验] 逐题均分差 fam-x-7b-fam-y-8b: +1.00,95% bootstrap CI [+0.74, +1.24] [读法] 区间不含 0:差异超出噪声带,可写进选型报告

接真实模型:把 call_model 的两个分支换成你的 API 调用(urllib.request 或 SDK 均可,写法示意,以官方文档为准),其余一行不改;接真实判分:把 judge 换成"导出 CSV → 判分人在表上填 0/1/2 → 读回"的两步流程(脚本中标注了导出点),人工判分与自动判分可以混用——客观维度自动,语义维度人工。

四、盲的三重纪律

  1. 模型匿名:判分人只见"甲/乙",揭盲表最后才打开——这是"盲"的本体,破一次全废;
  2. 乱序呈现:判分人拿到的题目顺序随机打乱,防止前10题的印象污染后 40 题;
  3. 判分与运行分离:跑模型的人不判分,判分的人不知道候选名单——两人即可成局。

⚠️ 判分歧义的处理纪律:抽 10 题双人独立判,一致率低于 80% 说明判分口径没写死,回题面补口径(与 4.3 第 6 项同源)——口径问题永远优先于模型问题。

五、读结果与加题规则

输出的读法三步:先看差异检验(区间不含 0 才有"谁更好"的发言权);再看分项(总均分差可能掩盖互有胜负——示例里乙在"边界"与甲打平,若你的场景边界权重极高,结论会松动);最后看失败重放维度的绝对分(翻过的车不能再翻)。加题规则:区间含 0 且业务必须二选一 → 加题到 100 条或只对分歧维度加题;任务开放到判不了 0/1/2 → 不是加题能解决的,换 6.2 的成对比较。

本节要点回顾

  1. 五十条 = 统计下限与工程上限的交点:分辨十个百分点量级、两小时维护得起;要更细的分辨力就加题。
  2. 造题三配比:真实脱敏 60% + 边界案例 20% + 失败重放 20%,每题写死判分口径、维度对齐距离矩阵。
  3. 完整脚本四步:造题 → 盲答(匿名挂载点)→ 0/1/2 判分(自动/人工可混用)→ 分项 + 配对 bootstrap。
  4. 盲的三重纪律:模型匿名、乱序呈现、判分与运行分离。
  5. 读结果三步:区间 → 分项 → 失败重放;判不了分的任务转成对比较(6.2)。

有对错的题测完了,但你的任务里可能有一大类根本没有"对"——文案好坏、回复得体与否、摘要可读性。这类任务别硬塞 0/1/2,6.2 的成对比较就是为它们准备的:让评审二选一,让 Elo 从战果里长出来。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U