把工具响应录下来回放,评测就公平了吗?

回放把环境钉死了:同一个请求,永远拿到同一份响应。换模型、换 prompt 时,比的终于是同一道题。

但它有三个坑:新模型走出录制时没走过的路(未命中),录下来的内容里藏着答案(数据泄漏),被测应用早就升级了而录制还是旧的(快照过期)。三个坑都会让回放结论偏离真实情况(差距被压扁、被抹平,甚至翻号),而且回放跑得越稳定,越不容易被发现。

1线上评测,分数里混着三种随机

BugHunt 的测试 Agent 在被测 Web 应用里找注入的 Bug。直接对着线上环境跑,一次评测的召回率里混着三样东西:

来源例子回放能去掉吗
环境的偶发故障某次请求 503、超时、页面没加载完能:录下来的响应每次都一样
被测应用在变今天发了版,昨天的 Bug 修了,又多了一个能:回放的是录制那一刻的版本
模型自己的随机性同一页面,这次看出了 Bug,下次没看出不能:回放的是工具响应,模型照样现场生成

所以回放之后照样要跑多次、照样要算区间(第 1 周)。回放解决的是"两个模型是不是在同一个环境里比",不是"一次就够"。

和第 3 周的假模型服务器方向相反:那里固定模型的响应,用来测 harness 写得对不对;这里固定工具的响应,用来比模型和 prompt。第 5 周断点续跑那一讲说"重放的是记录,不是动作",这里也一样:回放时不真的去访问页面,只把录下来的结果交给 Agent。

2回放什么:三层

层录 / 固定的是什么模型是真的吗用途
模型响应回放模型每一轮的输出(SSE 剧本)假的测 harness:重试、配对、停止条件(第 3 周)
工具响应回放每次工具调用的返回(cassette)真的换模型、换 prompt,在同一份环境响应上比
环境快照被测系统的初始状态(代码版本、数据、镜像);工具照常真实执行真的SWE-bench:每道题带 base_commit,评测在 Docker 里跑

工具响应回放最便宜:不用起被测应用,不怕它挂。代价是只能回答"录过的请求"。环境快照能回答任何请求,但要真的跑起被测系统。本章讲的主要是中间一层,第三层出现在"数据泄漏"那一节。

3录制:键、指纹、校验

  1. 键:回放按什么判断"这是同一个请求"。本章的合成实验用 (页面, 视图);Playwright 的 HAR 回放严格匹配 URL 和 HTTP 方法(POST 还匹配请求体);VCR.py 默认匹配 method、scheme、host、port、path、query。键太严,换个参数顺序就未命中;太松,不同的请求拿到同一份响应。
  2. 指纹:每条录制存一个内容哈希,算之前先去掉每次都变的字段(时间戳、请求 id)。不去掉的话,每一条都会被判成"和线上不一致"。
  3. 录制校验:录到 5xx、超时就重录,或者整条丢掉。不校验的话,录制那一刻的偶发故障会被冻结进 cassette,以后每次回放都复现它。
  4. 元数据:被测应用版本、录制时间、工具 schema 版本、答案表版本,和 cassette 存在一起。后面判断过期全靠它。
  5. 脱敏:请求头里的 token、cookie 不要进 cassette(VCR.py 有 filter_headers)。

4新模型走了没录过的路

cassette 是用基线模型 A 跑一遍录的,里面只有 A 发过的请求。候选模型 B 更爱探索:除了每页的默认视图,还会打开排序视图(?sort=price)。B 的这些请求在 cassette 里查不到,叫未命中。三种处理办法:

策略做法对应工具代价
当成报错返回 404 / 连接失败,让 Agent 继续HAR 回放 not_found="abort"(默认)B 永远看不到排序视图上的 Bug,系统性低估探索得多的模型
整回合作废抛异常,这一回合不计分VCR.py record_mode="none" / "once"(已有 cassette 时)不会算错,但走偏的模型可能一个有效回合都没有
回落线上去真实环境取,可以顺手补录HAR not_found="fallback"(发到真实网络);VCR.py new_episodes又引入了环境随机性;线上已升级时,一个回合里混着两个版本

本文推导(合成设定:答案表里 \(|K_\text{默认}|\) 个 Bug 在默认视图、\(|K_\text{排序}|\) 个在排序视图;模型看到 Bug 时以概率 \(d\) 认出来;线上每次请求以概率 \(f\) 返回 503;B 以概率 \(e\) 打开排序视图;cassette 里的默认视图都是 200):

