Chapter 06

Datasets:生产 trace → 回归集 → CI 闸门

评估之前先要有"标准答案"。Langfuse 让你从真实 trace 里圈 dataset item,像保存单测一样保存 case,每次 prompt/模型升级都跑一遍。

Dataset 是什么

一个 dataset 是一组带"正确答案"的测试用例,每条 item 结构:

{
  "input":            {...},   // 喂给你 LLM 应用的东西(通常是 user query)
  "expected_output":  {...},   // 期望输出(可以是自由文本、结构化、甚至 rubric)
  "metadata":         {...}    // 标签, 分组, 难度, source 等
}

它承担的角色就是传统软件里的测试夹具——每次改 prompt / 换模型 / 改 RAG,都在这组固定 case 上跑,对比前后分数,防止回退。

创建 dataset 的三个来源

① 手工挑几十条(冷启动)

from langfuse import Langfuse

lf = Langfuse()

lf.create_dataset(
    name="customer-bot-regression-v1",
    description="客服 bot 核心回归集, 每次上线前必跑",
    metadata={"owner": "qa-team", "env": "staging"},
)

for q, a in [
    ("我的订单 2024001 什么时候到", "根据物流信息预计 3 日送达"),
    ("怎么退款", "在订单页点申请退款, 3-5 个工作日到账"),
    # ... 再补 30-50 条
]:
    lf.create_dataset_item(
        dataset_name="customer-bot-regression-v1",
        input={"query": q},
        expected_output={"answer": a},
    )

② 从生产 trace 圈(最常用)

UI 上任意 trace 详情页都有 "Add to dataset" 按钮——看到一条表现好的 / 表现差的 trace,点一下就进 dataset。质量差的那些加进 dataset 特别有用:原本没 cover 的边界 case,下次不再翻车

批量从 trace 圈也支持:

# 把最近 7 天 score < 0.5 的 trace 全挑进 regression 集
traces = lf.get_traces(
    from_timestamp=seven_days_ago,
    limit=100,
)
for t in traces.data:
    if any(s.value < 0.5 for s in (t.scores or [])):
        lf.create_dataset_item(
            dataset_name="customer-bot-regression-v1",
            input=t.input,
            expected_output=t.output,  # 先用原输出占位, QA 再修正
            source_trace_id=t.id,      # 关键: 保留溯源
            metadata={"reason": "low-score", "imported_at": today()},
        )
低分 trace 别急着当金标
生产低分 trace 的 expected_output 八成也是错的。推荐流程:先打上 metadata 标记成"待审"放入 dataset,然后让 QA 人在 UI 里修正 expected_output。Langfuse 的 human annotation 队列就是干这个的(下一章细讲)。

③ 合成(LLM 生成)

冷启动期没有足够 trace,让 GPT-4 按 schema 生成一批,再人工抽查 10% 修正:

prompt = """生成 20 条客服咨询问题与标准答案, JSON 数组, 每条 {query, answer},
覆盖订单查询 / 物流 / 退款 / 售后 / 投诉 5 个类别。"""

items = json.loads(openai.chat.completions.create(...).choices[0].message.content)
for it in items:
    lf.create_dataset_item(
        dataset_name="customer-bot-synthetic-v1",
        input={"query": it["query"]},
        expected_output={"answer": it["answer"]},
        metadata={"source": "synthetic", "model": "gpt-4o"},
    )

跑一次 run

run 的含义是"把 dataset 里所有 item 过一遍你的系统,把每条输出挂回 dataset item"。Langfuse 的 dataset.run context 帮你自动关联:

from langfuse import Langfuse

lf = Langfuse()
dataset = lf.get_dataset("customer-bot-regression-v1")

# 这次跑的名字, 一般带版本号 + 时间, UI 里列表按 run 分组
run_name = "run-2026-05-07-gpt4o-mini-prompt-v8"

for item in dataset.items:
    # 关键: 用 item.run() 包裹, span 会挂到这个 run 下
    with item.run(run_name=run_name) as handler:
        output = my_app(item.input["query"])  # 你的应用入口
        handler.update_trace(output=output)

        # 顺手打个基础评分(字符串匹配之类), 下章再讲 LLM-as-Judge
        handler.score(
            name="contains_keyword",
            value=1.0 if "退款" in output else 0.0,
        )

lf.flush()

