什么是 Golden Dataset
Golden Dataset(黄金评估集)
一组精心挑选、标注准确、保持稳定的输入-期望输出对,用于回归测试与版本对比。它是你对"应用做得好不好"的契约定义。
Held-out(保留集)
从未被用于 prompt tuning 的数据。专用于最终评估,避免过拟合到评估集。
Adversarial Set(对抗集)
故意构造的困难样本——越狱尝试、边界情况、多义、歧义、噪声。
评估集的三个层级
一个健康的评估体系往往有三个集合:
| 集合 | 规模 | 用途 | 更新频率 |
|---|---|---|---|
| Dev Set | 30-200 条 | 调 prompt、快速迭代 | 每天可以改 |
| Regression Set | 200-2000 条 | CI 上的守门员,PR 必跑 | 每月 review 一次 |
| Held-out Set | 500-5000 条 | 发布前的终极考核,从不看 | 季度级更新 |
为什么要分 Dev 和 Held-out
如果你天天看着一个集合调 prompt,最后 prompt 会悄悄"拟合"到这个集合上,分数漂亮但上线依然翻车。Held-out 的价值就是提供一个未被你"污染"过的视角。
数据从哪来:四条路
路一:真实日志采样(首选)
最接近真实分布。流程:
- 开启日志记录:输入、输出、时间戳、session id、用户反馈(如有)
- 按时间窗口(如 7 天)聚合,做 PII 脱敏
- 去重(完全相同或近似 query 合并)
- 分层采样:按 intent、用户群、时段等维度保证覆盖
- 人工标注期望输出,或标注"可接受的回答特征"
# 从生产日志构建评估集的最小示例 import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.cluster import KMeans logs = pd.read_parquet("prod_logs_last_7d.parquet") # 1. 脱敏 import re PII_PATTERNS = [r"\d{11}", r"[\w.+-]+@[\w-]+\.[\w.-]+"] def redact(s): for p in PII_PATTERNS: s = re.sub(p, "[REDACTED]", s) return s logs["user_input"] = logs["user_input"].apply(redact) # 2. 去重 logs = logs.drop_duplicates(subset="user_input") # 3. 聚类分层(发现意图簇) vec = TfidfVectorizer(max_features=1000) X = vec.fit_transform(logs["user_input"]) logs["cluster"] = KMeans(n_clusters=20, random_state=42).fit_predict(X) # 4. 每簇采 10 条 sample = logs.groupby("cluster").apply(lambda g: g.sample(min(len(g), 10), random_state=42)) sample.to_csv("eval_candidates.csv", index=False)
路二:人工构造(无历史数据时)
新项目没日志。三种凑样本的办法:
- Dog-fooding:团队每人写 10 个问题(真实工作场景)
- 竞品参考:看竞品 App 的用户评论,提取用户会问什么
- 专家设计:领域专家按业务用例大纲写 case
路三:合成数据(规模化)
用强模型批量生成 test case。适合:想快速扩规模、覆盖稀有场景、构造对抗样本。
# 让 GPT-4 为每个真实样本生成 5 个变体 from openai import OpenAI client = OpenAI() SYNTHESIZE = """你的任务是为下面这个用户问题,生成 5 个语义相同但表达不同的变体。 涵盖:正式/口语、长句/短句、缺少主语、有错别字。 只返回 JSON 数组:["变体1", "变体2", ...] 原问题: {question}""" def paraphrase(q): r = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": SYNTHESIZE.format(question=q)}], response_format={"type": "json_object"}, ) return json.loads(r.choices[0].message.content)["variants"]
合成数据的两大陷阱
①同源污染:用 GPT-4 生成的测试集,评的还是 GPT-4 的输出,会系统性高估真实能力。
②分布偏移:合成数据风格相似度极高,失去多样性。真实用户永远比模型"更奇怪"。
对策:合成数据只做补充,主力还是真实日志。合成样本要至少 30% 人工 review。
②分布偏移:合成数据风格相似度极高,失去多样性。真实用户永远比模型"更奇怪"。
对策:合成数据只做补充,主力还是真实日志。合成样本要至少 30% 人工 review。
路四:开源基准(快速起步)
对通用能力,可以直接用公开 benchmark 的子集:
- MMLU / C-Eval:多学科知识
- HumanEval / MBPP:代码生成
- TruthfulQA:事实性 / 幻觉
- MT-Bench / AlpacaEval:对话能力
- HotpotQA / NaturalQuestions:开放域问答
警惕数据污染
主流基准都大概率进了训练集。公开测试的分数不再能反映真实能力。企业场景应以自有数据为主,公开 benchmark 只作辅助。
评估集要多大
经验曲线:
10-30
MVP 起步
100-300
日常迭代
500-1000
CI 回归
2000+
发布守门
从统计显著性视角:要区分"82% vs 84%"这种 2 个百分点差异,至少需要 ~800 条样本(双边 p=0.05,power=0.8)。要区分 5 个点,150 条够了。
# 两比例差异的样本量估算 from statsmodels.stats.power import NormalIndPower from statsmodels.stats.proportion import proportion_effectsize effect = proportion_effectsize(0.82, 0.84) n = NormalIndPower().solve_power(effect=effect, alpha=0.05, power=0.8, alternative="two-sided") print(round(n)) # 约 4900(单组)— 要区分 2pp 真的很贵
分层策略:不要一个桶到底
总体准确率 85% 可能掩盖:"简单问题 100%,困难问题 40%"。总是分层看:
# 每条样本带上层级标签 eval_set = [ {"input": "...", "expected": "...", "difficulty": "easy", "intent": "invoice", "locale": "zh"}, {"input": "...", "expected": "...", "difficulty": "hard", "intent": "refund", "locale": "zh"}, # ... ] # 跑完后按层聚合 import pandas as pd df = pd.DataFrame(results) print(df.groupby(["difficulty", "intent"])["score"].mean())
关键分层维度
业务维度
- 意图类型(退款/查询/投诉)
- 用户等级(新手/VIP)
- 渠道(App/微信/网页)
- 语种(中/英/方言)
技术维度
- 输入长度(短/中/长)
- 难度(easy/medium/hard)
- 是否含噪声(错别字、语音转写)
- 是否对抗样本
数据污染:最隐蔽的杀手
训练集污染
评估集里的样本以某种形式出现在预训练数据里。模型其实在"背答案",分数高但泛化差。
Prompt 拟合
长期盯着一个评估集调 prompt,prompt 慢慢"学会"了这个集合的特点,在这个集上表现虚高。
Judge 同源
用 GPT-4 生成数据 + GPT-4 作答 + GPT-4 打分 = 分数可信度坍塌。
反馈回路污染
把模型输出原封不动加回评估集作为"真值",模型越跑越自信,但离真实越来越远。
防污染的 6 条纪律
- Held-out 铁律:留 20-30% 评估集作为"从不看"的保留集,只在发版前跑一次
- 多轮 holdout:每季度换一批 holdout,防止长期污染
- Judge 异源:打分用的模型和生成答案的模型应不同厂商/系列
- 时间保鲜:持续从最新日志补充新样本,防止评估集老化
- 人工抽检:每月 sampling 30 条,人工独立评分,比对自动指标
- 污染检测:检测数据集样本的 n-gram 是否在模型训练语料中出现过(如 LAVA 方法)
标注一致性:Inter-annotator Agreement
若多人标注同一批数据,必须测一致性。常用 Cohen's Kappa:
from sklearn.metrics import cohen_kappa_score annotator_1 = ["yes", "no", "yes", "yes", "no"] annotator_2 = ["yes", "no", "no", "yes", "no"] κ = cohen_kappa_score(annotator_1, annotator_2) print(κ) # 0.6 左右
| κ 值 | 解读 |
|---|---|
| < 0.2 | 几乎无共识,任务定义不清 |
| 0.2 - 0.4 | 弱一致,需重审标注规范 |
| 0.4 - 0.6 | 中等一致,勉强可用 |
| 0.6 - 0.8 | 实质性一致,达标 |
| > 0.8 | 几乎完美 |
关键信号
人与人的 κ 上限就是 LLM Judge 的上限。如果人的一致性只有 0.5,不要指望 Judge 能做得更好——这时要先把 rubric 细化、例子讲透,再来评估 Judge 质量。
评估集的持续进化
评估集不是"做完就不管"——它必须和产品一起进化:
生产 bug 报告 ───┐
├──▶ 进入 Dev Set(修 prompt)
用户负反馈 ───┤ │
│ ├──▶ 稳定后进入 Regression Set
人工抽检差评 ───┘ │
└──▶ 季度 review 归档进 Held-out
TDD for AI
每个 "生产事故" 都是新 eval 样本的来源。修复流程应该是:
① 把坏 case 加入评估集 → 跑 eval,确认现在是 fail
② 改 prompt / 模型 → 再跑 eval,确认 pass
③ 全集合回归,确认没新退步
④ 部署。
① 把坏 case 加入评估集 → 跑 eval,确认现在是 fail
② 改 prompt / 模型 → 再跑 eval,确认 pass
③ 全集合回归,确认没新退步
④ 部署。
本章小结
- Golden Dataset 的质量 >> 数量,100 条好样本胜过 10000 条噪声
- 三层结构:Dev / Regression / Held-out,不能混用
- 数据优先级:真实日志 > 人工构造 > 合成数据 > 公开 benchmark
- 分层标签是评估集的"脊椎",没有分层就看不见失败模式
- 污染无处不在,Held-out + Judge 异源 + 时间保鲜是防火墙
- 标注一致性 κ 决定了 Judge 的性能上限
- 评估集要和产品一起进化,坏 case 先入库再修代码