ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

生成式召回:跳出向量检索内卷,把召回从匹配变成生成

生成式召回:跳出向量检索内卷,把召回从匹配变成生成 先聊一个现象搜索算法这个圈子聊召回几乎已经快被向量检索给“包场”了。双塔模型、ANN索引、多路召回很多人觉得召回优化的尽头就是把向量表征做得更好、把近似搜索做得更快。但做交易搜索做到后面你会发现这条路正在变得越来越拥挤——精度上不去了资源开销越来越大而那些真正影响成交的长尾商品、结构化约束、强购买意图反而是向量检索最不擅长的东西。我想认真聊一聊得物交易搜索里“生成式召回”这个思路它不是重新做一个向量模型而是把“召回”从“匹配”变成“生成”直接通过条件生成的方式产出商品标识。这篇内容适合正在做搜索召回、对召回架构有重构冲动的同学也可以给评估生成式方案价值的团队一个相对完整的参考。1. 为什么说向量检索已经“卷到头”了——召回范式问题的三层拆解1.1 第一层链路复杂度和超参敏感性的持续上升向量检索本身并不复杂双塔结构的本质是“把用户和商品分别映射到同一个隐空间用内积或余弦距离做近邻匹配”。问题在于当这套链路运行到后期几乎所有能提收益的动作都已经变成在“阿基米德螺线上追尾巴”。以我在实践中接触过的召回系统为例刚开始加向量召回时Recall100可能从30%提到42%效果非常明显。可当线上已经有了向量召回、swing召回、实时行为召回等多条路径之后再把精力放在向量模型上收益往往以肉眼不可见的速度递减。你需要调的参数越来越多负样本采样比例、in-batch负样本数量、温度系数、样本温度、embedding维度、索引的M和efConstruction、量化方式……每个参数单独看都有道理合在一起就是一场灾难。训练时A/B两组实验可能只差0.3个点但为了拿到这0.3个点整个团队要排查数据泄漏、难例采样、梯度更新等一大堆问题。更让人难受的是向量检索的收益天花板往往受限于“隐空间表达”的结构性瓶颈。双塔模型在训练时用复杂样本和负样本去拉近正例、推开负例可一旦某个商品的交互数据很少它的向量表征就会退化到类目中心附近和大量同类商品挤在一起。这种拥挤在召回阶段会造成“看起来相似实际完全不对”的bad case用户搜“通勤小方包”召回结果可能全是相近外观的学院风双肩包。你很难通过继续调模型参数去改变这种局面因为问题不是参数而是整条链路的范式假设。1.2 第二层长尾和低成本候选的“边际失效”交易搜索和内容搜索有一个显著差异内容的消费成本极低用户刷到不那么相关的视频顶多划走但交易搜索里用户是有明确购买意图的他搜“小白鞋”就是在清点自己要买的东西。在这种场景下长尾商品才是利润池而长尾恰恰是向量检索最容易失效的区域。原因不难理解。向量召回的候选质量高度依赖训练数据中的交互信号。头部商品每天有大量点击、收藏、成交embedding能学得非常充分长尾商品一周可能只有零星几次曝光它的向量基本由类目、品牌等粗粒度特征决定。更麻烦的是很多交易平台的上新速度极快新商品没有足够的用户行为数据冷启动阶段只能靠内容特征拼凑一个临时embedding。这种向量在ANN索引里要么被埋没在相似商品中要么距离旧商品太远很难被激活。有人会说那就用多路召回兜底加一路实时行为召回、加一路swing协同召回。这个思路没错但它本质上是在“外面套更多网”而不是让某一条网更密。每增加一条路就要增加一套资源维护、样本回流和评估成本。到了后期你会发现系统已经复杂到很难说清楚某一次线上指标波动是哪一路召回引起的。这时再去“卷向量检索”收益未必抵得上工程复杂度的上升。1.3 第三层从“相似”到“语义意图”之间的鸿沟向量检索的另一个结构性问题是它天然做的是“相似度匹配”而不是“目标生成”。这两者的差别在交易搜索里非常明显。用户输入“白色高跟凉鞋 方头 细跟”向量召回要做的是把这个query整体编码成一个向量然后在一个巨大的候选集合里找最相近的embedding。问题在于相似度的定义是被训练样本塑造的用户实际想表达的是“我需要一个满足这些属性组合的商品”但这个组合在向量空间里的语义重心并不一定落在训练数据的稠密区域。打个比方向量检索像是一个大型学生档案库老师拿到一份学生描述只能根据档案上的“相似程度”把最像的几个人挑出来。而生成式召回的做法是让模型直接“叫出学号”它把商品ID编码成可以预测的目标序列然后用条件生成的方式直接产出学号。这样就不需要先设定一个“相似度度量”再在候选空间里扫一遍而是把用户query里的意图直接翻译成候选商品标识。从数学角度看向量检索其实是在一个连续空间里做近邻查询而生成式召回是在离散序列空间里做条件概率建模。后者天然可以携带更结构化的信息比如把类目、品牌、价格带作为生成序列的前缀约束这在向量召回里需要额外加规则过滤而在生成式里是解码过程的一部分。这个差别恰恰是我认为值得跳出“只卷向量检索”的根本原因。2. 生成式召回到底解决什么问题——核心思路与设计拆解2.1 核心思路把“检索问题”变成“生成问题”生成式召回的基本建模方式不复杂给定用户query文本或行为序列模型直接输出一个商品ID序列或者输出一组商品ID作为候选集合。这里的“商品ID”不是普通数据库主键而是一个经过特殊编码的离散序列类似把商品映射到一个语义ID空间。具体地我们不能直接让模型生成一个几百万取值空间的唯一ID因为这种ID之间没有任何结构性模型根本学不出规律。行业内更常见的做法是把商品ID编码成语义化token序列例如按“一级类目→二级类目→品牌→价格带→唯一标识”的层级结构切分成多个token。这样相关商品天然共享前缀——“男鞋→运动鞋→耐克→500-800元”就能共用大部分token模型在学习时更容易把query里的意图与商品前缀对应起来。训练过程类似sequence-to-sequence任务输入是query序列可以是分词后的文本也可以是用户最近点击商品序列输出是商品的语义ID token序列。模型用交叉熵损失学习在推理时用beam search生成top-k个序列再把这些序列解码回商品ID。整个过程完全绕开了ANN索引、向量量化、距离计算这些环节把召回变成了一个“每一步都在做语义决策”的解码过程。这样做的好处非常直接第一模型不是学“query和商品的整体相似度”而是学“query在语义树上逐层落到哪个分支”每一步都有更强的可解释性第二生成序列天然可以加入约束解码比如限定只能解码当前仍在架、可售的商品前缀从机制上规避了向量召回里“向量近但商品已下架”的问题第三长尾商品只要能对应到一个稳定的语义前缀生成概率就不会完全被头部商品压死。2.2 交易搜索场景下的“得物式”设计考量得物交易搜索和一般电商搜索相比有几个特殊点让生成式召回显得尤其合适。首先是强结构化属性。得物平台上商品尤其是潮流单品有明确品牌、类目、款式、发售年份等结构化信息。把这些信息拆成语义ID的层级token非常自然。模型在解码时可以在每一步只激活当前合法前缀下的token比如生成完“品牌耐克”之后下一步只能在耐克品牌下的子类目里选择这样产出的候选天然满足结构化约束。其次是强购买意图。得物用户搜索行为往往非常明确用query里的词能够很直接地映射到“类目属性”的语义分支。向量召回把query压成一个稠密向量后这个映射关系是隐式的而生成式召回通过token生成的方式把它显式化相当于把“用户想要什么”的可解释性提升了。再次是上新速度和商品流动率。得物有很多新品发售这些新商品在没有用户行为数据之前向量召回基本无能为力。但生成式召回可以通过语义ID前缀做代码级的热启动只要新商品能挂到正确的类目/品牌节点下即使没有点击数据生成模型也可能在解码时把它作为“前缀对应的高概率商品”产出。这一点对交易平台非常重要——新品往往是拉新和促活的关键场景。当然这里要冷静地说一句生成式召回并不是万能的。它的训练样本要求比向量双塔更高因为要学习“query到商品ID序列”的映射如果交互数据稀疏模型会很容易过拟合到高频商品上。这也是我在后面几节里会重点讲实操细节的原因。2.3 和现有“多路召回”的关系不是替代而是新增一档很多团队听到“生成式召回”的第一反应是我们是不是要把向量召回全部下掉我的观点很明确不要这么做。生成式召回不应该被定位成“替代者”而应该被定位成“多路召回里新增的一条高性价比路径”。传统多路召回体系里向量召回负责“语义近邻”swing召回负责“行为关联”实时行为召回负责“短期意图即时反馈”热度召回负责“安全兜底”。生成式召回的定位是“语义意图到结构化目标的直接映射”。它和向量召回有关系但重叠不是完全重合。离线测试时很常见的情况是向量召回和生成式召回的命中集合有一定交叠但生成式召回能够在类目交叉、属性组合、品牌强约束这些场景下找到向量召回漏掉的商品。实操上可以把它当作一个独立的召回桶接入粗排。比如在100个召回池子里给生成式召回分配10个到15个候选位。这样做的好处是即使生成式模型在某类query上明显表现不佳也只是损失了一小部分召回位不会对整体效果造成灾难性冲击而且我们可以用线上AB实验单独衡量它的增量。这也是我建议所有准备尝试这个方案的团队遵循的节奏先并行验证再逐步放大。3. 从0到1搭建生成式召回系统的实操路线3.1 训练数据构造与商品ID编码方案的取舍生成式召回的第一项工作不是选模型而是把训练数据组织好。数据组织得好不好直接决定后面所有环节的成败。训练样本的基本形式是“输入序列→输出目标序列”。输入序列可以是query分词结果也可以拼接用户历史行为商品序列。输出目标序列则是商品ID的语义编码。样本的来源建议优先使用成交样本和有效点击样本并过滤掉“最后被用户忽略”的商品。这里面有一个采样细节如果直接把所有曝光样本都拿进来噪声会非常大如果只用成交样本召回率上会偏保守。行业里更常见的做法是分层采样成交样本全量保留点击样本按价格带和类目做降采样纯曝光样本只取负例的一部分用于对比学习。商品语义ID编码是方案里最关键的一个环节。我们当时的做法可以参考这样的设计把商品按四级结构展开——一级类目、二级类目、品牌、价格分档、商品唯一短码。前四部分映射为固定的token id最后一部分用商品ID做hash截断或映射到唯一token。注意商品唯一短码的token数量理论上等于上架商品总量这个量级可能达到百万甚至千万级。为了控制词表规模可以将唯一短码再做一次“聚合编码”比如用AutoEncoder把商品原始向量压缩到几个codebook索引类似RQ-VAE的方式用3到5个离散token表示一个具体商品。这样设计的核心理由是模型要学的不是“某个具体商品的唯一编号”而是“从query到商品结构特征的映射”。只要前缀预测对了具体商品在短码阶段可以通过前缀受限解码快速锁定到一个小集合再进行粗排打分即可。这里有一个非常容易踩的坑编码方案一旦训练完成线上推理时必须是同一个版本。训练时的token映射表要固化下来任何商品新增或类目调整都需要做对应上线更新。否则会出现训练时A商品的token在线上变成了B商品的情况这种问题极其隐蔽离线指标看不出来只有线上bad case会异常增多。3.2 模型选型与训练细节encoder-decoder还是纯decoder生成式召回在模型架构上一般有两种选择一是T5那种encoder-decoder结构二是类似GPT的纯decoder结构。我的默认推荐是encoder-decoder原因在于query通常比较短用一个专门的encoder去精读query比直接把query塞进decoder更直观训练也更好收敛。模型规模不需要非常大embedding维度512、6到8层transformer在大多数场景下已经够用。训练用Teacher Forcing即target序列每一步都使用真实token作为下一步输入配合交叉熵损失。关键的超参数可以参考这样一组初始值学习率1e-4到2e-4warmup步骤1000到2000batch size不低于1024最好用梯度累积把有效batch撑到4096以上训练轮次控制在5到10轮之间避免过拟合。下面是一段简化版本的数据和训练流程示意可以用作方案理解# 训练样本构造示意 def build_training_pair(query_tokens, item): input_ids tokenizer.encode(query_tokens) # query文本编码 target_ids semantic_id_encode(item) # item转语义ID序列 # item 的语义ID示例: [cat1, cat2, brand, price_bin, short_id] return input_ids, target_ids # 模型训练伪代码 for batch in dataloader: input_ids batch[input_ids] target_ids batch[target_ids] loss model(input_ids, decoder_input_idstarget_ids[:, :-1], labelstarget_ids[:, 1:]).loss loss.backward()训练时一定要做类别不平衡处理。高频商品的样本非常多如果直接按原始分布训练模型会退化成一个“只预测爆款”的复读机。常用的做法是对训练对按目标商品的曝光量做重要性采样高频商品降采样长尾商品升采样。具体倍率可以根据商品曝光分布的倒数的平方根来设定这样能在不彻底改变数据分布的前提下提升长尾商品的训练权重。另外负样本在这里的概念和双塔模型不太一样。生成式模型没有显式负样本它天然通过“从整个词表里预测正确token”来区分目标。所以需要在训练时保证batch内商品尽量来自不同类目增强模型判定错误前缀的难度。我们的做法是构造batch时按商品一级类目分组再混合保证同一个batch内的目标商品具有多样性让模型不能靠“猜类目”偷懒。3.3 解码策略与在线部署的延迟优化生成式召回落地时第一痛点永远是解码延迟。自回归生成是逐个token往外蹦的如果每出一个token都要完整过一遍模型线上压根扛不住。我的经验是把解码限制在非常短的序列上。通常情况下语义ID序列长度可以控制在3到6个token其中前面几位是类目、品牌等粗粒度前缀。在推理时采用beam searchbeam size取5到8就足够。再配合“分组解码”的技巧先生成粗粒度前缀然后锁死前缀只对最后一位做宽beam展开。这种做法在代码实现上略微复杂但能把解码深度从6步压缩到2到3步延迟下降非常明显。部署层面我们当时用的是基于Triton的GPU服务加载int8量化后的模型。生成式召回对数值精度不如粗排模型那么敏感int8量化对Recall100的影响通常控制在1%以内但显存占用和推理延迟都能改善50%以上。在线链路里还需要加一道“候选商品有效性过滤”把所有解码出的商品ID同步到商品中心过滤掉已下架、不可售、库存为0的商品。这一步不能省否则生成的候选里会混入无效商品。给一个我们最终优化后的指标参考单条query从输入到产生10个候选GPU上P99延迟可以控制在20到30毫秒之间单机QPS大约在80到120之间。如果线上流量很大可以加一层“query改写后置缓存”对完全相同的query直接返回生成结果能进一步瘦身。4. 多路召回融合与实验评估——不是替代而是跃迁4.1 融合策略生成式召回如何与其他召回路径协同生成式召回上线后初期不应给太多候选位我比较建议从5%的召回占比开始跑起。具体做法是在原有100个召回候选里固定分配5到10个位置给生成式召回桶剩下90到95个候选继续由向量召回、swing召回、实时行为召回等路径产生。这里涉及一个分数对齐的问题生成式模型输出的score是生成概率和向量距离分数不在同一个量纲。不能直接把两类分数做加权和。常见做法有两种一是把生成式召回当作候选池的一部分送入粗排模型时只保留“生成式候选”这个特征让粗排模型学习是否生成自该路径二是在融合阶段对生成式候选做一次简易的规则加权比如生成概率乘以一个路径置信系数。前者更干净也更方便做长期迭代。我特别想强调的是生成式召回和swing召回这类行为路径的配合很有价值。swing善于发现“看了A的人也会看B”这类隐式关联但它解释不了“为什么”。生成式召回则擅长把query的语义直接投射到商品结构上。两条路径同时命中同一个商品时可信度非常高。我们在线统计过当某个候选同时来自生成式召回和swing召回时后续点击率要比单路径候选高出30%以上这证明两条路径的信息互为补充。4.2 离线评估指标增量召回率比绝对召回率更重要很多团队评估生成式召回时喜欢直接看RecallK曲线这本身没错但不够公平。RecallK衡量的是生成式召回独自覆盖了多少真实成交/点击样本可它没有告诉我们这些样本是不是已经被向量召回覆盖了如果生成式召回找到的全是向量召回已经找到的商品那它对线上就是零增量。所以我建议增加一个“增量召回率”指标以当前线上多路召回池为基准计算生成式召回新贡献的、能在后续成交/点击中体现的候选比例。这个指标是决定生成式召回值不值得接入线上的核心。实际操作中我们会把生成式候选、向量候选、swing候选都打上来源标签然后统计交集和差集。如果生成式新增候选的成交转化率明显高于随机候选说明这条路径具备真实增量。另一个离线指标重点是分段评测。按商品热度把测试集切成头部、腰部、长尾三档分别计算RecallK和增量召回率。很多模型在头部商品上表现亮眼一到长尾就现原形。生成式召回至少在长尾上的表现应该做到不低于向量召回才算真正合格。当初我们做方案验证时就是在“头部不降、长尾提升”这个目标下推进的。4.3 线上实验与灰度的风险控制线上实验需要保守一些。建议先切1%到5%的用户流量只把生成式召回结果作为粗排阶段的新增候选位观察一星期甚至更久重点盯三个指标整体成交转化率、召回候选里长尾商品的占比、粗排后最终进入精排的候选分布。这里有一个很容易被忽略的坑一旦生成式候选进入线上它的结果会参与用户行为样本回流从而影响下一轮训练数据。这会产生“自证预言”式的偏差——模型生成的候选被用户点击然后又被当成训练正例进一步强化模型的生成偏好。时间一长模型会越来越推荐“模型喜欢的商品”而不是用户真实搜索意图下的商品。为了避免这个问题我们会在训练数据里给每个训练样本打上“来源标签”控制生成式召回自身回流的样本比例并且定期用纯历史真实行为数据做全量重训把偏差压下去。5. 常见问题与排查实录5.1 生成结果重复率高、多样性不足怎么办这个现象非常典型。上线初期我们曾遇到生成式召回的10个候选里有7个都是同一个品牌同一个小类目下的近似商品。这是beam search的通病它倾向于选择整体概率最高的序列而高概率的序列往往集中在同一语义路径上。排查时先看生成序列的token分布。如果发现重复项都共享很长的前序token说明是beam search的多样性瓶颈。解决方向有两个一是把beam search换成diverse beam search在分组解码时对不同组施加差异化的惩罚项二是在最后一位解码时引入top-k随机采样或者对“已经生成过的前缀”做显式惩罚。实测中把最后的候选去重逻辑也加上通过比对语义ID前缀做近冗余剔除效果非常明显。5.2 长尾商品召回还是上不来怎么办长尾问题一般不是模型能力不足而是训练信号不足。排查完数据分布后大概率会发现长尾商品的训练样本占比极低。此时先不要盲目加大长尾样本权重因为权重加得太猛模型会开始胡乱预测小概率商品导致精确率崩掉。更稳妥的做法有两步第一步是用类目、品牌等粗粒度特征做“伪样本补充”比如把同一个二级类目下的商品互相共享一部分训练样本相当于用类目级别的信息辅助冷启动第二步是对折扣系数做一个温和调整把长尾样本的loss权重设定为头部样本的1.2到1.5倍即可不宜超过2倍。线上效果需要回归验证。5.3 在线延迟超标、GPU资源消耗过大怎么办如果P99延迟超过50毫秒优先检查三件事一是输入token长度是否没有被截断特别长的query会严重拖慢首token输出二是beam search的展开宽度是否过大三是动态batch是否被关闭导致并发请求没有合批GPU利用率过低。在资源允许的情况下可以直接上一版“前缀预测-缓存剪枝”的改造。把粗粒度前缀部分类目、品牌从解码中抽出来用一个很小的分类头单独预测预测完成后所有候选共享同一个前缀解码层只需要在最后一层做一个小范围展开。这一步减少的延迟消耗最显著副作用是需要维护一个“前缀到商品候选集合”的映射表逻辑上多了一层映射管理但收益完全值得。下面是一组参考速查表方便排查时直接对照症状可能原因排查步骤解决方案生成候选重复率高beam search扎堆到同一前缀检查生成序列的公共前缀长度diverse beam search、前缀去重长尾召回率低长尾样本占比不足统计训练样本的长尾分布类目伪样本补充、温和调权线上延迟超过50ms输入过长/beam过宽/未合批分阶段打印解码耗时截断输入、限制beam、动态batch候选大量下架/不可售未做有效性过滤检查候选商品在架状态同步商品中心、过滤无效ID离线指标好但线上不变引入过强的回流偏差检查回流训练样本占比控制自回流比例、定期全量重训5.4 生成式召回上线前后的两个“隐藏雷区”最后分享两个我踩过的坑。第一个是语义ID编码的版本漂移。新商品持续上架、类目调整频繁如果token映射表没有做版本管理训练时的“耐克-运动鞋”和线上推理的“耐克-运动鞋”可能对应完全不同的短码。这个问题排查起来极度耗时最有效的办法是在评估集里固化一批“标准query-商品对”每次模型或映射表更新后先跑一遍回归比对命中是否保持一致。第二个是不要把生成式召回直接接入精排。生成式召回输出的概率分布来自解码器它与精排模型的特征空间差异很大直接把它当成精排特征会导致精排模型学到错误的非线性关系。更合理的方式是让生成式召回只负责“提供候选”精排阶段还是使用原始的商品侧特征、用户侧特征和交互特征。这条原则能帮你少走很多弯路。如果团队现在还在向量检索的内卷循环里纠结我的建议是先别急着全量替换。把商品语义ID化这个基础做扎实在现有召回体系里开一个“生成式候选桶”用增量评估去判断它是否值得进一步放大。这套逻辑做通了以后你会发现向量检索不再是一条独木桥而生成式召回提供的是一套更可控、更结构化的“目标直出”能力。
返回列表