
在电商搜索这个赛道上大家卷了好几年的向量检索从双塔到图神经网络从粗排到精排能优化的地方几乎都优化了一遍。但我在做得物交易搜索的过程中越来越觉得继续在“向量召回”这条路上加层数、调损失函数边际收益已经非常有限了。真正带来质变的是我们在召回阶段引入了一套基于生成式模型的方案。这篇文章不聊虚的直接把我们的思考过程、实现细节、踩过的坑讲透。先说清楚一个前提得物的交易搜索和传统电商搜索不完全一样。用户搜索“篮球鞋”可能想要球鞋也可能想要周边也可能想找二手。Query短、意图分散、商品更新快这给召回带来的压力比一般电商更大。我们之前用向量召回双塔模型学出来的向量确实能抓语义相似但有一个天生的痛点它只能做相似匹配不能做“条件条件下的内容构造”。也就是说不管怎么优化向量空间它永远是在已有的商品库里面找和Query最像的那些商品没法生成用户真正想要的、混合多意图的组合结果。这就是我们转向生成式召回的核心动机把召回从“在库里选”变成“按条件写”。有人可能觉得“生成”这个词在检索场景里很悬其实落地之后它就是一套Transformer解码器输入Query和用户行为序列输出一串商品ID序列再用这串ID去索引取商品。这个思路最早是从Google T5-based Retrieval那篇工作里得到启发的但我们并不是照搬而是结合交易搜索的场景做了大量的改造。接下来我把每个关键环节都拆开来说。1. 整体设计与思路拆解为什么非要换召回范式先说结论生成式召回不是要取代向量检索而是要在召回链路上新增一条“能聚合多意图、能感知用户状态”的通路。我们当前的召回架构其实是多路并行关键词倒排一路、向量召回一路、生成式召回一路三路结果最后在粗排阶段做融合。1.1 先看传统向量召回到底卡在哪里双塔向量召回的问题说白了就是“embedding 却不是万能的”的经典表现。第一个痛点是对多意图Query建模能力有限。用户搜“通勤穿搭”他脑子里可能是“看起来正式点但不死板”传统双塔只能把Query压成一个向量对这个向量做近邻搜索你压得再好它也只能表达一个中心语义表达不了“同时包含西装和球鞋”这种复合概念。第二个痛点是个性化效果太弱。双塔里虽然可以在User侧接用户的行为序列但最后算相似度的时候是非常粗粒度的匹配很难在召回阶段就精确体现“这个人最近买了什么品牌的鞋、接下来大概率想要什么”。第三个痛点更实际线上倒排和向量的融合成本高。我们早期召回阶段是倒排给一批候选向量给一批候选两批数据去重、加权重、排序每次调整权重都要重新看线上效果而且调参空间越来越窄。说白了多路召回的路数在一个范围内是有效的但你要想再上一个台阶就得靠“召回结果本身的质量变好”而不是靠“融合公式调得更准”。1.2 生成式召回的本质把召回当成序列生成任务生成式召回的基本范式很简单却特别反直觉我们不从商品库里做相似检索而是让模型直接用概率生成出“最可能被点击、最容易转化的一串商品ID”。你可以把它理解为一种“受约束的文本生成任务”Query是输入的提示词用户行为序列是上下文商品ID就是我们要生成的token。为什么这个思路在交易搜索里能work我发现一个关键原因电商搜索里的商品不是任意文本它天然有一种“语义结构”。品牌、品类、风格、价格带这些属性都能映射到ID上而且用户在搜索场景下的点击行为模式本质上就是一种“由Query条件逐步收敛到具体商品”的序列过程。用Transformer解码器来建模这个过程比用向量内积来表达“相关性”要自然得多。从实际效果来看生成式召回在长尾Query上的提升最明显。向量检索对于低频长尾Query经常找不到高质量的相似商品倒排又经常失配生成式模型因为具备条件概率建模能力即使某个Query没怎么出现过它也能根据Query里出现的词根和用户历史行为组合出合理的商品集合。我们在离线评测里长尾部分的Recall50提升了将近20%这在召回环节是很大的数字了。2. 核心细节解析与实操要点这一步决定成败生成式召回的难点不在“我们决定用Transformer”而在“怎么把商品空间转化成模型能学习的序列空间”。商品池子动辄几千万如果把商品原ID直接当成token来生成词汇表就是几千万级别训练和推理都扛不住。所以我们做了很多细节设计。2.1 商品ID如何编码物品ID到Token序列的映射我们采用的做法是把商品ID编码成子词单元。具体来说把所有商品的属性信息拼成一个“商品描述串”然后用SentencePiece或BPE去训练一个词表把数百万个商品映射成有限个子词序列。比如一件“Nike Air Force 1 白色 42码”的商品它的ID可能被编码成“Nike”、“Air”、“Force”、“1”、“白色”、“42”这些子词模型实际生成的序列是这些子词序列解码后再通过一个映射表还原成商品ID。这样做的好处很明显词表大小从几千万降到几万训练和推理效率大幅提升同时子词序列天然共享了语义信息模型在生成的时候可以利用品牌、品类这些跨商品的共性。但这里有一个非常关键的分寸商品描述串必须精炼。我一开始把商品的完整标题、卖点、类目、价格段全塞进去结果生成的子词序列太长解码器训练的难度剧增。后面我们把描述串控制在20~30个子词之内并且保证每个商品映射到的子词序列长度基本一致模型收敛速度明显加快。2.2 训练任务设计从“生成商品”到“生成行为序列”训练数据不是简单的Query到商品ID的匹配对。我们的样本形式是输入部分是Query加上用户最近的行为序列点击过的商品、加购商品、购买商品输出部分是用户在搜索结果中的点击商品ID序列。这里有个反直觉的设计输出部分我们不只生成“排序第一的商品”而是把用户点击过的前K个商品全部当成生成目标。那怎么把“多个正确目标”塞给模型呢我们采用的做法是“前缀生成”加“束搜索打分”。训练时对于同一个Query建模P(item_id_1, item_id_2, ..., item_id_k | query, user_history)。为了让模型的注意力更聚焦在Query和用户序列的语义关系上我们还专门加了position embedding来区分“Query token”、“用户历史商品token”、“当前候选商品token”这几类输入效果比直接拼序列要好很多。另一个值得分享的细节是商品ID序列的顺序敏感性。同一批点击商品你排列顺序不同模型学出来的特征就完全不同。我们实验下来按用户行为时间排序比按曝光位置排序效果更好。原因也好理解用户从“广泛浏览”到“逐步决策”的行为路径本身就是一个天然的序列信号模型能从里面学出意图演化。2.3 解码与后校验不能直接信模型的输出生成式召回线上推理的时候我们用束搜索beam search生成Top 50条商品ID序列。但这里有一个大坑模型生成的ID可能是“不存在的商品”或者“已下架商品品”因为商品库是动态变化的训练完的模型只记得旧库。所以必须加一个后校验层。我们的做法是对每个生成出来的商品ID做三重校验第一重是存在性校验查商品库确认ID存在且状态是上架第二重是类目一致性校验检查生成商品的类目是否和Query的意图类目匹配第三重是实时因素校验比如清仓、限购、区域库存等运营条件。 校验完之后生成式召回的结果才真正进入粗排。我特意强调这一点是因为不少团队在做生成式召回时把全部信任交给了模型结果线上出现大量“薛定谔的商品”——离线评估指标看着很好线上CTR却崩了最后排查发现就是生成了一堆不存在的商品。3. 实操过程与核心环节实现从零到一搭起整套链路理论讲完了接下来详细说一下我们是怎么落地的。整个系统分四条流水线训练样本生产、模型训练、在线推理、结果融合。每一块都有值得记录的细节。3.1 训练样本生产管道数据比模型更重要训练样本的生产我们踩了不少坑。最开始的版本直接拉搜索曝光日志把Query、商品信息、点击行为拼接起来做成样本结果模型学到的全是“曝光位置偏置”。比如某个商品排在搜索结果第一位不管相关不相关点击率都高模型就把这个位置信号学会了生成的结果全是热门的头部商品长尾商品完全没有生机。后面我们把样本生产逻辑改成了“多场景混合采样”第一类是点击样本直接从曝光日志里捞但是要做位置消偏用得比较多的是“倒数位次加权”法第二类是购买样本这类样本更稀疏但对交易搜索价值最大第三类是人工构造的“困难负样本”从向量召回里取一批不相关但语义接近的商品标注为负样本。 三类样本按一定比例混合模型训练的时候效果最稳。我试过把比例调到点击主导线上长尾效果急剧下滑调到购买主导又会出现整体召回覆盖不足。目前我们线上稳定跑的比例大约是点击60%、购买25%、困难负样本15%这个比例大家可以当参考但要结合自己场景调。训练样本的另一个关键细节是序列长度控制。用户行为序列我们最多取50个最近交互的商品但训练时采用“滑窗截断”策略而不是固定取最近50个。因为有些用户中间会有大段无关浏览历史固定截断容易把真正与当前Query相关的那部分行为截掉。滑窗的好处是能让模型在不同上下文长度下都能学到相对完整的意图链。3.2 模型训练并行的编码器与解码器我们用的基础架构是T5.1-base规模的模型参数量在2.5亿左右。但和原始T5有一个重要区别我们把输入分成两条独立的编码通道一条编码Query一条编码用户行为序列在解码器之前通过一个Cross-Attention融合模块拼接起来。这样做的考虑是Query和用户历史的语义密度差异很大如果混在一个编码器里互相干扰太严重。分开编码之后模型可以单独学Query的意图表达也可以单独学用户状态的长期偏好最后在解码阶段结合。训练超参数上我直接给出我们最终调通的一组数据batch size 1024按序列token数动态裁剪learning rate 3e-4使用逆平方根衰减且带warmupwarmup步数2000训练步数大约15万步dropout 0.1用于缓解过拟合序列最大长度输入侧256输出侧64优化器用AdamWweight decay设0.01这组参数下我们在一张8卡A100集群上训练了大约3.5天收敛。大家如果复现不一定非得这么大的规模关键是别一开始就把batch size开得特别大生成任务和对比学习任务不一样它对batch size的敏感度更高我建议从256开始往上加观察验证集上的生成准确率而不是loss下降速度。训练目标函数用的是标准的交叉熵损失没有额外加对比损失。这一点可能和很多做检索的同行预想的不一样。我们一开始也试过给生成结果加对比学习约束让生成序列和真实点击序列的向量更近但实际效果提升非常有限反而把训练复杂度拉高了。对于生成式召回来说交叉熵已经能有效建模“条件分布”刻意再去拉向量距离收益不划算。3.3 在线推理延迟和效率的极限拉扯在线推理是整个系统里工程难度最高的部分。生成式模型再快它也是自回归的要一个token一个token地出和传统的向量近似近邻检索相比延迟天生劣势。我们的目标是把单次召回延迟控制在50毫秒以内这需要做很多优化。第一个优化是序列解码的“提前截断”。因为我们用的子词编码很多商品有固定前缀比如品牌词模型生成到品牌词结束后后续子词其实已经被前缀高度约束了。我们做了一个“前缀命中早停”如果束搜索的候选集合中某个前缀对应的商品集合已经收敛到足够少就直接停止继续生成把当前前缀下所有商品ID拿出来。这个优化把平均生成步数从12步降到了7步左右。第二个优化是蒸馏。我们训练了一个T5-base的“教师模型”然后蒸馏出一个参数量只有1/4的“学生模型”用于线上推理。蒸馏目标不只看硬标签还加入了教师模型的软标签蒸馏就是把教师模型生成的token概率分布作为学习目标的一部分。学生模型在离线评测里Recall50只掉了3个百分点但延迟降低了55%。第三个优化是缓存。用户行为序列编码结果做了会话级缓存。用户在一次搜索会话里多轮改Query行为序列变化不大只编码一次就能复用。这个优化在移动端场景尤其重要因为用户经常是翻了好几屏、改了好几次词缓存命中率能到60%以上。3.4 结果融合策略生成式不是来抢饭碗的召回结果不能简单拼接我们最后在粗排阶段做了“软融合”。具体做法是把生成式召回的结果当作一个独立的特征通道和向量召回、倒排召回的结果一起按“来源召回分加粗排模型预测CTR加权分”的混合方式融合。这里有一个我们调了很久才发现的规律向量召回和倒排召回的ID重合度是相对高的但生成式召回和它们俩的ID重合度非常低大约只有15%左右。这说明生成式召回确实在发现“模型认为相关但近邻匹配找不到”的商品。但低重合也带来一个风险生成式召回的结果如果膨胀得厉害会把粗排喂糊涂。所以我们给生成式召回的结果设置了一个比例上限最多占粗排候选池的20%保证主链路的稳定性。这个比例不是拍脑袋定的我们试过30%和50%粗排阶段的AUC开始下降20%到25%区间最优。4. 常见问题与排查技巧实录所有坑都替你踩过了生成式召回的新鲜感过去之后真正的硬仗在于日常的debug。我列一下我们遇到的几个典型问题以及对应的排查思路和解决方案这个表可以直接拿去抄作业。问题现象根因分析解决方案离线评估高但线上CTR低模型生成大量已下架商品校验层不完整增加上架状态、库存、区域维度的实时校验生成结果全是头部热门商品训练样本里点击位置偏置太大做位置消偏按倒数位次加权采样长尾Query生成结果不稳定训练样本里长尾Query覆盖不足构造长尾Query改写样本按比例增补生成商品和Query明显不相关子词编码混淆了同品牌不同品类商品在输入描述串里强加类目标签前缀线上偶发延迟突刺束搜索步骤过长某些Query生成长序列前缀命中早停 学生模型蒸馏生成式召回和向量召回的融合后效果下降生成结果占比过高粗排被噪声污染限制生成式结果在候选池中的比例上限我再详细说两个最有代表性的问题。第一个是生成热度崩坏的问题。一开始我们的模型学到的是“永远生成最热门的几种商品”因为训练语料里头部商品的点击量占了绝对优势。这很像NLP里的“生成平庸文本”问题你让模型自由生成它就倾向于输出概率最高的平凡组合。我们的解法是“退火采样”加“重复惩罚”。训练的时候不是完全用teacher forcing来训练而是按一定的概率把真实目标换成模型自己预测的结果然后再算loss让模型适应自己的预测噪声。线上解码的时候对已经生成过的商品子词序列做惩罚降低重复生成高热度商品的概率。这两个操作叠加之后长尾商品曝光量上升非常明显。第二个值得一提的问题是Query和用户历史的时效性冲突。用户的长期行为是喜欢运动鞋但今天搜“通勤包”模型很容易被历史偏好引导生成一堆运动鞋。那我们就在输入上做了“近期行为加权”把用户最近24小时的交互行为在输入序列里的位置权重调高让模型能感知到“用户今天是来找包的不是来找鞋的”。这个调整看起来是一行权重的事但实际对用户体验的改善比很多复杂网络结构调整都要大。还有关于模型评估不能只看Recall。我建议在离线评估里加两个额外指标一个是生成商品和真实点击商品的类目一致性另一个是生成商品的新颖度即和向量召回结果的差异度。这两个指标能帮你提前预判线上实际效果比单纯追求Recall数值要靠谱得多。5. 技术边界和应用延展什么情况下它不一定work说完实现细节我想泼一点冷水。生成式召回并不是一种“万能药”它有明确的适用边界。最核心的一个约束是它适合场景意图丰富、商品属性结构化程度高的搜索场景但不太适合标准化的商品搜索比如用户搜“iPhone 15 256G”这种确定性极强的需求。这种需求用倒排索引已经很好了生成式召回反而会带来不必要的延迟和不确定性。生成式召回的另一个局限性在于新品。新商品没有历史交互数据冷启动问题比向量召回更严重因为模型压根没在训练数据里见过它很难生成出来对的新品。我们的应对是把新品通过“属性映射”转成子词序列在训练样本里随机注入一部分新品的曝光数据让模型在条件生成的时候有机会“遇到”它们。但这只是缓解不是根治。不过如果把这个技术思路的外延再放大一点它其实很适合“多模态搜索”的演进。我们在尝试把用户输入的图片信息也编码成一个视觉token序列喂给生成式召回模型让模型同时参考文本Query和图片条件来生成商品。这套方案正在实验阶段从目前的离线效果来看在“以图搜同款”和“穿搭推荐”这两个场景下的提升比纯文本Query更明显。另外生成式召回和推荐系统的衔接也是一个可深挖的方向。传统的推荐系统是两阶段先召回再排序排序结果本身是确定的。但生成式模型的输出本身带有一定的“概率分布”你可以在这个分布上做采样引入探索和随机性这天然适合做探索与利用的平衡。我们在猜你喜欢场景上做了个简单的实验用生成式模型替代一部分协过滤召回用户探索的新内容比例明显上升整体点击并没有下降。说到这想起来我们在做这个项目的时候团队里争论最多的一个问题是生成式召回到底算“搜索”还是算“推荐”我现在的答案是它恰好把这两个领域缝合在一起了它用搜索的Query条件作为生成的约束又用推荐的行为序列作为生成的上下文本质上是在“用生成的方式做条件化匹配”。这个定位帮我更好地理解了整个系统架构也提醒我搜索和推荐的边界在未来可能越来越模糊。如果你也想在自己的场景里尝试生成式召回我建议从小流量实验开始不要一上来就替换整个召回链路。找个长尾Query占比高、用户行为序列丰富的细分场景先离线做充分评估再切一小部分线上流量做A/B。这条路的坑很多但每跨过一坑你就会发现传统召回的天花板确实比想象中更低。