LLM 应用的 Eval 数据工程实践:Golden Set 构建、版本管理与生产 Trace 回放

你的 eval 跑绿了,但上线之后用户还是骂------因为你的测试数据集三个月没更新,早就过时了。

为什么 Eval 数据集比 Eval 逻辑更难维护

大多数团队把 90% 的 eval 精力花在写评估逻辑上:设计 judge prompt、选 LLM-as-judge 还是规则打分、确定阈值。

这没错,但有一个更隐蔽的问题:测试数据集本身会腐烂

我见过的典型失败路径:

  1. 产品上线初期,工程师手写了 30 条测试 case,跑通了 CI。
  2. 三个月后,产品功能迭代了 5 个版本,用户真实问题模式变了,但 eval 数据集还是那 30 条。
  3. 新来的工程师看到 eval pass rate 97%,以为系统很稳,大胆合入了一个 prompt 变更。
  4. 上线之后,一类新的用户问题全部失败,因为这类 case 从未进过测试集。

问题不在于 eval 的评估逻辑,而在于测试数据没有和生产流量一起演化。

这篇文章讲的不是如何写 judge,而是如何建立一套 eval 数据工程体系:从生产 trace 中挖取高价值 case、构建 golden set、对数据集做版本控制、检测数据集漂移,以及周期性回放生产 trace 来填补覆盖盲区。


1. Eval 数据的三个来源与各自的问题

在聊怎么管理之前,先想清楚数据从哪来。

1.1 手工构造(Handcrafted)

工程师或产品经理手写测试 case,通常在开发初期。

问题

  • 覆盖的是「工程师能想到的」,不是「用户实际会问的」。
  • 容易集中在 happy path,边界 case 靠人力补充效率极低。
  • 随功能迭代很快过时,但没人主动维护。

实际数量通常在 20-100 条,覆盖率极度不足。

1.2 从生产 Trace 采样

从真实用户请求里采样,是最接近生产分布的数据来源。

问题

  • 原始 trace 量大、噪声高(重复请求、无效输入、测试账号流量都在里面)。
  • 需要标注 ground truth:用户真正期望的输出是什么?这部分很贵。
  • 隐私合规:用户数据不能直接用于测试,需要脱敏处理。

1.3 合成生成(Synthetic Generation)

用 LLM 生成 case,近两年逐渐流行。

问题

  • 容易产生「自循环偏差」:用 DeepSeek-V3 生成测试数据,再用 DeepSeek-V3 当 judge,天然高分。
  • 合成数据的分布和生产分布不一致,覆盖的是「模型能想到的」而不是「用户实际遇到的」。
  • 边界 case 需要额外的 adversarial generation 策略才能触达。

正确姿势:三者混用,但以生产 trace 为主骨干。手工构造负责关键业务逻辑 case,生产 trace 采样负责分布真实性,合成生成负责扩充边界 case 和对抗 case。


2. Golden Set 构建流程

Golden set 是 eval 数据集中质量最高的部分:每条 case 都有明确的 ground truth,经过人工审核,代表系统的核心能力边界。

2.1 从生产 trace 中挖取候选 case

第一步是从 trace 存储里提取候选数据。假设你用 OpenTelemetry 采集了 LLM span:

python 复制代码
import json
from datetime import datetime, timedelta
from typing import Iterator
import psycopg2  # 或你的 trace 存储客户端

