设计 A/B 测试框架
问题
如何设计一个 Android 端的 A/B 测试框架?
答案
整体架构
SDK 核心设计
class ABTestSDK private constructor(context: Context) {
private val cache = ExperimentCache(context)
private val reporter = ExperimentReporter()
// 获取实验分组
fun getExperiment(experimentId: String): ExperimentVariant {
// 1. 优先读缓存(保证同一用户分组稳定)
cache.get(experimentId)?.let { return it }
// 2. 缓存未命中,用本地分流
val variant = localBucket(experimentId)
cache.put(experimentId, variant)
return variant
}
// 本地分流算法:hash 取模保证确定性
private fun localBucket(experimentId: String): ExperimentVariant {
val config = experimentConfigs[experimentId] ?: return ExperimentVariant.CONTROL
val hashInput = "${deviceId}_${experimentId}"
val bucket = abs(hashInput.hashCode()) % 100
// 遍历 variants 找到命中的分桶区间
var accumulated = 0
for (variant in config.variants) {
accumulated += variant.percentage
if (bucket < accumulated) return variant
}
return ExperimentVariant.CONTROL
}
// 曝光上报:用户实际看到了实验内容时调用
fun trackExposure(experimentId: String) {
val variant = getExperiment(experimentId)
reporter.report(
event = "ab_exposure",
params = mapOf(
"exp_id" to experimentId,
"variant" to variant.name,
"device_id" to deviceId,
)
)
}
}
使用示例
// 业务层使用
val variant = ABTestSDK.getInstance(context)
.getExperiment("new_checkout_flow")
when (variant.name) {
"control" -> showOldCheckout()
"treatment_a" -> showNewCheckoutV1()
"treatment_b" -> showNewCheckoutV2()
}
// 曝光打点(用户看到对应 UI 时调用)
ABTestSDK.getInstance(context).trackExposure("new_checkout_flow")
关键设计原则
| 原则 | 说明 |
|---|---|
| 分组稳定性 | 同一设备 + 同一实验 = 永远同组 |
| 确定性分流 | 基于 hash,不依赖随机数 |
| 互斥/正交 | 不同实验层之间用不同 hash salt |
| 最小曝光 | 只在实际展示时才上报曝光 |
| 兜底 | 配置拉取失败返回 CONTROL 组 |
曝光 vs 进组
"进组"(分配了分组)≠ "曝光"(用户实际看到了)。数据分析时应以曝光为准,避免意向分析偏差(ITT bias)。
常见面试问题
Q1: 为什么用 hash 而不是随机数分流?
答案:
- 确定性:
hash(deviceId + expId)的结果是固定的,同一用户每次请求都分到同一组 - 无需存储:不用在服务端记录每个用户的分组
- 一致性:即使 SDK 缓存丢失(卸载重装),只要 deviceId 不变,分组结果不变
随机数无法保证这些特性,会导致用户在实验期间"跳组",污染实验数据。
Q2: 如何实现多个实验之间的正交?
答案:
用分层正交设计。不同实验放在不同"层",每层用不同的 hash salt:
实验 A(UI 层): hash(deviceId + "layer_ui" + "expA") % 100
实验 B(算法层): hash(deviceId + "layer_algo" + "expB") % 100
不同层的 hash 结果相互独立,用户在 A 实验的分组不会影响 B 实验的分组,实现统计学意义上的正交。