A/B实验

1,069 阅读18分钟

背景

随着推广放量,许多场景都需要 ABTest 来验证其策略效果。 举例如下:

  • 首页内部推荐接入算法,需要验证算法效果
  • 搜索接入搜索中台后,支持自定义搜索策略,需要验证策略效果

ABTest 是什么

ABTest 是一种产品优化方法;为同一个优化目标制定两个方案,让同一部分用户中的一部分用户命中 A 方案,同时另一部分用户命中 B 方案,统计并比较不同方案的点击率、转化率等数据指标,通过不同方案的数据表现,在确定数据表现通过假设检验后,决定最终方案的实验方法。

ABTest简介

作用

例子:

  1. 小A凭着丰富的经验直接修改了产品的线上策略,一周后发现效果不升反降,遂下线。
  2. 小B和小C同时上线了两个产品feature,一周后产品数据有下降,都认为是对方的问题,谁也不肯接锅。
  3. 小D上线了一个新策略,随后进入十一黄金周,用户交互有所下降,小D觉得一定是假期埋没了自己的辛苦贡献,但也辩不明白,无处申冤。
  4. 小E辛苦工作一整年,开发了365个不同的feature上线,年终写OKR总结却发现写不出到底在哪些方面究竟贡献了多少。

以上几个问题,都有一个相同的根本原因,无法验证出数据变化和产品动作之间到底存在什么样的因果关系。

ABTest 意义

AB测试是支持数据决策最有力的工具。实验才是检验真理的唯一标准。

理论基础

假设检验

A/B实验的核心统计学理论是 (双样本)假设检验,首先做出假设,然后运用数据来检验假设是否成立。 逻辑上类似反证:假设我的新策略没用,然后通过做ab实验,从指标中收集证据来推翻这个假设

量化抽样误差的理论根基:中心极限定理

第一类错误&显著性水平(α)

第一类错误,指原假设正确(真),但是我们抽样数据的统计结论却显示原假设错误。这一过程中我们拒 绝了正确的原假设,所以第一类错误是“弃真”。 第一类错误在实验中表现为:实验结论显示我的新策略有用,但实际上我的新策略没有用。 在统计学中,我们用显著性水平(α)来描述实验者犯第一类错误的概率。

第二类错误(β )&统计功效(statistics power)

第二类错误,指原假设错误(伪),但是抽样数据的统计结论却显示原假设正确(真),这一过程中我们接受了错误的备择假设,所以第二类错误是“取伪”,对应犯此类错误的概率常被记做β。

这种错误在实验中表现为:我的新策略其实有效,但实验没能检测出来。 统计功效 = 1 - 第二类错误发生的概率。

统计功效在实验中表现为:我的新策略是有效的,我有多大概率在实验中检测出新策略有效。

显著性水平

统计过程中,我们可能犯第一类错误——弃真,即原假设正确(真),但是假设检验的结论却显示原假设错误。通俗地说,犯第一类错误意味着:实验结论显示我的新策略有用,但实际上我的新策略没有用。 在统计学中,显著性水平(α)用于描述实验者犯第一类错误的概率。

抽样误差带来的不确定性,使我们需要显著性水平,来量化这种不确定性有多大。

量化抽样误差的根基,在于中心极限定理。

学习“中心极限理论”之前,我们先看看什么是 “真值”。 在前面的课程中我们提到过,由于存在抽样误差,我们每次实验所得到的指标结果,都可能与我们期望得到的真正结果有误差。假设我们从总体中抽取样本,计算其指标的均值,每一次计算,样本均值都会受抽样误差影响。假如我们做无数次实验,那么理论上,这无数多个样本均值中,总应该有一个是“真的”,不受抽样误差影响的,这个值在统计学里被称为“真值”

仅从A/B 实验场景下出发,解释中心极限定理。中心极限定理告诉我们:如果从总体流量里不断抽取样本,做无数次小流量实验,这无数次抽样所观测到的均值,近似呈现正态分布(如图)。这个分布以真值为中心,均值越接近真值,均值出现的概率就越大;反之,均值越偏离真值,出现的概率就越小。