def fetch_candidate_traces(
    conn,
    start_time: datetime,
    end_time: datetime,
    min_tokens: int = 50,
    sample_rate: float = 0.05,  # 5% 采样率
) -> Iterator[dict]:
    """
    从生产 trace 中采样候选 eval case。
    过滤条件:
    - 用户输入长度足够(min_tokens)
    - 排除内部测试账号
    - 排除重复/相似请求(后续步骤处理)
    """
    query = """
        SELECT 
            span_id,
            trace_id,
            start_time,
            attributes->>'llm.input.messages' AS input_messages,
            attributes->>'llm.output.message' AS output_message,
            attributes->>'llm.usage.total_tokens' AS total_tokens,
            attributes->>'user.id' AS user_id,
            attributes->>'feature.name' AS feature_name
        FROM spans
        WHERE 
            span_kind = 'llm'
            AND start_time BETWEEN %s AND %s
            AND (attributes->>'llm.usage.total_tokens')::int >= %s
            AND attributes->>'user.id' NOT LIKE 'test_%'
            AND RANDOM() < %s
        ORDER BY RANDOM()
        LIMIT 5000
    """
    with conn.cursor() as cur:
        cur.execute(query, (start_time, end_time, min_tokens, sample_rate))
        for row in cur:
            yield {
                "span_id": row[0],
                "trace_id": row[1],
                "timestamp": row[2].isoformat(),
                "input": json.loads(row[3]) if row[3] else [],
                "output": row[4],
                "total_tokens": int(row[5] or 0),
                "user_id": row[6],
                "feature": row[7],
            }

2.2 去重:语义相似度过滤

原始 trace 里有大量语义相似甚至完全重复的请求。直接进 eval 数据集会导致特定 pattern 被过度代表。

用 embedding 做语义去重:

python 复制代码
import numpy as np
from sentence_transformers import SentenceTransformer

class SemanticDeduplicator:
    def __init__(self, model_name: str = "BAAI/bge-small-zh-v1.5", threshold: float = 0.92):
        self.model = SentenceTransformer(model_name)
        self.threshold = threshold
        self.seen_embeddings: list[np.ndarray] = []
        self.seen_texts: list[str] = []

    def is_duplicate(self, text: str) -> bool:
        if not self.seen_embeddings:
            return False
        
        embedding = self.model.encode(text, normalize_embeddings=True)
        
        # 批量计算相似度(向量化,比循环快 10x)
        seen_matrix = np.array(self.seen_embeddings)
        similarities = seen_matrix @ embedding  # cosine similarity(已归一化)
        
        return bool(np.max(similarities) >= self.threshold)

    def add(self, text: str) -> None:
        embedding = self.model.encode(text, normalize_embeddings=True)
        self.seen_embeddings.append(embedding)
        self.seen_texts.append(text)


def deduplicate_traces(traces: list[dict], threshold: float = 0.92) -> list[dict]:
    deduplicator = SemanticDeduplicator(threshold=threshold)
    unique_traces = []
    duplicates_removed = 0
    
    for trace in traces:
        # 用用户输入作为去重 key
        input_text = extract_user_input(trace["input"])
        
        if not deduplicator.is_duplicate(input_text):
            deduplicator.add(input_text)
            unique_traces.append(trace)
        else:
            duplicates_removed += 1
    
    print(f"Dedup: {len(traces)} -> {len(unique_traces)} (removed {duplicates_removed})")
    return unique_traces


def extract_user_input(messages: list[dict]) -> str:
    """从 messages 列表中提取最后一条 user message 内容。"""
    for msg in reversed(messages):
        if msg.get("role") == "user":
            content = msg.get("content", "")
            if isinstance(content, list):
                # 多模态 content,取文本部分
                return " ".join(c.get("text", "") for c in content if c.get("type") == "text")
            return str(content)
    return ""

实测数字:从 5000 条原始 trace 采样,去重后通常剩 800-1200 条有效候选,去重率约 75-85%,说明生产流量里确实有大量重复请求。

2.3 自动分层采样:按 feature 和 token 分布采样

去重之后,要确保覆盖不同 feature、不同输入长度。否则容易出现某个边缘 feature 完全没有测试 case 的情况:

python 复制代码
from collections import defaultdict
import random

