离线 0.89 线上 0.83:评测集漂移与分桶评级

举报
上云 发表于 2026/11/05 09:28:48 2026/11/05
【摘要】 离线评测高分上线却翻车,根因常是评测集样本来源偏离真实流量。文章拆解便利样本、静态快照、长尾坍缩三类偏差,给出流量画像与 PSI 漂移度量、按流量分层的采样配额、桶级门槛与置信区间判定,以及抽样集与回归集分离的线上回流闭环。

离线评测集跑到 0.89,上线一周客诉却集中在多轮追问和工具失败重试上。这类事故里,模型权重没有改动、prompt 没有修改,发生变化的却是评测样本的来源——评测集的分布一旦和真实流量分叉,离线分数就不再是质量指标,只不过是在说明“我们在哪些题上练过”。更麻烦的是,这种分叉通常不会自行暴露,分数仍然很高,直到用户开始抱怨。

一、离线分数失灵的三种形态

先分清原因,再讨论修改。评测集偏离真实流量,常见的有三类,且能同时出现。

便利样本偏差。 评测集往往来自内测用户、demo 脚本和标注同学自造的 query。这类样本天然偏向结构清晰、单轮、意图明确、措辞规范的请求。真实流量是另一回事:错别字、半句话、语音转写残留、一次贴三段材料、在第三轮才说出真实目的。

静态快照腐化。 评测集冻结在某个时间点,业务侧却一直在变化。新接入一个租户、新开一个小语种入口、把入口从 App 首页挪到搜索框,流量构成就变了,评测集却完全没有同步。

长尾坍缩。 为了让“覆盖全面”,构造时给每个类别都塞 20 条,结果把线上占比 1% 的长尾提到了 8%;或者反过来,长尾只有 3 条样本,桶内分数 0.33 和 0.67 在统计上根本没法区分。

还有一个容易被忽视的变体:把线上 badcase 全部追加进评测集。半年后评测集等价于历史事故合集,分数变成了“不重复犯同一类错”的比率,反而对新流量的代表性更差。抽样集和回归集必须分开管理,这一点在第五节展开。

把偏差量化出来看,问题的形状就很清楚。假设某问答应用有五个主要场景桶:

桶 线上流量占比 旧评测集占比 代表偏差 桶内分 拟设门槛
简单 FAQ / 单轮 15% 50% 3.3× 过采样 0.97 ≥0.95
多轮任务 30% 12% 0.4× 欠采样 0.83 ≥0.85
检索增强问答 28% 23% 0.8× 0.90 ≥0.88
长尾语言 / 方言 12% 5% 0.4× 0.70 ≥0.70
工具调用与失败恢复 15% 10% 0.7× 0.65 ≥0.75

按旧评测集自身分布加权,总分约 0.89;按线上流量分布重加权,总分约 0.83。如果放行线定在 0.85,同一批模型权重在两种算法下会得到完全相反的结论。结论本身没有错,错的是加权用的分布。

二、把流量画像量出来,漂移才有数字

重加权的前提是知道线上分布。这一步不需要复杂工具,需要的是把维度定对并固定下来。

必采维度:入口渠道、租户或用户分层、意图分类(哪怕先用规则或小模型打粗标)、query 字符长度分位、语言与方言标识、单轮还是多轮及轮数分布、是否携带检索上下文、是否触发工具调用、是否发生超时或错误码、请求时段。

漂移指标:类别型维度用 PSI,数值型先分箱再当类别型算。PSI 的实现很轻:

import math
from collections import Counter

def psi_categorical(base: Counter, cur: Counter, eps=1e-6) -> float:
    keys = set(base) | set(cur)
    nb, nc = sum(base.values()), sum(cur.values())
    total = 0.0
    for k in keys:
        b = base.get(k, 0) / nb + eps
        c = cur.get(k, 0) / nc + eps
        total += (c - b) * math.log(c / b)
    return total

经验阈值:PSI < 0.1 视为稳定;0.1~0.25 进入观察,评测集需要重加权;> 0.25 视为显著漂移,应触发重建。这不是绝对标准,但比“凭感觉觉得流量变了”要可靠得多。

两个坑要提前避开。

其一,PSI 对低占比层不敏感。一个占比从 0.5% 涨到 2% 的层,PSI 贡献很小,但它可能正是客诉集中区。对长尾层要单独看绝对样本数和层内错误率,不要只看总 PSI。

其二,日志采样器本身会丢长尾。网关按 1% 均匀采样,长尾层本就少,采完一周只剩几十条,画像直接失真。正确做法是分层采样:头部层低采样率、长尾层全量或高采样率,落盘后按采样率倒数加权还原。

三、评测集构造:按流量分层,而不是按方便程度

有了画像,评测集的构造规则就可以从“尽量覆盖”换成“按配额分配”。

分层配额 ∝ 线上流量占比,同时设最小样本数。 每层配额取 max(n_min, round(share * n_total)),n_min 建议按二项分布的置信区间反推——要在 95% 置信下分辨 5 个点的差异,单层样本量大致需要 100 以上。层内样本不足时过采样,并在打分时做权重校正:w_i = p_traffic(layer) / p_eval(layer)。这样既保证了长尾的代表性,又不让长尾在总分里被放大。

