1. 从“卷向量”到“拼生成”:交易搜索召回到底卡在哪
做电商搜索的同行这两年应该都有同感:向量检索这条赛道已经卷到不能再卷了。双塔模型、ANN索引、HNSW参数调优、量化压缩,能榨的油水基本榨干了。但真正在一线做交易搜索的人心里都清楚,向量召回的天花板其实早就摸到了——它解决的是“语义相似”的问题,却解决不了“交易意图匹配”的问题。
我拿一个真实场景举例。用户在得物搜“送男朋友生日礼物 球鞋 预算800”,向量检索会怎么做?它把整句话编码成一个稠密向量,然后去近邻索引里找距离最近的商品向量。结果往往是:搜出来一堆球鞋,但价格从200到3000都有,风格从实战到联名都有,甚至还有一些女款球鞋混进来。为什么?因为向量空间里“球鞋”这个语义维度权重太高了,把“预算800”“送男友”“生日”这些约束条件全稀释掉了。
这就是传统向量召回的根本问题:它把多条件约束压缩成了一个不可解释的稠密向量,信息在编码阶段就丢失了。你没法告诉索引“价格必须小于800”,也没法说“只要男款”,更没法表达“生日礼物场景下偏礼盒装”。这些结构化约束和场景化意图,在向量编码那一步就被平均掉了。
生成式召回要解决的就是这个问题。它的核心思路不是“把query编码成向量去匹配”,而是“让模型直接生成可能被点击的商品标识”。听起来很激进,但逻辑上完全说得通:既然大语言模型能根据上下文生成下一个token,那它当然也能根据用户query生成商品ID序列。而且生成的过程是自回归的,每一步都可以注入约束条件——价格、品类、性别、场景,全都可以作为条件token参与解码。
得物交易搜索团队提出的这个范式跃迁,本质上是从“判别式匹配”转向“生成式预测”。判别式匹配是“我给你一个query,你告诉我哪个商品最相似”;生成式预测是“我给你一个query,你直接告诉我用户最可能点哪个商品”。前者是检索问题,后者是预测问题。检索问题受限于索引结构和相似度度量,预测问题则只受限于模型能力和训练数据。
这个转变带来的最大好处是:召回阶段就可以做多条件联合建模,而不需要先召回再过滤。传统链路是“向量召回top1000 → 规则过滤 → 粗排 → 精排”,每一层都在做减法,信息逐层丢失。生成式召回则是在解码阶段就把价格、品类、场景等条件作为前缀token注入,生成的商品ID天然满足约束。相当于把过滤逻辑前置到了召回模型内部,链路更短,信息损失更小。
当然,这条路也不是没有代价。生成式召回最大的挑战在于:商品库是动态的,每天都有新商品上架、旧商品下架,而生成模型的输出空间是固定的词表。怎么让模型“认识”新商品?怎么保证生成的商品ID是真实存在的?怎么处理长尾商品的冷启动?这些问题在向量检索里都不存在,但在生成式召回里全是坑。得物的方案里用了“动态词表+前缀树约束解码”来解决,后面我会详细拆解。
先说说这个方案适合谁来参考。如果你正在做电商搜索、推荐系统、或者任何需要从大规模候选集中召回item的场景,并且已经感受到了向量检索的瓶颈,那这篇内容应该对你有直接帮助。如果你只是刚入门搜索算法,建议先补一下双塔模型和ANN索引的基础,再来看生成式召回会更容易理解。另外,这个方案对算力有一定要求,至少需要一张A100级别的卡做推理,训练阶段则需要更多。如果算力受限,可以先从“生成式重排”做起,把生成式模型用在精排阶段而不是召回阶段,效果也会很明显。
2. 生成式召回的核心设计:为什么是“生成”而不是“检索”
2.1 从判别式到生成式的范式转换逻辑
传统向量召回的本质是度量学习。训练阶段,模型学习一个映射函数,把query和item映射到同一个向量空间,使得点击过的query-item对距离更近,未点击的距离更远。推理阶段,用ANN索引加速最近邻搜索。这个范式统治了搜索召回将近十年,因为它简单、高效、可扩展。
但它有一个致命缺陷:向量空间是连续的,而交易约束是离散的。价格区间、品类层级、性别标签、场景标签,这些都是离散的、结构化的约束。当你把它们编码进一个连续向量时,模型只能学到“大概偏贵”“大概偏运动”这种模糊的语义,没法精确表达“价格在500到800之间”“必须是男款”“适合生日送礼”。
生成式召回换了一个思路:不学向量映射了,直接学条件概率分布。给定query和用户上下文,预测下一个可能被点击的商品ID。数学上就是从建模P(item|query)变成建模P(item_id_1, item_id_2, ..., item_id_n | query, context)。这个条件概率分布可以用自回归语言模型来建模,每一步解码都可以注入约束。
我打个比方。向量召回像是用一张模糊的照片去图书馆找相似的书——你只能找到“大概像”的,但没法精确指定“我要2023年出版的、价格低于50的、作者是某某的”。生成式召回则像是直接告诉图书管理员“我要2023年出版的、价格低于50的、作者是某某的”,管理员直接去书架上拿。前者是相似度匹配,后者是条件生成。
2.2 商品ID的语义化编码方案
生成式召回的第一个技术难点是:商品ID怎么表示?如果直接用数据库自增ID,那模型要学的词表就是千万级别,而且ID之间没有任何语义关系,模型很难学到“相似商品”的概念。
得物的方案用了语义化ID编码。具体做法是:先用商品的多模态特征(标题、图片、类目、价格、品牌)训练一个商品嵌入模型,得到每个商品的稠密向量。然后用层次化聚类或者乘积量化,把稠密向量映射成短序列的离散token。比如一个商品可能被编码成<A_12, B_45, C_78>这样的三元组,其中A代表一级类目,B代表二级类目,C代表价格区间和风格。
这样做的好处是:词表大小可控(每个位置只有几百到几千个候选token),而且token之间有语义关系。A_12和A_13可能都是运动鞋,但一个是篮球鞋一个是跑步鞋。模型在生成的时候,可以先确定大类,再细化小类,最后确定具体商品。这种层次化生成方式,比直接生成一个扁平ID要稳定得多。
注意:语义化ID的编码质量直接决定了生成式召回的上限。如果编码方案把两个完全不同的商品映射到了相近的token序列,模型就会混淆。建议在编码阶段用对比学习加强类内紧凑性和类间区分性。
2.3 多模态特征如何注入生成过程
交易搜索和通用搜索最大的区别是:商品有图片。用户搜“送男朋友生日礼物 球鞋”,不仅看标题,还会看图片。一双鞋的颜值、配色、联名元素,往往比标题更能决定点击。
得物的方案里,多模态特征不是简单拼接到query向量里,而是作为条件前缀注入到生成模型的解码器中。具体来说,query文本经过LLM编码后得到文本token序列,商品图片经过视觉编码器(比如CLIP的ViT)得到视觉token序列,然后文本token和视觉token拼接在一起,作为解码器的前缀。解码器在生成商品ID的时候,可以同时attend到文本和视觉信息。
这个设计很巧妙。传统多模态召回是把图片和文本分别编码成向量,然后做融合。但融合方式很粗糙,通常是拼接或者加权求和。生成式召回则是在解码的每一步都让模型“看着”图片和文本,动态决定下一步生成什么token。比如用户搜“白色球鞋”,模型在生成类目token时可能同时关注文本里的“球鞋”和图片里的“白色”,生成一个既符合类目又符合颜色的商品ID。
2.4 约束解码:让生成结果必然可交易
生成式召回最大的工程风险是:模型可能生成一个不存在的商品ID,或者生成一个已经下架的商品。这在向量检索里不会发生,因为索引里只有真实存在的商品。但在生成式召回里,词表是固定的,模型可能组合出无效的ID序列。
得物用了前缀树约束解码来解决。具体做法是:把所有在架商品的语义化ID序列构建成一棵前缀树(Trie)。解码的时候,每一步只允许生成前缀树中存在的token。如果某个分支下没有有效商品,就直接剪枝。这样生成的ID序列必然对应一个真实在架的商品。
这个机制还有一个额外好处:可以动态更新。新商品上架时,把它的ID序列插入前缀树;商品下架时,从树中删除对应路径。不需要重新训练模型,只需要更新前缀树即可。这就解决了生成式召回的商品动态性问题。
3. 工程落地实操:从训练到推理的完整链路
3.1 训练数据构造与样本加权策略
生成式召回的训练数据构造和向量召回完全不同。向量召回用的是pairwise样本(query, 正样本item, 负样本item),生成式召回用的是sequence样本(query, 点击item序列)。得物的方案里,训练样本是用户真实点击序列,按时间顺序排列,模型学习预测下一个点击商品。
这里有一个关键细节:样本加权。不是所有点击都同等重要。用户点了但立刻返回的,可能是误点;用户点了并且停留很久、加了购物车、甚至下单的,才是强正样本。得物的方案里用了多级加权:曝光未点击权重0,点击权重1,加购权重3,下单权重5。这样模型学到的生成分布更偏向高价值商品。
另一个细节是序列截断。用户点击序列可能很长,但生成式召回只需要最近几次点击作为上下文。得物用了滑动窗口,窗口大小设为10,即用最近10次点击预测下一次点击。窗口太大,模型注意力分散;窗口太小,上下文不足。10是一个经验值,可以根据业务调整。
实操心得:训练初期不要用全量商品库做负采样,先用热门商品做负采样,让模型快速学到基本分布。等模型收敛后,再逐步加入长尾商品做难负样本挖掘。这样训练更稳定,收敛更快。
3.2 模型架构选型与参数配置
得物的生成式召回模型基于Decoder-only的Transformer架构,参数量在1B到7B之间。为什么不用Encoder-Decoder?因为召回任务本质上是条件生成,Decoder-only的自回归生成更自然。而且Decoder-only可以复用预训练语言模型的权重,冷启动更快。
具体配置上,我根据常见实践推测:层数24到32层,隐藏维度1024到2048,注意力头数16到32,词表大小取决于语义化ID的编码方案,通常在10万到50万之间。训练用AdamW优化器,学习率1e-4到5e-5,warmup步数5000到10000,batch size尽量大,用梯度累积来模拟大batch。
推理阶段,用beam search生成top-k候选。beam size通常设为10到50,太小召回不足,太大延迟高。得物的方案里还用了分组beam search:先按类目分组,每个类目下生成若干候选,最后合并。这样可以保证召回结果的多样性,避免全是同一类目的商品。
3.3 前缀树构建与动态更新机制
前缀树的构建是工程落地的核心环节。假设语义化ID是三层结构<A, B, C>,A有100个候选,B有1000个候选,C有10000个候选。那么前缀树的根节点有100个分支,每个A节点下有1000个B节点,每个B节点下有10000个C节点。总节点数约100+1001000+1001000*10000,这个量级需要分布式存储和检索。
得物的方案里用了分层前缀树:第一层和第二层放在内存里,第三层放在SSD上。解码时先查内存中的前两层,确定大类和中类,再去SSD查具体商品。这样平衡了速度和容量。
动态更新方面,新商品上架时,先计算它的语义化ID,然后从根节点开始逐层插入。如果中间节点不存在就创建,存在就复用。商品下架时,从叶子节点向上删除,如果某个节点没有子节点了就剪掉。整个过程是增量的,不需要重建整棵树。
3.4 推理加速:量化、蒸馏与缓存
生成式召回的推理延迟是最大的工程挑战。一个7B模型,自回归生成10个token,每个token都要过一遍全量参数,延迟轻松超过100ms。对于搜索场景,100ms的召回延迟是不可接受的。
得物用了三招来加速。第一是量化:把FP16权重转成INT8,推理速度提升近一倍,精度损失控制在1%以内。第二是蒸馏:用7B模型蒸馏一个1B的小模型,小模型负责在线推理,大模型负责离线生成训练数据。第三是缓存:热门query的生成结果缓存起来,下次同样query直接返回缓存,不用重新推理。
这三招组合下来,在线推理延迟可以压到20ms以内,基本满足搜索召回的要求。当然,缓存命中率取决于query的分布,长尾query还是需要实时推理。
4. 踩坑实录:生成式召回常见问题与排查
4.1 生成结果重复与多样性不足
这是生成式召回最常见的问题。模型倾向于生成热门商品,导致top-k结果里全是爆款,长尾商品没有曝光机会。根本原因是训练数据里热门商品点击多,模型学到了“热门商品概率高”的先验。
解决办法有两个。一是训练阶段做样本加权:降低热门商品的权重,提高长尾商品的权重。二是推理阶段做多样性约束:在beam search里加入惩罚项,如果某个类目已经生成了多个候选,就降低该类目下其他候选的分数。得物的方案里还用了MMR(最大边际相关性)重排,在生成候选之后做一次多样性重排,确保结果既相关又多样。
4.2 新商品冷启动与词表外问题
新商品上架后,模型没见过它的ID,自然不会生成它。这是生成式召回的固有缺陷。得物的解法是动态词表+前缀树热更新。新商品上架时,立即计算语义化ID并插入前缀树,同时把该商品的ID序列加入模型的输出词表。虽然模型参数没变,但解码时的候选空间扩大了,新商品有机会被生成。
但这样还不够。模型对新商品的“偏好”是零,因为训练数据里没有它的点击记录。得物用了元学习的思路:用商品的多模态特征预测它的初始点击率,然后把预测值作为先验注入解码分数。这样新商品虽然没历史数据,但凭借图片和标题的语义信息,也能获得一定的曝光机会。
4.3 约束解码导致的性能下降
前缀树约束解码虽然保证了生成结果的合法性,但也带来了性能问题。每一步解码都要查前缀树,如果树很深,查询延迟会累积。得物的优化是预编译前缀树:把前缀树编译成有限状态机,解码时直接做状态转移,不需要逐层查询。这样单步解码延迟从毫秒级降到了微秒级。
另一个问题是过度约束。如果前缀树太稀疏,很多分支被剪枝,模型可能生成不出足够的候选。得物的方案里设置了最小候选数阈值:如果某个query下生成的候选少于阈值,就放宽约束,允许生成一些“近似合法”的ID,然后再做一次过滤。这样保证了召回率。
4.4 多模态特征对齐失败
多模态注入的难点在于对齐。文本说“白色球鞋”,图片里鞋子是白色的,但背景是灰色的。如果视觉编码器把背景也编码进去了,模型可能学到“灰色”和“白色球鞋”的关联,导致生成错误。
得物用了区域注意力来解决:视觉编码器只关注商品主体区域,背景区域做mask。具体做法是用目标检测模型先框出商品主体,然后只对主体区域做视觉编码。这样文本和视觉的对齐更准确,生成结果也更符合预期。
排查技巧:如果发现多模态召回结果和纯文本召回结果差异很大,先检查视觉编码器的注意力区域。很多时候问题出在背景干扰上,而不是模型本身。
5. 效果评估与迭代方向
5.1 离线评估指标怎么选
生成式召回的离线评估不能只看召回率。因为生成式召回的输出是序列,不是集合,所以要用序列级别的指标。得物用了Hit Rate@K和NDCG@K,同时加了覆盖率和多样性指标。覆盖率衡量召回结果中不同类目、不同价格区间的商品占比,多样性衡量结果之间的相似度。
还有一个关键指标是约束满足率:生成的商品中有多少满足query里的显式约束(价格、品类、性别)。这个指标直接反映了生成式召回相比向量召回的优势。得物的数据是,约束满足率从向量召回的62%提升到了生成式召回的89%。
5.2 在线A/B实验的注意事项
在线实验最大的坑是位置偏差。生成式召回的结果如果排在前面,点击率自然高,但这不代表召回质量好。得物用了随机化位置的方法:在实验组里,把生成式召回的结果随机插入到不同位置,然后看整体点击率的变化。这样可以消除位置偏差的影响。
另一个坑是新商品曝光。生成式召回可能会给新商品更多曝光,短期内点击率可能下降,但长期看商品生态更健康。得物的做法是设置长期指标:除了点击率,还看新商品的次日留存率、复购率。这些指标更能反映召回策略的长期价值。
5.3 后续迭代方向
生成式召回目前还在快速演进。得物的方案里已经用了多模态注入和约束解码,但还有优化空间。第一个方向是端到端训练:目前视觉编码器和语言模型是分开训练的,如果联合训练,多模态对齐会更好。第二个方向是强化学习:用用户反馈作为奖励信号,直接优化生成策略,而不是只做监督学习。第三个方向是个性化生成:把用户画像作为条件注入,让不同用户搜同一个query得到不同的召回结果。
我个人觉得,生成式召回和向量召回不是替代关系,而是互补关系。向量召回擅长语义泛化,生成式召回擅长约束满足。最好的方案是双路召回:向量召回保底,生成式召回做增量,最后融合排序。得物目前也是这么做的,生成式召回占比在30%左右,但贡献了超过50%的成交额。
这个方向后续还可以这样扩展:把生成式召回用到推荐场景,用用户历史行为序列生成下一个可能点击的商品。逻辑和搜索召回一样,只是条件从query变成了用户画像。得物已经在尝试了,效果比传统协同过滤好很多。如果你也在做推荐系统,不妨试试这个思路。