Skip to content

Dataset 与回归评测 ​

学习目标 ​

  • 能设计可版本化的 Agent 评测数据集,而不是把几个手工问题散落在脚本里。
  • 能区分 Golden Task、fixture、运行轨迹、断言和评测报告。
  • 能选择答案、轨迹、工具、安全和成本等不同层次的指标。
  • 能理解数据泄漏、样本偏差和回归门禁,知道什么结果不能直接下结论。

前置知识 ​

需要理解 Trace、Span 与事件日志 以及第 08 章的 RetrievalEval/GenerationEval。本文用 Python 标准库实现一个离线回归评测器,不依赖真实模型。

核心知识点 ​

Dataset 不只是问题列表 ​

评测 Dataset 是可复现输入、期望行为和评测元数据的集合。一个最小样本至少要说明:

  • id:稳定的样本标识,不能用数组下标代替。
  • input:用户任务或消息。
  • expected:可判定的目标,例如必须包含字段、必须拒绝某工具。
  • tags:场景、风险、语言、难度和来源。
  • fixture:模型、工具、数据库和时间等外部依赖的固定环境。
  • version:数据集版本和变更原因。

如果只保存问题和人工印象,下一次运行无法判断“系统变差”还是“题目、工具、模型变了”。数据集本身也要像代码一样进行评审和变更记录。

GoldenTask 的边界 ​

GoldenTask 是有明确验收条件的代表性任务,不是“唯一正确的自然语言答案”。对 Agent,golden 可以描述:

  • 最终结果的字段或事实必须满足什么条件。
  • 必须经过哪些工具,禁止调用哪些工具。
  • 遇到权限或不确定信息时必须停下并请求确认。
  • 最大步数、最大成本和允许的延迟。

一个任务可以有多个合法答案。把生成文本逐字比较当成唯一指标,常常会把正确的改写判成失败。

对工具型任务,契约还应显式写出 required_tools、tool_sequence 和 max_tool_calls:

  • required_tools:至少调用过的工具集合,适合声明必须经过的能力。
  • tool_sequence:工具名的精确执行顺序;如果不关心顺序就留空。
  • max_tool_calls:工具调用总次数上限,防止重复调用和成本失控。

这些字段不是装饰性元数据,评测器必须从真实 Trace 或运行结果中读取并验证;只检查最终答案无法证明工具行为符合契约。

Fixture 与隔离 ​

Fixture 是运行时固定的外部环境,例如假的天气工具、内存数据库和冻结时间。它的作用是把模型或代码的变化与网络、数据和时间变化隔离开。

常见隔离层次:

  1. 工具 Fixture:拦截真实网络和写操作。
  2. 数据 Fixture:使用只读快照或临时数据库。
  3. 模型 Fixture:使用确定性策略、录制响应或固定测试模型。
  4. 进程 Fixture:清空环境变量、工作目录和缓存。

离线回归应该默认禁止真实付款、发信、删除文件和访问生产数据库。需要真实服务的测试应单独标记为集成或演练,不与普通提交门禁混在一起。

回归评测的指标分层 ​

建议从硬到软分层:

  • 协议/结构:JSON 是否可解析、必需字段是否存在、工具参数是否符合 Schema。
  • 行为/轨迹:是否调用了允许的工具、是否遵守顺序和步数、是否在危险动作前请求审批。
  • 结果/事实:答案是否包含关键事实、数值误差是否在范围内、引用是否来自允许来源。
  • 质量/偏好:表达清晰度、完整性、帮助程度,可使用 LLM-as-Judge 或人工抽样。
  • 工程指标:成功率、P95 延迟、token/cost、重试率、超时率和安全拒绝率。

先判定硬门禁,再看质量分数。平均分上升但安全违规增加,不能算回归通过。

基线、回归与门禁 ​

把当前版本称为 baseline,把待测版本称为 candidate。不要只比较总分,应同时比较:

  • 总体通过率与置信区间。
  • 每个标签、风险等级和难度桶的通过率。
  • 关键样本是否从通过变为失败。
  • 延迟、成本和工具错误率是否超过预算。

门禁是发布决策,不是统计魔法。小数据集的 1 次失败可能只是波动,但高风险样本的 1 次越权也可能必须阻断发布。阈值要与风险和样本量一起记录。

评测数据流 ​

图中的 Runner 必须把环境固定,否则报告只是“这一次网络和模型的结果”。门禁失败后保留完整样本、轨迹和版本,先复现再修改。

一个最小的离线回归评测器 ​

用途:下面的例子用固定策略模拟 Agent,按样本执行、检查结构和行为,并与基线比较。真实项目只需替换 run_case,不要改变报告和门禁的语义。

python
from dataclasses import dataclass


@dataclass(frozen=True)
class GoldenTask:
    case_id: str
    prompt: str
    expected_answer: str
    allowed_tools: tuple[str, ...]
    required_tools: tuple[str, ...] = ()
    tool_sequence: tuple[str, ...] = ()
    max_tool_calls: int = 0
    tags: tuple[str, ...] = ()


