资讯动态

双塔召回模型实战指南:从架构设计到线上部署全流程解析

发布时间:2026/10/27 20:06:09 来源:尧图企业网站定制
“双塔召回模型”这几个字在搜推广算法岗的JD里出现频率极高但真正能在面试里讲透、在业务里用好的工程师其实不多。我在做推荐系统的这些年里前前后后折腾过协同过滤、FM、Graph Embedding最后在召回这个环节稳定下来的方案就是双塔。今天这篇我不打算科普什么是Embedding也不讲“双塔很强大”这种空话而是把从模型结构设计、样本构建、训练调参到上线部署、问题排查的完整链路按我实际落地时的思考过程一条条掰开揉碎写出来。无论是刚入门准备做召回项目的同学还是已经在线上跑着双塔想进一步优化的朋友这篇内容应该都能给你一些能直接抄作业的细节。1. 双塔召回模型的整体设计与核心思路1.1 召回问题的本质与双塔架构的解题逻辑推荐系统里召回面对的是百万甚至上亿规模的物料池。这个阶段的目标不是“精准”而是“不错过”。你要在几百毫秒内从亿级候选里捞出几百上千个用户可能感兴趣的东西交给后面的粗排精排去慢慢挑。所以召回模型设计的核心矛盾是表达能力和算力开销之间的取舍。双塔模型解决这个问题的方式非常巧妙把用户侧所有特征揉成一个向量把物品侧所有特征揉成另一个向量两者通过内积或者余弦相似度计算相关性。QR编码、CM2000这些选项先放一边关键在于——双塔一旦定型用户塔和物品塔就完全解耦了。物品塔可以离线把全量物料Embedding算好建好向量索引线上只需要跑用户塔拿到用户向量后去向量库做近邻检索。这个解耦带来的收益是巨大的。我在实际项目中测算过同样量级的候选集纯算力开销比传统的“用户x物品”交叉模型比如FM、DeepFM直接打分低两到三个数量级。没有这个解耦任何复杂模型都很难直接扛住召回阶段的海量请求。这也是为什么双塔虽然在单点精度上经常打不过精排模型但在召回这个位置它始终是工业界最稳的底座之一。1.2 双塔与传统召回方案的取舍为什么不是FM也不是协同过滤很多人有个困惑协同过滤ItemCF/UserCF也是把用户和物品映射到向量空间双塔和它有什么区别区别大了。协同过滤的向量是从共现矩阵里学出来的它本质是在利用“行为相似性”但完全无法利用用户和物品自身的属性特征。一个刚上架的新品没有任何行为记录协同过滤对它来说就是一张白纸永远召不回。而双塔模型可以把类目、价格、年龄、性别、统计 CTR 这类侧信息全部塞进特征新品的ID Embedding虽然是随机初始化的但它的类目、价格这些特征能帮助模型在一定程度上做出合理判断。这就是“归纳偏置”的差别。对比 FM 这种浅层交叉模型双塔的优势在于深度。FM 的特征交叉是二阶的而双塔中的多层非线性结构能捕捉更高阶的模式。我在一个短视频项目中做过对比同样用曝光点击样本训练FM 线的 Recall100 比双塔低了大概6个百分点在长尾物品上的差距更明显。当然FM 的线上 serving 成本非常低召回精度要求不高的场景它仍然有生存空间。但如果目标是撑住一个千万级 DAU 产品的召回双塔的综合性价比是最好的。1.3 相似度函数选型内积、余弦以及那个经常被忽略的温度系数双塔的最后一层通常用内积Dot Product或余弦相似度Cosine Similarity做相似度计算。选哪个不是拍脑袋它有实际后果。如果直接算内积向量的模长会影响最终分数。训练时如果没做约束某些高热物品的向量模长会偏大导致它“看起来”和所有用户都相关挤压长尾物品的曝光。这时候要么在训练中对 Embedding 做 L2 正则要么用余弦相似度强制把向量归一化到单位超球面上。工业界很多成熟的实现比如 YouTube 那篇著名的双塔论文都倾向用余弦相似度再加上一个温度系数来调节相似度分布的陡峭程度。温度系数这个细节我单独拉出来说。假设算完余弦相似度之后得到一个分数 s实际打分是 s / τ。τ 越大softmax 之后的概率分布越平缓模型更“一视同仁”τ 越小分布越尖锐模型对正负样本的区分越自信。实操中 τ 的取值对训练初期的稳定性影响非常大。我踩过的坑是直接把 τ 设成 1.0结果训练初期 loss 掉不下去梯度也不稳定。后来参照论文建议把 τ 设成 0.05 到 0.2 这个区间效果立刻正常了。原因在于未归一化的余弦相似度分布在 [-1, 1] 之间直接进 softmax梯度在饱和区几乎消失训练基本推不动。用小 τ 把 logits 放大相当于人为拉开正负样本的差距让梯度更陡。2. 核心细节解析与实操要点2.1 Embedding 层设计ID 哈希、分箱与特征组合的实操经验双塔的输入主要是类别特征和数值特征。类别特征比如用户ID、物品ID、类目ID、城市ID都要过 Embedding 层。这里第一个实操问题是ID 空间太大怎么办。拿物品 ID 举例一个小型电商平台的 SKU 也有几百万直接做全量 Embedding 表内存开销非常大。我的做法是两级映射一级是 Hash Embedding把原始 ID 用哈希函数映射到固定大小的桶里比如 100 万维另一级是让模型自己在这个桶空间里学 Embedding。这样即使未来 ID 空间增长也不影响线上 serving哈希碰撞带来的副作用微乎其微。具体哈希方式不必复杂murmurhash3然后用模运算落到桶内即可我试过 CRC32分布不均匀效果差一些。数值特征处理我的标准流程是先做分箱再加 Embedding。比如“价格”这个特征直接归一化后作为连续值输入模型很难学到“19.9 元和 20.5 元”在用户心智里的巨大差异而分箱之后转化为类别特征模型就能把“9.9 元包邮”这个档位单独建模。分箱的边界怎么定我用的是训练数据里的分位数让每个箱里的样本数量大致相等。这样最后的 Embedding 表不至于出现某些箱样本极少、学不动的尴尬。特征组合这里说一个重点双塔的两个 Tower 之间不能做交叉特征但同一个 Tower 内部是自由组合的。我见过很多新手把“用户性别 用户年龄段”拼成一个组合特征哈希后过 Embedding这完全没问题而且往往比分别 Embedding 再拼接效果更好。因为组合特征天然帮模型完成了特征交叉的一部分工作减少了网络深度的负担。2.2 损失函数与采样策略Sampled Softmax 的完整拆解召回训练本质上是一个多分类问题给定用户 u要从全量物品 V 中找出正样本物品 i。直接对全量物品做 softmax就是经典的 full softmax但物品量级动辄千万这头计算量谁也扛不住。所以工业界标准做法是 sampled softmax——从全量物品里采样一部分作为负样本用这些负样本去近似全量 softmax 的分母。具体形式是L -log( exp(s(u,i)/τ) / ( exp(s(u,i)/τ) Σ_j exp(s(u,j)/τ) ) )其中 j 遍历所有采样到的负样本。这个公式的直觉是把“这个正样本在所有物品中排在最前面”的目标近似成“这个正样本在所有采样到的物品中排在最前面”。采样质量决定了近似的质量这是双塔模型效果的生命线。采样策略怎么选我在项目里试过三种随机负采样uniform、批内负采样batch内其他物品当负样本、困难负样本hard negative。随机负采样最简单但效果一般。它的问题是采样出来的负样本绝大部分和用户毫无关联模型很快就能区分梯度信号很弱。批内负采样成本为零因为一个 batch 里 N 个正样本对天然能构造出 N×(N-1) 个负样本对。YouTube 论文用的就是这种。但它有个著名缺陷热门物品在 batch 内出现的频率高被当成负样本的次数也多模型会误以为热门物品天然是“差”的导致热门物品的 Embedding 质量被污染。现实里同一个 batch 的样本通常来自同一个 session 或相邻时间段的日志热门物品在其中的比例会异常偏高问题会被放大。所以我现在用的组合方案是主负样本用批内采样额外从全局物料池里均匀采样一部分作为补充分母项再混入一小部分 hard negative。Hard negative 怎么构造最省事的做法是拿一个已经训练好的双塔模型算出每条正样本相似度排名第 100 到 500 之间的物品当作负样本输入给模型。这些物品是模型当前“差点犯错”的样本梯度信息量最大。比例上我的经验是 10%~30% 左右太高会把训练搞崩。2.3 温度系数与 Embedding 归一化一个关乎训练稳定性的细节组合上文提过温度系数这里把它和 Embedding 归一化放在一起解释它们对训练过程的联合影响。先说 Embedding 归一化。我们在最后一层算出 embedding 之后习惯先做 L2 归一化再算相似度。写成代码就是user_vec l2_normalize(user_vec)、item_vec l2_normalize(item_vec)然后score tf.reduce_sum(user_vec * item_vec, axis-1)。这一步把相似度限制在 [-1, 1]能有效抑制任意一个特征维度取值过大导致的分数失控。不过归一化会把向量的模长信息全部丢弃。模长本身有没有信息量在一些业务里有比如高热物品的模长可能隐含着“大众化”的信号。这个信号丢掉后模型对物品流行度的感知会削弱。具体取舍要看业务如果发现长尾物品召回变多但 CTR 变差可以试试把exp(item_vec_norm)或者一个可学习的模长乘回去。再说温度系数和归一化的配合。归一化把相似度控制在 [-1,1] 后如果不加温度系数softmax 输入的分母项和正样本项之间的差值最多只有 2。在 softmax 的梯度公式里这个差值太小会导致所有样本对的梯度都接近一致模型虽然不会跑飞但收敛极慢。加上一个 0.1 左右的温度系数相当于把差值放大十倍模型能更清晰地感知“谁才是正样本”。2.4 特征一致性训练与线上 serving 的头号隐患这一节是我最想强调的。双塔模型上线后遇到的大多数“离线效果好、线上效果差”的问题根源都是特征不一致。常见的不一致有三类。第一类训练时用了未来信息。比如用全天的数据统计出的“当天曝光量”作为特征训练时它是已知的线上预测时当天的量根本还没累积完。这种特征在离线评估里会给出非常漂亮的指标上线就原形毕露。第二类线上特征取值和训练时使用的窗口不一致。比如训练用的是“近 7 天点击率”线上的实时特征服务只给了“近 1 天点击率”导致特征值域漂移。第三类物品 Embedding 的更新延迟。线上 serving 时物品向量是离线算好推到向量库的如果新物品的向量没及时更新它就只能用初始化时的随机向量召回效果当然差。我的排查思路很简单上线前逐个特征检查“训练时的取值逻辑”和“线上 serving 时的取值逻辑”列出对照表特征来源窗口、归一化参数、缺失值填充策略必须完全相同。不要相信“应该一样”要写代码验证。比如线上服务里对缺失值填充了 0训练时如果用的是均值填充这个特征就已经在制造偏差了。3. 实操过程与核心环节实现3.1 样本构造从行为日志到训练样本的完整链路第一步是明确正样本的定义。曝光点击算正样本这是常规操作。但要不要把“曝光未点击”当作负样本我的建议是不要。曝光未点击的样本存在严重的选择偏差——用户看到这个物品是因为系统在某些特征上判定它和用户相关所以你拿到的负样本本身就是被筛选过的把它们全当成强负样本会扭曲模型学到的决策边界。我现在的做法是正样本用曝光且点击的或者点击且完播的看业务目标负样本用全局随机采样加批内负采样。正负样本比例控制在 1:5 到 1:10 之间。比例太小模型分不清比例太大正样本信号被淹没训练时间翻倍但效果不涨。样本的时间窗口也值得注意。我见过有人拿最近 30 天的日志训练召回模型这种做法隐患很大。用户的兴趣会漂移30 天前喜欢“母婴用品”的用户今天可能已经在看“户外露营”了。用太旧的数据训练模型会把过期的兴趣学进去。我的经验是主训练窗口取最近 7 天如果数据量不足可以放宽到 14 天但不要超过一个月。3.2 模型结构与训练参数配置一份可以直接用的基线配置说了这么多理论给一份我实测下来稳定可靠的基线配置。模型结构是用户侧特征用户ID、性别年龄组合、城市ID、近7天点击类目分布、搜索词 Embedding 均值拼起来后过三层全连接每层 512 维激活函数用 ReLU最后一层输出 128 维向量。物品侧特征物品ID、类目ID、价格分箱、近7天 CTR、物品字数/时长等同样过三层全连接最后一层输出 128 维向量。训练参数batch size 1024优化器 Adam初始学习率 0.001训练 3~5 个 epoch 后降到 0.0001 再训 2 个 epoch。Loss 用上文说的 sampled softmax批内负采样数量由 batch size 决定通常 1024 个负样本再额外采样 2048 个全局随机负样本、混入 256 个 hard negative。Embedding 维度用户 ID 和物品 ID 用 128 维其余小特征用 32 维或 64 维。这套基线配置在我的几个项目里第一次跑出来的 Recall100 基本都能达到最终调优后的 80%~85%如果你从零开始搭双塔建议先按这个配置跑通全流程再逐步调整。3.3 向量索引构建与线上检索流程内积和欧式距离的换算问题模型训练完下一步是把物品侧向量导出来建索引。我用的向量检索库是 FAISS索引类型选 HNSW32。这里有一个经典的坑FAISS 的IndexFlatIP、HNSW 默认走的是内积检索但很多向量索引库比如某些版本里切换 index 时默认用欧氏距离。欧氏距离在向量未归一化时和内积排序不等价会导致召回结果和离线评测完全对不上。解决方案很粗暴物品向量统一做 L2 归一化之后内积排序等价于余弦相似度排序而余弦相似度排序和欧氏距离排序在向量模长固定时恰好一致因为||a-b||^2 2 - 2*cos(a,b)。所以只要保证物品向量全部归一化用 FAISS 的 L2 距离或者内积结果是一致的。我习惯显式地做归一化再计算宁可多一步操作也不留隐患。线上检索流程大概是用户请求进来实时算用户向量然后拿这个 128 维向量去 FAISS 索引里跑 ANN 检索取 Top 500。这 500 个物品再按业务规则过滤比如下架商品、地域不可用的物品剩下的进粗排。整体耗时我实测在 10 万 QPS 压测下单次检索平均 15ms 左右加上用户向量计算全链路在 30ms 内能扛住。注意 ANN 检索的召回率不是 100%HNSW 的 efSearch 参数要调我一般设置 efSearch256~512在检索速度和召回率之间平衡。3.4 离线评估与指标解读不能只盯着 Recall召回模型的离线评估最常见的是 RecallK在每个用户的测试集里看模型召回的 K 个物品里有多少是用户真实交互过的。但只看 Recall 会带来一个盲区——模型的打分分布是否合理它看不出来。举个例子。一个模型可能把所有用户的向量都学成了几乎相同的方向它照样能出很高的 Recall因为头部那些热门物品对所有用户都是“相关”的。这时候你需要看另一个指标用户向量分布的平均余弦相似度或者更直接一点——随机抽样 1000 对用户计算他们向量的余弦相似度如果平均相似度超过 0.9说明模型出现了明显的“塌缩”所有用户被挤在一个小区域根本没有区分度。我跑过几个新手搭的双塔这个指标经常翻车仅次于塔的最后一层不加归一化的时候。另一个值得关注的是分维度 Recall。比如按物品的类目维度拆开看 Recall100你能发现模型是不是在某个小众类目上完全召不回。长尾物品的召回率单独算一下如果一个模型在头部物品 Recall 90%、长尾物品 Recall 只有 20%那说明负采样或者特征设计出了问题。4. 常见问题与排查技巧实录4.1 召回效果差按这个顺序查我总结了一套排查流程踩坑多次后固化成自己的标准动作。第一步查样本。正样本是不是对的、时间窗口是不是太旧、正负样本比例是否失衡。我见过一个诡异案例某天开始召回指标突然大跌查了一圈发现是埋点变动导致点击日志里混入了大量自动播放的无意义点击正样本被污染了。第二步查特征。看训练和线上特征一致性这个在 2.4 节已经重点说了。特别提醒新加的特征尤其容易出问题上线前最好先跑几天 shadow 验证。第三步查向量分布。按照上文说的“用户向量平均相似度”方法看模型有没有塌缩。如果塌缩了检查是不是最后一层没做归一化、学习率过大、或者温度系数太小。第四步查索引。确认线上检索用的索引和离线评测时用的索引参数一致。HNSW 的 efSearch 大小、IVF 的 nprobe 设置都会影响线上召回结果。这类问题有个特点离线指标高线上 Case 分析时发现召回的物品在向量空间里确实近但远没有离线跑出来的那么“近”大概率是索引参数缩水了。4.2 冷启动物品召回不到怎么办双塔模型对冷启动物品天然不友好因为它的 ID Embedding 没有足够的样本去学。这个问题我一般用三个手段叠加解决。第一个手段在特征里强化“物品静态属性”的权重。类目、品牌、价格带这些特征不要只做一个 Embedding可以尝试把这些特征单独拼一路加大它们在最终向量中的占比。相当于人为告诉模型ID 你没见过没关系你可以先根据类目和价格来猜。第二个手段训练时加辅助 loss让物品的 Embedding 和它自身的内容向量比如 BERT 算出的文本向量尽量靠近。做法是给物品塔的最后一层输出加一个 MSE loss让它去拟合内容向量。这样新物品虽然没有行为 ID Embedding但内容向量已经能撑起它的向量表达。第三个手段是工程对策新物品在冷启动期内直接用物品属性特征算一遍向量存进索引不要等 ID Embedding 学好。我甚至会为冷启动物品单独建一个小索引检索时把常规索引和冷启动索引的结果合并去重用业务规则调一下权重。三步一起上冷启动召回率大概能提升 15 个百分点。4.3 离线在线不一致的经典案例复盘最后分享一个真实的案例。一个电商推荐项目双塔模型离线 Recall100 做到 0.42比线上老模型高 4 个点结果上线之后业务指标不升反降。我们排查了整整两天。最终定位到的问题在于训练样本里有一个“用户是否为会员”的特征离线训练时这个特征取自样本生成时刻的实时会员状态但线上 serving 时这个特征来自用户画像服务的一个 T1 更新缓存。也就是说一个用户昨天刚开的会员离线训练时特征是“会员”线上服务拿到的还是“非会员”。特征值在训练和推理时不一致模型在这个维度上学到的规律在线上完全失灵。修正方案是把“用户是否为会员”改成离线特征同样使用 T1 的画像更新逻辑让训练和线上用同一份数据口径。改完再上线业务指标恢复了预期离线在线也基本对齐了。这个案例我每次培训新人都要讲一遍双塔模型这个架构本身不复杂出问题几乎都出在数据链路的一致性上。从我个人的实际经验来说双塔召回模型的优化天花板其实不在模型结构而在你对业务样本和特征口径的理解深度。把样本做干净、特征做对齐、负采样做到位哪怕模型结构简单一点效果都能超过那些堆了很多花活的复杂模型。希望这篇能帮你少踩几个我当年踩过的坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