跳到主要内容

设计 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 实验的分组,实现统计学意义上的正交。

相关链接