换个模型上线,离线回归和线上 A/B 各管什么?
先离线、后线上。离线在冻结用例上配对比较,回答"会不会变差、哪几条变差";线上把真实流量随机分组,不配对,回答"真实用户那边怎么样"。
两道关的统计结构不同:线上要的样本量大一个数量级,还多了一个离线没有的坑:边跑边看结果。本页数据都是合成数据,用固定种子在本地生成,不调用任何模型。
1两道关,各管一件事
| 离线回归 | 线上 A/B | |
|---|---|---|
| 数据 | 冻结的用例集,工具响应可回放 | 真实用户的会话 |
| 设计 | 配对:同一条用例两个版本都跑 | 不配对:每个会话只进一个组 |
| 检验 | McNemar、配对区间(第 1 周已讲) | 两比例 z 检验,或分组序贯检验 |
| 需要的量 | 几百到几千条用例 | 每组几千到几万个会话 |
| 能发现 | 具体哪几条用例回归了 | 用例集外的分布、真实接受率、时延和成本 |
| 不能发现 | 用例集外的问题;用例集过期、泄漏 | 小类别上的回归(被平均掉) |
离线便宜、能定位到具体用例,先把明显的回归拦住;线上贵,只给离线已经过关的版本。两者不能互相替代。回放方式见本周「把工具响应录下来回放,评测就公平了吗?」一章。
2第一关:离线配对回归
400 条冻结的 BugHunt 用例,5 类各 80 条,模型 A、B 各跑一次(合成设定:真实通过率 A 73.6%、B 73.9%;B 在并发竞态类约 30% 的用例上明显更差)。
| B 过 | B 挂 | 合计 | |
|---|---|---|---|
| A 过 | a = 271 | b = 25(回归) | 296 |
| A 挂 | c = 30(修好) | d = 74 | 104 |
| 合计 | 301 | 99 | 400 |
A 296/400 = 74.00% → B 301/400 = 75.25%,差 \((c-b)/N = +1.25\) 个点,分歧率 13.8%,McNemar 精确 p = 0.590(推导见第 1 周「新 prompt 从 82% 涨到 86%,是真的变好了吗?」)。换模型要证明"没有明显变差",所以看差值区间的下界:事先定 margin = 3 个点,要求 95% 区间下界 > −3。
Newcombe(1998)方法 10:配对比例差的区间
出处:Newcombe RG, Improved confidence intervals for the difference between binomial proportions based on paired data, Statistics in Medicine 17:2635–2650。两个通过率 \(p_B=(a+c)/N\)、\(p_A=(a+b)/N\) 各取 Wilson 区间 \((l_B,u_B)\)、\((l_A,u_A)\),\(\hat\theta=(c-b)/N\):
$$L = \hat\theta - \sqrt{(p_B-l_B)^2 - 2\hat\phi(p_B-l_B)(u_A-p_A) + (u_A-p_A)^2}$$ $$U = \hat\theta + \sqrt{(u_B-p_B)^2 - 2\hat\phi(u_B-p_B)(p_A-l_A) + (p_A-l_A)^2}$$ $$\hat\phi = \frac{\max(ad-bc-N/2,\ 0)}{\sqrt{(a+b)(c+d)(a+c)(b+d)}}\quad(ad>bc\ \text{时})$$这份数据:Newcombe [−2.43, +4.93] 个点,配对 bootstrap [−2.50, +4.75],下界都高于 −3,整体非劣效通过。实现和论文 Table III 的四组例子对到小数点后 4 位。
3门禁过了,不等于可以切
| 类别 | 通过条数(每类 80)A → B | b | c | McNemar p |
|---|---|---|---|---|
| 表单校验 | 61 → 66(+6.25) | 2 | 7 | 0.180 |
| 权限 | 62 → 65(+3.75) | 2 | 5 | 0.453 |
| 状态同步 | 58 → 63(+6.25) | 3 | 8 | 0.227 |
| 并发竞态 | 57 → 50(−8.75) | 13 | 6 | 0.167 |
| 页面渲染 | 58 → 57(−1.25) | 5 | 4 | 1.000 |
单看并发竞态也不显著(p = 0.167)。把 b 里的 25 条用例两个模型各重跑 5 次(阈值是笔者的经验值:A 至少 4/5 过、B 至多 1/5 过算真回归):真回归 8 条,全部在并发竞态;flaky / 待查 15 条;偶发(重跑后 A、B 都稳定通过)2 条。合成数据里没有第 1 周的第三类"用例 / 判定有问题",这里多出的"偶发"是 A、B 重跑都至少 4/5 通过的用例。
4切换开关:两组只差一个配置键(Codex)
源码基于 commit 7993248,CLI 在本机 codex-cli 0.159.2 上用临时 CODEX_HOME 实测,只跑不调模型的 codex doctor 和 codex debug prompt-input。
| 开关 | 落到哪里 | 优先级 |
|---|---|---|
-m model-b | ConfigOverrides.model,let model = model.or(cfg.model);(core/src/config/mod.rs:3970) | 高于所有配置层 |
-c key=value | SessionFlags 层;值先按 TOML 解析,失败就当字符串(utils/cli/src/config_override.rs:49-84) | 30 |
项目 .codex/config.toml | 目录不受信任时加载但禁用 | 25 |
-p cand | $CODEX_HOME/cand.config.toml 叠在用户层上 | 21 |
config.toml | 用户基础配置 | 20 |
优先级数值见 config/src/config_layer_source.rs:33-51;还有两种旧式托管配置是 40、50。下面是实测结果的摘要(model 由 codex doctor 读出,提示由 codex debug prompt-input 读出;完整输出见博客正文):
[基础配置] model-a PROMPT_V1
[-c model=model-c] model-c
[-m model-d -c model=model-c] model-d
[-p cand] PROMPT_V2
[-p cand -c developer_instructions=PROMPT_CLI] PROMPT_CLI
[-p cnad(拼错)] PROMPT_V1 ← 静默跑了基线
config.toml 里的旧写法 [profiles.cand] 配合 -p cand 会报错(loader/mod.rs:302-322)。5第二关:线上 A/B 的样本量与偷看
线上每个会话只进一个组,回到两个独立比例的比较。Kohavi 等人(2009,Controlled experiments on the web: survey and practical guide,Data Mining and Knowledge Discovery 18:140–181)公式 (2),置信度 95%、功效 80%,每组:
$$n = \frac{16\,\sigma^2}{\Delta^2},\qquad \sigma^2 = p(1-p)\ \text{(二值指标)}$$原文注明出自 van Belle(2002)第 31 页,90% 功效时 16 换成 21。基线 40%、检测 2 个点:每组 9600 个会话。离线配对(Connor 公式)在分歧率 10% 时只要 1960 条用例。
偷看:每次查看都按 \(|Z_k|\ge 1.96\) 判,总的一类错误率会上升。Armitage、McPherson、Rowe(1969)最早算出这个膨胀,等间隔 5 次约 0.142。两种事先定好的边界能把它压回 5%:Pocock(1977)每次用同一个临界值 c;O'Brien & Fleming(1979)第 k 次用 \(c\sqrt{K/k}\)。c 没有闭式解,本页的 c 是本文数值积分求出的。
▶演示 A:离线配对门禁
改 2×2 表的四个格子和 margin,实时算 McNemar 精确 p(双侧)和 Newcombe 方法 10 的 95% 区间。门禁规则:区间下界 > −margin 才放行。
▶演示 B:线上 A/B 要多少会话
每组样本量:Kohavi 等(2009)的 16σ²/Δ²(90% 功效用 21),旁边是系数不取整的 \(2\sigma^2(z_{0.975}+z_{1-\beta})^2/\Delta^2\)。下面一行是同样差距下离线配对设计(Connor 公式,第 1 周「要跑多少次才够?」)要的用例数。
▶演示 C:偷看模拟
把最大样本量分成 K 次等间隔查看,每次看累计数据的 z 统计量。用正态增量模拟(和本章 Python 的数值积分是同一个模型),最后一次查看时 \(Z\) 的期望由基线、真实差和总会话数算出。理论值来自 Python 的数值积分(只列真实差为 0 的一类错误率)。
设定:基线 40%,每组共 9600 个会话(960 × 10 天,K 次查看平分)。
✎练习
$CODEX_HOME 里有 config.toml 和 cand.config.toml。实验组的命令把 -p cand 误写成了 -p cnad。在 commit 7993248 / 本机 0.159.2 上会发生什么?-p cnad 生效的仍是基础配置里的 PROMPT_V1。A/B 静默变成 A/A,所以每次运行都要把生效配置打进 trace 并断言。6面试要点
一句话讲清楚
换模型、改 prompt 分两道关。离线:冻结用例、工具响应回放,两个版本跑同一批用例做配对比较,事先定非劣效 margin,看配对差值区间下界,旧过新挂的用例逐条重跑归类、按类别看集中回归。线上:按会话随机分流,事先按最小可检测差算样本量(基线 40%、检测 2 个点每组约 9600),中途要看就用 Pocock 或 O'Brien-Fleming 边界。切换只改配置,每次运行把生效配置打进 trace。
追问准备
- 离线过了为什么还要 A/B?用例集只覆盖你想到的情况;真实分布、开发者是否接受报告、真实时延和成本只有线上能看到。反过来,线上平均值会把小类别的回归平均掉,离线回归清单不能省。
- 线上为什么要的样本量大这么多?不配对。离线每条用例和自己比,难度在相减时抵消;线上每个会话只有一个版本的结果,难度差异全算进了噪声。
- 每天看结果有什么问题?每看一次就是一次检验。等间隔看 5 次总误报约 14%,10 次约 19%。用事先定好的分组序贯边界,或 always-valid 的方法(Johari 等,KDD 2017)。
- Pocock 和 O'Brien-Fleming 怎么选?Pocock 每次一样严,容易提前停,但最大样本量不变时功效损失大(本章 80.7% → 68.9%);O'Brien-Fleming 前期极严、最后接近 1.96,功效几乎不损失(79.2%),适合"主要担心变差、出大问题才提前停"的场景。
常见错误说法
❌ "整体非劣效过了就能切":小类别的真回归会被平均掉,要逐条重跑回归清单。
❌ "线上 A/B 也用 McNemar":线上没有配对,用两比例检验。
❌ "每天看一眼、p < 0.05 就停,还是 5% 显著性":10 次查看约 19%。
❌ "profile 名字写错会报错":不存在的 profile 文件被当成空表,静默跑基线。
公式显示依赖 KaTeX(CDN),断网时公式会显示成原始 LaTeX 源码,交互部分不受影响。