为什么样本均值越接近真值,均值出现的概率越大呢?我们继续用人均收入来类比。如果从全中国人这个总体中,抽取很多很多次样本,计算很多很多次平均收入。可以预见,我们会因为样本不同而得到很多个不同的平均收入值。这些数值确实有可能因为偶然抽到顶级富豪而偏高,或因为抽到极贫困的人口而偏低。但是,上述两种情况毕竟是少数(均值越偏离真值,出现的概率越小)。随着抽样次数增多,我们会发现,平均收入落在大多数普通人收入范围内的次数,会显著增多(均值接近真值,出现的概率大)。并且,有了中心极限定理的帮助,我们可以知道每个均值出现的概率是多少。

在实验中显著性水平的应用

了解了显著性水平的理论依据,我们该怎么利用显著性水平,来评判我的新策略是否有用呢? 假设我现在要进行一个实验,看看新策略会不会提升产品的人均阅读数。原策略记为 A1,新策略计为 A2。为了方便理解,我们再假设,原策略 A1 生效时,全流量下(总体用户)的人均阅读数真值是 10。如果我对全流量推广原策略 A1,并从中不断抽样,根据中心极限定理,我们可以知道,不断抽样所得到的人均阅读数会呈现正态分布(如图)。

现在我们从总流量中抽取一小部分样本,对样本实施新策略 A2,并得到一个人均阅读数 x。现在问题来了,怎么评判新策略 A2 有用没用呢?

为了方便大家理解,我们假设人均阅读数的标准差是 2,如图,人均阅读数间隔是 2。 先看图中靠近 10 的深蓝色区域,在原策略 A1 生效时,单凭抽样误差,人均阅读数就很可能落在这一区域里。如果新策略 A2 生效时,人均阅读数 x 也落在这一区域里,我们就很难确定新策略 A2 到底有没有用。因为单凭抽样误差,也可以得到与 x 一样的值。 假如 x 值落在12~14 这个区域里呢?

从图中我们可以发现,单凭抽样的随机性,x 落在这一区的概率下降了(概率从 34.1%下降至 13.6%)。直觉上来说,我们的新策略可能是产生了一定影响,才使得 x 落在了12~14 这一区。 当 x 的值超过 14 呢?

由图可知,旧策略 A1 条件下,凭借抽样误差使得人均阅读数>14 的概率只有 2.2%。 如果 A2 组的观测结果 x>14,我们就有很大的信心表示,我的新策略 A2 生效了。因为,如果我们的新策略没 有啥用,我们仅有 2.2%的机会能在一次实验里观察到大于 14 的人均阅读指标。

在上面的例子,假如我们的新策略 A2 没有用,那么在抽样误差影响下,人均阅读数呈现在这个 x 值或者 更高 x 值的概率是多少呢?这个概率在统计学中,被量化为一个指标,叫做 p-value(p 值)。 我们已经知道,显著性水平(α)描述的是:实验结论显示我的新策略有用,但实际上我的新策略没有用, 我们在一个实验中犯这种错误的概率有多少在统计学里,如果 p-value<α,我们就可以说,实验效果达到 “统计显著”。 在 Libra 平台里,按照业界的普遍标准,我们将显著性水平规定为 5%。在使用 Libra 进行实验时,当 p-value<5%,我们便称实验已经“显著”。 这意味着,你在两个实验组之间观测到的差异是由抽样误差造成的,这种概率已经小于 5%。反过来说,你的策略大概率(大于 95%)是有效的(这种说法从统计学上来说并不严谨,但此处我们可以暂时如此理解)。

p-value 到底是什么

www.zhihu.com/question/23…

在前面的内容中,我们引出了 p-value 这个概念。尽管我们将这一概念置于具体的例子之中,但恐怕仍旧有很 多同学无法理解,感到一头雾水。为了能够让大家更好的理解 p-value,我们单设一个小节,试着用一个更为生 动的例子,帮助大家搞明白什么是 p-value。 如果你对魔术有所了解,你就会知道,魔术师们经常会使用一些 tricky 的小道具,比如两面都是花/字的“虚假 的硬币”。现在试想一下,我站在你面前,手里握着一个硬币,你没法看到我手里的硬币,你需要回答我一个问 题:我手里的硬币是真正的硬币(一面字,一面花),还是虚假的硬币(两面都是字/花)。 当然,我不能直接把硬币给你看。接下来,我会抛很多次硬币,然后告诉你硬币朝上的一面究竟是字还是花,你 需要通过抛硬币的结果来判断我手里的究竟是真正的硬币,还是虚假的硬币。 现在,我们开始抛硬币,对硬币向上的一面观测结果如下图所示。

