跳到主要内容

不可信的 Trace Replay:为什么你的新模型评估在撒谎

· 阅读需 14 分钟
Tian Pan
Software Engineer

LLM 升级的标准流程往往具有单元测试那种令人安心的形态。捕获上周现有模型(incumbent model)的生产追踪数据(traces)。在候选模型(candidate model)上回放这些数据。对比输出差异(Diff)。如果不一致率低于某个阈值——比如 3% ——就发布。差异很小,仪表盘显示绿色,迁移看起来很安全。一周后,值班频道里充满了各种报告,称新模型在跨轮次对话中丢失上下文、调用工具时使用了无法解析的参数,并且自信地引用了早已从语料库中删除的文档。

回放并没有真正撒谎。它测量的是真实的东西。它只是测量了生产模型从未真正见过的上下文中的行为,而那个绿色的数字,只是一个除了在回放测试环境(replay harness)之外,在任何地方都不存在的分布上的置信区间。

回放中隐藏的假设是:一条追踪记录(trace)就足以完整记录一个轮次(turn)。在无状态的函数调用中——给定输入 (x, y),返回 f(x, y) ——这个假设是成立的。但在生产环境的 LLM 系统中,它几乎在任何地方都不成立。追踪记录捕获了请求、响应,或许还有中间的工具调用。但它没有捕获该轮次中已经预热的 KV cache,因为来自同一会话的前五十个轮次都在前缀(prefix)中。它没有捕获因为那一分钟内同一批次的其他三个用户发送了重叠前缀而命中的 prompt cache。它没有捕获智能体(agent)的内存存储,这些存储可能由凌晨 03:00 UTC 的一个早期会话写入,并被一个激进的 TTL 在 09:00 删除。它没有捕获在追踪数据创建和回放任务运行之间修改了数据库的工具副作用(side effect)。它没有捕获第三方 API,其昨天的响应是“200 OK 且包含负载 X”,而现在的响应是“404 —— 该记录已删除”。追踪记录是特定上下文中行为的快照,而回放仅仅重构了行为。

模型行为取决于回放无法重构的状态

剥离回放的表象看其假设,你会得到一张为了让绿色数字有意义而必须成立的清单。而其中的每一项都是不稳定的。

在任何现代推理栈中,KV cache 都是在具有重叠前缀的请求之间共享的。在 API 层级看起来完全相同的两个生产轮次——同样的系统提示词,同样的用户消息——可能会产生不同的延迟;而且在激进的前缀缓存设置下,如果缓存淘汰策略对前缀的切分不同,甚至可能产生不同的推理路径。回放时,每个轮次都是在冷启动、全新初始化的缓存下运行的,而候选模型的缓存布局在设计上就与生产模型不同。回放的延迟数据并非生产系统的延迟数据,任何由此得出的指标——包括拒绝率(因为超时表现为拒绝)——都带有冷启动系统对比热状态系统的偏差。

智能体的内存存储是第二个无形的依赖。追踪回放假设智能体在第 7 轮读取的内存与生产环境中第 7 轮时的内存一致。实际上,生产环境中智能体的第 7 轮读取的是同一会话中第 1 轮到第 6 轮写入的内容,可能还包括同一用户的另一个会话的第 N-K 轮内容,以及在第 4 轮和第 5 轮之间运行的后台摘要任务。回放要么 (a) 同时也回放内存写入,在这种情况下,它不再是单元测试,而是一个包含所有状态管理 bug 的会话模拟器;要么 (b) 冻结追踪捕获时的内存,在这种情况下,它衡量的是相对于一个可能比生产缓存淘汰策略所允许的还要陈旧的内存快照的行为。

工具副作用是第三个。昨天的追踪记录显示智能体调用了 create_invoice(amount=400, customer=42),工具返回了 {"invoice_id": "INV-99"}。今天回放同样的调用,针对同样的工具,工具要么创建了重复的发票(导致测试环境被污染),要么因为客户 ID 是已被清理的测试数据而返回 404,或者返回了一个新的发票 ID,导致下一轮的提示词不再符合预期。为了保留追踪记录的下游上下文,你必须要么模拟(mock)工具——这意味着你不再测试工具的行为,而只是测试模型对冻结工具存根(stub)的遵循情况;要么真实运行工具,并接受“真实”现在意味着“在一个从第 2 轮开始就与生产环境背离的沙箱中”。