跑完后 UI 里 Datasets → customer-bot-regression-v1 → Runs 能看到这次 run 的:

多 run 对比:A/B 跑 prompt 与模型

把"prompt 版本 + 模型"当变量,跑几次 run,直接在 UI 对比。典型操作:

for prompt_label in ["v7", "v8"]:
    for model in ["gpt-4o-mini", "claude-haiku-4-5"]:
        run_name = f"prompt-{prompt_label}-{model}"
        for item in dataset.items:
            with item.run(run_name=run_name) as h:
                output = my_app(item.input, prompt_label=prompt_label, model=model)
                h.update_trace(output=output)

跑完在 UI Datasets → Runs 能并排看四个 run 的聚合指标。胜出组合就是下一版生产候选。

CI 集成:回归闸门

把上面那段逻辑塞进 GitHub Actions,每次 prompt 改动都自动跑一次 dataset,低于阈值就挂。

脚本 scripts/run_regression.py

import os, sys
from langfuse import Langfuse
from my_app import answer  # 被测应用

PASS_THRESHOLD = 0.80  # 平均分低于此分数 CI 挂

lf = Langfuse()
dataset = lf.get_dataset("customer-bot-regression-v1")
run_name = f"ci-{os.environ['GITHUB_SHA'][:7]}"

scores = []
for item in dataset.items:
    with item.run(run_name=run_name) as h:
        try:
            out = answer(item.input["query"])
            h.update_trace(output=out)
            score = simple_match_score(out, item.expected_output["answer"])
            h.score(name="match", value=score)
            scores.append(score)
        except Exception as e:
            h.score(name="match", value=0.0, comment=str(e))
            scores.append(0.0)

lf.flush()

avg = sum(scores) / len(scores)
print(f"regression run {run_name}: avg={avg:.3f} (threshold={PASS_THRESHOLD})")
if avg < PASS_THRESHOLD:
    sys.exit(1)  # 挂 CI

.github/workflows/regression.yml

name: llm-regression
on:
  pull_request:
    paths:
      - "prompts/**"
      - "src/llm/**"

jobs:
  regression:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install -r requirements.txt
      - run: python scripts/run_regression.py
        env:
          LANGFUSE_HOST:       ${{ secrets.LANGFUSE_HOST }}
          LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
          LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }}
          OPENAI_API_KEY:      ${{ secrets.OPENAI_API_KEY }}

这个 workflow 只在 prompt 和 LLM 相关代码改动时才触发,避免纯前端 PR 浪费 LLM 额度。

基线(baseline)进 Git

CI 除了"有没有跌破绝对阈值",还需要"比上次降没降"。做法:把每个 PR 的 run 结果写到 benchmarks/latest.json,commit 进主干。新 PR 的 run 跟这个文件 diff:

import json

baseline = json.load(open("benchmarks/latest.json"))
current = {"avg_match": avg, "run_name": run_name}

# 允许 2% 容忍, 超出就挂
if current["avg_match"] < baseline["avg_match"] - 0.02:
    print(f"REGRESSION: {baseline['avg_match']:.3f} -> {current['avg_match']:.3f}")
    sys.exit(1)

# 通过则更新基线(仅在 main 分支 push 时)
if os.environ.get("GITHUB_REF") == "refs/heads/main":
    json.dump(current, open("benchmarks/latest.json", "w"))
别把基线当绝对真理
模型升级、prompt 变更都会带来合理的"不同",不一定是回归。基线降了应该触发人工 review,而不是自动挡人。把它当成 notification,不是 absolute block。

运营侧:QA 修正 expected_output

生产圈进来的 item 经常 expected_output 是错的。UI 里有 Annotation Queue,推荐流程:

  1. 自动化任务:每天把前一天低分 trace 批量加入 dataset,metadata 打 needs_review=true
  2. UI 里 QA 点进 item,能看到关联的原 trace、模型原输出、当前 expected
  3. QA 修改 expected_output 后,把 metadata 改成 reviewed=true
  4. CI 脚本只对 reviewed=true 的 item 跑,保证回归集质量

Dataset 的版本治理

dataset 本身没有版本号,只有"你自己怎么命名"这一条约定。建议用后缀标注:

重大结构变更(比如 schema 改了)就开 v2,旧 v1 保留用于长期趋势对比。

本章小结