我们知道,如果硬币是正常的,投掷一次硬币,字/花朝上的可能是 50%。那么正常情况下,连续不断投掷硬币, 字/花朝上的比率应该接近 50%。但是在实际的过程中,我们没办法一直不断的抛硬币,所以只能用有限的次数, 去判断硬币是不是 tricky,这就是我们所说的“抽样”。在这个检验过程中,我们设置: 原假设(H0)=硬币是真的,并不 tricky 备择假设(H1)=硬币是虚假的,是 tricky 的 从图中可知,我们连投了 6 次硬币,每一次都是字朝上。从某一个点开始,你开始觉得事情有一些不对劲,你开 始觉得: 如果是真正的硬币(原假设为真的情况下),不该出现这种情况吧,似乎硬币有问题。 这就是 p-value 在持续下降,并超过了一个临界点(显著性水平α)所造成的。在统计学中,p-value 就是:当 原假设为真时,我们得到当前的样本观察结果或者出现更极端结果的概率。套用抛硬币这一例子,p-value 就是, 如果硬币是真的(原假设为真),硬币抛 6 次都是字朝上(当前样本观察结果),或者抛更多次都是字朝上(出 现更极端结果),这种概率有多大。 根据我们前面所讲的,p-value 越小,我们拒绝原假设的理由越充分,结果也就越显著。

在 Libra 中找到“显著” 明白了 p-value 与显著性水平之间的关系,我们就更容易理解 Libra 实验报告中的正向显著、负向显著、不显著等标签。在实验报告页中,选择“显示图表”,就可以在指标表格下方看见 p 值标签。 1. p 值<5%,且数据涨了,即为正向显著,图标底色为绿色。数据涨了,且有 95%的概率该数据可以相信。 2. p 值<5%,且数据跌了,即为负向显著,底色为红色。数据跌了,且有 95%的概率该数据可以相信。 3. p 值>5%,代表数据结果不显著,底色为白色。即可以相信这份数据的概率小于 95%。

统计功效

统计功效是什么 A/B 实验中可能会出现一种情况:我的新策略其实有用,但是我没有检测出来。这便是统计学中第二类错误的表现。也就是说,在原假设是错误的前提下,实验者也可能会接受原假设,这在统计学中被称为第二类错误。这种错误的概率被记为β。而我们今天要讲的统计功效(power,也被称为检验效力),被定义为 1-β表示的是“假设我的新策略是有效的,我有多大概率在实验中检测出来。” 统计功效的理论依据 现在我们举个例子,尽可能直白地解释一下统计功效到底是怎么一回事。 假设现在我们要进行一组实验,我们认为:将旧策略 A 调整至新策略 B,能够将用户的人均观看时长提升 3%。从假设检验的角度去分析我们要进行的实验可以得出,在这组实验中:

  • 原假设=新策略 B 没用
  • 备择假设=新策略 B 有用

同时,通过前面学习的中心极限定理,我们了解到:实验中我们不断抽样所得出的样本均值,大致会呈现正态分布。接下来看看下面这张图。

图中,左侧钟形曲线代表原假设成立时,经抽样统计,平均阅读数所呈现出的分布形态。这条曲线对应的策略实际上是“旧策略 A”。假设旧策略 A 生效时,用户平均阅读数的真值为 25s(意味着钟形曲线的中轴线在横轴上对应的值是 25);且在这一分布中,当平均阅读数均值超过 25.718s 时,我们会拒绝原假设,认为新策略 B 有效。那么。从图中来看,深灰色区域即拒绝域。 右边钟形曲线对应的策略是新策略 B。我们现在假设这一策略生效时,用户的人均观看时长真值是 25.75s(即右侧钟形曲线的中轴线,在横轴上对应的值是 25.75)。 先来想一下,右边钟形曲线的真值为 25.75,落在旧策略 A 的拒绝域里,这意味着什么呢?这意味着,理想状态下,新策略 B 其实是有效的。但是,通过前一篇文章的学习,我们也知道,在对新策略 B 做抽样统计时,由于抽样误差,我们得到的平均阅读数均值大概率不会是 25.75。

看到图中横轴上的蓝色区域了吗?如果对新策略 B做抽样统计时,其平均阅读数均值可能是蓝色区域内的任意一个数字。

  • 假如在统计时,新策略 B 的样本均值,落在原策略 A 的拒绝域之中或者拒绝域右侧。此时我们可以得出 结论:拒绝原假设,新策略 B 是有用的。
  • 假如新策略 B 的样本均值<25.718,对应上图,此时样本均值落在图中浅灰色的范围内。此时,由于样本 均值并不在原策略 A 的拒绝域之中,所以我们没办法拒绝原假设,也就无法证明新策略 B 是有用的。