智能体调用的上游服务——搜索 API、知识检索、第三方 SaaS——是第四个。在追踪和回放之间,世界已经发生了变化。维基百科的文章变了。Stripe API 的版本更新了一个小版本。内部搜索索引重新索引并降权了智能体引用的那一半文档。回放测量的是新模型在一个已不存在的上游世界快照中的表现。

回放的置信分数测量了错误的方差

即便承认回放是一种有用的冒烟测试,它所产生的置信分数也比团队所理解的区间要窄得多。回放的方差是“在相同状态下,两个模型对相同输入的输出”的方差。而生产系统的方差是“在实际状态分布、实际输入分布、实际的上游服务噪声以及实际的 Prompt 缓存命中率分布下,输出”的方差。后者严格来说比前者大得多,在团队最关心的故障模式上通常要大出一个数量级。

这至关重要,因为大多数团队为通过回放验证的迁移设定的门槛——无论是“一致率高于某个标准”还是“下游任务准确率在某个 delta 范围内”——都假设了回放的方差可以作为生产环境方差的可用估计。事实并非如此。那些生产系统行为取决于回放无法复现的状态的案例,系统性地正是新模型在生产环境中的行为会与旧模型产生分歧的案例。它们也系统性地被回放评分为“一致”,因为两个模型在给定相同的冻结上下文时,都会产生合理的输出。回放告诉你模型在简单情况下达成了一致。它无法告诉你当上下文碎片化、内存被部分剔除、工具调用返回意外错误,或者模型隐含依赖的前缀发生 Prompt 缓存未命中时,它们会表现如何。

有一种更尖锐的方式来表述这个问题:Trace 回放是在候选模型被“免费”赋予上下文的情况下,对其输出可复现性的测量。而生产环境则是衡量模型在一个噪声多、易变且部分可观测的世界中,自行构建上下文时的输出质量。这是不同的任务。一个擅长前者的模型可能在后者表现糟糕。回放产生的数据对你面对的是哪种情况保持沉默。

影子流量不是换了标签的回放

那些被回放的盲点坑过的团队,其本能反应是升级到影子流量:将每一条实时请求的副本发送给候选模型,将输出与现有模型进行对比,当分歧率足够低时发布。影子流量修复了回放最严重的一类 Bug——它让候选模型针对实时状态运行,涉及实际的上游服务、实际的存储、实际的工具副作用以及实际的 Prompt 缓存竞争。这确实是一个更强的信号。

然而,它并不是一个完整的信号。影子流量有其自身会悄然失效的假设。候选模型接收到的输入是由“现有模型”的行为塑造的。会话第 7 轮的输入取决于第 6 轮的输出,而在生产环境中,第 6 轮是由现有模型产生的。影子候选模型看到的第 7 轮输入反映的是现有模型的推理,而非它自己的。如果候选模型本会产生不同的第 6 轮,那么它在影子流量中看到的第 7 轮输入就不是它在实时运行环境下真正会看到的输入。这就是强化学习文献二十年来一直在攻克的“离策评估”(Off-policy Evaluation)问题,而 LLM 迁移的故事中几乎从未提及它。

影子流量在成本上也与回放不同。回放是针对捕获的数据离线运行的。影子流量则是针对实时负载实时运行的,这意味着在影子测试期间,你正在支付双倍的推理成本。那些将影子流量定价为“我们就开两周”的团队事后会发现,两周的双倍推理成本是一笔可观的支出,于是窗口被缩短,候选模型在长尾状态相关的故障模式有机会显现之前就发布了。

诚实的说法是,回放和影子流量是两种不同的验证原语,具有不同的成本剖析和不同的盲点。回放成本低、速度快,但在结构上倾向于对状态相关的行为过度自信。影子流量成本高、信号积累慢,但在结构上倾向于对反事实会话动态过度自信。两者都不能独立作为发布门禁。

具备状态感知能力的迁移规范是什么样的

那些经历过几次迁移并幸存下来的团队,已经收敛出了一些“回放后发布”脚本中不具备的规范转变。