$$\mathbb{E}[\text{召回}_\text{线上}] = d\,(1-f)\,\frac{|K_\text{默认}| + e\,|K_\text{排序}|}{|K|},\qquad \mathbb{E}[\text{召回}_\text{回放, 报错}] = d\,\frac{|K_\text{默认}|}{|K|}$$

回放那一项里没有 \(e\):B 探索得再多,回放分数也不涨。在默认设定(\(d_A = 0.80,\ d_B = 0.85,\ f = 0.1,\ e = 0.5\),4 个 Bug 两两分布)下,B − A 的期望差距线上是 +21.375 个百分点,回放只有 +2.50。各跑 20 次的实际结果更极端:线上 B 比 A 高 20.00 个百分点,回放 B 反而低 3.75 个百分点。期望差距是 +2.50,负号是 20 次的抽样误差;多跑几次 B 会回到略高于 A,但追不回线上那 20 个百分点。

未命中次数本身就是一个必须报告的指标。未命中率高的回放分数不能直接拿来比:它测的是"在 A 的轨迹上 B 表现如何",不是"B 表现如何"。

5数据泄漏:答案混进了录制

类型例子怎么查
答案藏在响应里录制用的是 debug 构建,Bug 页带注入标记;页面源码里的注释直接写着 Bug 在哪(本章用 HAR 录第 5 周的购物车页,真的录到了 // buggy 版忘了乘数量)按答案表的关键词和注入标记扫一遍 cassette
未来的信息SWE-bench 的 issue #465(2025-09-03)报告:Agent 用 git log --all 看到了修复这个问题的未来提交快照里删掉录制时间点之后的东西;工具返回的数据按时间截断
评测集进了训练或调优拿评测用的 cassette 调 prompt,再在同一批上报分数;公开评测集被爬进训练数据留出集;BIG-bench 的 canary 字符串约定
泄漏的典型症状:分数突然变好,而且方差变小。在合成实验里,打开 debug 录制后 A 和 B 的回放召回都是 50.00%,20 次的标准差是 0:默认视图上的两个 Bug 被标记"送"给了模型,排序视图上的照样看不到。

6快照过期:应用升级了,录制还是旧的

录制之后被测应用从 v1 升到 v2:p07 和 p05 排序视图的 Bug 修了,p09、p12 多了新 Bug,p02 改了文案。回放还在放 v1,它测的是一个已经不存在的应用。

过期了就重录过期的条目(或整份重录),并把重录前后各跑一遍的分数都留着,不然分数的变化说不清是模型变了还是应用变了。

▶互动演示:录一次,回放给两个模型

合成数据,不调任何模型。基线 A 只看 12 个页面的默认视图,录制用的就是它的一次运行;候选 B 还会以"探索比例"打开排序视图。答案表 v1 有 4 个 Bug(p03、p07 在默认视图,p05、p10 在排序视图)。每个模型在线上和回放各跑 k 次,同一个回合编号在两种模式下用同一串模型随机数(配对)。所有数字由页面上的 JS 现算,和本地 replay_eval.py 的输出逐位一致。

未命中时
线上版本
回放答案表
录制方式 构建
模型线上召回 均值 ± sd回放召回 均值 ± sd未命中作废回合回落线上混版本回合回放误报 / 回合
A 基线
B 候选
B − A

每个回合的召回(每个点是一次运行,同一值的点横向排开)

cassette:录了什么,现在还对不对

✎练习

题 1(算):按上面的推导,\(d_B = 0.85\)、\(f = 0.1\)、\(e = 0.5\),4 个 Bug 里 2 个在默认视图、2 个在排序视图。B 在线上的期望召回率是多少?
%
0.85 × 0.9 × (2 + 0.5 × 2) / 4 = 0.85 × 0.9 × 0.75 = 57.375%。回放(未命中当报错)时是 0.85 × 2 / 4 = 42.5%,和 e 无关。
题 2(诊断):线上 B 比 A 高 20 个百分点,回放里差距几乎消失(甚至是负的),B 的回放有 123 次未命中。最主要的原因是?
A:503 对 A、B 的影响是同方向的,造不出 20 个百分点的差距。C:未命中当报错时,回放期望差距是 +2.50(B 仍略高于 A),−3.75 是 20 次的抽样噪声。多跑几次 B 会回到略高于 A,但追不回线上那 20 个百分点,因为回放期望值里没有 e。未命中次数就是线索,它必须和分数一起报告。
题 3(泄漏):换了一份新录的 cassette 后,A、B 的回放召回都变成 50.00%,20 次的标准差都是 0。先查什么?
分数变好、方差消失,两个模型还一样,是"答案被送到嘴边"的症状。扫描会在 p03、p07 的录制里找到 BUGHUNT-INJECTED 标记;指纹比对也会把这两条标成和线上不一致(debug 构建 ≠ 线上构建)。
题 4(过期):被测应用从 v1 升到 v2,指纹比对标出 4/12 条过期。下面哪种做法对?
A 正是常见错误:合成实验里 A 的回放召回从 43.75% 掉到 21.25%,还多出 0.90 个"误报"/回合。C 让一个回合里混着 v1 和 v2 的响应,也重新引入了环境随机性。
题 5(算):录制时不校验 5xx,每次请求 503 的概率是 10%,cassette 里有 2 个 Bug 页(各请求一次,相互独立)。至少有一个 Bug 页被冻结成 503 的概率是多少?
%
1 − 0.9² = 19%。合成实验换了 200 个录制种子,36 次冻结到了 Bug 页(18%)。被冻结的那些录制里,A 的回放召回均值只有 19.79%,没冻结的是 43.75%。

7面试要点与代码

一句话讲清楚

回放评测把工具响应录成 cassette,换模型时按键回放,环境就固定了,但模型的随机性还在,照样要跑多次。它有三个坑:新模型走了没录过的路(报告未命中率,别把未命中当成"页面打不开"直接计分);录制里藏着答案或未来信息(扫关键词、截断时间、留出集);被测应用升级了(指纹比对,cassette 和答案表同版本,过期就重录)。

追问准备

  1. 回放和第 3 周的假模型服务器有什么区别?假模型服务器固定模型响应,测 harness;回放固定工具响应,比模型。两者可以叠加:模型和工具都录下来,就是一份完整的回归剧本。
  2. 未命中率多少算高?没有通用阈值。至少做两件事:分数和未命中率一起报;在未命中为 0 的回合子集上单独算一遍分数,看结论变不变。
  3. 为什么不全部回落线上?那就回到了线上评测:有环境随机性,有版本漂移,还贵。回落可以做,但要记录哪些响应来自线上、来自哪个版本。
  4. SWE-bench 怎么固定环境?每道题带 base_commit,2024 年 6 月宣布评测改用 Docker 容器。即便如此,2025 年 9 月还有人报告 Agent 用 git log --all 看到了未来的修复提交:快照里留了不该留的东西。

常见错误说法

❌ "回放是确定的,跑一次就够":回放固定的是环境,模型照样随机。
❌ "回放分数低,说明新模型差":先看未命中率。在这个合成设定下,B 线上高 20 个百分点,回放低 3.75:排序视图都没录过,把 B 的优势压到只剩约 +2.5 个百分点,20 次的抽样误差又把它翻了号。
❌ "分数变好、方差变小,说明更稳定了":也可能是泄漏。
❌ "应用升级了,更新答案表就行":cassette 和答案表要同版本,否则修好的 Bug 会被算成误报、新 Bug 永远看不到。
❌ "录到什么就存什么":录制时的偶发 503 会被冻结,每次回放都复现。

回放环境的核心(Python,合成实验里的写法)

class ReplayEnv:
    def __init__(self, cassette: Cassette, policy: str, fallback: LiveEnv | None):
        self.cas, self.policy, self.fallback = cassette, policy, fallback
        self.misses = 0
        self.fallbacks = 0

    def fetch(self, page: int, view: str) -> Response:
        entry = self.cas.entries.get((page, view))
        if entry is not None:
            return Response(entry["status"], entry["body"], "cassette")
        self.misses += 1
        if self.policy == "strict":
            raise Unreplayable(f"p{page:02d}/{view} 没有录制")
        if self.policy == "live":
            self.fallbacks += 1
            return self.fallback.fetch(page, view)
        return Response(404, "not recorded", "miss")  # policy == "error"

完整代码在 week07_回放评测与模型对比/code/replay_eval/:sim.py(环境、录制、回放、评测)、replay_eval.py(打印全部实验结果)、test_replay_eval.py(16 个单测,含公式和 20000 次模拟的核对)、har_replay.py(用 Playwright 的 HAR 录制 / 回放第 5 周的购物车页面)。

下一讲换到评测框架。

公式显示依赖 KaTeX(CDN),断网时公式会显示成原始 LaTeX 源码,交互部分不受影响。