def stratified_sample(
    traces: list[dict],
    target_count: int = 200,
    feature_weight: dict[str, float] | None = None,
) -> list[dict]:
    """
    分层采样:按 feature 和 token 桶分层,确保分布均匀。
    feature_weight: 可以给重要 feature 更高权重,默认均匀分配。
    """
    # 按 feature 分组
    by_feature: dict[str, list[dict]] = defaultdict(list)
    for trace in traces:
        feature = trace.get("feature", "unknown")
        by_feature[feature].append(trace)
    
    features = list(by_feature.keys())
    
    # 计算每个 feature 应采多少条
    if feature_weight:
        total_weight = sum(feature_weight.get(f, 1.0) for f in features)
        quotas = {
            f: max(1, int(target_count * feature_weight.get(f, 1.0) / total_weight))
            for f in features
        }
    else:
        per_feature = max(1, target_count // len(features))
        quotas = {f: per_feature for f in features}
    
    sampled = []
    for feature, traces_for_feature in by_feature.items():
        quota = quotas.get(feature, 1)
        
        # 在每个 feature 内,按 token 桶再分层(短/中/长)
        short = [t for t in traces_for_feature if t["total_tokens"] < 500]
        medium = [t for t in traces_for_feature if 500 <= t["total_tokens"] < 2000]
        long_ = [t for t in traces_for_feature if t["total_tokens"] >= 2000]
        
        # 按 token 分布比例分配
        total = len(traces_for_feature)
        if total == 0:
            continue
        
        short_n = max(0, int(quota * len(short) / total))
        long_n = max(0, int(quota * len(long_) / total))
        medium_n = quota - short_n - long_n
        
        sampled.extend(random.sample(short, min(short_n, len(short))))
        sampled.extend(random.sample(medium, min(medium_n, len(medium))))
        sampled.extend(random.sample(long_, min(long_n, len(long_))))
    
    return sampled[:target_count]

2.4 标注 Ground Truth

这是最贵的步骤。常见的三种方案:

方案 A:人工标注

  • 最准确,但人力成本高
  • 适用于高价值 case(核心功能、已知失败案例)
  • 工具推荐:Label Studio 或自建轻量标注界面

方案 B:LLM 辅助标注 + 人工审核

  • 用强模型(DeepSeek-V3 / Qwen-Max)生成参考答案
  • 人工只需判断「accept / reject / edit」,效率提高 3-5x
  • 注意:审核标准要明确,避免审核员 rubber-stamping

方案 C:用户隐式反馈作为弱信号

  • 用户点赞/踩、follow-up 问题模式、session 放弃率
  • 作为「quality signal」标记 case,而不是直接作为 ground truth
  • 适合 ranking/relevance 类任务,不适合生成类任务

实际落地建议:三种方案分层使用。核心功能 case 用方案 A,批量候选用方案 B,难以标注的 case 用方案 C 作为辅助信号。


3. Eval 数据集的版本管理

很多团队用 Google Sheet 或 GitHub CSV 管 eval 数据,这在数据集超过 200 条、有多人协作之后会失控。

3.1 数据集 Schema 设计

先定一个清晰的 case schema:

python 复制代码
from dataclasses import dataclass, field
from typing import Any, Literal
import uuid
from datetime import datetime

@dataclass
class EvalCase:
    # 唯一标识
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    
    # 来源追踪
    source: Literal["handcrafted", "production_trace", "synthetic"] = "handcrafted"
    source_trace_id: str | None = None    # 来自哪条生产 trace
    source_timestamp: str | None = None  # 生产 trace 时间戳
    
    # 输入
    messages: list[dict] = field(default_factory=list)  # 标准 messages 格式
    context: dict = field(default_factory=dict)          # 额外上下文(RAG 结果、用户配置等)
    
    # Ground truth
    expected_output: str | None = None
    expected_criteria: list[str] = field(default_factory=list)  # 评估标准描述
    
    # 分类
    feature: str = "general"
    tags: list[str] = field(default_factory=list)
    difficulty: Literal["easy", "medium", "hard"] = "medium"
    
    # 生命周期
    created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
    created_by: str = "system"
    reviewed: bool = False
    reviewed_by: str | None = None
    deprecated: bool = False
    deprecation_reason: str | None = None
    
    # 历史结果(用于检测 regression)
    baseline_score: float | None = None
    baseline_model: str | None = None
    baseline_run_id: str | None = None

3.2 用 DVC 对数据集做版本控制

代码用 Git,数据集用 DVC(Data Version Control)。DVC 跟踪数据文件的哈希,实际内容存在 S3/GCS/本地 remote,Git 里只存 .dvc 指针文件。

bash 复制代码
# 初始化
dvc init
dvc remote add -d storage s3://your-bucket/eval-datasets

# 数据集文件结构
eval-data/
  golden/
    core-features.jsonl    # 核心功能 golden set
    edge-cases.jsonl       # 边界 case
    regression.jsonl       # 历史失败 case(防回归)
  production-sampled/
    2026-07-29.jsonl       # 当期生产采样
  synthetic/
    adversarial.jsonl      # 对抗 case

# 跟踪数据集
dvc add eval-data/
git add eval-data.dvc .dvcignore
git commit -m "eval-data: add production samples 2026-07-29"
dvc push

版本标签规范:

bash 复制代码
# 每次重要的数据集更新打 tag
git tag eval-data-v1.2.0
git push --tags

# tag 命名规范:eval-data-v<major>.<minor>.<patch>
# major: schema 变更(破坏性)
# minor: 新增 feature 的 case 集合
# patch: 修复已有 case 的 ground truth 错误

3.3 数据集变更的 Review 流程

eval 数据集是测试合同,不能随便改。建议用 PR + review 流程管控变更:

yaml 复制代码
# .github/workflows/eval-data-review.yml
name: Eval Data Review

on:
  pull_request:
    paths:
      - 'eval-data/**'
      - '*.dvc'

jobs:
  validate-changes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup DVC
        run: pip install dvc[s3]
      
      - name: Pull data
        run: dvc pull eval-data/

      - name: Validate schema
        run: python scripts/validate_eval_schema.py eval-data/

      - name: Check case count regression
        run: |
          python scripts/check_case_count.py \
            --before HEAD~1 \
            --after HEAD \
            --max-reduction-pct 5  # 不允许 case 总数减少超过 5%

      - name: Generate diff report
        run: python scripts/diff_eval_data.py > eval-data-diff.md
      
      - name: Post diff as PR comment
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const diff = fs.readFileSync('eval-data-diff.md', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Eval Data Changes\n\n${diff}`
            });

4. 数据集漂移检测

eval 数据集会「过时」:生产流量的分布变了,但测试数据没有跟上。如何检测这种漂移?

4.1 生产流量分布 vs 数据集分布的对比

定期(每周)运行一次分布对比,检查数据集的覆盖是否跟上了生产的变化:

python 复制代码
from sklearn.metrics.pairwise import cosine_similarity
from sentence_transformers import SentenceTransformer
import numpy as np
import json

class DatasetDriftDetector:
    def __init__(self, model_name: str = "BAAI/bge-small-zh-v1.5"):
        self.model = SentenceTransformer(model_name)
    
    def compute_distribution_distance(
        self,
        dataset_cases: list[dict],
        production_traces: list[dict],
        sample_size: int = 500,
    ) -> dict:
        """
        计算 eval 数据集和生产流量的分布距离。
        返回:距离指标 + 覆盖盲区分析。
        """
        import random
        
        # 采样
        ds_sample = random.sample(dataset_cases, min(sample_size, len(dataset_cases)))
        prod_sample = random.sample(production_traces, min(sample_size, len(production_traces)))
        
        ds_texts = [extract_user_input(c["messages"]) for c in ds_sample]
        prod_texts = [extract_user_input(t["input"]) for t in prod_sample]
        
        # Encode
        ds_embs = self.model.encode(ds_texts, normalize_embeddings=True, batch_size=64)
        prod_embs = self.model.encode(prod_texts, normalize_embeddings=True, batch_size=64)
        
        # 计算 dataset 中心 vs production 中心的距离
        ds_centroid = ds_embs.mean(axis=0)
        prod_centroid = prod_embs.mean(axis=0)
        centroid_distance = float(1 - np.dot(ds_centroid, prod_centroid))
        
        # 找出生产流量中「距离数据集最远」的 case(覆盖盲区)
        # 对每条生产 trace,计算它到数据集中最近邻的距离
        sim_matrix = cosine_similarity(prod_embs, ds_embs)
        max_similarities = sim_matrix.max(axis=1)
        
        # 最相似度 < 0.7 的生产 trace 视为「未被数据集覆盖」
        coverage_threshold = 0.7
        uncovered_mask = max_similarities < coverage_threshold
        uncovered_rate = float(uncovered_mask.mean())
        
        # 提取覆盖盲区的典型 case(供人工审查)
        uncovered_indices = np.where(uncovered_mask)[0]
        uncovered_examples = [
            prod_texts[i] for i in uncovered_indices[:10]  # 最多取 10 条示例
        ]
        
        return {
            "centroid_distance": centroid_distance,   # 0 = 完全相同,1 = 完全不同
            "uncovered_rate": uncovered_rate,          # 生产流量中未被覆盖的比例
            "uncovered_examples": uncovered_examples,
            "drift_alert": centroid_distance > 0.15 or uncovered_rate > 0.30,
        }

实测基准:

  • centroid_distance < 0.10:分布基本一致,数据集健康
  • 0.10 ≤ centroid_distance < 0.20:中度漂移,建议补充采样
  • centroid_distance ≥ 0.20:严重漂移,数据集需要重建

4.2 Feature Coverage Gap 检测

比分布更具体的指标:数据集在各个 feature 上的覆盖是否均匀?

python 复制代码
def check_feature_coverage_gap(
    dataset_cases: list[dict],
    production_traces: list[dict],
    alert_threshold: float = 0.03,  # 生产流量占比超过 3%,但数据集覆盖不足的 feature
) -> list[dict]:
    """
    检测数据集中 feature 覆盖缺口。
    返回:需要补充 case 的 feature 列表。
    """
    from collections import Counter
    
    # 统计生产流量的 feature 分布
    prod_feature_counts = Counter(t.get("feature", "unknown") for t in production_traces)
    prod_total = len(production_traces)
    
    # 统计数据集的 feature 分布
    ds_feature_counts = Counter(c.get("feature", "unknown") for c in dataset_cases)
    ds_total = len(dataset_cases)
    
    gaps = []
    for feature, prod_count in prod_feature_counts.items():
        prod_ratio = prod_count / prod_total
        ds_ratio = ds_feature_counts.get(feature, 0) / ds_total if ds_total > 0 else 0
        
        # 生产占比超过阈值,但数据集覆盖不足一半
        if prod_ratio >= alert_threshold and ds_ratio < prod_ratio * 0.5:
            gaps.append({
                "feature": feature,
                "production_ratio": prod_ratio,
                "dataset_ratio": ds_ratio,
                "coverage_gap": prod_ratio - ds_ratio,
                "recommended_cases_to_add": max(
                    5,
                    int((prod_ratio - ds_ratio) * ds_total)
                ),
            })
    
    return sorted(gaps, key=lambda x: x["coverage_gap"], reverse=True)

5. 生产 Trace 回放:让 Eval 跟上真实分布

漂移检测告诉你「哪里覆盖不足」,Trace 回放让你「用真实问题来测」。

5.1 回放 Pipeline 架构

css 复制代码
生产 Trace 存储
        ↓
   [采样 + 去重]
        ↓
   [脱敏处理]      ←── 必须在进入 eval 之前
        ↓
   [分层采样]
        ↓
   [LLM 辅助标注]  ←── 批量生成参考答案
        ↓
   [人工快速审核]  ←── 只需 accept/reject,效率高
        ↓
   [加入数据集]    ←── 版本控制,PR review
        ↓
   [运行 Eval]
        ↓
   [比较 baseline] ←── regression 检测

5.2 脱敏处理

生产数据进 eval 之前,必须脱敏。最常见的几类需要处理的信息:

python 复制代码
import re

class PIIAnonymizer:
    PATTERNS = [
        # 手机号(中国)
        (r'\b1[3-9]\d{9}\b', '<PHONE>'),
        # 邮箱
        (r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '<EMAIL>'),
        # 身份证号(18位)
        (r'\b\d{17}[\dXx]\b', '<ID_NUMBER>'),
        # IP 地址
        (r'\b(?:\d{1,3}\.){3}\d{1,3}\b', '<IP_ADDRESS>'),
        # 银行卡号(16-19位连续数字)
        (r'\b\d{16,19}\b', '<BANK_CARD>'),
        # 中文姓名(简单启发式:2-4个汉字,前面有「我叫」「姓名」等)
        (r'(?:我叫|我是|姓名[::是]?\s*)([^\s,,。!?]{2,4})', r'我叫<NAME>'),
    ]
    
    def anonymize(self, text: str) -> str:
        for pattern, replacement in self.PATTERNS:
            text = re.sub(pattern, replacement, text)
        return text
    
    def anonymize_messages(self, messages: list[dict]) -> list[dict]:
        anonymized = []
        for msg in messages:
            new_msg = msg.copy()
            content = msg.get("content", "")
            if isinstance(content, str):
                new_msg["content"] = self.anonymize(content)
            elif isinstance(content, list):
                new_msg["content"] = [
                    {**c, "text": self.anonymize(c.get("text", ""))}
                    if c.get("type") == "text" else c
                    for c in content
                ]
            anonymized.append(new_msg)
        return anonymized

5.3 批量回放 + baseline 对比

有了新数据集之后,用批量回放检测是否有 regression:

python 复制代码
import asyncio
import aiohttp
from dataclasses import dataclass

@dataclass
class ReplayResult:
    case_id: str
    score: float
    passed: bool
    latency_ms: float
    actual_output: str
    judge_reasoning: str

async def replay_trace_batch(
    cases: list[EvalCase],
    model: str,
    judge_model: str,
    max_concurrent: int = 20,
) -> list[ReplayResult]:
    """
    批量回放:并发运行 eval case,收集结果。
    """
    semaphore = asyncio.Semaphore(max_concurrent)
    
    async def run_single_case(case: EvalCase) -> ReplayResult:
        async with semaphore:
            start = asyncio.get_event_loop().time()
            
            # 调用被测模型
            response = await call_llm(
                model=model,
                messages=case.messages,
                context=case.context,
            )
            
            latency_ms = (asyncio.get_event_loop().time() - start) * 1000
            actual_output = response["content"]
            
            # 用 judge 评分
            score, reasoning = await judge_response(
                judge_model=judge_model,
                input_messages=case.messages,
                expected=case.expected_output,
                actual=actual_output,
                criteria=case.expected_criteria,
            )
            
            return ReplayResult(
                case_id=case.id,
                score=score,
                passed=score >= 0.7,  # 阈值根据业务调整
                latency_ms=latency_ms,
                actual_output=actual_output,
                judge_reasoning=reasoning,
            )
    
    tasks = [run_single_case(case) for case in cases]
    return await asyncio.gather(*tasks, return_exceptions=False)


def compare_with_baseline(
    current_results: list[ReplayResult],
    baseline_scores: dict[str, float],  # case_id -> baseline_score
    regression_threshold: float = 0.05,
) -> dict:
    """
    与 baseline 对比,检测 regression。
    regression_threshold: 分数下降超过此值视为 regression。
    """
    regressions = []
    improvements = []
    
    for result in current_results:
        baseline = baseline_scores.get(result.case_id)
        if baseline is None:
            continue
        
        delta = result.score - baseline
        if delta <= -regression_threshold:
            regressions.append({
                "case_id": result.case_id,
                "baseline_score": baseline,
                "current_score": result.score,
                "delta": delta,
            })
        elif delta >= regression_threshold:
            improvements.append({
                "case_id": result.case_id,
                "baseline_score": baseline,
                "current_score": result.score,
                "delta": delta,
            })
    
    all_scores = [r.score for r in current_results]
    baseline_avg = np.mean(list(baseline_scores.values()))
    current_avg = np.mean(all_scores)
    
    return {
        "current_avg_score": current_avg,
        "baseline_avg_score": baseline_avg,
        "overall_delta": current_avg - baseline_avg,
        "regression_count": len(regressions),
        "improvement_count": len(improvements),
        "regressions": regressions[:10],  # 只返回最严重的 10 条
        "pass_rate": sum(1 for r in current_results if r.passed) / len(current_results),
        "alert": len(regressions) > 0 or (current_avg - baseline_avg) < -0.03,
    }

6. 实际落地:一个最小可行的 Eval 数据工程体系

上面讲了很多,但很多团队没有足够的带宽一次性全部实现。下面是分阶段的落地路径:

Phase 1(第 1-2 周):建立基础 Golden Set

目标:从现有生产 trace 里挖 50-100 条高质量 case,人工标注 ground truth,进 Git。

bash 复制代码
deliverables:
  - eval-data/golden/core-features.jsonl  # 50-100 条
  - scripts/validate_eval_schema.py
  - README.md(case 格式说明 + 如何新增 case)

产出:一个稳定的回归测试基线,解决「eval 集是 30 条手写 case」的问题。

Phase 2(第 3-4 周):自动化采样 Pipeline

目标:每周自动从生产 trace 中采样 20-30 条候选,LLM 辅助标注,人工快速审核后入库。

bash 复制代码
deliverables:
  - scripts/sample_production_traces.py
  - scripts/auto_label.py              # LLM 辅助标注
  - .github/workflows/weekly-sampling.yml
  - DVC 接入(数据集版本化)

产出:数据集跟上生产演化,每周增加覆盖。

Phase 3(第 5-8 周):漂移检测 + CI 集成

目标:把 eval 集成进 CI/CD,PR 合入前必须通过 eval;定期检测数据集漂移。

bash 复制代码
deliverables:
  - .github/workflows/eval-on-pr.yml
  - scripts/drift_detection.py
  - Grafana dashboard(eval pass rate + drift distance 趋势)

产出:eval 成为真正的质量门,而不是可选的检查项。


关键经验总结

数据集腐烂比评估逻辑错误更危险,因为前者是静默失效的。

几个数字值得记住:

  • 5000 条原始 trace 采样 → 去重后约 1000 条有效候选(75-85% 去重率)
  • 100 条 golden case 通常能覆盖 60-70% 的生产失败根因
  • centroid_distance > 0.15 是数据集需要更新的警戒线
  • 每周 20-30 条新 case 足以维持数据集健康

更重要的是流程纪律:eval 数据集变更需要 PR + review,不能随便删 case。每一条 case 的删除都需要明确理由------是功能下线、是 ground truth 标注错误、还是真的重复了?

最后一点:eval 数据集是团队的「集体记忆」。它记录了系统曾经失败过的模式、修复过的 bug、用户真实遇到的边界 case。维护好它,是避免「过去踩过的坑再踩一遍」的最低成本方案。

相关推荐
浮生望1 小时前
大模型 Benchmark 全面解析:MMLU、GPQA、HumanEval,读懂 AI 模型的"成绩单"
llm
浮生望1 小时前
GPT 5.6 的真正变革:从"最强模型"到"最合适模型",AI 工程范式正在重置
llm
DO_Community10 小时前
Claude Opus 5 现已上线 DigitalOcean AI 推理云
人工智能·llm·agent·claude
天然玩家13 小时前
【生产力】GPT-5.6 发布:前沿智能、智能体协作与更高性价比
llm·openai·gpt5.6
chaors13 小时前
DeepResearchSystem 0x03:HITL
llm·github·ai编程
武子康15 小时前
GAIA、Cosmos、Genie 等视频模型纷纷自称"世界模型",为什么说视觉逼真度不构成决策证据?
人工智能·llm·agent
武子康16 小时前
Video VAE 不是末端编解码器:5 维-潜网格-主模型 Token 的“表示合同“
人工智能·llm·agent
孙启超17 小时前
【AI应用开发】 RAG篇(一):概述与核心架构
llm·embedding·向量数据库·rag·向量化·ai应用开发·chunking
DigitalOcean18 小时前
Kimi K3 现已上线 DigitalOcean 无服务器推理
llm·agent·ai编程