大模型评测集构建的工程实践:从业务目标拆解、样本分层抽样到高一致性人工标注

很多团队一上来就想做模型对比,结果跑完一轮发现结论不稳。问题往往不在模型,而在评测集本身。样本混杂、标注口径漂移、线上目标和离线指标对不上,最后只能得到一份“看起来很多,实际不好用”的评测数据。

我这半年在做几个 LLM 应用项目时,最常见的坑就是这个。说实话,模型换了三版,Prompt 改了五轮,最后把问题追到评测集,才发现离线分高的方案,线上工单转人工率反而更高。根因很直接:评测集没有按业务目标拆开,困难样本占比也失真。

这篇文章我把一套可复现方案整理出来,重点放在工程细节:

  • 怎么把业务目标拆成可执行的评测维度
  • 怎么做分层抽样,避免数据“看着全,实际偏”
  • 怎么把人工标注一致性拉到可用水平
  • 怎么沉淀成能持续迭代的评测集版本体系

文章里的代码示例都用 Python,存储层默认是 CSV/Parquet,方便直接复用。


一、先别急着抽样,先把业务目标拆开

很多评测集失败,第一步就错了:直接从日志里抽几千条,开始标。这样做很快,但很难回答“这个模型到底对业务有没有帮助”。

我一般会先做一张目标拆解表。不要复杂。够用就行。

以智能客服问答为例,业务真正关心的通常不是“回答是否流畅”,而是下面几类问题:

业务目标 对应评测问题 样本来源 指标类型
降低转人工率 是否能直接解决用户问题 历史会话、转人工工单 任务成功率
降低错误回答风险 是否出现事实性错误或越权答复 风险工单、投诉样本 风险错误率
提升响应体验 输出是否简洁、可执行、格式稳定 高频问答、标准流程问题 响应质量分
提升召回效果 检索上下文是否覆盖答案依据 RAG 日志、知识库命中记录 证据覆盖率

这里有个经验:业务目标和模型能力不要混写。比如“推理能力强”这种说法在评测集设计里很难落地,但“多条件退款规则判断是否正确”就很清楚。

短句很关键。

我通常会把业务目标再拆一层,落到“可标注单元”。像“客服问答质量”这种概念太大,标注员很难统一。改成下面这种粒度,口径会稳定很多:

  • 是否回答了用户主问题
  • 是否引用了错误规则
  • 是否遗漏关键限制条件
  • 是否输出了不可执行建议
  • 是否暴露不该说的信息

到了这一步,评测维度才算能进入数据构建阶段。


二、定义评测单元:一条样本到底是什么

很多团队默认“一轮用户输入 + 模型回答”就是一条样本,这在单轮问答里没问题,但放到真实业务里经常不够。

我建议先固定评测单元,不然后面抽样、标注、统计都会乱。

常见有四种:

评测单元 适用场景 优点 风险
单轮问答 FAQ、简单检索问答 标注快 丢上下文
多轮会话片段 客服、Copilot 助手 接近真实使用 标注成本高
问题 + 检索上下文 + 回答 RAG 场景 能分析召回与生成 数据组织更复杂
指令 + 工具轨迹 + 最终结果 Agent 场景 能看过程错误 标注规则难统一

这篇文章讨论的是通用 LLM 应用,所以我更推荐下面这个结构:

{
  "sample_id": "cs_000001",
  "biz_scene": "refund_consult",
  "user_query": "商品拆封后还能退吗?",
  "dialog_context": [
    {"role": "user", "content": "我上周买的耳机到了"},
    {"role": "assistant", "content": "请问有什么问题?"}
  ],
  "retrieved_context": [
    "规则1:非质量问题,拆封后不支持7天无理由退货",
    "规则2:质量问题需提供检测依据"
  ],
  "reference_answer": "若为非质量问题,商品拆封后通常不支持7天无理由退货;若存在质量问题,可按售后流程申请处理。",
  "meta": {
    "source": "online_log",
    "date": "2026-03-21",
    "difficulty": "medium",
    "risk_level": "high"
  }
}

字段不要贪多。够分析就行。

我踩过一个坑:早期把 20 多个字段全塞进去,标注平台看起来很全,实际标注员根本抓不住重点,最后一致性反而下降。后面我只保留任务判断必需的字段,IAA 直接从 0.62 提到 0.79。


三、样本池怎么来:别只从“正常日志”里抽

评测集如果只来自历史线上正常流量,通常会有两个问题:

