fedorthinks
全部笔记

METHODOLOGY · 2026年7月3日

你没法给一次掷骰子写单元测试

开发者把一个 LLM 塞进系统,围着它写一个普通的通过/失败测试,眼看着它时灵时不灵,然后要么删掉测试,要么把模型 mock 到毫无意义。两种做法都错。一个概率性组件在结果有波动时并不是坏了——但「它会波动」不是停止测试它的通行证。你只是得改成去测分布,而不是测样本:给一组黄金样例带容差打分、用通过率把关、断言每次都必须成立的不变量,并在随机的内核和它外围确定性的外壳之间画一条硬边界。

你没法给一次掷骰子写单元测试

有个 bug,我一次又一次地看着聪明的工程师把它发上线。他们把一个 LLM 接进某个功能,然后做了件负责任的事——写测试:输入进去,断言输出恰好是那串预期的字符串。在他们的机器上通过。在 CI 里失败。重跑一次又通过了。于是他们做了两件事之一——删掉测试,或者把模型 mock 成返回一个罐头答案——于是系统里最可能出岔子的那一部分,成了唯一一个完全没有测试的部分。

错的不是那个不稳定的测试。错的是那个框架。你是在试着给一次掷骰子写单元测试。

对单次运行做断言是用错了工具

单元测试问的是一个关于确定性函数的是/否问题:给定 x,我是否恰好得到 y?对一个解析器来说这是对的问题,对一个模型来说这是个荒唐的问题——模型是被允许的,而且是 按设计 被允许的,把同一件真事用五种不同的说法讲出来。跑得够多,同一个提示词给你的是一个分布,不是一个值。对着那个分布里的单次抽样做断言几乎什么都告诉不了你;它只是把那次抛硬币挪进了你的 CI 流水线。

这就是为什么 AI 编程的生产力数字那么浑浊。个人感觉快,但有经验的开发者在真实任务上被测出来是 更慢,团队看到的是更多的拉取请求、但更长的评审时间和上升的返工率。这里头有很大一部分,是人们在用确定性的习惯去校验非确定性的输出,一次一个脆脆的断言,然后被淹死。

「它不是确定性的」是关于模型的一句真话,也是关于系统的一个偷懒借口。你没法把骰子钉住。你完全可以约束它被允许落在哪些点上。

测分布,别测样本

要做的事,是别再去测某一个正确答案,而开始测行为的形状:

  • 给一组黄金样例打分,别去匹配字符串。 留 30–100 对真实的输入/输出,然后用相似度、评分标准或模型充当裁判来给新的运行打分——而不是精确相等。你衡量的是质量,质量有一个区间;不是同一性,同一性没有区间。
  • 用通过率把关,而不是单次一片绿。 把这个用例跑 N 次,然后要求,比方说,95% 通过。二十次里错一次不是构建坏了——那是骰子在像骰子一样行事。而通过率越过一条线 才是 一个你能抓住的回归。
  • 断言那些每一次都必须成立的不变量。 内容会变;契约不能变。永远是合法的 JSON。永远不泄露另一个用户的数据。永远在 token 预算之内。对那三个必须拒绝的提示词一定拒绝。这些 就是 确定性的,你像测别的任何东西一样测它们。
  • 把随机的内核和确定性的外壳拆开。 模型外围的路由、解析、校验、重试和兜底都是普通代码——把它们狠狠地做单元测试,同时把模型 mock 掉。把概率性的测试留给那唯一一个真正是概率性的边界。

归根结底

非确定性不是跳过测试的理由——它是换一种方式测试的理由。一个你按不住的模型,恰恰是你系统里最需要一根被量化过的牵绳的那一部分,因为「它通常能用」正是不可靠软件从内部感受起来的样子,一直到它突然不能用的那一刻

别再对着一次走运的投掷做断言。给一组黄金样例带容差打分、用通过率把关、把不变量锁死,然后像你的命全系于此一样去给那个确定性的外壳写单元测试——因为在生产环境里,正是那个外壳在托着骰子。

评论

暂无评论

登录以参与讨论。

做第一个分享想法的人。