第一点是将回放视为冒烟测试,而非发布门禁。绿色的回放结果意味着候选模型在简单情况下没有退化——即在相同上下文中对相同问题的回答没有退化。这值得了解,但不等同于“可以安全发布”,团队不应让领导层这样解读。回放的作用是尽早捕获微不足道的回归,这样更昂贵的验证原语就不会浪费在连简单情况都处理不好的候选模型上。

第二点是在得出任何结论之前,审计回放集的“状态依赖性”。回放语料库中的每一个 Trace 都带有隐性依赖——依赖于早期会话的内存写入、工具副作用、上游服务状态或缓存配置。状态依赖性审计会遍历语料库,并标记出回放无法忠实复现的 Trace。有些 Trace 是干净的:无状态的单轮请求,输入完全决定输出。有些是脏的:具有内存依赖和工具副作用的多轮会话。回放的总指标应该分别报告干净切片和脏切片的一致性,并且要理解脏切片的数值与其说是信号,不如说是噪声。

第三点是按信号量而非仪式感来确定影子流量的规模。两周 1% 流量的影子窗口产生的数据,其参考价值不如三天 10% 流量的窗口,因为你最需要捕获的故障模式是长尾行为,它们需要大量的绝对样本量才能浮现。影子规模的决策应该由你需要以多大的统计置信度检测出多大的故障率来驱动,而不是由哪个比例“感觉安全”来驱动。如果计算结果显示,要在你负担得起的预算内检测出你关心的故障率,窗口必须长达三个月,那么这就是一个信号:影子流量不是这次迁移的正确原语。

第四点是金丝雀发布规范,将回放和影子流量的数值视为先验概率,而非后验结论。金丝雀发布是候选模型第一次产生生产系统会采取行动的输出,也是候选模型的行为第一次塑造它在下一轮将看到的输入分布。金丝雀发布的最初 1% 阶段比后续阶段更值得关注回滚敏感度,因为之前验证无法看到的那些状态相关的故障模式最有可能在那里浮现。一个在 24 小时内从 1% 增加到 10% 的金丝雀发布,并没有学到任何之前验证没说过的新东西。而一个维持在 1% 流量一周,并监测长尾结果指标(按任务类别的拒绝率、下游用户行为率、升级率)的金丝雀发布,才是在完成回放无法完成的工作。

第五点是诚实的置信区间。一个团队说“候选模型在回放集上的一致率为 97.2%”,只是在报告测试框架产生的一个数字。而另一个团队说“候选模型在回放集上的一致率为 97.2%,但回放集中有 34% 的状态依赖 Trace,我们对这些结果进行了降权处理;影子窗口在长尾故障模式上有 4 天的信号积累缺口;我们估计在第一阶段金丝雀发布中出现未检测到的回归的概率约为十五分之一”,这才是报告了一个包含验证过程中无法观测到的方差的置信区间。第二个团队在上线前的评审中可能不会受欢迎,但在第一次没有让他们感到意外的迁移之后,他们会备受尊敬。

重放能告诉你什么,不能告诉你什么

这背后架构层面的实现并不浪漫。Trace 是特定上下文中的行为记录。重放(Replay)重构了行为,但它并不重构上下文。如果一个团队仅凭重放一致性(replay agreement)的数值就进行迁移,他们发布的是对一种在重放测试环境之外并不存在的分布(distribution)的信心声明。而生产系统则会通过引发用户可见的事故,来迫使团队发现系统实际所处的真实分布,以此作为“发现”的代价。

没有任何一种验证原语(validation primitive)能让你以零风险的方式,获得新模型生产行为的、状态忠实(state-faithful)的预览。重放带给你的是廉价且有偏差的结果。影子模式(Shadow)带给你的是昂贵且在反事实层面有所偏离的结果。金丝雀发布(Canary)带给你的是真实且小规模的结果。如果一个迁移方案将这三者视为“成本 vs 信号”曲线上的不同点,并为每一点预留诚实的置信区间,它未必能避免所有的迁移事故。但它至少能知道,每个新模型最可能在哪个失效类别上带来“意外”——而这正是该团队最初搭建重放测试环境时,原本以为自己购买到的核心价值。

References:Let's stay in touch and Follow me for more thoughts and updates