跳到主要内容

快照追踪测试:将生产环境追踪作为你的回归测试套件

· 阅读需 12 分钟
Tian Pan
Software Engineer

大多数团队作为回归测试套件运行的评估集,是由一名工程师在项目第三周手工挑选的。到了第六周,因为没人想在发布前动它,它就被冻结了;而到了第九个月,它正被用来拦截部署。产品已经调整了两次。用户群翻了三倍。LLM 在生产环境中实际遇到的案例与那个冻结的测试集重合度可能只有 40%。当测试集通过时,没人相信它;当它失败时,没人知道是真实的失败,还是案例已经过时。团队写了一份提议“v2 评估集”的文档,却从未真正动手。

与此同时,系统在生产环境中处理的每一个请求都已被记录在追踪后端中。每一个提示词、每一次工具调用、每一项中间输出、每一次拒绝、每一次重试——所有这些都存储在对象存储中,按时间索引并带有 span 标签,随时准备回放。团队所能拥有的最高保真度的测试语料库已经在磁盘上了。他们却从零开始构建了一个评估集,而不是从中读取。

这就是快照追踪测试(snapshot trace test):固定一部分具有代表性的生产环境追踪,在每次候选变更上回放它们,并对回放输出断言行为不变性。案例来自真实用户,而不是你的工程实习生在 Notion 文档中起草的合成提示词。其分布追踪的是真实产品,而不是测试集冻结时的产品。测试套件保持生命力,是因为流入其中的数据是活的。

诀窍不在于记录。大多数团队已经有了追踪。诀窍在于固定什么断言什么以及忽略什么的规范——并意识到,除非你的追踪记录本身被视为一个版本化的、可查询的一等公民数据集,否则这些都行不通。

停止手工挑选,开始采样

手工挑选的评估集有一个结构性问题:它代表了编写它的工程师所设想的系统处理情况,而不是用户实际发送的内容。在三周大时,这种设想的分布可能有 90% 的准确率。六个月后,在一次营销活动带来了新客群、一项改变调用模式的功能发布、一次模型升级微妙地改变了查询路由之后——设想的分布就成了一块化石。

生产环境追踪没有这个问题。根据定义,它们就是分布。唯一的问题是保留哪一部分。

简单采样——“从上周随机抓取一千条追踪”——比不采样还糟糕。大部分生产流量都是简单的:任何模型都能干净处理的查询,通过两次工具调用就能完成的工作流,以及彼此看起来完全相同的请求。随机采样会对简单案例进行过采样,而对你真正需要防范的失败模式进行欠采样。你的回归测试套件最终测试的是模型是否仍能回答“巴黎的天气怎么样”,这毫无意义。

你想要的是向边缘案例倾斜的分层采样。沿着重要的轴对追踪语料库进行分桶——用户分段、查询意图、工具调用深度、延迟区间、拒绝 vs. 补全、错误 vs. 成功——并独立地从每个桶中采样。为长尾分配一个不小的预算比例:触发了五次工具调用的查询、用户反驳的拒绝案例、模型自我修复的追踪。这些是模型更换或提示词更改最容易发生静默退化的案例。

定期更新固定的测试集。每周运行一个定时任务,淘汰最旧的 10% 追踪,并用来自相同层级的新样本替换它们,使测试集锚定在当前的分布上。这听起来像是在增加开销,直到你将其与替代方案进行比较:一个每周都离现实越来越远的冻结测试集,直到某人察觉到问题时,才不得不惊慌失措地进行全面重写。

精确相等陷阱

这是一个浪费了四分之一工程时间的失败模式:团队搭建了追踪回放,断言新模型的输出必须与记录的输出逐字符相等,然后在第一次运行时看着一半的测试集变红,并花费三个月时间去追踪良性差异。

LLM 的输出不像函数输出那样具有确定性。即使温度为零,相同的提示词在不同的模型版本、不同的服务商,甚至在推理内核重新调整后的同一服务商的不同日期,也可能产生不同的 token。措辞会变。列表顺序会变。模型会说“你可以执行 X”而不是“要执行 X,你可以”。这些对用户来说无关紧要。但它们都会破坏精确相等断言。

发布精确相等回放测试的团队会通过缓慢的方式吸取这一教训。他们会将案例标记为“预期不同”,直到异常列表比测试列表还要长。他们发明了正则归一化工具来去除空格并将所有内容转为小写,正则工具能捕获简单案例,但捕获不了真正的语义转变。他们最终得出结论“追踪回放不适用于 LLM”并回到人工编写的标准输出,这是一个错误的结论。

正确的结论是:相等是一个错误的原语。随机系统需要随机断言:不是“输出是否匹配”,而是“输出在这个特定的轴上是否落在准则定义的区间内”。你真正需要的断言库与 Jest 提供的不同,假装它们相同就是测试套件被废弃的原因。

你真正需要的断言库

在生产环境中有效的快照追踪测试(snapshot trace test)包含三类断言,每一类都针对系统的不同层级进行了优化,且没有一类是精确相等(exact equality)的。

输出的语义等价性(Semantic equivalence)。 面向用户的回答应与记录的回答含义相同,即使措辞有所不同。这就是“LLM 作为评审员”(LLM-as-judge)的用武之地:一个独立的模型——理想情况下是一个更强大的模型,配有结构化的评分标准(rubric)——来评估新输出是否保留了记录输出中的核心论点、安全属性、事实内容以及格式承诺。固定(Pin)评审模型,对评分标准进行版本化管理,并将两者视为测试套件的一部分,而不是可以被默默替换的基础设施。评审模型的升级如果导致评分漂移,这是一种回归(regression),你可能会误以为是被测系统中的模型发生了回归。

