Chapter 03

Golden Dataset 方法论

评估集是评估系统的"尺子"。尺子刻得不对,再精密的仪器也是白搭。本章讲如何构建一个"能反映真实分布、没有污染、可持续迭代"的评估集。

什么是 Golden Dataset

Golden Dataset(黄金评估集)
一组精心挑选、标注准确、保持稳定的输入-期望输出对,用于回归测试与版本对比。它是你对"应用做得好不好"的契约定义。
Held-out(保留集)
从未被用于 prompt tuning 的数据。专用于最终评估,避免过拟合到评估集。
Adversarial Set(对抗集)
故意构造的困难样本——越狱尝试、边界情况、多义、歧义、噪声。

评估集的三个层级

一个健康的评估体系往往有三个集合:

集合规模用途更新频率
Dev Set30-200 条调 prompt、快速迭代每天可以改
Regression Set200-2000 条CI 上的守门员,PR 必跑每月 review 一次
Held-out Set500-5000 条发布前的终极考核,从不看季度级更新
为什么要分 Dev 和 Held-out 如果你天天看着一个集合调 prompt,最后 prompt 会悄悄"拟合"到这个集合上,分数漂亮但上线依然翻车。Held-out 的价值就是提供一个未被你"污染"过的视角

数据从哪来:四条路

路一:真实日志采样(首选)

最接近真实分布。流程:

  1. 开启日志记录:输入、输出、时间戳、session id、用户反馈(如有)
  2. 按时间窗口(如 7 天)聚合,做 PII 脱敏
  3. 去重(完全相同或近似 query 合并)
  4. 分层采样:按 intent、用户群、时段等维度保证覆盖
  5. 人工标注期望输出,或标注"可接受的回答特征"
# 从生产日志构建评估集的最小示例
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)

路二:人工构造(无历史数据时)

新项目没日志。三种凑样本的办法:

路三:合成数据(规模化)

用强模型批量生成 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。

路四:开源基准(快速起步)

对通用能力,可以直接用公开 benchmark 的子集:

警惕数据污染 主流基准都大概率进了训练集。公开测试的分数不再能反映真实能力。企业场景应以自有数据为主,公开 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 条纪律

  1. Held-out 铁律:留 20-30% 评估集作为"从不看"的保留集,只在发版前跑一次
  2. 多轮 holdout:每季度换一批 holdout,防止长期污染
  3. Judge 异源:打分用的模型和生成答案的模型应不同厂商/系列
  4. 时间保鲜:持续从最新日志补充新样本,防止评估集老化
  5. 人工抽检:每月 sampling 30 条,人工独立评分,比对自动指标
  6. 污染检测:检测数据集样本的 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
③ 全集合回归,确认没新退步
④ 部署。

本章小结