Agent 评测集怎么攒:自己编的题跑得挺好,一上线就翻车
数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。
评测集不管用,九成不是评测方法的问题,是样本来源的问题。你坐在工位上编出来的题,本质是你已经想到的失败模式;而线上真正把 agent 打崩的,恰恰是你没想到的那些——用户把两个需求塞进一句话、粘进来的代码带着 BOM 头、仓库里同名文件有三个、需求描述里藏着一个已经废弃的接口名。这些东西你编不出来,只能捞。
先说清楚本篇跟站内另外两篇的分工,免得你翻错文章:怎么评估一个 Agent 好不好用 解决的是”评什么维度、怎么定义完成”,Agent 怎么做回归测试 解决的是”改动之后断言写在哪一层”。这两件事都假设你手里已经有一批像样的题。本篇只管这批题从哪来、怎么加工、怎么维护,不重复讲维度和断言。
一、先判断你的评测集卡在哪一环
评测集失效有好几种长得很像的表现,成因完全不同,处置动作也不一样。别一上来就”多加点用例”,先对一下现象。
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 评测全绿,线上投诉不断 | 样本全是自己编的,覆盖不到真实输入分布 | 随机抽 20 条线上真实请求,直接灌进评测流程,看通过率是不是明显低于评测集 | 停止扩充手写题,转去建线上捞取管线(第二节) |
| 评测集越加越大,跑一轮太久,没人愿意跑 | 用例之间高度重复,同一失败模式反复收录 | 按失败模式给用例打标签,统计每个标签下的用例数,看是否有标签占比过半 | 按模式去重,每个模式保留难度阶梯上的两三条(第四节) |
| 同一批用例,今天过明天挂,改都没改 | 用例依赖了会漂移的外部状态:当前日期、真实网络、线上数据库、仓库 HEAD | 把整套用例连跑三遍,把不稳定的挑出来,检查它们的输入里有没有时间、网络、外部服务 | 冻结依赖,用例自带固定输入快照 |
| 某类线上故障从来没被评测集抓到过 | 采集口径漏了这一类,通常是”没报错但结果不对”的那类 | 翻最近一段时间的人工介入记录,看有多少故障在日志里根本没有 error 级别记录 | 把采集触发条件从”报错”扩展到”人工介入”(第二节) |
| 评测通过率天天在小幅上下跳 | 采样次数太少,把模型随机性当成了质量变化 | 同一个用例连跑多次,看单条用例自身的通过率方差 | 改用多次采样的通过率,别用单次判定 |
| 新加的用例一加进去通过率暴跌 | 收录了本来就不该由 agent 完成的任务 | 让熟悉业务的人按同样的输入手工做一遍,看是不是人也做不了 | 这条不进评测集,进”已知能力边界”清单 |
这张表的用法是:先定位到行,再决定要不要往下读第二节。如果你的问题是第三行那种依赖漂移,捞再多线上样本也救不了。
二、从线上捞样本:三个来源,别只盯着报错
多数团队的采集只挂在异常分支上,结果捞回来一堆超时和限流——429、500、ETIMEDOUT、ECONNRESET 这些确实该记,但它们是基础设施问题,不是 agent 的能力问题。修好重试就没了,进评测集意义不大。真正值钱的样本在下面三个地方。
第一个来源是人工介入点。 用户看到输出之后没有采纳,而是自己动手改、或者重新描述一遍需求再发一次,这个动作本身就是最强的失败信号,比任何自动判定都准。埋点很简单:记录”生成结束”到”用户下一个动作”之间发生了什么。如果下一个动作是撤销、是回滚、是把同一个意图换个说法重发,就把这一整轮的输入输出标记为候选样本。这类样本的价值在于,它们全都是”跑通了但不对”的情况,而这正是自动化监控最抓不到的一类。Agent 谎报成功怎么办 里讲的那种情况,几乎只能靠这个来源捞出来。
第二个来源是重试轨迹。 agent 内部自己重试过、换过工具、换过参数最后才成功的那些请求,是难度梯度上最有信息量的一批。它们没失败,所以不会进错误日志;但它们贴着能力边界。采集口径可以定成:单轮内工具调用次数超过日常中位数的请求,或者同一个工具被连续调用多次且参数相近的请求。这批样本进评测集之后,往往能提前暴露下一次退化。
第三个来源才是硬失败。 硬失败里值得留的是那些结构性的:工具参数拼错、输出结构对不上下游、文件路径找错、编码问题。基础设施类的直接过滤掉。过滤规则写死在采集端就行,别让它们流到人工筛选环节浪费时间。
采集埋点不需要很复杂,关键是一开始就把该记的字段记全。一条候选样本至少要有:完整的原始输入、agent 实际调用的工具序列和参数、最终输出、以及触发采集的原因标签。少记一样,后面加工的时候就得回去猜。日志字段的取舍和敏感信息的处理,可以对着 日志里混进敏感信息 那篇一起定,采集端和脱敏规则最好一次设计到位,别等样本堆了几万条再回头改。
三、原始日志变不成用例,中间有三道加工
从日志里导出来的东西不能直接当评测用例,它太大、太脏、也没有判定标准。三道加工缺一不可。
脱敏必须发生在落盘之前,不是之后。 一旦真实用户数据写进了评测集仓库,你就有一个长期存在的合规隐患,而且评测集通常会被到处拷贝、进 CI、进各种分支,收不回来。可行的做法是在采集端就做替换:邮箱、手机号、身份证、密钥、内网域名、真实人名,全部按类型替换成占位符,替换要保持长度和格式特征,否则会改变输入的难度。密钥类的东西最好在提交前再拦一道:
# .git/hooks/pre-commit
hits=$(git diff --cached --name-only --diff-filter=ACM -z \
| xargs -0 -r grep -nEI \
'(sk-[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----)')
if [ -n "$hits" ]; then
printf '%s\n' "$hits" >&2
echo '暂存区里有疑似密钥,清掉再提交' >&2
exit 1
fi
这段在暂存区里扫常见密钥形态,扫到就拦下提交。三个细节容易写错:--diff-filter=ACM 用来排掉本次删除的文件,不加的话 grep 会对着已经不存在的路径报错、把整个钩子的退出码搞乱;xargs -r 是防止暂存区筛完为空时 grep 拿不到文件名转去读标准输入,那样钩子会直接挂住;最后是判定方式,grep 匹配成功返回 0、匹配不到返回 1,直接拿它的退出码当”该不该拦”用会正好反过来,所以这里改成看输出是不是空的。这类正则只认几种最常见的密钥形态,当兜底可以,别当成完备检测——真要严一点,就在这一层之外再挂一个专门的密钥扫描工具。
最小化是把一条真实日志裁成能复现问题的最小输入。 线上一次请求可能带了十几轮上下文,其中真正导致失败的可能只是其中一句。逐段删,删完还挂,说明删掉的部分无关;删完就过了,说明刚才删的那段就是关键。这个过程和缩最小复现用例是一回事,值得花时间,因为最小化之后的用例跑得快、失败原因指向明确、也更不容易因为无关改动而假红。
标注期望是最花人力的一步,也决定了这条用例值不值钱。 期望不要写成一段自然语言描述,写成可判定的条件:输出必须包含哪几个字段、某个数值必须等于多少、哪个工具必须被调用过、哪个文件不许被改动。写不出可判定条件的,说明这条样本的问题还没想清楚,先放着别收录。
加工完的用例,我建议用一份扁平的 JSONL 存,一行一条,方便 diff 也方便按行采样:
python -c "
import json, random
lines = open('cases.jsonl', encoding='utf-8').read().splitlines()
random.seed(42)
for l in random.sample(lines, min(20, len(lines))):
print(json.loads(l)['id'])
"
min(20, len(lines)) 这层保护别省:random.sample 在样本量大于总数时直接抛 ValueError,评测集刚起步、只有十来条的时候就会踩到。固定随机种子这件事看着小,但它决定了两次抽样评测能不能横向比较。种子不固定,你分不清是模型变了还是抽到的题变了。要注意种子固定的是”抽哪几条”,不是模型的输出——同一批题重复跑,通过率照样会因为采样随机性小幅波动,这也是第一节表里”通过率天天在小幅上下跳”那一行讲的事,处置办法是多次采样取通过率,而不是换种子。
四、集合要分层,否则一年之后没人敢动它
评测集会自然膨胀,膨胀之后跑一轮的时间和成本都会变得难以承受,然后就没人跑了。分层是唯一有效的办法。
冒烟层放十几条,覆盖最核心的路径,要求秒级到分钟级跑完,每次改动都跑。主回归层放几十到一两百条,按失败模式分组,每个模式留难度阶梯上的两三条,合并前跑一次。全量层放所有历史样本,包括那些已经修好的、暂时不打算修的、以及能力边界之外的,只在大版本变更或者换模型的时候跑。
分层之后有一条纪律要守住:修好的 bug 对应的用例不能删。 它从”当前失败”变成”防回归”,正是它最有价值的时候。删掉之后同样的问题会以完全一样的形式再回来,这在换模型、改系统提示词的时候尤其常见。
另外要给每条用例记两个元信息:来源(哪次线上事故、哪个用户反馈)和收录日期。来源用来在争论”这个行为到底算不算 bug”的时候翻回去看原始场景;日期用来定期审查——超过一定时间没有再触发过、且对应功能已经下线的,可以移到归档层,但仍然不删。
成本也要纳入评测集本身的管理。一轮全量评测烧掉的量不算小,跑之前心里得有数,具体的记账口径可以参考 Agent 成本失控怎么办。如果全量层跑一次的代价大到需要审批,那说明分层没做好,主回归层的规模该往下压。
五、什么情况下别再往评测集上投入
这一节的判断依据比前面几节更重要,因为攒评测集是个很容易上瘾的活儿——它看起来一直在产出,实际上可能早就没有边际收益了。
止损点一:连续三次线上故障,都不是评测集能覆盖的类型。 比如三次都是上游接口结构变了、依赖版本冲突、运行环境不一致。这类问题的正确解法是契约测试和环境固化,不是往评测集里加题。这时候继续扩充样本,是在错误的层面上加班。
止损点二:新增用例的边际发现率掉到很低。 具体做法是记录每批新收录的用例里有多少条一进来就是红的。这个比例如果从一开始的过半掉到个位数,说明当前的失败模式已经基本被覆盖,再捞也是重复。这时候该做的是把精力转到判定标准的精度上——很多时候你以为覆盖够了,其实是判定太松,绿得不真实。
回滚点:评测集本身改坏了。 判定条件写严了会让一批本来正确的输出被判红,这种情况的信号是”通过率单日大幅下跌,但产品行为没有任何变化”。评测集要和代码一样进版本管理,出现这种信号时先 git log 看评测集自己最近的改动,把判定条件的那次提交单独 revert 掉验证一遍,别急着去改 agent。判定标准的改动和用例的新增最好分开提交,就是为了这一刻能干净地回滚。
换条路的判断依据: 如果你的场景本身没有稳定的正确答案——比如创意生成、开放式写作——那自动评测的天花板很低,硬做投入产出比很差。这种场景更实际的办法是抽样人工评分加线上采纳率监控,把评测集缩小到只覆盖硬性约束(格式、禁用内容、必含要素),剩下的交给线上指标。承认这一点比硬凑一套假装客观的自动评测要诚实。
六、避坑清单
坑一:用线上样本时忘了它自带答案偏见。 你从线上捞回来的失败样本,标注期望时很容易照着”当时人工是怎么改的”来写。但人工那次修改不一定是最优解,也可能带着当时的临时决定。避法是标注期望时只写硬约束,别把当时的具体措辞写进判定条件。
坑二:采集比例没设上限,样本被高频用户带偏。 少数重度用户会贡献绝大部分请求,如果按原始比例采集,评测集会变成这几个人的专属回归。避法是按用户或按会话分桶采样,每个桶设收录上限。
坑三:脱敏改变了输入难度。 把一个 47 字的真实需求描述替换成”XXX”,这条用例就不再考验长上下文理解了。避法是替换时保持长度量级和结构特征,替换后自己读一遍,确认难点还在。
坑四:把评测集放进 agent 能读到的目录。 这一条特别隐蔽。如果评测用例和期望答案就躺在仓库里,而 agent 有全仓读取权限,它可能顺手读到答案,评测结果直接失去意义。避法是评测集单独仓库,或者至少在运行时把答案文件排除在可读范围之外。上下文范围的控制可以参考 Agent 上下文管理。
坑五:用例里写死了绝对路径和本机环境。 换台机器或者进 CI 就全挂,然后大家就默认”评测集在 CI 上本来就是红的”,从此这套东西彻底废掉。避法是用例只用相对路径,需要的环境变量在用例里显式声明,缺失时直接报错而不是回退到默认值。
坑六:把限流和超时当成 agent 的失败记进通过率。 429 和网络超时混进统计里,通过率就变成了对基础设施稳定性的测量,看不出模型和编排的变化。避法是在评测框架里把这类错误单独归一类,重试若干次仍然失败就标为”未测出”而不是”失败”,并且单独报出来。
坑七:一次性把几百条历史样本全部收录。 看起来效率很高,实际上没经过最小化和标注的用例,后面每次失败你都得重新读一遍原始日志才能判断该不该修,维护成本会把整套东西压垮。避法是限速收录,每批控制在能当场加工完的量。
收尾
攒评测集这件事,价值密度极不均匀。真正有用的是那些”跑通了但结果不对”的样本,而它们只出现在人工介入的痕迹里,不出现在错误日志里。埋点方向搞对了,几十条就够用;搞错了,几万条也只是在重复测同一件事。
开工前对一遍这份自检:
- 采集触发条件里,有没有”用户没采纳”这一类,而不只是报错?
- 脱敏是在落盘之前做的吗?密钥类的有没有在提交环节再拦一道?
- 每条用例的期望,是可判定的条件还是一段描述?
- 用例分层了吗?冒烟层能在几分钟内跑完吗?
- 抽样的随机种子固定了吗?两次评测结果能横向比吗?
- 评测集自己进版本管理了吗?判定标准的改动和用例新增是分开提交的吗?
- 限流、超时这类错误,有没有从通过率里摘出去单独统计?
- 最近三次线上故障,有几次是这套评测集原本可能抓到的?如果一次都没有,先别扩充,去看看问题是不是根本不在这一层。