样本要带完整上下文。 只存“最后一问”的评测样本,无法复现线上失败。多轮对话要保留历史轮次,检索增强要保留当次召回文档与排序,工具调用要保留入参、返回结构和错误码。缺了这些,模型在评测里答对,上线答错,你连归因的入口都没有。

冷启动不要用验收用例充数。 新功能没有真实流量时,先跑一到两周影子流量或内部灰度,采到分布再建集。产品经理的 20 条验收用例是需求验证材料,不是评测集,它的分布是“我们认为用户会怎么问”,而不是“用户实际怎么问”。

版本化。 每次发布评测集都记录:版本号、流量快照日期区间、各层配额与实际样本数、已知缺口(例如“方言层仅覆盖粤语与川渝,缺闽南语”)。没有这些元信息,三个月后没人能解释某次分数跳变是模型变了还是评测集换了。

脱敏先行。 回流样本进集前完成 PII 替换,映射表放受控环境,设定留存期限。这一步放在管道最前端,不要等到入集后再补。

四、分桶评级:让总分可归因

单一加权总分的问题是会被高占比、易分的桶主导。头部桶掉 1 个点、长尾桶掉 10 个点,总分可能只动 0.3——而客诉恰恰来自长尾。所以要把“能不能上”从单点分数改成一组约束。

定桶。 按业务关键度 × 流量规模切,一般 5~8 个桶足够。核心交易意图、多轮任务、检索问答、长尾语言、工具调用与失败恢复,是很常见的一组。

每桶独立门槛,核心桶一票否决。 核心桶不允许回退,非核心桶允许在置信区间内波动。加权总分降级为参考指标,用于横向对比版本。

每桶报 bootstrap 置信区间,避免 40 条样本的桶因为两个样本波动就被判死:

import random

def weighted_score(rows, field="score", w="share"):
    tw = sum(r[w] for r in rows)
    return sum(r[w] * r[field] for r in rows) / tw

def bootstrap_ci(rows, fn, n=2000, alpha=0.05, seed=17):
    rng = random.Random(seed)
    stats = sorted(fn([rng.choice(rows) for _ in rows]) for _ in range(n))
    lo = stats[int(n * alpha / 2)]
    hi = stats[int(n * (1 - alpha / 2)) - 1]
    return fn(rows), lo, hi

判定规则可以写成三条:核心桶点估计不低于门槛且 CI 下界不低于门槛减 2 个点;非核心桶 CI 下界不低于门槛;加权总分不低于上一基线。满足则放行,核心桶不满足则阻塞,其余情况进灰度并挂观察项。

这样输出的不是“0.83 分”,而是“多轮任务与工具恢复两个桶阻塞,长尾桶处于门槛边缘,头部桶通过”。后者才能直接决定发不发、发多少流量、修哪里。

五、线上回流闭环:样本要持续更新而非一次建好

评测集是一次性资产的想法必须放弃。需要一条常态化的回流管道:

线上日志 → 脱敏 → 分层采样(按配额) → 标注 → 候选池 → 评审 → 评测集新版本
                    └→ badcase 单独进回归集(不混入抽样集)

关键设计点有四个。

抽样集与回归集分离。 抽样集按流量配额采,代表分布,用于评级;回归集收历史 badcase,用于防复发。两者混在一起,抽样集会被逐渐污染成事故合集,评级随之失真。

影子评测。 新版本发布前,对当日抽样流量离线跑一遍,得到“当日分布下的分数”,而不是“上周建好的固定集上的分数”。这一步成本很低,但对分布漂移的抵抗力最强。

监控三件事:分布漂移(各维度 PSI,按周计算)、桶级分数趋势(7 日滑动窗口)、样本覆盖率(评测集各层样本量 / 线上各层流量,低于阈值说明该层缺乏覆盖)。

告警必须挂动作。 PSI 超 0.25 → 自动触发重加权并生成新配额建议;某桶分数连降 3 天 → 触发该桶人工抽检 50 条;覆盖率低于下限 → 提升该层采样率。只发通知不挂动作的告警,两周后就会被静音。

落地建议

  1. 本周做一次偏差对照:从线上日志按 5 个维度出流量画像,与现有评测集逐层算占比和 PSI,产出一张偏差表。这张表通常就能解释最近一次“离线好、线上差”。
  2. 把放行标准从单点分数改成桶级约束:定义 5~8 个桶,每桶设门槛与最小样本数,核心桶一票否决,加权总分只作参考。
  3. 建分层采样回流任务:按配额每周入集,抽样集与回归集分库存储,评测集带版本号、流量快照日期与已知缺口。
  4. 上新前跑影子评测:用当日抽样流量出报告,与固定集结果并列对比,两者差异超过 3 个点时先查分布再查模型。
  5. 给漂移和桶级回退配动作:每条告警写明触发后的处置(重加权 / 重建 / 人工抽检),并在值班手册里固化。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。