很多团队做 Agent 测试的起点是:临时想几个问题,手动问一遍,觉得“还行”就上线。这等于考试不出题,随便在路上拉个人问“你觉得怎么样”——样本随机、标准主观、不可复现。上线后效果好不好,全靠用户投诉才知道。数据集解决的就是这件事:把“考什么”固定下来,让每次改动都能在同一套卷子上比分数。
学完你能干什么
- 1.能规划自己团队的数据集体系:分几个集、各自装什么、多久更新一次
- 2.能写出合格的 dataset item(含上下文轮、待判轮、期望轨迹与 metadata)
- 3.能说清参考答案从哪来,以及没有标准答案时该怎么判
一、四种来源、五类数据集:分工不同,别混在一起
benchmark 的数据到底从哪来、存成哪几类、为什么必须各管各的——这是本章要理清的一件事。很多团队的测试集越用越乱,根因就在这里:开发随手塞的题、线上回灌的题、红队攻击题全堆在一个目录里,最后谁也说不清某道题在考什么、该不该更新、能不能删。下面分两步讲清:先用一张表看「四种来源 → 五类数据集」的归集关系(来源本身的特性,以及它落到哪一类),再用一张表看「五类数据集」各自的定位和边界。
先不管分类,看源头。题目有四种来源,最终都会归集到五类数据集里;这张表讲「来源本身的特性 + 它落到哪一类」。注意「线上流量回灌」一行对应 weekly-* / ood-hard / regression-pitfalls 三样,因为它们都来自线上流量。
| | | | | |
|---|
| | 按业务场景手写:正常流 / 边界流 / 异常流各占 1/3 | | | seed-core |
| | | | | cand-* |
| | | | | weekly-* / ood-hard / regression-pitfalls(三样,均源自线上流量) |
| | | | | redteam-fixed |
冷启动建议:先手写 30~50 条种子题,覆盖三类场景各三分之一。别一上来就想做自动化回灌——没有基准卷,回灌回来的题也没法判好坏。
接着说五类数据集本身。为什么非得把数据分成五类、而不是一个大题库?因为五类的更新频率、判定方式、跑批位置完全不同:基准卷每次必考、低频修订;踩坑集只增不删、跑 PR 门禁;攻击集攻破即加;发散集只发版前跑。混在一起会出三件事——基准卷被稀释(对比变慢)、踩坑题被误删(防线失守)、攻击题混入常规评分(分数失真)。所以每类得单独管。下面这张表讲清五类各自的定位、更新节奏、用途,以及一个生活化类比,帮你判断一道题该往哪放。
| | | | |
|---|
seed-core | | | | |
weekly-YYYYWww | | | | |
regression-pitfalls | | | | |
redteam-fixed | | | | |
ood-hard | | | | |
表里第五类 发散硬题集(ood-hard)最容易被漏掉:题目来自线上低概率场景、人工或模型变异改写、跨业务串场的问题,判的是「遇到没见过的情况会不会胡编、会不会乱调工具」,与基准卷(考基本功)互补。
- •与基准卷互补:基准卷考基本功(正常 / 边界 / 异常全覆盖),它考泛化
- •与踩坑集的区别:踩坑集是「已经出过事的题」,它是「还没出事、但很像会出事」的题
注意:它不进 PR 门禁(太贵也不稳定),只在 Nightly 或发版前跑。
1.1 谁能看到谁:开发集与测试集怎么分家
为什么要分开:开发自测集(开发拿去反复跑、调 prompt、验改动)和测试集(做版本判定、上线门槛)必须分开,根因只有一个——防数据串味(污染)。测试集的价值在于它「开发阶段没见过」,才能公正衡量模型真实水平;一旦开发能看到测试题,就会不自觉地针对它优化,测试集就退化成「练习题」,比出来的分量的是背题而不是能力,测试集随之作废。所以分开的本质,是让测试题在开发阶段始终对开发不可见。
怎么分开(保障不串):给题目设一道单向门——不分「谁写的 / 谁采集的」,只分「谁能看到」。所有题目进来先落候选池(开发可见、可自用),只有在候选池里过「评审 + 入卷」审核、被冻结后,才升级成私有卷「考卷」(只对测试可见)。下面这张表是两条轨道的边界,四条机制是「保障不串」的硬约束。
回灌题只有两条轨道,中间隔着一道单向门
| | | |
|---|
候选池cand-* | | | 只是候选:题面与期望值随时会改,不能当版本对比的判定标准 |
| | | |
保证不混的四条机制
- •① 单向门:题只能从候选池流向私有卷,不能反向。审核通过(题面清晰、预期可判定、判据能写出来)才提升;提升后立刻从候选池移除,开发侧的数据集里不再出现这条。
- •② 入卷即冻结:冻结 input、expected_output、metadata,并记一个 content_hash 与版本号。以后回灌再采到同一题(去重命中)只更新 metadata,不覆盖题目内容——否则「这周比上周掉了 5 分」分不清是代码退化还是题目被改了。
- •③ 命名与权限隔离:数据集前缀就是可见性契约(cand-*开发可读,seed-core/ weekly-*/ pitfalls-*只有测试可读)。别靠「我们说好不使用那份数据」来隔离——靠命名和权限,机器才查得出来。
- •④ 报告只给聚合:跑批报告对开发输出「通过率 + 失败题号」,不给题面;要复盘某道题,由测试陪同单条开出。开发拿到题面,就等于拿到了调参目标。
1.2 踩坑集的一个真实例子
线上事故:用户说"退订单 A1029",Agent 因字符串截断退成了 A102。复盘后测试做两件事:
- 1. 把这条 input 与期望轨迹存进 regression-pitfalls
- 2.在 PR 门禁加断言:assert order_id == "A1029"(验证不截断)
从此这个 bug 不可能再溜上线。踩坑集的价值 = 已经证明会出事的题,一条都不能少跑。
1.3 黄金基准卷怎么维护
- •低频修订:只在业务口径变了(规则 / 话术 / 工具变化)或发现题目本身写错时才改;不允许为了「让分数好看」改题
- •改题必动基线:任何加题、改期望都会让历史分数不可比,改完要重跑基线并升版本号,否则分不清「分数掉了」是代码退化还是题目变了
- •加题走审核、记来源:新增题写清 source(人工 / 事故 / judge 低分)与 risk_level,便于按风险筛题
一句话:基准卷是「教材课后习题」,修订要慢、要留痕、要和基线同步。
1.4 题目多了怎么选着跑
选题优先级:风险等级高 → 必跑;最近改动影响到的模块 → 定向跑;历史复现率高 → 提高权重;其余按分层抽样。一句话:重要的全跑,最新的一批全跑,其余抽样加归档。
1.5 防漂移到底在防什么
漂移 = 同一套题、同一套判定,分数却随时间下滑。防漂移就是拿固定卷子定期复考,因为下面这些变化都会让效果悄悄变差:
- •模型侧:版本升级 / 下线、供应商默认模型被替换
- •代码侧:prompt 改动、编排规则改动、工具返回值变化
- •数据侧:上游字段含义变化、用户表达方式变化(去年说「退款」,今年说「退一下」)
- •环境侧:检索库内容变旧、依赖服务超时变多
验证对象是测试环境 / 预发:题从线上 trace 只读采集而来,跑批在 staging 执行;生产只做在线评估与告警,绝不在生产跑批。节奏:Nightly 跑基准卷 + 最近 4 周回灌集,与基线比,掉出容忍区间就报警。
二、一条合格的 dataset item 长什么样
2.1 先分清四层单位:session / trace / turn / segment
写题之前先把这四层分清,否则很容易把「一轮对话」当成「一条 trace」:
| 单位 | 是什么 | 例子 |
|---|
| | |
| 一次请求触发的完整执行(用户一句话 → 内部跑 N 圈) | |
| | |
| | |
所以多轮对话 ≠ 一条 trace,通常是「同一 session 下的多条 trace」;用了 checkpointer 的框架也可能把整段会话做成一条 trace,那就变成一条 trace 含多个 turn。回灌切题的正确单位是 segment。
2.2 三段式 item 结构
推荐统一成三段式结构:
{ "context_turns": [ {"role": "user", "content": "帮我退掉订单A1029"}, {"role": "assistant", "content": "确认为您退款吗?"} ], "target_turn": {"role": "user", "content": "确认"}, "expected_tools": [ {"name": "stage_refund", "arguments": {"order_id": "A1029"}} ], "expected_output": null, "metadata": { "source": "incident-2026-09-20", "source_trace_id": "tr-c3d4", "source_session_id": "sess-8899", "context_dependent": true, "case_type": "normal", "risk_level": "high" }}
三个字段最容易漏,但很关键:
| |
|---|
context_turns | 多轮依赖的题,脱离上下文就是废题("确认"两个字模型根本不知道确认什么) |
metadata.source | |
metadata.case_type | 区分 normal / ambiguous_clarification(追问类)/ distractor(抗干扰类),跑批策略不同 |
追问类题目别丢:像"确认""好的""退这个"这类信息不全的输入,本身就是高价值考题——测的是 Agent 会不会主动追问,而不是瞎猜一笔订单直接退款。线上大量差评正源于 Agent "自作主张"。
三、参考答案从哪来(三种补法)
回灌题大多没有标准答案,按成本选补法:
| | | |
|---|
| 用线上真实轨迹当 expected_tools | trace 里记录了实际调过的工具,直接转成参考轨迹 | | |
| 用线上 output 当弱参考 | | | |
| 人工或 judge 标注 | | | |
判断口诀:能免费的不用花钱的,能确定的不用波动的。judge 只用于"确实没有标准答案"的题。
四、维护机制:数据集是会腐烂的
数据集建完不管,三个月后就会变成一堆跑不通的脏数据。四个维护动作:
| | |
|---|
| 去重 | 对 input 做哈希查重;同 session 的整段题按 source_session_id 去重 | |
| 脱敏 | | |
| 归档 | 超过 90 天的 weekly 集转低频档(不删,只是不每次跑) | |
| 增题审核 | 踩坑集增题需确认"确实是 bug 且已修复",避免塞进误报题拖慢门禁 | |
4.1 回灌脚本骨架
# sync_dataset.py —— 由 cron 每日触发import hashlibfrom langfuse import Langfuself = Langfuse()seen = load_existing_hashes("weekly-2026W39")for sess_id, turns in group_by_session(fetch_traces(since, until)).items(): for segment in split_segments(turns): # 一 session 可能含多个任务 if not should_sample(segment): # 分层采样:错误层 100%、正常层 1% continue item = build_item(segment) # context_turns + target_turn item["metadata"]["source_session_id"] = sess_id if hashlib.md5(str(item).encode()).hexdigest() in seen: continue # 去重 item = desensitize(item) # 脱敏 if not dry_run_pass(item): # 空跑验证可用性 continue lf.create_dataset_item(dataset_name="weekly-2026W39", **item)
dry_run_pass 这一步别省:新灌进来的题先空跑一次,确认 Agent 能正常响应(不是格式错乱的脏数据),通过才入库。自动化流水线最容易堆垃圾,这是唯一的闸门。
4.2 空跑在防什么
空跑(dry run)= 不判分、不固化结果,只把这条题真跑一遍看会不会炸。查三件事:
- •能不能正常调通 Agent(不是 500、不超时)
- •返回结构是否合法(JSON 能解析、字段齐全)
- •有没有明显异常(空回复、死循环)
与正式跑批的区别:正式跑批的分数进基线与门禁;空跑只看「这条题能不能用」。回灌是自动化流水线,没有这道闸门,题库会被脏数据堆满,还会污染分数统计——几十条格式错的题就能把通过率拉下来。
五、避坑点
| | | |
|---|
| | | |
| | | |
| | | |
| | | |
| | | 分层:错误层 100%、边缘层 50%、正常层 1% |
| | | |
六、需要开发配合什么
| | |
|---|
| | |
| 线上事故复盘后移交 trace id,供固化成踩坑题 | |
| | |
| | |
七、小结
- 1.数据集是评测体系最值钱的资产——工具可以换,题要一直养
- 2.五类数据集分工明确:基准卷 / 周回灌 / 踩坑集 / 攻击库 / 发散硬题集,尤其踩坑集只增不删
- 3.item 用三段式(context_turns + target_turn + expected),metadata 必须写清来源
- 4.参考答案优先用真实轨迹(免费确定),judge 只补没有标准答案的题
- 5.数据集会腐烂,去重/脱敏/归档/审核四个动作要常态化
- 6.题只能从候选池流向私有卷(单向门);入卷即冻结,开发只看聚合分与失败题号
自测清单(5 题)
- 1.你们团队目前有几个数据集?分别对应本篇五类里的哪一类?缺哪一类?
- 2.一条多轮题只存了最后一句"确认",为什么它是废题?正确结构该怎么写?
- 3.线上出了一次金额截断事故,应该固化成哪类数据集的题?还要做什么配套动作?
- 4.回灌题没有标准答案,有哪三种补法?你会优先选哪种?
- 5.三个月后 weekly 数据集涨到 5000 条,跑批要 2 小时,该怎么处理?
下一篇预告:第 5 篇《线上流量自动回灌 dataset:把真实用户变成你的考官》——有了考卷,考题能不能自动生产?讲清分层采样、session 分段、定时任务与消息队列两种触发方式,并给出完整可跑的回灌脚本。