我们在前面已经讲到过,新策略 B 在抽样检验中,它的真值实际落在原策略 A 的拒绝域里。理论上来说,新策 略 B 实际上是有效的。但是,如果新策略 B 进行抽样统计后,样本均值<25.718,我们便无法出检验出新策略 B 有效。这就犯了我们所说的第二类错误:“我的策略其实有效,但是我没检验出来。” 在上面的图中,浅灰色区域就是我们犯第二类错误的域,其面积占右侧钟形图形面积的比率,就是我们犯第二类错误的概率。这一概率被记作β。在图中的例子里,β的取值为 47%(在此不对计算过程进行解释)。也就是说,在上面的例子里,即便我的新策略 B 有效,我仍有 47%的概率没法检验出它是有效的。而统计效力=1-β=53%。这代表着我有 53%的概率可以检测出新策略 B 是有效的。

聪明的小伙伴可能已经发现,实际上,统计效力的大小,取决于 原假设 备择假设 下,两个分布曲线的交叠关系。

统计功效在 Libra 中的使用 相对于显著性水平来说,统计效力更难让人理解。为了让大家在实验中更方便地应用统计功效,Libra 平台引入了一个额外参数:检验灵敏度( MDE 。“检验灵敏度”,指我们所关心的指标,在实验中可检测出来的最小提升值。这个提升值越小,也就意味着检验越灵敏。 灵敏度来和统计效力有什么关联呢? 抛开理论知识,从直觉上来看,A/B 实验应该遵从以下两个事实:

  1. 新策略的效果越大,越容易被检验出来;
  2. 样本的数量越多,检验能力越强,越能分辨新旧策略间细小的不同。

换句话说:

  1. 新策略的效果越大,检验所需的灵敏度越低(意味着检验灵敏度的取值更大。灵敏度 10%和灵敏度 1%相比, 10%灵敏度低),同时实验结果出现显著的概率越高,实验的统计效力也就越大;
  2. 实验调用的样本数量越多,其灵敏度也就越高。 那么,在实际的实验操作中,灵敏度(或者说统计效力)有什么应用场景呢?

No.1 实验不显著,要不要结束实验 这几乎是每个实验操作者都会遇到的问题:辛辛苦苦做了个实验,数据是涨了一点,但是实验结果不显著。咋办? 现在把实验停了,换新的思路?还是让实验再跑一段时间,这会不会影响实验结果的可信性(有一点必须说明, 理论上,只要你的 A/B 实验跑的时间足够长,样本足够多,实验总会显著的)。 在这个问题上,“灵敏度”就能帮你了。 当实验结果不显著时,实验者可以在实验报告中选择“多天累计指标”这一选项。在这里,我们需要查看当前实 验中,我们所关注的指标,其“检验灵敏度 MDE”是多少,并判断灵敏度是否大于指标的预期提升值。

  • 如果指标的灵敏度比预期提升值大,那么我们可以将实验延长几天,再观察一段时间;
  • 如果灵敏度已经比预期提升值小了,那么很遗憾,我们的实验结果没有置信的可能了,另起炉灶吧。

为什么要这样做呢?我们举个例子🌰。如果在一个实验中,我期望我的新策略将平均阅读数提升 3%。经过一段 时间的实验后,我发现新策略组(实验组)的平均阅读数提升了 2.5%(虽然没有预期中好,但也不错),实验 结果不置信。按图索骥,此时我们应该查看平均阅读数这一指标的 MDE:

  • 如果灵敏度>3%,意味着可能你的实验可能会有 3%的显著提升,只是灵敏度还不够,我们还没检验出来。所 以我们可以再将实验延长一段,让更多样本进入实验,使得实验更灵敏;
  • 如果灵敏度<3%,意味着现在的灵敏度已经足够检测出 3%的显著提升了。但是现在你的实验还是不显著,那 么也许是你的策略真的没那么有用。我们该再选个新策略了。

No.2 实验还没开,到底要多少流量 通过前一个例子我们了解到,当一定流量进入实验后,我们可以了解到实验的灵敏度。反过来讲,如果用户愿意指定自己需要的灵敏度,Libra 同样可以推算出实验需要多少流量。 使用这一功能的操作也很简单。在选择流量大小时,先配置好我们所需要的流量过滤条件(filter),再点击「流量推荐」按钮,选取自己关注的核心指标,然后输入我们期望的目标提升率和实验组个数。这样,Libra 就可以轻松计算出「开启 7 天实验至少需要多少流量」。

目前这一功能只对部分核心指标开放(意味着如果你的指标是自己定义的而不是 Libra 自有的,流量推荐功能可能暂时无法支持)。

「如何确定灵敏度大小」 在这里我们也会面临一些问题:我怎么确定自己需要多大的灵敏度呢?其实这相当于,我们需要知道:在目前的 实验策略下,我所关注的指标最低提升百分之多少,我才能够接受。 如果我们想上线(全量发布)一个新策略,我们需要考虑到开发成本、上线成本、潜在风险等等问题。这时我们 需要从产品层面去思考:指标要提升多少,才能 cover 我上线这个新策略所要付出的成本。假如我认为:“新 策略至少要让人均阅读数指标提升 1%,否则新策略就不值得全量上线。”那么,我们预期提升值的最低值就是 1%,我们在实验中所需的灵敏度也应当是 1%。 在实验前,我们必须明确自己的实验目的、具体的指标,以及指标预期的提升值是什么。灵敏度,应该与我们指 标预期的最低提升值相同。 「灵敏度不可过高/过低」 同时,我们也需要指出,灵敏度太高或者太低,都有其弊端(你也可以理解为,指标的预期提升值定的太高或太 低都不好)。

  • 灵敏度太低

    • 比如理论上指标提升 1%收益就很大了,但我们却将灵敏度设置为 5%,可能导致我们与有潜力的策略失之交臂。
  • 灵敏度太高

    • 设置过高的灵敏度,一方面会导致调用过多流量,形成不必要的流量浪费;另一方面,过大的流量往往会使 指标更轻易地形成显著,但其实这种显著并没有实际意义。
    • 还记得我们前面说的吗,理论上来说,只要样本足够多(比如无穷多时),实验组和对照组之间任何一点差 异都会形成统计显著。这就像是一个哲学问题,策略 A 和策略 B,无论他们多相像,但终归是不一样的,终归会造成实验结果的差异。但这种差异的显著有意义吗?比如新策略使指标提升 0.001%,且统计显著,这真的有意义吗?

综上,灵敏度的选择,要结合产品的实际情况,慎重判断。

接入 Libra

Libra 简介

Libra 是数据平台自研的大规模在线 A/B Testing 平台。 服务于公司内所有产品线的实验评估,覆盖推荐、广告、搜索、UI、产品功能等业务场景,提供了从实验设计、实验创建、指标计算、统计分析到最终评估上线等贯穿整个实验生命周期的服务。支撑着全公司业务在快速迭代的试错之路上,大胆假设、小心求证。

ABTest 生命周期

注册产品

已完成 bytelingo 注册

不再过多阐述,详情看接入指南文档。

产品名称: bytelingo

网址: data.bytedance.net/libra/acces…

接入分流服务

接入方式选择

接入方式简介优缺点
直接调用 ABTest rpc 服务1. IDL: abtest.thrift
  1. PSM: data.abtest.vm_framed

  2. 调用方法:

    1. VersionRsp2 get_ab_versions2(1:VersionReq req)
      
  3. 具体详情可见此文档:AB分流服务接入文档 - Thrift RPC | 优点:1. 理解和实现简单缺点:1. 需要自行对齐封装

  4. 对性能有要求的,需要自行优化 | | 使用 AB SDK | 1. SDK 代码:code.byted.org/iesarch/abt…

  5. 此 SDK 封装了rpc调用、ab参数解析等逻辑,向用户提供直接读取ab参数的接口

  6. 具体详情可见此文档: Abtest SDK | 优点:1. 性能更优

  7. 规范化缺点:1. 如果有特殊需求,不方便定制实现

  8. 有一定学习成本 | | VM Agent 接入 | vm_agent 是分流服务团队使用公司 sidecar 实现的本地化分流功能部署方案,支持 TCE 和 ByteOS。适用于峰值流量过大(QPS 超过 10W)或推送一类定时触发的的场景。具体可看此文档: VM Agent接入文档 | 优点:- 通过进程间通信,在本地进行计算,完成实验分流,对于调用的服务性能更优,延时更短

  • 对高 QPS (超 10w)能够稳定支持缺点:1. 架构复杂
  1. 需要单独进程,实际资源消耗更大 |

结合我们当前业务特点:

  1. 新业务接入
  2. QPS 低

结论:

我们推荐使用 AB SDK 方式接入

接入流程图

流程图.jpeg

分流参数

// 一些比较基础的的分流参数,如下:
paramMap := map[string]interface{}{
   "token":     consts.AB_TOKEN_BYTELINGO, 
   "uuid":       userID, 
   "device_id": req.GetAgwCommonParam().GetUnifyArgs_().GetDeviceId(),
   "app_id":    req.GetAgwCommonParam().GetUnifyArgs_().GetAppId(),
   // access_type eg: 3g, 4g, wifi, mobile, web, unknown
   "access_type":         strings.ToLower(req.GetAgwCommonParam().GetCommonArgs_().Ac),
   "app_name":            req.GetAgwCommonParam().GetCommonArgs_().AppName,
   "channel":             req.GetAgwCommonParam().GetCommonArgs_().Channel,
   "device_platform":     req.GetAgwCommonParam().GetCommonArgs_().DevicePlatform,
   "device_type":         req.GetAgwCommonParam().GetCommonArgs_().DeviceType,
   "os_version":          req.GetAgwCommonParam().GetCommonArgs_().OsVersion,
   "update_version_code": req.GetAgwCommonParam().GetCommonArgs_().UpdateVersionCode,
   "version_code":        req.GetAgwCommonParam().GetCommonArgs_().VersionCode,
   // 操作系统 api 版本号
   "os_api": req.GetAgwCommonParam().GetCommonArgs_().OsApi,
   // todo 更多参数
}
  • token: 用于描述接入方需要访问哪个Libra功能模块下的实验。 这里传入bytelingo 服务端功能模块(bytelingo_server_all)

  • uuid: 由于bytelingo uid 并非非 odin 提供的统一 uid,且是string 类型, 因此,需要将 uid 传入uuid。另外,在实验配置中,需要选择 uuid 分流的实验层。实验指标也需要以 uuid 作为 uid。

  • device_id: 用户的 did

  • app_id: bytelingo 的 app_id ,可通过参数,也可以直接写死

  • 其他参数,可以根据需要添加进去,用于分流。

Client API 层

  1. 通过 SDK 请求分流服务

// option 配置可以参考此文档 https://bytedance.feishu.cn/wiki/wikcn0MVwyh1RmIBR4LikTiw5Tb
options := []ab.Option{
   ab.UseLibraV2(),
   ab.DoubleCheckAsync(),
   ab.DesignationMode(ab.LibraNamespaceOpti),
   ab.UseCustomDelimiter("."),
   ab.SetJsonEngine(ab.LazyJsonSonicEngine),
   ab.EnableNsReveal(),
}
// 初始化
abClient := ab.MustNewClient(options...)

// paramMap 为前面的配置的分流参数 map
options2 := []ab.Option{
   ab.SetHttpPropertiesFn(func(propNeed interface{}) string {
      str, _ := sonic.MarshalString(propNeed)
      return str
   }, paramMap),
}
// 请求分流服务
abtestObj, err := abClient.CreateWithRemote(ctx, options2...)
if err != nil {
    // log 
}
  1. 通过 context 进行存取 abtest

const ABTestObj = "ab_test_obj"


ctx = context.WithValue(ctx, ABTestObj, abtestObj)
abtest, ok := ctx.Value(ABTestObj).(*ab.AbTest)
  1. 下游 RPC 层引入IDL

引入 IDL code.byted.org/cpputil/ser…

// 描述传递的数据是请求 Libra 的 v1 接口还是 v2 接口
// 参考:https://bytedance.feishu.cn/docs/GQDqUkF8OQ52A8llXzBiLf
enum AbVersion {
    V1 = 0,
    V2 = 1,
    V3 = 3,  // 该版本不存在,预留
}

// 用于描述在多个微服务之间传递 abparam 数据
struct AbParamData {
    1: required AbVersion AbVer,

    // 强烈不建议使用的参数
    // 只是为了兼容可能存在的业务正好使用了 vid 并且还接入 SDK 的服务;
    // 在 SDK 内部会通过一些监控手段做到耕细粒度的监控;
    2: optional string VersionNames,
    3: optional string ClientVersionNames,

    4: optional string Parameters,
    5: optional string ClientParameters,
    6: optional map<string, binary> NsParameters,
    7: optional map<string, binary> NsClientParameters,

    // 过渡期参数;
    // 用来解决 old abparam 大多数情况必然被 unmarshal 的性能问题
    8: optional map<string, binary> TranParameters,
    9: optional map<string, binary> TranClientParameters,

    // 内部保留字段,业务伙伴请勿使用;
    // 用于向未来拓展更多可能的优化方案,包括但不限于:透传 request 和 vidList 等信息;
    254: optional binary RequestData,

    // 保留字,SDK 会以 const 的模式约束这个值的具体含义
    // 在满足兼容性和上下一致性的同时,可以表达 63 种状态值
    255: optional i64 ReservedWord,
}
  1. 调用下游服务,传入 ABParam

abParamData := abtestObj.TransferAbParam()

req.AbParamData = &AbParamData{
    AbVer: abParamData.GetAbVer(),
    Parameters: abParamData.Parameters(),
    //.... 直接赋值即可
}

RPC 层

  1. 从上游服务主动传递的 ABParam 初始化 abtest 对象


abParamData := req.GetAbParamData()

options := []ab.Option{
   ab.SetRpcProperties(abParamData),
}
abtestObj, err := ab.RpcInit(ctx, options...)
if err != nil {
    // log
}
  1. 获取 ABParam 数据

// GetParamByNs 从 server param 中得到指定 namespace 的数据,并向下传递
// 假设原始数据 AbParam 是 {"namespace1":{xxx},"namespace2":{yyy},"namespace3":{zzz}}
// 当 nameSpace = [namespace1,namespace2],得到的值为: {namespace1:{xxxx},namespace2:{yyy}} 的 json str
// 当 namespace 的 len 为 0,得到的值为 server(uid 分流) 的全部数据,即:{"namespace1":{xxx},"namespace2":{yyy},"namespace3":{zzz}} 
testNsABParam := abtestObj.GetParamByNs([]string{"testNs"})

// 获取全部 ABParam 数据
allABParam := abtestObj.GetParamByNs([]string{})

// 获取特定 ABParam 数据
b := abtestObj.GetBoolV([]string{"namespace1", "testBool"}, true)
str := abtestObj.GetStringV([]string{"namespace1", "testString"}, "test")

接入 AB 指标

主要有两种方式。

  1. (推荐)在Libra配置Tea指标
  2. 通过Dorado创建实验指标

目前主要由 DA 同学来进行指标组建设

Libra A/B实验

新建普通实验

site.bytedance.net/docs/860/ha…

  1. 新建实验

  1. 填写实验信息

实验基础信息

类型说明
实验名称填写对应名称, TODO 是否需要制定规范
实验类型选择服务端实验
实验 owner实验的owner才有权限编辑实验信息和操作实验。建议添加需求相关的研发、产品、分析师同学,便于完善实验信息和操作实验。
实验描述实验内容简述,方便相关人员理解和维护实验
实验预期确定明确的实验目标
关联 Meego 需求方便管理需求和相关实验
实验标签用于实验管理和分类

生效逻辑设置

类型说明
生效逻辑无特殊情况,选择正常分流即可
选择实验层需要选择bytelingo 服务端 uuid 分流,如果没有可用实验层,需要添加实验层
实验时长通用实验时长是7-14天,可以根据实际情况自行调整。实验到期前一天「ABTest 机器人」会在lark上提醒实验owner
实验总流量AB实验是基于全用户抽样的小流量试验,流量大小代表了实验样本在全用户中的占比。Libra提供了工具来计算实验所需流量,在确保检验效果的前提下不浪费流量,详见样本流量计算器:使用指南
流量生效方式决定了流量如何生效,可根据具体实验自行决定1)立即生效——实验开启后,符合命中规则的请求流量将都会立即生效。2)平滑生效——实验开启后,会根据设置的平滑时长逐步线性放量至所需的实验流量。
预定流量如果需要后续放量,且不方便在其他实验层重开的实验,可以提前预定该实验层流量,防止该实验层流量被其他实验占用
流量过滤条件用于过滤出实验流量内符合条件的用户。一般需要在请求参数里传入相关参数。这里需要提前配置请求参数。配置流量过滤条件case如下:
指定用户分群按照用户名单进行流量过滤,仅在分群名单中的用户才有可能命中实验。请在用户分群管理中创建分群并上传用户文件。操作说明
指定生效 PSM仅来自于指定 PSM 的请求可以通过 AB 拉取该实验信息,判断是否命中实验。未指定 PSM 的实验,任何 PSM 发来的请求只要携带了对应 token,就可以通过请求 AB 读取 token 下全部实验信息来判断是否命中。Bytelingo 目前仅 toutiao.bytelingo.client_api 需要拉取实验信息,故这里可以为空

