生成式 AI 质量工程(三):明明只换了一个模型,为什么实验仍然无法解释
副标题:用 Freeze、Replay 和配对评测证明一次 AI 改动真的有效
作者:James Xie
这是《生成式 AI 质量工程:发现、定位与证明》系列的最后一篇。第一篇讲发现——HTTP 200 之下答案可能已经坏了;第二篇讲定位——用最早异常边界找到从哪一层开始坏。
这一篇讲最后一步:根因找到了,模块改了,你怎么证明真的修好了?
排序模块修完的那个周五,团队的气氛很好。
改动的来龙去脉都清楚:第二篇那个案例,排序模型对新增的”合同范本”类文档打分偏高,把判例压了下去。修复方案是加文档类型权重。改动不大,review 也过了。负责的同学拿了十几条之前投诉过的 query 跑了一遍,逐条看答案:“这条对了""这条也对了""这条比之前好多了”。十几条全绿。
周报会上大家达成共识:修好了,上线。散会的时候有人说了一句”要不要再多跑点 case”,另一个人说”都对了还跑什么”,这事就过去了。
两周后,合同纠纷类目的点踩率又抬头了。不是原来的问题——排序的回归没有复发——而是另一批 query 的答案变差了。修复挪动了排序的分数分布,受益的是判例类文档,受害的是法规条文类文档。拆了一端的跷跷板,另一端翘了起来。没有人发现,因为验收那天跑的十几条 query,全是当初报修的那十几条。
复盘会上最值得记录的一句话,是那个负责的同学说的:“我当时真的以为我验证过了。”
他确实验证了——用错误的方式。这个失败不是技术的失败,是实验的失败。它让我认真想明白了一件事:在 AI 系统里,“跑了一下感觉好了”和”证明修好了”之间,隔着一整套受控实验方法。 这篇就讲这套方法:Freeze、Replay、配对评测,以及最后那个签字画押的准入决定。
一、“参数一样”为什么不代表实验可比
先从一个常见的错觉说起。
很多团队做 AI 实验的方式是:改一个东西,跑一遍评测集,看分数涨没涨。控制变量的意识是有的——“我们只改了模型,其他参数都一样”。temperature 一样,max_tokens 一样,prompt 文件没动过,git diff 里干干净净只有一行。
但”参数一样”和”实验可比”之间,差着一整个隐藏变量清单:
- 知识库快照变了吗? 你的知识库每天凌晨增量更新。今天跑的评测和昨天跑的,检索的是两个语料库。模型没换,但模型看到的材料换了。
- 检索策略变了吗? 召回 top-k、相似度阈值、混合检索的权重——这些配置不在你的改动 diff 里,但可能在别人的 diff 里,昨天刚上线。
- 工具返回变了吗? 链路里调的外部服务(法条查询 API、裁判文书库接口),它们返回的内容随时间变化,不归你管,也不通知你。
- Judge 变了吗? 用 LLM 评分,Judge 模型本身升级了,分数的口径就变了。你测的是被测系统,还是测的尺子的刻度?
- 流量变了吗? 在线对比时,今天和昨天的用户问的不是同一批问题。月底咨询劳动纠纷的多,月初咨询合同审查的多,这不是模型在波动,是生活在波动。
“明明只换了一个模型”,但实验仍然无法解释——因为模型只是你知道自己改了的那个变量。真正毁掉实验的,是那些你没意识到也在变的变量。
这种实验的结局通常有三种,哪一种都不好受:结论是假的(提升来自别的变量,回滚后”效果”还在);结论是真的但没法证明(评审的人无法复现你的数字,只能信你的人格);最惨的是结论无法复现也没人追究,下次有人用同样松的方法”证明”了一个假提升,把真提升冲掉了。
一次实验的结论要可信,第一步不是跑实验,而是把所有相关变量钉死。钉不死全部,至少要钉死那些会影响结论的,并且把没钉死的记录在案。
二、一次实验必须绑定哪些版本
我们的做法是给每次实验生成一份 Experiment Manifest——实验的”出生证明”,开跑之前写死,跑完之后归档。核心内容是一份版本绑定清单:
- 被测改动:哪个模块、哪个 commit、什么配置
- 模型版本:生成用哪个、Judge 用哪个(是两个独立绑定,混用是常见错误)
- prompt 模板版本
- 检索策略(Policy)版本
- 知识库快照 ID
- 评测集版本和评分 rubric 版本
- 配对样本集 ID
- 随机种子策略
- 已知未冻结的变量(诚实声明,比如”外部裁判文书接口未录制”)
这份清单看起来官僚,但每一条都是用教训换来的。知识库快照 ID 缺席的那次,“模型升级效果显著”的结论三天后被推翻——效果来自知识库刚好收录了一批新法规,换不换模型它都在那里。Judge 版本缺席的那次更荒唐:两个模型对比实验,Control 用 Judge-v3 评的,Test 用 Judge-v4 评的,“提升”里有一半是尺子换刻度。
Manifest 的另一个价值在三个月后。总有人会问:“当时那个提升是真的吗?“没有 Manifest,这个问题靠回忆回答,回忆靠不住;有了它,任何人可以按清单把实验原样复跑一遍,自己得出结论。可复现性不是美德,是实验和故事会的分界线。
没有 Manifest 的实验,结论只是一个观点。
三、Freeze/Replay 的依赖闭包
清单列出来,怎么钉死?靠两个机制:Freeze 和 Replay。
概念不复杂。一个模块的行为不只取决于它自己的版本,还取决于它吃进来的所有输入,而输入又有输入。把”会影响实验结论的所有上游依赖”追到底,这个集合叫依赖闭包。
追闭包这件事比听起来容易漏。以检索模块为例:它依赖向量库里的知识库快照,依赖 embedding 模型的版本(换了 embedding 模型,同一个 query 的向量就变了),依赖索引的构建参数,还依赖查询改写那一步用的 prompt。四个依赖里,大多数人只想得起第一个。
Freeze 是把闭包里的依赖固定在实验开始时刻的状态:知识库打个快照锁定,外部 API 的响应录下来,prompt 和策略绑定版本号,embedding 模型钉住版本。Replay 是实验重跑时不碰活的上游,而是回放冻结时刻的输入——检索不再查活的向量库,而是返回冻结的那批文档;外部工具不再实时调用,而是返回录制的响应。