一是样本过于“平均”,高频简单问题太多,模型差异被稀释。

二是风险样本太少。上线后最容易出问题的,往往恰好是日志里占比不高的边缘 case。

所以我会先建一个候选样本池,来源尽量分散,但字段结构统一。常见来源如下:

来源 适合补充的样本 注意点
线上日志 高频真实问题 去重、脱敏、时间切片
转人工记录 模型失败样本 往往带强噪声
投诉/质检数据 高风险错误样本 标注标准要更严格
知识库变更记录 新规则、新产品问题 覆盖时效性
人工构造样本 低频边界场景 容易偏离真实表达

这里我一般做两层处理。

1)清洗与脱敏

import re

def mask_sensitive(text: str) -> str:
    text = re.sub(r"1\d{10}", "<PHONE>", text)
    text = re.sub(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+", "<EMAIL>", text)
    text = re.sub(r"\b\d{15,18}[0-9Xx]\b", "<ID>", text)
    return text

2)近重复去重

如果不做这一步,评测集会被高频模板问题占满。模型看起来分数很高,其实只是吃到了重复样本红利。

from rapidfuzz import fuzz

def is_near_duplicate(text, existing_texts, threshold=92):
    for t in existing_texts:
        if fuzz.ratio(text, t) >= threshold:
            return True
    return False

实测里,如果原始候选池来自客服日志,近重复占比经常超过 25%。有一次我在退款咨询场景里跑过去重,1.8 万条候选样本最后只剩 1.2 万条可用样本,重复比我预期高很多。


四、分层抽样:别让高频样本把评测集“冲平”

抽样不是简单随机。随机只适合数据已经非常均匀的情况,而业务数据基本都不均匀。

我常用的分层维度有这些:

  • 业务场景,如售后、物流、发票、会员权益
  • 问题难度,如直接问答、多约束条件、歧义表达
  • 风险等级,如低风险、高风险、合规敏感
  • 流量频次,如高频、腰部、低频
  • 来源类型,如线上日志、投诉工单、人工补样

为了避免一段里硬列满三个项,我通常把分层设计做成配置。这样后续复用很方便。

sampling_plan = {
    "biz_scene": {
        "refund": 0.25,
        "logistics": 0.20,
        "invoice": 0.15,
        "membership": 0.15,
        "promotion": 0.10,
        "other": 0.15
    },
    "difficulty": {
        "easy": 0.35,
        "medium": 0.45,
        "hard": 0.20
    },
    "risk_level": {
        "low": 0.50,
        "medium": 0.30,
        "high": 0.20
    }
}

接下来用加权分层抽样:

import pandas as pd
import numpy as np

def stratified_sample(df: pd.DataFrame, total_n: int, group_cols):
    grouped = df.groupby(group_cols, dropna=False)
    group_sizes = grouped.size().reset_index(name='count')
    group_sizes['ratio'] = group_sizes['count'] / group_sizes['count'].sum()
    group_sizes['sample_n'] = (group_sizes['ratio'] * total_n).round().astype(int)

    sampled_parts = []
    for _, row in group_sizes.iterrows():
        condition = pd.Series([True] * len(df))
        for col in group_cols:
            condition &= (df[col] == row[col])
        sub_df = df[condition]
        n = min(len(sub_df), row['sample_n'])
        if n > 0:
            sampled_parts.append(sub_df.sample(n=n, random_state=42))

    return pd.concat(sampled_parts).reset_index(drop=True)

不过真实项目里,我更常用目标配额抽样,而不是纯比例抽样。原因很简单:你真正想评估的是“关键业务问题是否可控”,不是完全复刻线上分布。

举个例子。某客服系统线上高频是物流催单,占 40% 以上,但真正最怕出事故的是退款规则和合规表达。如果按真实流量抽样,评测集大半会被物流问题占掉,模型在高风险场景的差异反而看不清。

所以我一般会做两套集:

  • 运营分布集:接近真实流量,用来看整体收益
  • 挑战集:提高难例和风险样本比例,用来看上限和短板

这一招很实用。


五、样本量怎么定:别拍脑袋定 1000 条

经常有人问,评测集是不是 500 条、1000 条就够。我的回答通常是:看你要比较多大的差异。

如果只是粗看版本有没有明显退化,几百条能用。

如果你想区分两个模型 2 到 3 个百分点的差异,样本量不够时结论会很飘。今天 A 高,明天换一批数据 B 又高,很常见。