填写实验组

类型说明
分组类型对照组:与当前线上逻辑一致实验组:新策略逻辑一般至少都会有一个对照组和一个实验组。
分组描述简单说明该分组
配置参数配置参数是实验业务策略的配置化映射,决定了每个实验分组生效什么策略。 格式为 json,且一般至少两层嵌套, 首层为 namespace。 这里的配置,需要和代码里面的逻辑保持一致,配置前需要和对应RD同学沟通。注意:一般情况下,我们禁止在不同的实验里面,出现相同配置参数key。 比如 分别配置 {"a":{"b": 1}}{"a":{"b": 2}} 则出现相同各配置参数 key 冲突
测试用户填写对应测试用户的 uid 即可,英文逗号分隔

添加指标组

选择需要分析指标的变化,来评估AB实验的效果。这里需要DA同学提前配置指标组。选择对应指标组时,需要和 DA 同学确认。

点击提交后,实验状态将处于 调试中;点击发起实验review后,该实验状态将处于调试中 + review中。

  1. 实验测试

  • 可以在对应对照或实验组,填入测试用户 id,用英文逗号分隔,即可。
  1. 实验Review

  • 每一次会影响线上的修改都会需要先经过实验 Review。服务端实验要求review人至少包含mentor、DA,以及相关功能模块上下游(如有)同学。当完成 Review 之后,会通知发起人触发线上变更。
  • 对于调试中 or 调试中 + review中 状态的实验,仅测试用户会命中,且不影响实验指标数据。
  • 涉及服务端实验的需求,要在技术评审时给出实验方案
  1. 实验上线

