生成式召回在交易搜索中的原理、落地与踩坑实践
1. 先看清现状传统召回为什么够了又不够这几年的搜索架构圈提到召回言必称向量检索。尤其交易搜索感觉不做个embedding召回都不好意思出门。但真正在线上打磨过搜索链路的人心里都清楚向量检索是很强可它解决的是语义近邻问题对于很多交易场景里颗粒度很细、约束条件很多的 query向量检索其实一直在靠粗排和重排给它兜底。另一个方向开始被认真讨论就是生成式召回。它换个玩法不比较向量距离而是让模型把商品 ID 当成目标序列直接生成出来。这篇文章我会从得物交易搜索这类标品、潮流品交易平台的视角把生成式召回的原理、难点、工程落地和踩坑过程讲清楚。1.1 倒排索引负责稳定但稳定不等于懂你倒排索引在交易搜索里面依然是不可动摇的地基。用户搜西装外套倒排能精确命中标题里的西装、外套搜nike dunk low品牌词和型号词命中。它最大的优点是可控返回结果是可解释的词项权重也好调。但问题也很明显query 一旦口语化、带情绪、带场景就抓瞎。用户搜通勤穿的不累脚的单鞋倒排结果大概率只有单鞋匹配而通勤不累脚这些词在商品标题里几乎不存在。再比如错别字高绑鞋和高帮鞋倒排拿到的是完全不同的词项召回直接掉底。还有更麻烦的是交易搜索的属性很多都是结构化字段颜色、尺码、价格带、品牌这些在倒排里要靠额外字段过滤或嵌进 query 里索引就越来越重。我见过不少团队把倒排索引里的 term 加到几个 G还不是因为商品多而是为了把各种属性都塞进词表结果长尾 query 的 docFreq 越来越大相关性排序越来越难做。所以说倒排负责的是稳定召回它不是不懂你而是它只用字面来懂你。1.2 向量检索补上了语义但露出了更大的成本向量检索解决了字面不匹配的问题。不累脚的单鞋可以 embedding 到一个和舒适 单鞋相近的位置召回一批标题里带舒适软底的商品这是倒排做不到的。但向量检索并不是没有代价。第一它做的是全局相似度比较Query 向量和几百万个商品向量一起算距离即使有 ANN 结构本质上还是在空间里找近邻对结构化约束无能为力。用户说800 元以下的黑白 AJ1query 向量根本表达不清这个筛选条件只能把它压到一个稠密向量里过滤全扔给下游。第二向量模型的训练数据天然有头部倾斜。点击和成交集中在爆款上商品向量对头部门类学得好尾部商品、新品没有那么充分的行为信号表征质量就差。结果就是向量召回往往召回的还是那些本身就容易买到的商品。第三工程上 embedding 要维护双塔商品侧索引要增量更新模型要定期重训链路越长监控面越多。这不是说向量检索不好而是想说明它在交易搜索里并没有解决所有问题只是把最难的那部分往后挪了。1.3 生成式召回把检索从找相似变成写答案生成式召回的核心思路是把商品 ID 当成一个目标序列模型输入 query输出一个有序的 ID 序列。前提是有一个足够好的 ID 表示以及一个足够大的模型来记忆 query 到商品 ID 的映射。你可以这样理解倒排索引像翻字典向量检索像在地图上找最近的点生成式召回更像是一个熟悉商品的导购你问一句下雨天能穿的高帮鞋它直接告诉你对应的商品编码。这不是一个纯理论概念。业界已经有“可微搜索索引”这类工作并且已经有平台把生成式召回从论文搬到线上。对交易搜索来说它最有价值的地方在于生成过程不是单纯求相似度它可以隐含地把用户意图、商品属性和上下文一起编码进生成目标。所以严格说它不是来替代向量检索的它是给召回链路注入一个之前没有的信息维度。2. 交易搜索里的生成式召回第一步是把商品 ID 变成模型能生成的东西2.1 为什么不能直接让模型生成百万级商品 ID一开始最容易踩的坑就是直接让 seq2seq 模型去生成几十万甚至几百万的商品 ID。问题很明显输出词表太大训练样本稀疏模型很难收敛。而且商品 ID 本身是随机分配的模型要死记硬背AJ1 对应 13820001这种映射学不到泛化能力。所以现在比较通用的做法是先把商品空间做聚类和分层生成语义 ID 路径。比如鞋子篮球鞋实战篮球鞋商品ID或者品牌系列类目商品ID商品在路径上越深模型需要预测的 token 越少。这一步做到位的标准是同一个 query 下相关的商品尽量共享一条较短的 ID 前缀路径路径上的每一级都有明确的业务含义。这样模型不需要把每个商品的独有 ID 都记住只需要预测到叶子类目或 cluster如果商品 ID 空间太大也可以只生成 cluster 级 ID再用一个传统的倒排/向量召回在这个 cluster 内取商品。等于让生成式模型先做好意图到目录分类把精细匹配交给后面的检索这是目前工程上最高性价比的玩法。2.2 如何构造语义 ID聚类、层级和映射表缺一不可构造语义 ID 不是拿商品 ID 随便拼个前缀它需要一套完整的映射体系。我的建议是至少包含三层层级含义示例作用第一层业务类目男鞋-篮球鞋-实战鞋表达品类意图第二层高频属性簇品牌-系列-风格-价格带表达交易筛选条件第三层商品级 ID商品独立编码或共享簇 ID定位到具体商品第二层是交易搜索特别需要的因为用户经常搜联名黑武士配色这些词跟类目不是一个维度得单独做出来。第三层不应该全量给每个商品一个独立 ID否则词表又膨胀了。工程上会做“冷门商品共享 cluster ID”的处理把所有商品按向量或内容先聚类到几十万个簇里每个簇给一个 ID线上再用一个就近映射表把商品挂到簇下面。新商品上线时可能还没有独立簇先挂到相似商品簇这样冷启动问题也缓解了一部分。映射表要支持动态更新比如类目调整、商品资料修改。我见过直接把语义 ID 拼接字符串存到数据库的上线一个月之后类目改名商品全找不到了这就是没有把映射表当基础设施来设计的下场。2.3 训练数据的构建不能直接拿点击日志当标准答案生成式召回的本质是监督学习目标序列是商品 ID 路径那训练数据从哪来直接拿点击日志的话模型学到的多半是热门商品比我相关而不是这个 query 真正该召回什么。我的做法是把训练样本按行为强度分级点击给基础权重收藏/加购权重高一档下单支付权重再高一档。同一个 query 下如果用户点击了 A 但买了 B那 B 的样本 loss 权重应该大于 A。而且要控制一个 query 下的正样本数量不能把整个点击列表都灌进去否则模型会试图在一个生成序列里包含所有点过的商品最终学成推荐一堆互不相关的爆款。除了正样本负样本也很重要。同一个类目下的曝光未点击商品是很好的硬负样本其他类目的随机商品可以作为软负样本。损失函数上主流还是用序列交叉熵但光交叉熵会让模型只优化 next token 预测忽略商品之间的排序关系。有条件的话可以叠加一个 pairwise loss让生成序列里相关商品的概率排序跟用户真实偏好一致。这一步做不做直接决定生成式召回到线上之后是能看还是能打。训练样本构造这部分可以用一段伪代码来理解重加权的思路# 伪代码训练样本重加权 def build_training_sample(query, behaviors, base_hit_set): weight 0.0 for hit in behaviors: if hit.is_order: weight 3.0 elif hit.is_cart: weight 2.0 elif hit.is_click: weight 1.0 # 旧链路已能召回降低学习权重 if (query, hit.item_id) in base_hit_set: weight * 0.3 else: # 旧链路漏掉的成交样本重点学习 weight * 2.0 return (query, semantic_id_path(hit.item_id), weight)这段代码不能直接跑但思路很关键让模型去学现有链路漏掉的成交而不是学一个全量点击分布的复读机。3. 模型训练和服务端设计从能生成到快生成3.1 生成式召回跟向量检索不是互斥训练目标里要写清楚我见过很多团队评估生成式召回习惯性地拿它和向量检索的 RecallK 做对比比不过就说方案不行。这其实犯了一个方向性错误。生成式召回的目标应该是在向量检索和倒排检索拿不到的高相关候选里补充出新的成交机会。所以训练的时候正样本不应该是全库的正样本而应该是在现有链路中没有被召回到但最终成交了的样本。换句话说生成式模型要学的不是模仿一个更好的向量召回而是学习现有召回链路的盲区。如果直接把全量曝光点击样本拿过来模型很容易变成向量检索的孪生兄弟线上融合之后重叠度高没有任何增量。这一点想明白之后整个训练流程就不一样了你需要先生成现有 Base 线的训练集把 Base 能够召回的 query-doc 对从训练集里去掉或者降权把 Base 漏掉的成交样本提权。这听起来有点绕但它才是范式跃迁里“跃迁”二字真正的含义——不是同一个目标的精度提升而是换一个目标函数。3.2 服务端不能直接架一个大模型做 beam search延迟扛不住生成式召回看起来很美但服务端直接套一个 T5 规模的 seq2seq 模型线上 p99 延迟分分钟过百毫秒交易搜索根本接受不了。这里有几道绕不开的坎。第一输出空间不是纯文本。你需要用 constrained decoding保证每一步生成的都是合法 ID 路径上的 token否则模型随便生成一个不存在的类目组合下游映射不到商品。工程上可以维护一棵前缀树解码时把非法分支直接 mask 掉。第二beam search 不能太大。beam4 和 beam16 延迟差很多收益却不一定差很多。我自己的经验是生成式召回在交易搜索里先做意图到类目路径的预测beam size 只用 4 到 8 就够后面接向量检索去取商品等于承担的是粗略但是准确的过滤。如果把生成式直接做商品级beam 开到 32 甚至更高延迟不可控线上压力很大。第三必须做模型量化和服务化。实际操作中int8 量化能省一半显存和 30% 左右延迟效果损失在 1% 以内。如果还想再快就蒸馏一个 student 小模型专门做商品 ID 预测teacher 负责复杂 query 的意图生成student 负责快速解码。小模型在线上的吞吐量提升非常明显。3.3 在线链路怎么融合给生成式一路候选而不是让它当唯一主干线上最稳的接入方式是把生成式召回作为一路新的候选源跟倒排、向量并行最后在粗排/精排阶段做融合。具体配合上我推荐两种模式。第一种模式是生成式只出目录向量在目录内取商品。生成式输出几个叶子类目或商品簇之后由向量检索在限制条件下做近邻搜索候选被限制在更相关的局部空间准确率更高。第二种模式是生成式直接出商品候选适合那些意图非常明确的 query比如搜具体的型号、联名款生成式可以直接吐出 5 到 10 个高相关的商品 ID跳过向量这一跳。线上实际跑的时候我会给生成式这一路单独一个召回配额比如总候选里给 10% 到 20% 的配额。这个配额不能拍脑袋要看增量效果。初期可以先所有 query 都接之后按 query 意图分流用户搜明确型号词时给生成式更多配额搜宽泛风格词时少给一些。这个过程需要做好埋点区分每一路召回的来源、位置和成交归因否则后面根本说不清楚到底哪一路在真正贡献增量。4. 落地过程中踩过的大坑和排查实录4.1 新商品几乎永远不被生成冷启动要从 ID 设计就解决生成式召回上线后第一个被业务挑战的问题就是刚上新的商品怎么一条都生成不出来这是预料之中的因为训练数据里新品没有行为记录模型没有见过它的 IDbeam search 时它的概率天然很低。但这个坑其实是 ID 设计的问题。我做第一版时把商品 ID作为最终的生成目标新品一多词表就要扩容老模型完全没有能力预测新 ID。后来改成类目/属性簇 ID商品挂接的方式后新品只要挂到了已有簇下面模型生成簇时就能把新品带出来。尤其对于鞋类这种上新节奏快的品类这个改动基本解决了冷启动问题。另外还需要做新 ID 映射兜底生成式模型如果输出了一个簇 ID在线通过映射表去拉商品列表时直接把该簇最新上架且库存充足的商品排进去。相当于模型不需要认识每个新商品只需要对上位目录有信心剩下的事情交给系统。4.2 生成结果越来越像复读机爆款霸屏严重上线一段时间后运营反馈搜索页太单调同一个 query 下生成式召回的 5 个结果里可能有 3 个是同一个畅销款。这个问题本质是样本不均衡成交样本里爆款占了绝大多数模型只要不停输出高概率爆款loss 就能降得很快。我这里有两个处理手段。一是对同一类目下的训练样本做数量上限约束把每个叶子类目下的正样本数压在某个阈值内避免爆款类目样本量把其他类目压死。二是在 beam search 解码时加多样性策略比如已经选了某个商品或某个簇之后对同一个簇内的其他候选做降权惩罚。这个惩罚系数需要调太强会伤相关性太弱等于没加。我通常从 0.8 开始试看线上翻页率和多样性指标的变化。还有一个容易被忽略的点业务方可能把多样性误解成均匀分配结果到处都有但都不是用户想要的。多样性的正确目标是在同样相关的前提下给用户多几种可能而不是牺牲相关性硬凑。做评估时要单独统计生成式新召回里的成交率而不是只看展示多样性。4.3 生成式召回的候选和向量召回重叠率超过 60%等于白做有一次我们做融合效果分析发现生成式召回的候选和向量召回重叠率很高新增成交几乎没有。复盘之后问题出在训练数据上——我用了全量点击行为作为正样本这等于让模型去拟合一个和向量模型差不多的语义相似度函数从数据分布上就没法产生增量信息。后来训练样本改成Base 召回遗漏但实际产生成交/加购的样本为主并且专门给这类样本更高的 loss 权重。这样生成式模型才会把注意力放在那些向量模型觉得没那么像但交易价值很高的序列上。调整之后重叠率降到 30% 左右单路召回率虽然看着没涨但融合之后的成交转化明显提升。这里也想给团队提个醒评估生成式召回不要单看召回率。因为它的贡献是用低重叠的高质量候选去给排序模型提供新输入。如果一个候选在向量召回里已经有了排序模型早都见过了信息量是零反而那些向量召回没有、生成式独有的候选哪怕质量看起来普通才可能是真正的增量。4.4 线上延迟和算力成本量化蒸馏之外还能做 query 分流生成式召回上线后算力和延迟永远是躲不开的成本。模型量化、蒸馏、beam 裁剪都做了之后还可以在 query 分流上下功夫。实际线上有一部分 query 是极度热门的比如dunk空军一号这些词倒排和向量召回已经非常准用生成式去生成是典型的过度设计。可以让生成式模型只处理长尾、口语化、属性叠加复杂的 query热词直接走原有链路。判断条件可以做一个轻量分类器核心维度大致是这样分流维度适合生成式召回保留原链路是否包含明确品牌型号词否是是否包含多个属性词是否过去 7 天该 query 在 Base 链路无成交比例高低query 长度长短意图口语化程度高低按这些维度把 query 分成适合生成式和保留原链路两拨线上 A/B 后你会发现整体延迟指标下降一大截而成交没有折损甚至由于分摊了算力高价值 query 反而获得了更多计算资源。5. 评估体系不是只看召回率交易搜索要盯住增量成交5.1 离线阶段合法率、目录准确率、增量 Recall 缺一不可离线评估生成式召回如果只看 RecallK很容易被带偏。我建议至少同时看三个指标指标说明目标合法率生成的 ID 路径是否真实存在于商品映射表大于 99.5%目录准确率生成的叶子类目和 query 意图是否一致越高越好增量 RecallBase 线召回不到的成交商品里生成式召回到的比例重点看趋势合法率如果低于 99.5%说明 constrained decoding 有问题或者是语义 ID 字典和线上映射表不同步。目录准确率是中间目标可以用 Fine-grained 的意图标注样本去评估。增量 Recall 才是生成式召回存在的意义。有些团队还会看生成序列的稳定一致性比如同一 query 在近重复样本下输出是否抖动。生成模型天然有随机性如果 beam search 的 top1 经常变会让排序模型以为用户意图变了所以线上最好固定 seed 或使用确定性解码。5.2 线上阶段A/B 实验要单独拆出增量成交线上评估最怕的是把生成式召回塞进一个大的搜索实验里整体效果看涨了但说不清是哪一路的作用。更麻烦的是整体效果没涨但某一细分人群明明有提升结果被大盘稀释掉。我的建议是做双层实验先做生成式召回开关的 A/B 实验即对照组走原链路实验组在原链路上多并一路生成式召回这样能看到全局增量再做分桶配额实验在开启生成式召回的基础上把生成式候选配额从 10% 调到 20%看边际收益是否递减。核心指标不要只盯点击率。交易搜索最好直接看新增召回带来的支付成交额和新增召回商品的次留/复购等。有些生成式召回出来的商品用户没点可能只是时机不对不能否定它的价值。至少要给这个实验跑一到两周覆盖不同的消费节奏不要跑一两天就下结论。5.3 从交易视角理解召回率悖论交易搜索里有一个经常被忽视的现象纯召回率指标越高不一定 GMV 越高。原因在于旧链路召回来的候选大多是爆款排序模型对爆款具备很强的转化预估能力而召回率稍低一点可能反而把位置让给了更有购买意图的长尾新品。生成式召回真正的贡献是提高候选集合的信息量而不是提高候选集合的平均相似度。评估它的时候要用排序模型在拿到生成式候选后能否学到新信号来定义价值而不是把每一路召回的结果单独拿出来和全部成交样本算 Recall。想明白这一点你才会理解为什么我说生成式不是第二个向量检索。6. 我个人的实践体会和后续可以做的方向6.1 别把范式跃迁理解成推翻旧链路这篇文章写到最后想强调一点生成式召回在交易搜索里落地最高效的路径不是一步到位用生成模型取代整个召回链路而是把它变成一条独立的、有增量信号的候选通路跟倒排和向量互相配合。它真正的优势不是比向量更懂语义而是换了一种目标函数能发现旧链路看不到的交易机会。我在实际项目中的体会是生成式召回的前期投入主要在数据清洗和语义 ID 设计上模型本身反而不是最难的。如果一上来就调模型结构忽略 ID 表示和训练样本构造大概率上线之后会发现它只是一个花钱很多的向量召回替代品没有任何新意。6.2 下一步值得尝试的方向从得物交易搜索这样的结构化商品场景出发我觉得有三个方向很有潜力。一是把用户历史行为序列拼到 query 前缀里让生成式模型直接做个性化召回生成的商品 ID 会更贴近个人偏好。二是引入多模态输入用户用图片搜相似款、搜这件衣服的同款不同色图片特征和文本特征一起作为 encoder 输入生成式的灵活性比向量融合更强。三是用强化学习直接优化交易目标把成交、客单价放进解码策略里让生成结果不只看相关性还看商业价值。这些东西短期内不一定都能在线上落地但它们的共同点是从模型记住映射走向模型理解交易意图。对做搜索的人来说这才是真正有意思的部分。最后分享一个小技巧如果你也想在小规模流量上验证生成式召回先别急着搭完整训练平台拿一周的搜索日志把 Base 线漏掉的成交样本统计出来看看这类样本在类目分布上是否有明显的结构性特征。如果答案是有那生成式召回就值得做如果答案是没有那说明问题可能不在召回而在排序或商品供给你先别折腾模型。