工程上我会用一个简化方法先估算:

from math import ceil

def estimate_sample_size(p=0.8, delta=0.03, z=1.96):
    # 近似估计二项分布指标的样本量
    return ceil((z ** 2) * p * (1 - p) / (delta ** 2))

print(estimate_sample_size(p=0.8, delta=0.03))

如果任务成功率预计在 0.8 左右,允许误差 3%,算出来通常要 600 到 700 条以上。若还要按场景切片看,就不能只看总量,得看每个关键分层里有没有足够样本。

我自己的做法偏保守:

  • 主评测集:800 到 1500 条
  • 高频核心场景单独保底:每个场景至少 100 条
  • 高风险场景即使流量低,也保底 80 条左右

这不是标准答案,但在大部分业务项目里够稳。


六、人工标注方案:先写规则,再招人标

评测集一旦进入人工标注阶段,很多团队会急着拉人开标。说实话,这一步最容易返工。

真正影响质量的不是标注人数,而是标注规则是否可执行。规则写得含糊,十个标注员就有十种理解。

我一般会先出一版《标注手册》,至少包含这些内容:

模块 说明
任务定义 这一维度到底在判断什么
正例/反例 用真实样本举例
边界规则 模糊情况如何判
优先级规则 多问题同时出现时按什么口径记
拒判条件 信息不足时何时标“无法判断”

一个可落地的标注模板

{
  "sample_id": "cs_000001",
  "labels": {
    "task_success": 1,
    "factual_correctness": 1,
    "policy_compliance": 1,
    "missing_constraint": 0,
    "answer_clarity": 2
  },
  "overall": "pass",
  "comment": "回答覆盖主问题,规则引用正确,但未说明质量问题需要凭证。",
  "annotator_id": "ann_03"
}

这里有个细节:

能二分类就先别五分类。

例如“是否回答主问题”,用 0/1 往往比“差/中/良/优/极优”一致性高很多。主观评分维度如果必须保留,我建议控制在 3 档,比如 0/1/2,不要上来就打 5 分制。

我之前做过一次对比,同一批 300 条样本:

标注方式 平均耗时 一致性(Cohen’s Kappa)
五分制主观打分 118 秒/条 0.41
三档评分 96 秒/条 0.58
二分类 + 备注 74 秒/条 0.73

结论很直接。评分越细,不代表越准。


七、一致性控制:先试标,再正式标

人工标注最怕的不是慢,是口径悄悄漂移。

我通常把标注流程分成四段:

1)试标 50 到 100 条

所有标注员看同一批样本。目的不是产出数据,而是找分歧。

2)开对齐会,修订手册

对分歧大的样本逐条过,补边界定义。别泛泛聊原则,要把“这种句子到底算不算回答到位”写进规则。

3)双人标注 + 仲裁

正式标注阶段,核心样本集采用双标。冲突样本再交给资深审核员仲裁。

4)滚动抽检

每天或每批次抽 5% 到 10% 回看,监控标注员漂移。

下面给一个简单的一致性计算代码:

from sklearn.metrics import cohen_kappa_score

labels_ann1 = [1, 1, 0, 1, 0, 1, 1]
labels_ann2 = [1, 0, 0, 1, 0, 1, 1]

kappa = cohen_kappa_score(labels_ann1, labels_ann2)
print("Cohen's Kappa:", round(kappa, 4))

常见参考范围我一般这么用:

Kappa 解释
< 0.4 规则不稳,先别继续扩大标注
0.4 - 0.6 勉强可用,但要补规则
0.6 - 0.75 基本可用
> 0.75 一致性较好

别迷信一个数。

如果某个维度 Kappa 很低,要看它是不是天然主观,比如“语言是否自然”。这类维度天生比“是否引用错误规则”更难统一。我的经验是,把主观维度和客观维度分开统计,不然你会误判整体质量。


八、标注平台与数据版本:从第一天就要留痕

评测集不是一次性文件,它应该是一个持续演进的数据资产。这里我不用大词,实际就两件事:版本可追溯修改有记录

最少要保留这些字段:

{
  "dataset_version": "eval_v2026_04_01",
  "sample_id": "cs_000001",
  "source_version": "rawlog_2026w13",
  "annotation_version": "ann_rule_v3",
  "annotator_id": "ann_03",
  "reviewer_id": "rev_01",
  "label_status": "reviewed"
}

