☰
生成式召回:跳出向量检索内卷,把召回从匹配变成生成
2026/10/2 16:58:28 网站建设 项目流程

先聊一个现象:搜索算法这个圈子聊召回,几乎已经快被向量检索给“包场”了。双塔模型、ANN索引、多路召回,很多人觉得召回优化的尽头就是把向量表征做得更好、把近似搜索做得更快。但做交易搜索做到后面,你会发现这条路正在变得越来越拥挤——精度上不去了,资源开销越来越大,而那些真正影响成交的长尾商品、结构化约束、强购买意图,反而是向量检索最不擅长的东西。我想认真聊一聊得物交易搜索里“生成式召回”这个思路:它不是重新做一个向量模型,而是把“召回”从“匹配”变成“生成”,直接通过条件生成的方式产出商品标识。这篇内容适合正在做搜索召回、对召回架构有重构冲动的同学,也可以给评估生成式方案价值的团队一个相对完整的参考。

1. 为什么说向量检索已经“卷到头”了——召回范式问题的三层拆解

1.1 第一层:链路复杂度和超参敏感性的持续上升

向量检索本身并不复杂,双塔结构的本质是“把用户和商品分别映射到同一个隐空间,用内积或余弦距离做近邻匹配”。问题在于,当这套链路运行到后期,几乎所有能提收益的动作都已经变成在“阿基米德螺线上追尾巴”。

以我在实践中接触过的召回系统为例:刚开始加向量召回时,Recall@100可能从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-4,warmup步骤1000到2000,batch 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_ids=target_ids[:, :-1], labels=target_ids[:, 1:]).loss loss.backward()

训练时一定要做类别不平衡处理。高频商品的样本非常多,如果直接按原始分布训练,模型会退化成一个“只预测爆款”的复读机。常用的做法是对训练对按目标商品的曝光量做重要性采样:高频商品降采样,长尾商品升采样。具体倍率可以根据商品曝光分布的倒数的平方根来设定,这样能在不彻底改变数据分布的前提下提升长尾商品的训练权重。

另外,负样本在这里的概念和双塔模型不太一样。生成式模型没有显式负样本,它天然通过“从整个词表里预测正确token”来区分目标。所以需要在训练时保证batch内商品尽量来自不同类目,增强模型判定错误前缀的难度。我们的做法是构造batch时按商品一级类目分组再混合,保证同一个batch内的目标商品具有多样性,让模型不能靠“猜类目”偷懒。

3.3 解码策略与在线部署的延迟优化

生成式召回落地时,第一痛点永远是解码延迟。自回归生成是逐个token往外蹦的,如果每出一个token都要完整过一遍模型,线上压根扛不住。我的经验是把解码限制在非常短的序列上。

通常情况下,语义ID序列长度可以控制在3到6个token,其中前面几位是类目、品牌等粗粒度前缀。在推理时采用beam search,beam size取5到8就足够。再配合“分组解码”的技巧:先生成粗粒度前缀,然后锁死前缀,只对最后一位做宽beam展开。这种做法在代码实现上略微复杂,但能把解码深度从6步压缩到2到3步,延迟下降非常明显。

部署层面,我们当时用的是基于Triton的GPU服务,加载int8量化后的模型。生成式召回对数值精度不如粗排模型那么敏感,int8量化对Recall@100的影响通常控制在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 离线评估指标:增量召回率比绝对召回率更重要

很多团队评估生成式召回时,喜欢直接看Recall@K曲线,这本身没错,但不够公平。Recall@K衡量的是生成式召回独自覆盖了多少真实成交/点击样本,可它没有告诉我们:这些样本是不是已经被向量召回覆盖了?如果生成式召回找到的全是向量召回已经找到的商品,那它对线上就是零增量。

所以我建议增加一个“增量召回率”指标:以当前线上多路召回池为基准,计算生成式召回新贡献的、能在后续成交/点击中体现的候选比例。这个指标是决定生成式召回值不值得接入线上的核心。实际操作中,我们会把生成式候选、向量候选、swing候选都打上来源标签,然后统计交集和差集。如果生成式新增候选的成交转化率明显高于随机候选,说明这条路径具备真实增量。

另一个离线指标重点是分段评测。按商品热度把测试集切成头部、腰部、长尾三档,分别计算Recall@K和增量召回率。很多模型在头部商品上表现亮眼,一到长尾就现原形。生成式召回至少在长尾上的表现应该做到不低于向量召回,才算真正合格。当初我们做方案验证时,就是在“头部不降、长尾提升”这个目标下推进的。

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化这个基础做扎实,在现有召回体系里开一个“生成式候选桶”,用增量评估去判断它是否值得进一步放大。这套逻辑做通了以后,你会发现向量检索不再是一条独木桥,而生成式召回提供的是一套更可控、更结构化的“目标直出”能力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询