跳到主要内容

删除评估用例是决策,而非清理

· 阅读需 11 分钟
Tian Pan
Software Engineer

每个评测套件(eval suite)最终都会被精简。有人注意到套件运行需要 9 分钟,每次运行成本 40 美元,而且里面充满了没人记得为什么要写的用例。他们提交了一个名为 “清理陈旧评测用例” 的 PR,删除了 40 条 “看起来不再相关” 的条目,CI 运行时间降到了 4 分钟。PR 获得了点赞。没人反对,因为删除测试看起来就像是在做维护。

这不是维护。每一个评测用例都是团队对自己做出的承诺:这种失败模式不会再静默地发生。 删除用例就意味着撤销了这项保证。通过率没有变化,仪表板依然是绿色的,唯一消失的是团队对这项保证曾经存在过的记忆。六个月后,一次模型迁移重新引入了被删除用例所防范的回归,复盘(postmortem)重新发现了团队已经支付过代价的教训,然后有人写道 “我们应该为此添加一个测试” —— 而这个测试正是之前在清理 PR 中被删除的那个。

这种不对称性正是问题的核心。添加评测用例是可见的工作 —— 它会出现在代码审查中,有作者,通常还会引用某个事故。而删除用例则是不可见的工作,因为一个仍然通过的小型套件看起来绝对比大型套件更好。因此,套件在严密审查下增长,却在缺乏关注时萎缩。这恰恰反了。删除是风险更高的操作,但却是没人审查的操作。

评测套件是承诺的分类账

传统的单元测试套件具有一种宽容的属性:它的大多数用例都是可推导的。如果你删除了一个纯函数的测试,一个胜任的工程师可以通过查看该函数,从头开始编写一个等效的测试。测试编码了在代码中仍然可见的逻辑。

评测套件并非如此运作。其中很大一部分用例是无法从任何地方推导出来的 —— 它们是 从现实中收割而来的。用例之所以存在,是因为特定用户输入了特定内容,模型给出了特定的错误答案,而有人判定这类失败值得防范。这种渊源正是该用例的全部价值所在。输入是一个任意字符串。预期的行为是某人在真实事故的压力下做出的判断。删除该用例,代码库中就没有任何东西可以用来重建它。你不是移除了一项测试;你是移除了一项你再也无法观测到的关于世界的实情。

这就是为什么 “像对待代码一样对待评测数据集,进行版本控制,精简过时用例” 这种标准建议只对了一半。对数据集进行版本控制保留了已删除用例的 内容 —— 你可以通过 git log 找回 JSON 文件。但版本控制无法保留的是 信号。未来的工程师不会去检索一万行数据集历史,想知道某个失败模式是否曾经被测试过。他们看的是当前的套件,发现没有针对他们刚发布的 Bug 的用例,于是得出结论:团队从未考虑过这个问题。被删除的保证在功能上已经消失了,尽管在技术上那些字节是可以恢复的。

因此,与其将套件理解为数据集,不如将其理解为分类账(ledger)。每一行都是一个承诺:我们已经决定这种行为很重要,并且我们致力于检测它的回归。 一个删除了 40 行的清理 PR,就是 40 个被打破且没有记录在案的承诺。

为什么套件会因为错误的原因被精简

精简的压力是真实的,且理由通常在表面上是合理的:

  • 成本。 每个用例都是一次 LLM 调用,如果你使用 LLM 评审员(LLM judge),通常需要多次调用。每个 PR 运行数千个用例是一笔实打实的支出。
  • 延迟。 一个 9 分钟的评测门控会让工程师养成合并后就走开的习惯,这使门控失去了意义。
  • 噪点。 用例会漂移。产品发生了变化,用例的预期输出现在是错误的,它因为与模型质量无关的原因而失败。一个不稳定的用例会削弱对整个套件的信任。

注意这三点的共同之处:它们都是关于用例 成本 的论点。没有一个是关于该用例所编码的保证的 价值 的论点。而进行精简的工程师几乎总是在优化他们能看到的东西 —— CI 时间、API 账单 —— 而忽略了他们看不到的东西,即这种特定失败模式再次发生的概率。

这种信息差距就是组织层面的裂缝。从结构上讲,为了速度而精简套件的人,并不是当保证失效时被传呼(paged)的人。精简人员在本赛程中进行基础设施维护。而错误删除带来的代价则落在未来的值班工程师身上,可能是在另一个季度,甚至在另一个团队,他们无从得知这种失败曾被预料到。清理工作之所以感觉是免费的,恰恰是因为它的账单被寄给了别人。

真正陈旧的用例 —— 其预期输出与当前产品行为相矛盾的用例 —— 应该 被移除。重点不在于套件必须只增不减。重点在于 “这个用例有噪点” 和 “这个保证不再重要” 是两个不同的命题,而清理 PR 将它们混为一谈。

借鉴 API 的弃用生命周期

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates