维护一套评测集

9 分钟读完

E15
运维 · 评估与可观测性

维护一套评测集。

你的评测集每尽职一次,就贬值一次:每修好一个回归,就意味着那条用例此后再也分不开两个候选。而一套所有候选都拿同样分数的评测,已经不再是一次测量,却仍在花钱、仍在产出一个大家当真的数字。把评测集当作一件带折旧、需要有更换预算的资产;而第一件该量的东西不是你的智能体,是"所有候选都已经通过"的那部分用例占了多少。

STEP 1

饱和是你自己的成功造成的,不是疏于打理。

这个生命周期是机械的。你用已知的失败搭起一套集合。你不停发修复,直到它们全过。每一次修复,都把一条有区分力的用例变成一次永久通过——而那正是有意思的行为原本所在之处。半年后,套件有 300 条用例、通过率 94%,其中大约 280 条,无论你可能上线哪个候选,都会给出一模一样的判定。你在为跑 300 条付钱,而只从 20 条里学到东西。

这不是放着烂掉,是被拟合掉。它与公开基准变陈旧是同一种现象,只不过发生在私域、而且快得多——因为你的团队针对这一套特定集合做优化的迭代次数,远多于任何实验室针对 MMLU 的次数。这套件如今是一台回归护栏——用来抓住"已经修好的东西又回来了",这仍然真实有价值——但它不再是一台鉴别器,而团队却还在按鉴别器去读它。

告诉你身处何处的那个数字:

saturation = cases passed by ALL candidates in the last N comparisons
             ---------------------------------------------------------
                              total cases

# candidates = model versions, prompt versions, harness changes
# you actually compared over the last quarter

高过约 0.8,你的评测账单大部分是在买确认。一套成熟系统上如果低于 0.5,那你的集合大概太难,不该当发布闸门,应该拆开。

STEP 2

给用例打分,而不只给系统打分。

套件里的每条用例都有一个没人记录的属性:它改变过多少次别人的主意。把它存下来。每跑一次比较——一次模型升级、一次提示词改动、一次工具目录编辑——就按用例记下两个候选是否给出了不同结果。一个季度之后,你手上就有了每条用例的区分次数;而这个分布总是比人们预想的更偏:极少数用例扛着几乎全部信号,中间一长段偶尔发声,还有很大一条尾巴一次也没分开过任何东西。

由此直接得出两件事:

  • 你可以在不损失覆盖的前提下缩小 CI 闸门。高区分力的用例每次提交都跑,全套改成每晚或每次发布跑。在开发者真正要等的那个循环上,这通常是 5–10 倍的成本削减——而这很重要,因为评测开销随变更速率而非流量增长,这笔账在评测的成本里。
  • 你能看出自己已经不再测哪一项能力了。按每条用例的标签(工具选择、从坏结果中恢复、多步规划、拒答处理)把区分次数分组。某一类下的用例全都不再发声,要么是这一类被解决了,要么是它没被测——而这两者在仪表盘上长得一模一样。

一条从没分开过两个候选的用例并不自动等于没价值——有些用例的存在,就是为了抓住某个灾难性回归,而它确实(正确地)从未再现。把这些明确打上 tripwire 标签并豁免掉。这项计数的意义,是不让没打标签的那大多数悄悄变成压舱物。

STEP 3

给集合定一个更换速率,并且守住它。

评测集只会单调变大,因为"加"是某个人的职责,而"删"不是任何人的职责。用一个常设速率而不是周期性大扫除来解决它:每个周期更换掉套件里固定比例的一部分——一个合理的起点是每季度 10–15%,再按"在一套你每周都改的系统上,用例年龄中位数低于一年"去校准。

退役需要一条规则,免得它变成一场争论。当一条用例满足下列全部条件时就退出套件:它至少参与过 N 次比较、在其中一次也没区分出结果、没有被打上 tripwire 标签,并且它那条能力标签仍有其他在役用例在覆盖。要归档而不是删除:留着它、留着标签,并且每年把归档重跑一次——因为"已解决"是一个有保质期的断言,一次换模型或一次上下文策略调整,可能一口气让一整类退役用例复活。

把更换所隐含的标注成本也算进预算。新用例需要标准答案,而标准答案要花标注者的时间,那份时间与其他一切争抢;如果你的两位专家只在 72% 的轨迹上意见一致,那么一条建立在单个专家意见之上的新用例,就是你即将拿来卡发布的噪声。这条约束正是标注运营的主题,也是"一套集合能刷新多快"的真实天花板。

STEP 4