如果没有版本管理,后面你会遇到几个很烦的问题:

  • 同一个 sample_id 在不同轮次被改过,没人知道原因
  • 模型分数变了,不清楚是模型变了还是评测集变了
  • 标注规则升级后,旧数据和新数据混在一起

我一般把评测集放在 Git + 对象存储里管理,小文件用 JSONL/CSV,大文件用 Parquet。每次发布一个新版本时,固定输出下面几份产物:

  • dataset_manifest.json
  • label_guideline.md
  • sampling_report.csv
  • iaa_report.csv
  • diff_report.md

这套东西很朴素,但特别省事。后面做模型回归对比时,基本不会因为数据口径问题扯皮。


九、离线评测时怎么用:主分数之外,一定要切片

评测集建好了,不能只看一个总分。总分高,不代表关键场景稳。

我在项目里通常会固定输出这些切片结果:

切片维度 示例
业务场景 售后、物流、发票
难度层级 easy / medium / hard
风险等级 low / medium / high
样本来源 日志、投诉、人工构造
是否需要检索 RAG / 非 RAG

一个简单评测脚本如下:

import pandas as pd

def calc_accuracy(df, pred_col='pred', label_col='label'):
    return (df[pred_col] == df[label_col]).mean()


def evaluate_by_slice(df, slice_col):
    rows = []
    for key, sub in df.groupby(slice_col):
        rows.append({
            'slice': key,
            'count': len(sub),
            'accuracy': round(calc_accuracy(sub), 4)
        })
    return pd.DataFrame(rows).sort_values('accuracy')

我很少只把“整体准确率 84.3%”拿出来汇报。更有价值的是这种信息:

  • 退款规则场景从 78.1% 提到 86.4%
  • 高风险样本错误率从 9.2% 降到 4.8%
  • 挑战集上的提升不明显,说明上限还没拉开

这种对比更接近真实业务判断。


十、一个可复现的最小流程

如果你想把这套方法先跑起来,我建议直接按这个最小流程做:

Step A:确定评测目标

把业务目标写成一张表,每个目标对应可标注维度。

Step B:构建候选池

从日志、转人工、投诉、知识库变更记录拉样本,统一 schema,做脱敏和去重。

Step C:设计抽样方案

设定主评测集和挑战集,按业务场景、难度、风险等级做配额。

Step D:编写标注手册

用真实样本写正反例,先试标再修规则。

Step E:执行双标和仲裁

核心集双标,监控 Kappa,低于阈值先修规则再继续。

Step F:版本化发布

冻结样本、规则、IAA 报告和抽样报告,形成可回放的数据版本。

这个流程不复杂,但很管用。


十一、我自己总结的几个坑

最后补几条很实际的经验,都是踩过的。

1)不要把“线上 badcase”当成全部评测集

badcase 很有用,但它会放大失败模式,导致你误以为模型整体很差。评测集要同时覆盖常规样本和高风险样本。

2)不要让标注员自己理解业务规则

业务规则一定要前置写清楚。尤其是金融、医疗、售后政策这种场景,标注员如果靠常识判断,结果会非常散。

3)不要频繁改标签定义

一旦正式开标,标签定义就不要轻易变。真要改,开新版本,别把旧数据悄悄覆盖。

4)人工构造样本别占太高比例

人工补样适合覆盖边缘场景,但比例太高时,评测集会偏离真实用户表达。我的经验是控制在 10% 到 20% 较稳。

5)主观维度可以留,但别让它主导结论

“语言自然”“回答亲和”这类指标可以做参考,不过版本决策最好还是优先看任务成功、事实正确、风险错误率这类更稳定的指标。

这里也承认一个局限:高一致性人工标注并不等于绝对正确。遇到本身规则模糊、业务口径还在变化的场景,再好的流程也只能先做到“团队内一致”,做不到永远正确。所以评测集需要定期回看,而不是一次建完就不动。


十二、结语

大模型评测这件事,真正费时间的通常不是跑模型,而是把评测集做对。业务目标没拆清,后面全是偏的;抽样策略没设计好,分数会失真;标注规则不稳定,结论很难复现。

我现在做项目时,基本都会先把评测集当成正式工程来做,而不是顺手整理一个 Excel。这样前期会慢一点,但后面版本回归、方案对比、上线验收都会轻松很多。

如果你也在做 LLM 应用评测,我建议先从 300 条试点集开始,不求大,先把目标拆解、分层抽样、双标仲裁这几步跑通。跑通一次,后面扩到 1000 条并不难。

有了这套基线,很多讨论才有意义。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