为什么必须 Replay 而不是”每次现查”?因为活的上游是动的。两次实验之间知识库多收录了五十篇文档,你测出来的差异里就混着这五十篇文档的贡献。Replay 把”时间”这个变量从实验里摘了出去——同一批冻结输入,Control 跑一遍,Test 跑一遍,差异只能来自被测改动。
工程实现上,Replay 不需要什么高大上的平台。在最薄的地方拦一刀:给检索客户端和工具调用层加一个”录制/回放”开关,录制模式把每次调用的输入输出存成文件,回放模式按输入查文件返回。几百行代码,回报是整个实验体系的根基。
四、为不同模块选择 Freeze 边界
Freeze 边界画在哪,取决于你改的是哪个模块。原则:边界画在被测模块的输入口,把它和活的上游隔开;被测模块的下游尽量不动,让改动的影响自然流过去。
具体到我们链路上的几种典型改动:
改排序(我们那个案例):冻结检索输出——Replay 固定的文档候选集,Test 只换排序逻辑,看下游上下文选择和答案的变化。这样检索层的任何日常变化都污染不了实验。
改 prompt:冻结整个检索和上下文链路——模型输入的材料部分完全相同,只有 prompt 模板是变量。prompt 实验最常见的不严谨之处是顺手”优化”了一下检索参数,两个变量一起变,结论对半劈。
改模型:冻结模型输入的字面内容——连拼好的 prompt 都固定,唯一变量是模型本身。这是对 Freeze 要求最高的改动,也是”只换了一个模型”这类实验最容易翻车的地方:你以为只换了模型,实际上模型输入每天都在随着知识库漂移。字面级冻结之后还会剩最后一个不可冻结项:托管模型服务端的静默变化,这个只能用重复实验和时间戳记录来兜底。
改检索:这个最麻烦,因为检索的上游是知识库,知识库是活的。必须冻结知识库快照,否则”检索策略优化”和”知识库日常更新”的贡献永远纠缠在一起。
边界画错的代价是结论失真;边界不画的代价是没有结论。还有一个常见错误是边界画得太宽——把被测模块的下游也冻结了,结果实验只能证明”模块输出变了”,证明不了”答案变好了”。下游必须保持活着,改动的效果才传导得到最终答案。排序实验里如果只比较排序前后的文档顺序,就永远发现不了”法规条文类文档被压下去后,答案开始引用错误的法条”——伤害发生在下游三站之外。
五、为什么必须使用配对样本
输入冻结了,样本怎么选?
答案是配对(paired):Control 和 Test 跑同一批样本,逐条对齐比较,而不是各跑各的、最后比平均分。
不配对的问题在于样本难度是不均匀的。一批 query 里,有送分题(“劳动合同法什么时候施行”),也有地狱题(“关联公司混同用工,经济补偿金的工作年限怎么算”)。地狱题的分数天然低,波动天然大。两组各跑一百条,哪怕从同一个池子随机抽,难度分布的运气差异就足以淹没一个中小幅度的真实提升——A 组多抽中两条地狱题,均分掉三分,你的真实提升只有两分,结论直接写反。
配对消除了这个噪音:每条样本都是自己和自己比——同一条 query,Control 答得怎么样,Test 答得怎么样。难度差异在减法里被消掉了,剩下的才是改动的效果。这是统计学里最古老的设计,吃药的和吃安慰剂的最好是同一批人,隔周互换。
样本集的构成直接决定实验的效度。我们的配对集四部分组成:
- 报修样本:这次修复针对的 bad case(验证”病治好了”)
- 同域样本:同类目、相邻类目的正常 query(验证”没误伤邻居”)
- 远域样本:其他类目的抽样(验证”没误伤全局”)
- 健康标杆:历史上答案一直很好的 query(验证”没打破本来对的东西”)
开头那个周五的验收为什么失败?样本只有第一类。修复在报修样本上当然有效——它就是为了这些 case 调的,拿调参的训练集当验收的测试集,这是机器学习入门的禁忌,在 AI 工程里换个马甲天天上演。第二类样本哪怕抽二十条法规条文类的 query,跷跷板另一端的翘起当天就能看见。
只验证”想让它变好的”,不验证”不想让它变坏的”,是 AI 实验最普遍的自欺。
六、win/tie/loss 和差值分布
配对跑完,怎么读结果?
不要看平均分。平均分把一切都揉碎了。我们的基本报表是 win/tie/loss:每条配对样本,Test 比 Control 好、持平、还是差,三个计数。
这张表比平均分诚实得多。平均分 +1.5 可能是”普遍小好”,也可能是”一半样本大幅变好、另一半悄悄变坏”。win/tie/loss 把这两种故事分得清清楚楚:前者是 60/30/10,后者是 45/10/45。同样的平均分,一个是发布候选,一个是事故预警。
在 win/tie/loss 之上,再看差值分布:每条配对的分数差画成直方图。一个健康的提升,分布应该整体右移、形状不变;一个危险的提升,分布往往是右峰没变、左尾拉长——大多数样本没变,少数样本明显变好,同时少数样本明显变坏。重点看左尾:变差的那些样本,差了多少、是哪些、属于哪个类目。提升幅度再大,左尾只要出现”从优秀掉到不可用”的样本,就必须逐条人工过。
还有一个统计常识不能不提:样本量决定你能分辨多小的效果。几十条样本的配对集,只能检测”翻天覆地”级别的变化;百分之几的提升,需要几百条以上的配对样本才说得清。样本量不够的时候,诚实的结论是”差异不显著”,而不是”看起来有提升”。“看起来有提升”这五个字,葬送过的工程资源比任何 bug 都多。
七、随机生成如何做重复实验
生成式系统有内在的随机性——哪怕 temperature 调低,decoding 路径仍然有方差。同一个输入,同一个模型,跑两次,答案不完全一样。
这意味着单次运行的 win/loss 里混着运气。一条样本 Test 赢了,可能是改动真有效,也可能是这次采样运气好。我们的处理是重复实验:关键样本跑 N 次(通常 3 到 5 次),把结论建立在重复后的统计上:
- N 次全胜才算 win,全败才算 loss,其余归入 unstable
- unstable 占比本身是一个指标——一个改动如果让答案变得忽好忽坏,哪怕平均分涨了,也是质量恶化的一种形式。生产系统里,不稳定的体验和稳定的坏体验一样伤用户信任,前者甚至更伤,因为用户无法形成预期
固定随机种子能压掉一部分方差,但压不掉全部——托管模型的服务端行为不完全由种子决定,负载、batching、服务端版本都在悄悄影响输出。所以重复实验这步省不掉。它直接回答开头清单里的那个问题:质量变化不是随机波动——是还是不是,重复五次就知道。
八、LLM Judge 如何用人工样本校准
评测靠 LLM Judge 打分,那 Judge 自己准不准?这是整套实验体系里最脆弱的一环:如果尺子不准,前面所有控制变量的努力都白费。
LLM Judge 有已知的系统性偏好:偏爱更长的答案、更自信的语气、更工整的格式、更多的分点。这意味着一个让答案”看起来更专业”的改动,天然会在 Judge 那里得分——不管它是否更正确。法律场景里这个偏差尤其危险:法条引用错了但表述自信的答案,Judge 给的分常常高于引用正确但表述谨慎的答案。
我们的办法是给 Judge 配一套人工锚定样本:几十条答案,由团队里法律背景最强的同事逐条人工评分,写清评分理由,作为金标准。锚定样本的制作是一次性投入,评的时候很痛苦,但做好之后长期复用。
锚定样本有两个用法。一是开跑前校准:每次实验开跑前,先让 Judge 跑一遍锚定样本,看它的评分和人工评分的相关度。相关度不达标,实验暂停——先修尺子,再量东西。二是漂移检测:Judge 模型提供方会静默升级,今天和上周的 Judge 可能不是同一个 Judge。锚定样本是固定考点,Judge 分数在上面出现异动,说明尺子变了,所有跨时间的分数比较都要重新校准。
这直接回应开头清单里的”Judge 变化没有制造虚假提升”。虚假提升最常见的来源就是 Judge:被测答案变得更长、更自信、格式更漂亮,Judge 的偏好被迎合了,分数涨了,真实质量没动。人工锚定样本的存在,让这种”骗过尺子”的行为有机会被抓住——因为人工评的是真实质量,对不上就是 Judge 的问题。
九、为什么严重错误不能被平均分抵消
法律场景有一条铁律:一次严重错误,抵消一百次正确回答。
用户问”工伤认定时效”,AI 答错了一个期限数字——对用户来说,这不是”这道题扣两分”,这可能是他错过申请时效、丢掉赔偿的实体权利。这类错误的危害性是悬崖式的,不是斜坡式的。答得平庸,用户将信将疑再去核实;答得自信而错误,用户可能直接拿去行动。
所以评测体系里必须有一票否决项(hard fail)。我们的否决清单包括:法条引用错误(条款张冠李戴)、数字性结论错误(时效、金额、比例、年限)、虚构不存在的条款或判例、把单方行为说成双方协商、把强制性规定说成任意性规定。这些错误出现一次,该样本直接判 loss,无论其余部分写得多漂亮,也无论整体平均分多高。
win/tie/loss 报表之外,我们的准入检查单列一行:hard fail 计数,任何改动不允许引入新增 hard fail,一个都不行。 平均分涨 5 分但新增两条法条引用错误的改动,结论不是”利大于弊”,而是”不予发布”。这条线没有谈判空间——一旦开了”用收益抵消严重错误”的口子,每个改动都会带着自己的账本前来游说。
平均数思维来自互联网产品的习惯——亿级用户里坏 0.1% 的体验可以接受。但法律 AI 的用户不是流量,是一个个拿着具体麻烦来求助的人。对他们,平均数没有意义,个案就是全部。
十、从离线实验到 Shadow、灰度和生产验证
离线配对实验全绿,是不是就能全量上线?还不是。离线环境再逼真,和生产之间还隔着真实流量——离线样本是”你以为用户会问的”,生产流量是”用户实际在问的”,两者之间永远有长尾的落差。我们的验证分四级,逐级放行:

离线实验:前面九节讲的全部内容。成本最低,迭代最快,用来枪毙大多数改动。通过标准:win/tie/loss 显著为正、左尾人工过审、无新增 hard fail、unstable 占比不升。
Shadow(影子运行):改动部署到生产环境但不服务用户——真实流量进来,新旧两套逻辑并行跑,只记录不展示。它验证离线覆盖不到的东西:真实 query 的长尾形态、真实负载下的延迟和资源消耗、离线样本集完全没见过的问法。Shadow 跑几天,新旧答案的 win/tie/loss 在真实流量上再算一遍。
灰度:小比例真实用户切到新版本,监控三层指标——Component、Funnel、Product——任何一层出现异常,自动回滚。第二篇的三层指标体系在这里第二次救命:Funnel 层的空召回率异动,往往比 Product 层的投诉早到两三天,那就是你的撤退窗口。
生产验证:全量之后持续盯 Product 层的滞后指标。有些问题只有量级和时间才能暴露——比如某类低频 query 的回归,灰度期根本碰不到几条。承认这一点,就不该在全量那一刻宣布胜利。我们的惯例是全量后观察一个完整的业务周期,指标稳住了,实验才算真正关闭。
四级每一级都有明确的通过标准和回滚条件。改动在任意一级被枪毙,都不是失败——被便宜的环节枪毙,恰恰是这套流程在替你省钱。 离线枪毙一次,成本是几小时;灰度枪毙一次,成本是几天和一批用户的信任;生产事故枪毙一次,成本写进复盘会。
十一、把生产 bad case 变成长期回归案例
最后一环,也是让整个体系自我增值的一环。
每一个在生产上被发现的 bad case——投诉的、点踩的、人工抽检抓到的——处理完都不该结束。它应该被加工成一条 Regression Case 入库:query(或脱敏后的等价物)、当时的版本四元组、错误的答案、正确的期望、根因分类、最早异常边界位置。
这个库的价值随时间复利增长。以后任何改动上线前,配对集里自动带上与改动模块相关的历史回归案例——修过一次的坑,不允许以任何形式塌第二次。第二篇那个排序案例修复上线时,它自己就变成了回归库里的一条案例:文档类型权重的任何后续调整,都必须先在这批案例上验证不复发。那个周五验收漏掉的法规条文类 query,也在库里——它成了后来所有排序改动的必考科目。
回归库还改变团队的记忆方式。没有它,机构的记忆依赖个人——“这个问题小王以前修过”,小王离职了,记忆就离职了。有了它,每次质量事故都沉淀成可执行的资产,团队换血不伤筋动骨。
至此,整个系列形成了一个闭环:生产上的 bad case 进入观测(第一篇),被定位到根因(第二篇),修复被受控实验证明(第三篇),然后 bad case 本身变成回归资产,在下一次改动时替你站岗(回到第一篇的观测体系里)。环一旦转起来,系统的质量就不再依赖某个人的水平,而是依赖流程本身。
结语:证明,是质量工程的最后一块拼图
这篇的核心产物,五件东西:
- Experiment Manifest:每次实验的版本绑定清单
- Frozen Inputs:Freeze/Replay 钉死的依赖闭包
- Paired Results:配对样本的 win/tie/loss 与差值分布
- Admission Decision:带一票否决项的准入决定
- Regression Cases:由生产 bad case 转化的长期回归资产
回头看这三篇,它们其实是一套递进的纪律:
第一篇说,别信 HTTP 200,给质量建一套自己的观测。第二篇说,别从最终答案猜,沿链路找第一次异常的地方。第三篇说,别信”跑了一下感觉好了”,用受控实验拿出证据。
可观测性发现问题,阶段比较定位问题,受控实验才有资格证明问题已经修复。
这套东西没有一项是高深的。它没有要求你发明新的模型,只要求你像对待实验科学一样对待自己的系统:控制变量、配对比较、重复验证、保留证据。生成式 AI 的不确定性是真实的,但”不确定”不等于”不可测”。工程的意义从来不是消除不确定性,而是在不确定性之上,建一座让人敢做决定的桥。
那个周五验收通过的同学,后来成了团队里实验规范执行得最较真的人。踩过坑的人最懂坑的形状——这大概是这套体系在流程之外,留给团队的另一笔资产。
🖼️ [封面图]: 极简数据主义风格,一座精密的天平,左侧托盘是 frozen 状态的冰蓝色数据晶体,右侧托盘是流动的橙色火焰数据流,天平指针微微倾向一侧,深邃黑背景,冷峻高智力感,8k 分辨率