DATASET = (
    GoldenTask(
        case_id="sum-01",
        prompt="计算 2+3",
        expected_answer="5",
        allowed_tools=(),
        max_tool_calls=0,
        tags=("deterministic",),
    ),
    GoldenTask(
        case_id="search-01",
        prompt="查找文档",
        expected_answer="found",
        allowed_tools=("search_docs",),
        required_tools=("search_docs",),
        tool_sequence=("search_docs",),
        max_tool_calls=1,
        tags=("tool",),
    ),
)


def run_case(task):
    if task.case_id == "sum-01":
        return {"answer": "5", "tools": [], "trace_status": "ok"}
    return {"answer": "found", "tools": ["search_docs"], "trace_status": "ok"}


def evaluate(task, result):
    tools = result.get("tools", [])
    answer_ok = task.expected_answer in result["answer"]
    tools_ok = set(tools) <= set(task.allowed_tools)
    required_tools_ok = set(task.required_tools) <= set(tools)
    sequence_ok = not task.tool_sequence or tuple(tools) == task.tool_sequence
    call_count_ok = len(tools) <= task.max_tool_calls
    return {
        "case_id": task.case_id,
        "answer_ok": answer_ok,
        "tools_ok": tools_ok,
        "required_tools_ok": required_tools_ok,
        "sequence_ok": sequence_ok,
        "call_count_ok": call_count_ok,
        "passed": (
            answer_ok
            and tools_ok
            and required_tools_ok
            and sequence_ok
            and call_count_ok
            and result["trace_status"] == "ok"
        ),
        "tags": task.tags,
    }


report = [evaluate(task, run_case(task)) for task in DATASET]
passed = sum(item["passed"] for item in report)
pass_rate = passed / len(report)
print(f"passed={passed}/{len(report)} pass_rate={pass_rate:.0%}")
# 输出:passed=2/2 pass_rate=100%

这个例子只展示确定性门禁。真实评测还要保存 dataset_version、code_version、model_version、fixture_version、seed 和运行时间;否则两个 100% 很可能不是同一个实验。

防止数据泄漏与判定失真 ​

训练数据泄漏 ​

如果题目、答案或轨迹已经出现在模型训练、提示模板或缓存中,分数不能代表泛化能力。应将评测集与开发集、提示调优集分离,必要时使用私有、时间切分或新生成的样本,并记录来源。

工具和答案泄漏 ​

如果工具 Fixture 直接返回 expected_answer,Agent 可能只是在读取答案。Fixture 应模拟真实接口的噪声、错误和权限,但不能把验收标签混入模型上下文。

样本偏差 ​

只测成功、中文、短问题或单一工具,会高估质量。按任务类型、风险、长度、语言和失败原因分桶;总体分数之外必须显示每个桶。

版本混淆 ​

更换模型、系统提示、工具 Schema、检索数据或评测器,都可能改变结果。报告必须记录这些版本,不能把“重新跑了一遍”叫作代码回归。

源码阅读锚点 ​

  • OpenAI Evals:看 Registry、Task、Run 和记录结果如何分离,重点理解评测定义与执行器边界。
  • LangSmith Evaluation:对照 dataset、target、evaluator 和 experiment 的数据流。
  • OpenAI Agents SDK:结合 tracing 和 run result,查一次运行如何生成可评测轨迹。
  • DeepSeek Harness:寻找任务样本、工具环境和事件轨迹的落盘位置,确认测试是否真的隔离副作用。

易混点 ​

  • Dataset 不等于生产日志:日志是观测记录,Dataset 是经过选择和版本化的评测输入。
  • 通过率不等于质量:还要看分桶、风险、成本、延迟和关键失败样本。
  • Fixture 不等于真实环境:它提高可复现性,但不能替代少量受控集成测试。
  • 高分不等于无泄漏:题目过拟合、答案泄漏和 judge 偏差都会虚高分数。
  • 门禁不等于任意阈值:阈值必须结合风险、样本量和发布策略解释。

课后小问(含解析) ​

问题 1:为什么 golden task 不建议逐字比较最终答案? ​

答案:一个目标可能有多个等价的正确表达。

解析:逐字比较会把换序、同义表达和额外说明误判为失败。应按结构、事实、行为和安全要求分层断言,只有格式契约才适合严格比较。

问题 2:回归评测中为什么要记录 Fixture 版本? ​

答案:外部工具和数据变化也会改变结果。

解析:如果天气、搜索索引或时间没有固定,结果变化无法归因于代码。Fixture 版本让失败可以复现,并提醒我们哪些结论只适用于模拟环境。

问题 3:所有样本通过但高风险样本越权一次,可以发布吗? ​

答案:不能仅凭总体通过率发布。

解析:安全违规应作为独立门禁,可能需要阻断发布、保留轨迹并进行人工复核。平均分不能抵消高风险边界失败。

本节小结 ​

Dataset 用版本化任务描述评测范围,Fixture 固定外部环境,Runner 产生轨迹,断言和 Judge 生成报告,门禁决定是否发布。可靠回归不只看平均分,还要检查分桶、风险、成本、延迟、数据泄漏和判分偏差。

快速回顾 ​

  • 能写出 Dataset、GoldenTask 和 Fixture 的最小字段。
  • 能区分结构、行为、事实、质量和工程指标。
  • 能解释 baseline/candidate、关键样本和回归门禁。
  • 下一篇将学习确定性断言与 LLM-as-Judge 的组合边界。

官方依据 ​