大模型评测集构建的工程实践:从业务目标拆解、样本分层抽样到高一致性人工标注
大模型评测集构建的工程实践:从业务目标拆解、样本分层抽样到高一致性人工标注
很多团队一上来就想做模型对比,结果跑完一轮发现结论不稳。问题往往不在模型,而在评测集本身。样本混杂、标注口径漂移、线上目标和离线指标对不上,最后只能得到一份“看起来很多,实际不好用”的评测数据。
我这半年在做几个 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.jsonlabel_guideline.mdsampling_report.csviaa_report.csvdiff_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 条并不难。
有了这套基线,很多讨论才有意义。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)