模型迭代是算法团队的日常,但每次新版本上线都面临同一个难题:新模型到底比旧模型好多少?离线评测的指标再漂亮,也可能与线上真实表现存在偏差,因为离线数据分布往往滞后于线上流量。A/B测试是解决这个问题的标准手段——将线上推理请求按照一定比例分流到不同的模型版本,同时收集各版本的预测结果与性能数据,再用统计方法判断差异是否显著。本文将从分流原理、框架实现和工程细节三个层面,完整讲解如何为一个推理API搭建A/B测试框架。

一、流量分流的核心原理与策略选择
分流的本质是一个确定性映射函数:给定一个请求标识,输出该请求应该路由到哪个模型版本。这个函数必须满足两个条件,一是稳定性,同一个请求标识在一段时间内应始终路由到同一个版本,否则用户侧会出现体验不一致的问题,比如推荐系统里同一用户两次请求返回风格完全不同的结果;二是均匀性,各版本实际分到的流量比例应与配置一致,避免统计样本偏差。
常见的分流策略有三种。第一种是纯随机分流,为每个请求生成一个随机数,根据随机数落在的区间决定版本。实现最简单,但缺点是同一用户可能被分到不同版本,适合无状态的一次性推理场景。第二种是哈希分流,对请求中的用户ID或会话ID做哈希取模,保证同一用户始终进入同一分组,是推荐、搜索等用户敏感场景的首选。第三种是分层正交分流,当多个实验同时进行时,通过不同的哈希盐值将流量空间分层,避免实验之间的相互干扰,这是大型实验平台的标准做法。
哈希分流在实践中需要特别注意哈希函数的选择。简单的取模运算容易导致流量倾斜,建议使用一致性较好的哈希算法如MurmurHash,并将哈希值映射到0到9999的整数区间,再按权重区间划分,这样配置百分比时更加直观。例如版本A占80%,则哈希值落在0到7999区间的请求进入版本A。
二、分流网关的实现方案
分流逻辑最好实现在网关层而不是业务代码里,这样可以做到对下游服务透明。整个框架由三部分组成:配置中心负责存储各实验的分流规则,分流网关负责执行路由决策,指标采集模块负责记录每个请求的版本标签和结果数据。下面是一个用Python实现的分流网关核心逻辑,支持配置热更新和哈希分流:
import hashlib
import threading
import time
class ABRouter:
def __init__(self):
self._lock = threading.Lock()
self._experiments = {}
def update_config(self, experiments):
"""配置热更新:由配置中心推送或定时拉取触发"""
with self._lock:
self._experiments = experiments
def _hash_value(self, key, salt):
"""对请求标识做哈希,映射到 0-9999 区间"""
raw = f"{salt}:{key}".encode("utf-8")
digest = hashlib.md5(raw).hexdigest()
return int(digest[:8], 16) % 10000
def route(self, experiment_id, request_key):
"""返回请求应路由到的模型版本"""
with self._lock:
exp = self._experiments.get(experiment_id)
if not exp:
return "default"
value = self._hash_value(request_key, exp.get("salt", experiment_id))
cumulative = 0
for version, weight in exp["versions"].items():
cumulative += int(weight * 100)
if value < cumulative:
return version
return list(exp["versions"].keys())[-1]
# 使用示例
router = ABRouter()
router.update_config({
"exp_model_v2": {
"salt": "exp-001",
"versions": {"v1_stable": 0.8, "v2_candidate": 0.2}
}
})
version = router.route("exp_model_v2", "user_12345")
print(f"路由到版本: {version}")
这段代码的关键点有三个。第一,读写锁保护配置对象,热更新时不会出现半更新的状态。第二,哈希时混入实验专属的盐值,这样同一个用户在不同实验中的分组相互独立,天然实现了分层正交。第三,权重用累积区间的方式计算,新增版本时只需在配置中追加一项,无需改动代码。
网关拿到版本标签后,需要将请求转发到对应的后端服务。如果不同版本是同一个服务的不同实例,可以在请求头中带上版本标签由服务内部路由;如果不同版本是独立部署的服务,网关直接按服务地址转发即可。无论哪种方式,都要把版本标签透传到响应日志中,这是后续统计分析的基础。
三、指标采集与统计显著性判断
A/B测试的价值最终体现在数据分析上。每个推理请求需要记录四类信息:请求标识、命中的实验与版本、模型输出结果、业务反馈信号。前三类在请求返回时即可记录,第四类是难点——推理效果往往需要等待用户行为才能确认,比如推荐场景中用户是否点击。通常的做法是在响应中下发一个埋点Token,业务系统上报用户行为时携带该Token,再与请求日志关联。
判断两个版本是否存在真实差异,不能只看指标数值的高低,必须做统计显著性检验。假设版本A的点击率是5.2%,版本B是5.4%,差距可能只是随机波动。常用的方法是双比例Z检验,下面是简化实现:
import math
def z_test_proportions(clicks_a, n_a, clicks_b, n_b):
"""双比例Z检验,返回Z值和是否显著(95%置信水平)"""
p_a = clicks_a / n_a
p_b = clicks_b / n_b
p_pool = (clicks_a + clicks_b) / (n_a + n_b)
se = math.sqrt(p_pool * (1 - p_pool) * (1 / n_a + 1 / n_b))
z = (p_b - p_a) / se
return z, abs(z) > 1.96
z, significant = z_test_proportions(5200, 100000, 5400, 100000)
print(f"Z值: {z:.3f}, 差异显著: {significant}")
除了业务指标,性能指标同样重要。推理延迟的P99分位数、超时率、错误率都应按版本分别统计,避免出现业务指标持平但新版延迟翻倍的尴尬情况。指标建议按分钟级粒度聚合,既能及时发现异常,又不会产生过大的存储压力。
四、与灰度发布和回滚机制的衔接
A/B测试不应该孤立存在,而应与发布流程形成闭环。推荐的流程是:新版本先以1%的小流量接入,观察一到两天确认没有稳定性问题,再逐步提升到正式实验比例开始数据收集。这个渐进过程本质上就是灰度发布,分流框架天然支持——只需修改配置中的权重即可,无需任何代码变更。
回滚机制同样依赖权重配置。当监控发现某个版本的错误率或延迟超出阈值时,自动化系统可以将该版本权重降为零,流量瞬间回到稳定版本。为了支撑这个能力,配置中心需要提供审计日志,记录每次权重变更的操作者和原因,防止误操作导致线上事故难以追溯。
最后需要提醒两点工程实践中的经验。一是实验周期要覆盖完整的业务周期,比如周中与周末流量特征不同的业务,至少跑满一周再下结论,否则样本存在周期偏差。二是避免同时改动多个变量,一次实验只验证一个模型版本的变化,如果同时调整了分流逻辑和模型结构,观察到差异将无法归因。把这两点与上述框架结合,你就能搭建出一套可靠且可持续迭代的推理API实验体系。