实验review无误后,点击上线,实验状态变为“进行中”

  1. 实验关闭

  • 一般由 PM 根据实验数据来决策是否需要全量,以及需要全量的实验组
  • 由 RD 进行关闭实验,以及对相关实验组进行推全
  1. 实验全量发布

  • 选择快速全量发布

  • 选择配置中心全量

  • 选择对应的实验组,并点下一步,进行发布

  • 可以作为短期全量的方式,不建议长期保留。

分流原理

一个request在一个实验层上至多命中一个实验组。Layer的概念是为了用户的复用而产生的。开在同一个layer下的实验,进入实验的用户是互斥的;不同layer下的实验,用户是复用的。

  • 首先,测试用户检查。分流服务检查是否这个实验层上的实验有配置测试用户,如果request中的 uid 或者 device_id 命中某个实验组的测试用户列表,那么认为该request在这个实验层(Layer)上命中这个实验组;
  • 接着,计算流量分桶。 Libra 将全流量平均分配为1000个哈希桶。每个实验创建的时候都要分配流量。例如5%的流量就是从1000个哈希桶中任意选择50个桶分配给这个实验。每个request都会按照一定规则来判断落入哪一个桶。如果是按照uid分流的实验层上的实验,分流服务以“uid_type:uid:layer_name”为key计算哈希值;如果是按照did分流的实验层上的实验,分流服务以“did:layer_name"为key计算哈希值。如果该request落入的哈希桶并没有被分配给任何实验,那么在该实验层不会命中任何实验。
  • 然后,检验过滤条件。根据上一步流量分桶,可唯一对应到一个实验(Flight)。检验实验上的过滤规则,判断request是否能进入实验。如果不能进入实验,则该实验层不能命中任何实验。
  • 然后,检查 用户分群 如果实验有配置用户分群而uid没有命中分群中的用户列表,则该实验层中不能命中任何实验。
  • 最后,计算 实验组。以 "uid_type:uid:flight_name" 或 "device_id:flight_name" 为key计算哈希值并对实验组数目n取模,来判断该request进入哪个实验组(Version),并记录下来。以谁为key取决于实验层的分流策略。

A/B测试原理-实验机制篇 | A/B Testing Mechanisms

A/B实验分流服务详解:分流,真的不简单

参考文档

site.bytedance.net/docs/860/10…

AB Test

Libra快速接入指南