工具调用序列的结构化相等(Structural equality)。 Agent 应该以相同的顺序、使用相同形状的参数调用相同的工具,即使自然语言推理过程有所不同。这正是“精确相等”确实奏效的地方,因为工具调用是结构化的:函数名、参数 Schema 和顺序是离散且确定的,而不像正文那样。如果在某个追踪中,模型之前调用的是 search_db → fetch_doc → summarize,而现在调用的是 summarize → search_db → fetch_doc,这就是一种行为变更,而正文层面的语义检查会漏掉这一点。参数值通常需要更模糊的比较(例如,表达相同意图的查询字符串应当通过),但调用图(call graph)本身应该被严格固定。

延迟范围(Latency bands)而非点估计(point estimates)。 每个追踪都会记录每个 span 的实际 wall-clock 延迟。断言“延迟必须等于 1.42 秒”是荒谬的;断言“此追踪存储桶(trace bucket)的 p95 延迟必须保持在两秒以下”才是你与用户之间真正的契约。重新运行固定的测试集,计算延迟分布,将其作为范围(而非点)与记录的分布进行比较,并标记超过阈值的偏移。在工具密集型追踪中,模型更换导致尾部延迟翻倍,这类回归是精确相等回放永远无法发现的,而延迟范围断言在第一次运行时就能捕捉到。

心理模型:输出被评审(judged),工具调用被对比(diffed),延迟被限定范围(bounded)。每一层都针对其拥有的属性进行断言,而不是针对相等性。

追踪基础设施是前提条件

这是一个组织层面的认知,其内化过程往往比预想的要长:评估套件(eval suite)是追踪记录(trace-recording)基础设施的下游,而不是相反。

如果追踪记录是采样的、有损的,或者缺失了回放所需的字段——请求负载(payload)、系统提示词(system prompt)版本、工具定义、模型配置、随机数种子(RNG seed)——那么无论运行团队的技术多么高超,都无法构建评估套件。你无法回放一个没有捕获系统提示词的追踪。你无法对比一个参数 Schema 被 JSON 序列化器截断的工具调用序列。你无法针对一个因追踪后端限流而丢失时间数据的追踪来限定延迟范围。

能够出色运行快照追踪测试的团队首先会在追踪层做好三件事。他们捕获完整的请求信封(request envelope)——包括模型名称和版本、系统提示词哈希、工具定义、解码参数、完整的用户输入——这样六个月前的追踪就可以与今天的候选版本进行回放,实现公平的(apples-to-apples)比较。他们对追踪 Schema 进行版本化管理,这样在 Schema v3 下记录的追踪在记录层发布 v4 时不会默默丢失字段。并且他们将追踪作为可查询的数据集(queryable dataset)公开,而不是作为一行行日志流:包括 SQL 支持、元数据过滤、与用户细分表的关联,具备完整的数据仓库态势。对于任何比抽查单个失败更严肃的评估规程来说,只能通过请求 ID 搜索的追踪存储都是一种累赘。

这重新构架了许多关于预算的讨论。当团队询问“我们是否应该雇人来负责评估”时,更准确的问题往往是“我们是否应该投入资金建设拥有评估套件所需上游数据的追踪后端”。评估套件是可见的产物,而追踪后端是它生长的土壤。如果一个团队在只记录 80% 字段的追踪系统之上构建评估套件,那么随着追踪层慢慢填补空白,他们将不得不把同一个评估写上四遍。

周一该做什么

从“我们有追踪”进化到“我们有一套能捕捉真实回归的快照追踪回归套件”,需要采取以下三个具体行动。

第一,审计你的追踪记录的“回放完整性”。挑选三个生产环境的追踪,询问你今天是否可以重构出完全相同的请求——包括系统提示词、工具列表、模型版本、解码参数——并再次运行。如果其中任何一个字段的答案是否定的,那就是首先要解决的问题。评估套件可以等一等。

第二,挑选五十个追踪,针对你的核心用例进行分层并特意向边界情况(edge cases)倾斜,将它们固定为 v0 版本的回归测试集。抵制从五百个开始的诱惑——一个小规模、精心选择、定期更新的集合优于一个庞大且僵化的集合。根据意图和细分市场对它们进行标记,这样评估报告就能按层级分解失败,而不是只给出一个模糊的综合通过率。

第三,分三层构建断言层,而不是一层。输出语义使用 LLM 作为评审员。工具调用序列进行结构化对比(diff)。延迟采用分布范围。将每一层作为评估报告中的独立信号,这样一个改善了输出质量但使延迟翻倍的变更就能准确地呈现为——改进与回归并存——而不是一个单一且模糊的“分数下降”结论。

做到这一点的团队不再会讨论“我们的评估是否真实”。追踪是真实的,因为它们是用户发送的。评审是诚实的,因为评分标准经过了版本化。延迟检查是可靠的,因为记录捕获了它。套件保持生命力,是因为流入其中的数据保持着生命力。这就是全部的秘诀——也是那些内化了这一点的团队能够每周发布模型升级,而其他团队还在试图弄清楚上周四的部署是否导致了任何回归的原因。

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