搜索业务做AB实验很多团队一开始会觉得“不就是分流量对比两组数据嘛”。真正上手才发现这套东西最难的从来不是算法本身而是流量怎么切、桶怎么分、指标怎么定、实验什么时候收。我自己第一次把AB测试系统接到搜索场景时以为只是后端多一个if-else分支结果踩到地雷的全是实验设计生效前那些没被重视的基础设施问题。今天这篇就把我在搜索场景做AB实验的完整实践拆开讲清楚。适合刚准备搭建实验能力的后端工程师、策略产品经理、算法同学也适合那些已经在跑AB但总感觉“数据结果不敢信”的团队参考。我不会讲太多夸大的平台化架构只讲一套够用、能落地、结果经得起追问的实现方案。1. 整体设计与思路拆解搜索场景为什么比推荐更适合做AB1.1 搜索是具有天然反馈闭环的实验场景搜索用户的行为路径非常明确输入query、浏览结果、点击、翻页、离开或者进一步转化。相比推荐流无限下拉的“沉浸式”场景搜索结果页的展示边界清晰用户的每次点击都能直接对应到具体的排序位置、具体的物品ID实验组和对照组之间的效果差异可以精确归因到“哪一条策略改变了结果顺序”。反过来看这种清晰也会带来归因上的假象——如果实验组把某条结果从第4位移到第1位点击率上涨到底是位置带来的还是这条结果确实更匹配所以搜索场景做实验首先要把指标设计清楚而不是直接看CTR涨了就说赢。我见过不少团队第一版实验系统只记录了两组UV和CTR上线一个排序策略跑了两天数据变好就欢呼结果过了一周回看GMV反而掉了。问题出在哪里出在策略让首屏点击更多聚集在了低价商品上用户点得很开心但转化率、客单价被拖了下去。这不是“实验系统背锅”是指标设计的时候漏掉了业务方向上的关键结果指标。1.2 整体方案选型先想清楚边界再做功能这里说一个我踩过坑后的体会搭建AB实验系统别一上来就画庞大的平台架构图。搜索场景的“简单实践”指的是用最少的代码先打通一条可复用的实验链路保证三件事最优先流量切分规则稳定同一个用户在同一实验内永远落在同一组能同时支撑多个互不影响的实验并行跑报表结果能快速定位到“哪个实验、哪个分组、哪个指标”产生变化。在这个前提下技术选型不用过度设计。我采用的方案是客户端/服务端分层分桶 配置中心下发实验参数 日志反作弊校验 离线报表计算。这套组合没有引入实时计算框架也没有做复杂的正交分层只依靠hash分桶和实验层隔离来完成。这样做的好处是新同学接手实验系统时不需要理解一堆内部概念改动策略只需要在配置平台加一条实验带一个分桶比例QA验证也直观。先跑起来有了经验以后再逐步抽象出独立实验层、正交因子这是一条更稳妥的路径。1.3 区分“实验平台”和“实验策略”两个层面很多刚接触AB的人会把“实验系统”和“策略本身”混在一起。其实它们是两个层面的东西实验系统负责流量分配到哪个组、实验参数如何注入、日志如何打点、指标如何聚合计算实验策略负责业务侧的逻辑改动比如排序权重的变化、召回数量从10改成20、过滤规则的开启。系统层要保证的是“无偏、稳定、可重复”。策略层则只关心“业务目标是否被优化”。这个区分想明白后做AB实验系统时的设计就不会跑偏。所谓的无偏就是流量切分不能受时间、地域、登录态等变量干扰稳定就是同一用户同一实验不可跨组跳跃可重复就是理论上任何一次实验都能用同一条分流算法复现。搜索场景的AB系统如果把这三点扎扎实实做对了后续策略迭代质量会上升一个台阶因为每一条策略上线前都有客观数据兜底而不是靠拍脑袋争上线。2. 核心细节解析与实操要点分桶、实验层与指标设计2.1 一致性分桶设计与流量切分计算分桶最简单的理解就是把一个用户idx乘以一个随机系数后取余将结果映射到不同实验组。但实际工程中有一个非常重要却容易被忽略的问题要保证“同一次实验过程中用户一旦进入A组后续每次请求都必须命中A组”这里必须采用一致性分桶而不是每次请求临时取随机数。我常用的做法是用一个不可变的输入因子做hash。输入因子一般包含用户ID、实验ID、层ID三者拼接后的字符串。用户ID可以是登录用户ID也可以是未登录态的cookie唯一标识实验ID和层ID用于将不同实验的流量域隔离。典型的伪代码如下import mmh3 # murmurhash3 def assign_bucket(user_id: str, layer: str, experiment_id: str, treat_ratio: int 5000) - str: raw_key f{user_id}|{layer}|{experiment_id} bucket mmh3.hash(raw_key, seed42) % 10000 if bucket treat_ratio: return treatment return control请务必注意如果代码里没有带上实验ID而只使用user_id做分桶那么两个不同实验会共享同一批用户进实验组导致实验之间互相污染。只要把experiment_id拼进去同一个用户在不同实验里即使都命中10%流量分到的组也可能不同——这正是我们希望的结果实验之间尽量独立。treat_ratio用万分之几表示默认给5000就是50%流量实验组50%对照组。你可以按迭代节奏自行调整但我建议从低比例开始比如2%或5%流量先观察一段时间确认没有明显负向后再放量。2.2 实验层设计独立层与正交层的取舍实验层设计是整个AB系统中比较费脑筋的环节。简单实践中我推荐先用“独立层”的方式也就是一条搜索链路只运行一个实验跑完一个再开下一个虽然串行但好排查。当需要同时验证多个策略时再升级成“重叠层”让不同策略在各自的层位上运行。“重叠层”的原理是把全量流量复制成多份视图每一层独立把用户再次hash分桶。这样同一用户在第1层实验落在A组在第2层实验可能落在B组互不影响。听起来很完美但它需要承担交互效应——两个实验同时改变搜索结果最终表现可能是A策略单独正向、B策略单独正向但AB同时上线的组合变成负向。搜索场景尤其容易遇到这种问题因为实验结果不只是独立排序变化还会影响用户的浏览路径。因此我建议如果团队刚上手不要把所有实验都做成重叠层。采取收敛一点的做法两个实验影响的模块在搜索链路中相距较远时例如一个改召回数、一个改价格展示形态可以并行两个实验都在改排序权重或展示样式时最好串行或者放到互斥组里跑。表格式对比会更直观实验层类型优点缺点适用场景独立层逻辑简单、结果归因清晰实验串行效率低初期团队、小流量验证重叠层实验并行效率高存在交互效应需额外拆解成熟团队、因子正交经验丰富我团队的实际选择是底层做一套支持多层的hash分配逻辑配置上默认让搜索场景的核心排序实验使用同一层。交互问题在大流量下出现频率不高但一旦出现排查成本足以让人崩溃所以实验并行前先问问“是否影响同一环节”是必须养成的习惯。2.3 指标体系设计不能只看CTR一个数搜索AB实验的指标设计是区分“会做实验”和“只会跑实验”的分水岭。我把指标粗略拆成三层表现层指标点击率、次均点击、翻页深度、无结果率行为层指标用户访问频次、人均搜索次数、会话长度结果层指标转化率、客单价、成交总额、毛利率。简单实践中每组实验至少要确定一个核心指标和一到两个护栏指标。核心指标用来判断实验是否胜出护栏指标用来确认没有把业务带偏。举个例子一次排序实验的核心指标选定为“人均搜索点击次数”护栏指标必须包含“翻页深度”和“无结果率”。如果实验组人均点击涨了翻页深度明显下降说明用户更快找到了想要的结果这通常是正向的反过来如果点击涨了但翻页深度也涨了说明用户可能更难找到结果被迫翻更多页这个实验就得重新审视。另外一个容易踩的坑是只看整体CTR不看首位点击分配。搜索结果的点击高度集中在首屏前三条如果实验组通过把某个高点击商品顶到第一位整体点击率当然会上涨但这只是“分配效应”并不是搜索整体质量提升。这种场景要额外计算各组首屏点击率、首位点击占比如果是头部位置变化引起的差异需要在指标解读时特别说明。2.4 样本量与显著性计算别跑三天就下结论统计显著性的计算按我的经验听一百遍不如自己算一遍。简单公式是n (Zα/2 Zβ)² × 2 × p × (1 − p) / δ²其中p是基线指标均值δ是最小可检测效应Zα/2是显著性水平对应的分位数95%置信度取1.96Zβ是统计功效一般取0.84对应80%功效。举个例子当前搜索点击率基线是8%我们希望最小检测到0.5个百分点的提升即从8%提升到8.5%p取两者均值0.0825δ为0.005。计算过程n (1.96 0.84)² × 2 × 0.0825 × 0.9175 / 0.005²先算括号部分(2.8)² 7.84。 再算0.0825 × 0.9175 ≈ 0.0757乘2得到0.1514。 除以0.000025(0.000025是0.005²得到7.84 × 0.1514 / 0.000025 ≈ 47493这意味着每组约需要47500个独立用户样本。如果搜索产品日活不高3天就跑完实验很可能因为样本量不足产生假阴性的结果。因此在实验启动前建议先根据历史日活估算所需天数再设定实验运行周期而不是在结果不满意时不断延长实验“碰运气”。关于显著性检验业界有一个不太好但很常见的现象叫p-hacking——每次不显著就加样本继续跑跑着跑着显著性出现了。搜索场景中同一个人多次搜索会产生相关性并不是传统假设检验要求的独立样本。我建议在离线计算时尽量按天做聚合或按用户一次搜索会话做聚合不要直接拿“点击事件数”当样本量不然置信区间会被严重缩短看起来更显著其实是假象。3. 实操过程与核心环节实现从配置到报告全链路3.1 实验配置与接入流程拆解搜索场景的AB系统配置项不需要弄得特别复杂。一条实验配置至少包含以下字段配置项示例值说明实验IDsearch_rerank_v1全局唯一实验层rank_exp_layer分层标识分桶比例10%实验组流量比例白名单用户user_qa, product_owner直接命中实验组参数内容weight_a1.2, weight_b0.8注入给策略引擎的改动配置变更发布到配置中心后搜索后端会在请求处理最前端读取分组信息。很多人会问“分流放在哪里比较好”我的答案是最前端。越靠近入口后面所有模块拿到的实验参数越一致。如果放到排序模块内部召回模块可能拿不到实验信息日志埋点也会漏掉。3.2 实验参数注入重新排序链路的实战代码以搜索后端一次完整的排序请求为例实验分流的入口类大概长这样import json class SearchExperiment: def __init__(self, exp_config: dict): self.exp_config exp_config def decide(self, user_id: str, query: str): group assign_bucket(user_id, self.exp_config[layer], self.exp_config[id], self.exp_config[ratio]) if self._in_whitelist(user_id): group treatment return { exp_id: self.exp_config[id], group: group, params: self.exp_config[params] if group treatment else {} }在搜索引擎处理流程里这个decide方法返回的params会塞进搜索上下文SearchContext。后续的召回、粗排、精排、重排模块只需要从上下文取参数即可def rerank_process(context, results): exp_params context.get_experiment(search_rerank_v1) if exp_params and weight_a in exp_params: for item in results: item.final_score item.score * exp_params[weight_a]这段代码最关键的是“实验参数只作用于实验组对照组必须走原有路径”。有些开发同学图方便在对照组也塞了一份默认参数结果实验组和对照组代码路径不一样埋点日志结构也不一样数据对比就失去意义了。我建议在日志设计上埋三个关键字段exp_id、group、params的摘要。虽然搜索服务日志量很大但AB日志建议单独落一份到独立Kafka topic或者离线Hive表别混在主搜日志里。否则后面排查问题时翻日志的成本会高到让你怀疑人生。3.3 数据上报与离线报表形成日志上报阶段至少需要记录用户ID用于确认分桶组别请求ID、实验ID、分组ID搜索query、结果列表含物品ID和位置曝光、点击行为事件发生时间戳。离线的报表计算我给的步骤非常简单按天聚合曝光PV、点击PV计算各组点击率 点击PV / 曝光PV计算人均搜索点击 总点击人数 / 总活跃搜索人数计算转化环节口径和业务方确认后输出。实际产出报表时需要展示实验组/对照组的指标值、变化幅度、置信区间、p值。另外建议在报表头部明确标注“样本量是否达到预估阈值”没达到阈值的数据一律只能作为参考不能直接用于上线决策。3.4 上线决策与周期管理该收手时就收手做实验最怕的不是结果不好而是结果不好时没人敢停实验。我定的规矩是每场实验至少运行7天包含完整周末如果实验动机涉及季节性调整则至少运行14天。运行过程中可以每日盯一下指标但不做正式统计推断等到样本量达标后统一看结果。上线决策分成四种情况核心指标显著正向护栏指标无显著负向 → 可以推进上线核心指标不显著但方向为正 → 延长观察3天仍不显著则判负核心指标负向 → 立即停止并回滚配置核心指标显著正向但护栏指标崩塌例如客单价大跌 → 需要业务方一起评估通常不建议立即上线。之所以“立即停止并回滚”要果断是因为在搜索场景中策略负向不仅损失当天的点击还会影响用户对这个搜索产品的长期使用习惯。真实发生过案例某团队做了一个排序实验点击率微跌0.3%看起来好像还好实际连续跑了半个月次留数据明显下降。后续即使回滚用户的搜索习惯已经被改变恢复周期至少两三个月。这种代价远不是AB实验本身能指标化衡量的。4. 常见问题与排查技巧实录搜索AB实验的隐藏深坑4.1 辛普森悖论一览分组报表与整体报表打架我见过最经典的AB实验翻车现场是实验组在“男性用户”和“女性用户”两个维度都优于对照组但合并整体后反而劣于对照组——这就是辛普森悖论。原因通常是实验组和对照组的用户结构比例不一致。比如实验组分配到的活跃高点击用户更多而对照组低活跃用户更多尽管实验策略本身没效果只看分组均值时却能产出“显著正向”。排查技巧很简单每次出报表时把query类型、用户新老、设备系统、城市等级四个维度固定拆开对比并检查实验组/对照组在各维度下的用户占比是否接近。如果某维度的分布差异超过2%优先怀疑分流不均匀而不是急着看策略结论。最好的解决方式是在统计报表里加上“标准化率”或“分层加权”权重可以按对照组的历史占比计算。4.2 位置偏差正在偷走你的判断搜索结果的点击存在极强的位置偏好。第1位的点击率可能是第5位的10倍以上这个跟结果相关度完全无关。当实验组对排序做调整后如果高相关结果被从第1位降到第2位点击率下降不一定因为排序策略变差而是位置带来的天然流量倾斜。应对位置偏差常用的做法是记录每个结果在每个曝光位置上的点击情况构造位置归一化指标只比较相同位置区间上的点击率变化或者使用“排序翻转率”来判断策略是否真的把更相关的结果排到前面。我在实际报表中会单独输出一个“位置内点击率对比”——把第1位、第2位、第3位的点击率分别分开算。如果实验组第1位点击率比对照组高、第2位同样高那这个排序策略大概率是真正提升效果如果只是数据整体上涨但每一位都差不多那多半是位置变化带来的“流量转移”策略价值存疑。4.3 磁石效应与用户行为路径改变搜索界面的主要目标是把用户送到他需要的内容中去但如果实验策略把结果页的形态改得太明显用户可能“不进反退”——看到首屏不满意会重新发起新query而不是继续翻页。这种情况下实验组翻页深度下降看起来是“更快找到结果”的正向信号实际上却是用户不满意的逃避行为。磁石效应就是指这种结果变化吸引或排斥用户后续行为路径的现象。比如首屏第一个结果变差不只影响本次会话的点击还会让用户在这段时间内减少整体使用率。搜索实验不能只看即时点击指标应当把“后续30分钟内的搜索次数”、“次日回访率”这类后置行为也加到护栏指标里。4.4 新奇效应实验组刚开始暴涨的数据很多搜索排序策略刚上线时用户会对变化后的结果产生好奇点击率会短期上涨。这种新奇效应在3天到1周内会逐渐衰减。有一种典型的错误是实验进行了3天数据大涨团队直接宣布上线两周后指标回落甚至低于基线那时数据已经无法再次归因。我的做法是观察周期内记录每日趋势而不是只看期末均值。当实验组和对照组的差距随时间逐渐收窄说明大概率存在新奇效应需要延长实验周期等到指标走稳后再做决策。尤其是涉及用户界面样式、结果展示形式的改动新奇效应强烈实验周期不能压缩。4.5 实验期间配置变更数据无效的烂账实验跑到一半突然发现排序参数的数值需要微调于是有人直接改了配置中心的值——这是我反复强调绝对不能做的事。一旦实验开始人和一条链路都不能改。实验期间的任何配置变更都会让实验结果失效因为无法判断指标变化来自参数调整还是策略本身。正确的做法是停掉当前实验重开一个实验ID使用新的参数配置重新跑。虽然浪费了一点进度但至少数据干净可信。另一件要注意的是实验期间排查问题时只允许看日志和后端配置绝不允许单独给某个用户手动改分组做特殊验证特殊验证一律通过QA白名单完成。按照这个表格排查能覆盖绝大多数问题异常现象可能原因排查方式整体指标与分组指标矛盾辛普森悖论 / 分桶不均匀按维度拆表检查各维度用户占比实验组点击率涨但转化跌行为层指标虚高拆解客单价、品类分布、目标转化率上线后指标前3天暴涨后回落新奇效应按日趋势曲线观察拉长实验周期同用户每天命中不同组hash输入不稳定检查分桶键是否包含不变用户ID对照组也有实验参数生效代码路径未隔离检查对照组上下文注入逻辑置信区间异常窄样本单位取了点击事件改为用户粒度或会话粒度聚合写在最后的一点个人建议搜索方向做AB实验系统的搭建只是第一步它更像是一套基础设施撑起后续所有策略迭代的可信决策。真正难的是方法论层面的执拗敢于让实验跑完敢于接受实验失败敢于在数据不显著时承认“这个策略没达到预期”——这比写一万行分桶代码都难。我的体感是先用固定的实验模板跑通流程沉淀出报表和评审机制再逐步把实验层抽象得更灵活最终慢慢向平台级演进。配合固定的月维度实验复盘团队对策略效果的理解会越来越深。如果你正在推进这类系统先把搜索指标体系和QA白名单机制做好这两个位置投入的改进在未来每一次实验中都会以肉眼可见的方式回报你。
阅读完成 · 觉得有帮助?