从生产环境取新用例——并为生产看不见的那部分做修正。

最好的新用例来自真实流量,这条流水线值得建一次:采样轨迹、筛出有意思的、打标签、再收敛成一条可复现的用例,配一个固定环境和一个可核查的结果。生产反馈信号讲了哪些信号值得挖,轨迹采样与留存讲了要留多少 trace 才重建得出一条用例。

陷阱在于,这条流水线会继承一种你必须手工修正的偏差。你只能收割你注意到了的失败,于是集合里塞满了吵闹的失败——报错、超时、点踩、上报人工——并系统性地少收了那种"自信的错误答案、没人标记"的情况,而后者才是真正让你付出代价的失败模式。三种配重:

  • 把更换预算切出一片给对抗性用例与合成用例,专门对准那些无声的失败:伪造的工具返回、互相矛盾的指令、被注入的内容,以及一个本该以拒答或弃答收场的任务。
  • 随机采样一部分轨迹,而不只是那些被标记过的,并且盲标。"被标记的"与"随机采到的"之间的分歧率,会告诉你你的探测盲区有多大。
  • 保住自然的难度配比。一套纯从失败里收割来的集合会漂向病态输入,从此对中位请求不再有任何预测力——见为什么智能体评估很难
STEP 5

留一份你从不拿来调试的留出集,并假定其余部分都在泄漏。

创建时就把套件劈开:一份开发集,你可以看、可以迭代、可以针对它去修;一份留出集,只在发布时跑,从不逐条查看。这条纪律不受欢迎,可它是横在你与"一个衡量你有多努力的数字"之间的唯一一样东西。一名工程师打开留出集里的某条用例、去弄清它为什么挂了的那一刻,那条用例就已经加入开发集——把这写成明规则,把它挪过去,再补一条新的。

在此之上还要假定泄漏。发往托管 API 的用例已经不在你的控制之内;团队成员会记住他们调试过的那些;粘贴进某个 issue 线程的用例可能被索引。这是基准污染那个问题的私域版本,而实际后果是:一份留出集的保质期以季度计,不以年计。按计划轮换它,另留一小份从未跑过的备用集用于轮入;并且,看到留出集上一个可疑的大幅跃升时,请像对待公开基准上的同类跃升那样处理它——在排除之前,先当作一个污染假设。

STEP 6

给集合定版本,用重设基线代替跨版本比较。

一个分数如果不带着套件版本,就什么也不意味着;而"一次真实退步被当作升级发布出去"最常见的路径,正是拿一次比较跨过了中间变过的集合。两条规则能让这件事变安全:

  • 钉住并盖章。套件要有版本,而每一个上报的分数都要同时带上套件版本、裁判版本与外壳版本,与模型并列——就是灰度发布与版本化里那套元组纪律。两次运行之间被改过的一条裁判提示词,是对尺子的一次无声改动;而如果你用模型当裁判,它的校准会按自己的时间表衰减,这正是 LLM 充当裁判里的论证。
  • 每次改集合都重设基线。退役与新增之后,先把当前生产候选在新套件上重跑一遍,再谈别的。那个数字,而不是上个季度的数字,才是标尺。它只多花一次运行,却消掉了评测里最昂贵的那一类错误。

要预料到重设基线看上去会像一次退步,并且提前把话说在前面。用鲜活的失败替换掉已解决的用例,机械地就会拉低通过率;一个没被打过招呼的团队,会把一次健康的刷新读成质量下滑,然后回滚点什么。把两个数字并排公布——旧套件与新套件、同一个候选——这场讨论一分钟就能收场。这里同样适用别处那份统计上的谨慎:在一个靠评判的指标上,几百条用例上几个百分点的移动可能只是噪声,而告诉你是哪一种的,是评测方差与统计功效

下一周就按这个顺序做。一:把上个季度的比较运行拉出来,按用例算出它分开过两个候选多少次——然后看看"一次都没有"的那部分占多大。二:取区分力最高的那一成,把它做成每次提交的闸门,其余移到每晚跑。三:定一条退役规则,照它把死掉的用例归档,把腾出来的预算投进从生产收割来的替换用例,再留一片专给无声失败。四:切出一份留出集,并把"打开留出集里的用例即视为把它挪走"写成制度。哪怕你只做第一步,一个下午里你对自己评测套件的了解,也会超过通过率这一整年告诉你的。

延伸阅读:评估驱动的智能体开发,闸门是怎么搭的;质量回归检测,生产侧的对应物;结果评估 vs 轨迹评估,一条用例该断言什么;以及评估入门,最底下那一层。