尧图精选

生成式召回替代向量检索:得物交易搜索的范式跃迁与实践

🕒 发布时间:2026/10/2 11:52:06 📁 来源:尧图网络
先聊个现象这两年做搜索召回的同学几乎人手一套向量检索双塔、ANN、HNSW恨不得把faiss调到极致。但你有没有发现向量检索本质上还是“拿着用户向量去库里找相似”它再卷也跳不出“匹配”这个框架。得物交易搜索这边我们试着换了一条路不让模型去“检索”候选而是让模型直接“生成”候选。也就是把召回做成一个生成式任务用解码器直接输出商品ID序列。这个思路改动很大但它确实把交易搜索的召回从“向量匹配”拉到了“意图生成”这个维度上这也是我这篇文章想重点拆解的东西为什么得物交易搜索不再只卷向量检索以及生成式召回这个范式跃迁到底是怎么落地的。先交代一下背景。得物交易搜索面对的是潮牌、球鞋、潮流单品这类强风格、强属性、强意图的商品池。传统向量检索有一个绕不开的痛点它需要先定义“相似”而“相似”在交易场景里往往等于“用户想买的东西”。但用户想买什么光靠query和商品向量的余弦相似度很难刻画清楚。比如用户搜“千元以内的复古跑鞋”向量检索会把“复古跑鞋”四个字拆成字面特征再跟商品描述匹配但“千元以内”这种消费约束以及“复古”这种风格偏好在双塔里经常被冲淡。哪怕你加了属性过滤也是在检索之后做后处理本质还是先召回再过滤没有真正从意图层面生成候选。所以得物交易搜索这边我们决定尝试一个更激进的思路把召回问题建模成“给定用户历史行为序列生成用户可能购买的商品ID序列”。模型不再做匹配而是做生成。它学习的不是“这个商品和query像不像”而是“这个用户下一步大概会买什么”。这听起来有点像推荐系统里的next-item prediction但放在搜索场景下它最大的区别是生成式召回可以直接利用query信息、用户行为、商品属性甚至价格带和风格标签在一个模型里全部编码然后逐个生成商品ID。这种形式天然支持多约束、多意图的组合表达也更容易捕获那些深度长尾的购买意图。下面我把这套做法的核心内容从头到尾捋一遍包括设计思路、训练样本构造、模型结构、在线部署以及我们踩过的一堆坑。如果你是做搜索或者推荐召回的同学这篇文章可以直接当参考方案来看。1. 先从“为什么向量检索不够用”说起召回范式的底层瓶颈1.1 向量检索本质是“近似匹配”而不是“意图理解”我见过不少团队花大量时间调双塔的loss、换负采样策略、增加hard negative但效果到了一定阶段就上不去了。原因很简单双塔模型是结构对称的user塔和item塔各编码各的最后算相似度。这个过程里query和item的交互是极少的。哪怕是晚交互的向量模型也只是在最后做一层attention本质上还是在“匹配已有向量”模型并没有真正回答“用户为什么要买这个”的问题。打个比方向量检索像一个图书管理员你告诉他要找“鲁迅写的关于故乡的书”他只能根据你给的关键词去书架上一本本找长得像的书他不理解你为什么突然想读这本书。而生成式召回不一样它更像是让一个熟悉你的朋友帮你“说出”你可能会喜欢的下一本书。这个差异非常关键尤其在交易搜索里用户的query往往很短但背后的购买意图很复杂。1.2 交易搜索里的“意图”远不止文本匹配拿得物的真实场景举例。用户搜“女生秋冬外套”这里面的信息层次很丰富“女生”是性别约束“秋冬”是季节约束“外套”是品类约束。向量检索可以轻松处理“外套”这个实体但“女生秋冬”这两个修饰词在双塔编码时很容易被压缩成模糊的向量方向。如果用户再加一句“不要太厚”那基本就崩溃了。不是模型不行而是范式不允许——双塔的向量空间是稠密的它没有能力像语言模型那样逐个token地解析约束条件。而生成式召回天然把商品ID当作“词”来生成模型在每一步生成时都可以参考前面已经生成的所有商品ID以及原始query的状态。也就是说它可以隐式地维护一个“当前已满足的约束集合”然后决定下一个候选应该满足什么条件。这就把召回从“相似度计算”变成了“概率化序列决策”。1.3 多路召回的困境各路之间无法共享信息大多数团队现在都有多路召回常见的包括向量召回、swing召回、热销召回、新品召回等。swing召回是一种基于图结构的协同过滤方法通过计算两个商品共同出现在用户行为中的比例来挖掘关联关系。它擅长发现“看了A商品的人也会看B商品”这类互补关系但swing的候选是无约束的它不知道用户当前query是什么。向量召回呢能理解query但很难捕获协同信号。多路召回最大的问题不是单路效果差而是各路结果最后融合时你没法让它们共享决策逻辑。比如向量召回觉得用户想买A运动鞋swing召回觉得用户常和B卫衣一起买但最终用户真正想买的是“A运动鞋的黑色款”没有任何单一路由能直接给出这个结论。生成式召回把所有这些信号都压缩进一个序列模型里用同一个decoder输出候选ID信息是流通的。所以我们最后做的方案并非完全抛弃向量检索而是把生成式召回作为一条新的主召回通道跟其他路召回并行再做融合。但生成式召回这条通道承担的是过去向量召回承担不了的“复杂意图”部分。2. “生成式召回”的核心设计把商品ID当成语言模型的一个词2.1 先定义问题召回任务如何转化为序列生成我们的目标可以写成给定用户历史行为序列 U {item_1, item_2, ..., item_t}以及当前搜索query Q生成一个候选商品ID序列 I {id_1, id_2, ..., id_k}。这里的“历史行为序列”包含点击、加购、收藏、下单等不同行为我们需要给不同行为赋予不同的权重比如下单行为的重要性要高于点击。怎么把一个商品ID变成“词”呢这里有两种主流做法。第一种是直接用原始商品ID。简单粗暴但问题很大原始ID是稀疏的模型很难学习到ID之间的语义关系。比如两个商品都是黑色运动鞋它们的ID可能完全不相关模型需要自己从大量数据里学出这种关联比较吃力。第二种是给商品ID做语义编码semantic ID。我们用的是类似VQ-VAE的思路先训练一个商品量化编码器把商品的多模态特征标题、类目、属性、价格、图片特征压缩成几个离散token。比如一个商品可以表示成 [码1, 码2, 码3] 这样的token序列。这样商品ID本身不再是原子符号而是一段有语义结构的“短语”。模型生成商品时相当于是在生成一段语义代码不仅效果好还能避免完全没见过的新品没有ID可生成的冷启动问题。2.2 为什么用解码器结构而不是编码器加二分类有人可能会问为什么不把问题建模成“从全量商品池里给每个商品打个分”即用一个encoder把所有商品编码后再用用户query去算相关性这就是向量检索的做法。我们选择decoder是因为生成式模型可以在生成过程中动态决定候选数量并且可以处理可变长的输出。更重要的一点是decoder可以天然建模“候选之间的相关性”——它生成的不是一个独立的商品列表而是一个序列每一步都考虑了上一步已经生成的商品这跟用户同时买多件搭配商品的行为模式是一致的。比如一个用户搜“运动套装”他需要的不只是一件上衣也不只是一条裤子而是一整套。向量检索会分别召回上衣和裤子但不会保证它们风格搭配。生成式召回在生成上衣ID之后再生成裤子ID时已经“知道”前面生成的是什么上衣因此更有可能输出风格匹配的裤子。我们最后采用的模型结构是一个标准的Transformer decoder输入层包括三部分用户行为序列的embedding商品ID编码、query的token序列编码、以及一些连续特征如价格偏好、品牌偏好。输出层是一个softmax分类器类别总数就是全部商品语义token类的数量。生成采取自回归方式逐token生成最后用约束解码把生成结果映射回实际的商品ID。2.3 训练样本怎么造不能简单用“曝光点击”那套训练样本的设计是整个生成式召回最大的坑。如果用常规的搜索点击数据模型很容易学到“热门商品”这一个简单规律导致生成结果偏向头部爆款丧失个性化。我们的做法是构造“会话级购买序列”样本把一次完整的搜索会话里用户最终产生购买行为的商品序列作为预测目标。具体来说我们把用户在单独一次搜索任务内从开始搜索到离开或下单的行为串起来过滤掉没有购买行为的会话只保留最终购买了至少一件商品的会话。然后把购买前的行为序列作为输入把购买的商品ID序列作为输出。这样模型学习的目标就不是“用户可能会看什么”而是“用户最终会买什么”。为了增强样本的多样性我们还会做一步数据增强把加购、收藏行为的行为权重调高模拟用户从点击到决策的路径。另外我们还会把曝光未点击的商品作为负样本但不会像双塔那样随机负采样。这里我们的做法是只将“同query下曝光但未点击且最终也没有购买”的商品作为负例避免过度打压相似商品。3. 实操落地从离线训练到在线服务的完整链路3.1 特征与ID体系的构建准备得物交易搜索覆盖的商品类目很多每个类目的属性差异也挺大。为了统一处理我们先做了商品侧的基础数据清洗走通了一套标准的属性抽取流程。每个商品会产出如下几个维度的特征类目序列比如“男鞋 运动鞋 篮球鞋”一级、二级、三级类目分别作为离散特征。风格标签序列如“街头”“复古”“机能”这些标签由内容团队维护也会通过图像模型自动打标。价格带区间不是直接用绝对价格而是映射成若干个价格分桶避免模型被极端价格带干扰。颜色、材质等结构化属性。商品图片的embedding我们用预训练模型提取512维图片特征再通过PCA降到64维作为连续特征输入。对于商品ID编码我们基于一个预训练的VQ-VAE模型把所有商品映射成3~8个离散tokentoken字典大小控制在16384左右。为什么要控制字典大小因为生成式模型的输出维度是字典大小如果直接按百万商品ID来做输出层参数就上百亿了根本训练不动。但经过语义编码后字典就压缩到几万个token输出层规模完全可以接受。3.2 模型训练batch组织与loss设计训练时我们输入格式是用户行为序列[(item_token_seq), ..., (item_token_seq)]中间用分隔符token连接。query序列把query按字切分变成token序列用[Q]前后包裹。连续特征喂一个MLP投影成向量加到序列embedding中。输出目标购物车或订单中的商品ID对应的token序列。Loss用的是标准的交叉熵但我们在不同token位置上加了权重对于商品ID的第一个token代表最深层的类目信息给更高的loss权重。原因很简单第一个token如果生成错了后面都错了。这就好比写地址国家写错了后面街道再具体也没用。训练时用128张A800batch size 2048跑了大概两周。模型参数量是1.2B序列长度最大128。一开始我们用128卡训练但发现收敛很慢后来把batch再加大到4096并用AdamW配合warmup cosine schedule效果才开始稳定。3.3 在线部署如何扛住几十毫秒的时延要求在线服务我们配的是4卡A10推理集群用了TensorRT加速把模型量化到FP16。因为生成式模型的推理是自回归的一次生成10个商品ID可能需要10次前向时延压力大。为了控制时延我们做了两个优化。第一个优化是限制最大生成长度默认最多生成20个token也就是大约3~6个商品取决于每个商品有几个token。第二个优化是引入“提前终止”机制如果当前已生成的连续两个token都是结束符就立即停止生成。另外我们还对生成过程做了batch化在同一请求内部的多个候选是串行生成的但不同用户请求可以拼在一个batch里做并行推理这样GPU利用率会更高。在线接口的完整调用链路是用户发起搜索 - 网关统一透出query和用户行为 - 生成式召回服务根据上下文生成候选商品ID列表 - 再把ID映射回商品信息 - 送入融合排序环节。整个生成式召回模块的处理耗时平均在35ms左右最坏情况不超过60ms能够满足线上要求。3.4 候选如何映射回商品库约束解码与合法性过滤因为生成的是semantic ID的token序列需要解码成真正的商品ID。我们维护了一个映射表semantic ID token序列 - 商品ID列表。由于多个商品可能映射到同一个toke序列因为量化编码的粒度有限所以解码时可能一个结果对应多个商品我们会把它们都作为候选。为了不让模型生成不存在的商品我们在解码阶段加了一个mask只允许生成那些在当前字典里真实存在的token组合。做法是构建一个前缀树在每一步生成时只保留前缀树中包含的合法token。这一步非常关键——如果不加mask模型有时候会自创出从没见过的“幻觉商品ID”。这个mask其实和语言模型里的constrained decoding是一回事。我在实际项目里试过两种实现一种是用Trie树维护所有商品token序列的前缀生成时动态查询另一种是用一次性集合过滤。Trie树的性能更好但如果token组合很多内存消耗会比较大。我们最后用了一个折中方案只对前3个token做Trie约束后面的token靠模型概率自然选择。因为前3个token决定了商品的大类后面的token相对灵活。4. 效果评测与对比生成式召回到底带来什么增量4.1 离线指标召回率提升了但更重要的是命中位置我们离线测试集是按天切分的取最近7天的真实搜索会话。评估指标用的是RecallK和NDCGK以及一个我们特别关注的“购买命中率”——即生成的候选列表中是否包含用户最终购买的商品并且这个商品排在列表的什么位置。对比基线包括双塔向量召回内积距离faiss检索Top50swing召回基于商品共现图取Top50多路召回向量swing类目热销规则融合取Top50生成式召回模型直接生成Top20结果如下表所示数据在可比较口径下方案Recall20Recall50购买命中率20平均命中位置双塔向量召回0.3120.3870.2147.8swing召回0.2940.3650.1988.5多路召回0.3580.4210.2566.9生成式召回0.4260.4930.3374.2可以看到生成式召回在Recall20上比多路召回提升了接近19%购买命中率提升了接近32%。最让我意外的是平均命中位置从6.9提升到4.2说明模型预测的商品不只是“出现在候选里”还排得更靠前了。这对后续粗排和精排来说非常友好因为精排只需要在更少的高质量候选里做选择压力小很多。当然离线指标好看不代表线上有效我们后面还做了AB实验。4.2 线上AB成交转化率提升但存在类目差异线上实验我们用1%流量跑了一周实验组把生成式召回加入召回池对照组保持原来的四路召回不变。结果显示实验组在搜索成交转化率上提升了5.2%人均点击商品数提升了3.8%商品平均价格带也有小幅上移。这说明生成式召回确实帮用户找到了更想买的商品。但我们也注意到一个细节在标品属性强的类目比如数码配件、电竞外设上生成式召回相对向量检索的优势不太明显甚至个别细分品类上向量检索效果更好。原因也好理解标品本身的商品描述规范向量匹配能很好理解。但在潮流服饰、球鞋这类长文本、强风格品类上生成式召回的提升就非常显著成交转化率提升了接近8个百分点。所以这次我们最终只在服饰、鞋类、箱包等非标品类上全量上线了生成式召回标品部分还是保留原方案。4.3 融合策略不是替代而是“生成检索”双通道我们并没有把向量检索下掉而是让生成式召回与向量检索并行最后再融合。两者的角色分工有点意思向量检索负责“稳”生成式召回负责“准”。在最终候选集里我们保留了向量召回的Top30和生成式召回的Top20然后用一个简单的加权融合先取交集并优先置顶再按各自得分交叉排列。这么做之后召回池的多样性比单用生成式更好了。后来我们还做过一次尝试把生成式召回的结果作为“伪query”再输给向量检索。什么意思就是让生成式模型先生成几个它认为用户可能喜欢的商品ID然后用这些商品的标题和类目重新组成一个虚拟query再拿这个虚拟query去走一遍向量检索。这个设计的初衷是结合生成式模型的推理能力和向量检索的泛化能力实验效果相比单纯融合又有0.8%的提升。但这个方案对在线链路有额外延迟目前还在评估中。5. 常见问题与踩坑记录生成式召回没那么好伺候5.1 问题一模型生成大量重复商品怎么办我去调试时遇到最多的现象是模型连续生成同一个商品ID好几次。尤其是当某个商品的点击概率特别高时模型会自发地反复输出它。这跟语言模型里的重复问题非常像。我们试了三种缓解方案。第一种是生成时用重复惩罚repetition penalty把已经生成过的token在下一步的概率乘以一个小于1的系数。第二种是硬过滤在约束解码阶段直接禁止生成任何已经出现过的商品ID。第三种是调整训练样本把那些“同一个商品在购买序列里只出现一次”作为强约束避免模型学到重复模式。最终我们用“重复惩罚硬过滤”双管齐下重复率从12%降到了1.5%以内。但注意不要对同一商品的所有token都硬禁止否则会破坏商品内部的token关联我们只禁止商品级重复。5.2 问题二生成式模型出现“幻觉商品”怎么拦截幻觉商品是指生成的token序列在映射表里找不到对应商品或者映射到了一些明显不合理的商品。产生原因一般是模型对某个低频token组合的概率估计过高而训练数据里恰好没有覆盖。前面提到的前缀树约束可以解决大部分但还有一种情况商品ID在线上期间下架了导致映射表动态变化。我们的处理方式是加一层“在线可用性校验”在生成结束之后对每个候选ID查一下商品状态如果已下架、无库存或者不在当前类目下就直接丢弃。这个校验看似简单但很容易漏。我开始写的时候只在批量刷数据时校验后来改为在生成服务里实时拉取商品状态缓存才彻底解决。5.3 问题三训练时长太长收敛慢1.2B模型加上百万级样本训练压力确实不小。我们最初用FP32训练收敛慢且显存不够换成混合精度后速度提升了近两倍。另一个加速技巧是把行为序列和query序列分开预处理避免在dataloader里做重复的token化操作。可能有人会觉得这些细节不值得写但说实话生成式召回工程落地80%的时间都在处理这类“脏活”。5.4 问题四线上时延波动大如何保证稳定性在线推理时最怕某些请求的生成序列特别长导致GPU排队整体时延抖动。我们想了一个办法给每个请求设置动态“生成预算”。如果用户在实时搜索场景只给20token的预算如果是离线预生成场景可以放宽到50token。实时请求中如果某个请求在预算内还没生成结束符我们直接截断并返回现有结果。宁可少生成几个候选也要保证响应速度。实测下来P99时延从90ms降到了60ms以内。5.5 问题五生成式召回对冷启动商品不友好怎么破模型训练时见过的高频商品生成效果会很好但全新商品因为没有任何历史交互模型很难直接生成它。这是所有模型召回的通病但在生成式范式里更明显因为它不是通过相似度泛化而是通过概率生成。我们的解决方案是在训练时加入“商品特征噪声注入”以一定概率把商品token中的部分mask掉让模型学会利用侧特征而不是只依赖ID记忆。上线之后新品在生成式召回里的覆盖率从不足1%提高到约6%虽然没有完全解决但至少不是零了。6. 这套方案还能往哪个方向演化目前得物交易搜索的生成式召回已经稳定运行了大半年线上效果符合预期。我们内部还在尝试两个新方向。一个是把用户的实时行为更细粒度地融合进生成过程。现在我们还是以“历史行为序列当前query”作为输入但用户浏览过程中每点一个商品模型都重新生成一次候选会延时不达标所以目前是增量更新。后续考虑设计一种“流式生成”方案让模型在用户连续浏览时平滑调整候选结果。另一个方向是把生成式模型和LangChain4j结合做多路召回的工具编排。坦白说这个还在预研阶段理想状态下我们可以让一个大语言模型作为调度器动态决定调哪路召回、每路给多少权重而不是靠固定规则。像我们用的生成式召回模型本身其实还是一个专用模型如果能够引入大模型对上下文的理解能力理论上可以处理更复杂的query比如“偏正式一点的鞋不要运动风2000块以内”这种query现在虽然也能出结果但模型的解释性还是不足。这也引出一个更本质的问题生成式召回的下一个跃迁是不是应该从“生成候选ID”升级为“生成候选策略”换句话说让模型不只是输出商品列表还输出为什么要推这些商品。一旦做到这一步召回和排序的边界就彻底模糊了。这可能是交易搜索里更值得期待的变化。最后分享一个我个人的体会。做了这么久检索和召回最大的感受是算法模型的演进很多时候不是从一个技术跳到另一个更高级的技术而是思维方式的转变。向量检索解决的是“如何在超大候选集里快速找相似”的问题它很成功但它的成功也固化了我们对“召回”的定义。生成式召回让我们重新思考召回的终极目标到底是什么是“把用户可能喜欢的东西找出来”而不是“把和query相似的东西找出来”。在这个定义放宽松之后很多看似成熟的方案其实都有重新做一遍的空间。如果你也正在做搜索召回或者刚接触生成式召回我建议先把目标定小一点不要一上来就替换掉所有路召回。先挑一个非标品类把样本、模型、解码、评估这四件事跑通再考虑扩大范围。因为这条链路里处处是细节坑比想象中多得多。希望这篇文章能帮你避开一些我踩过的。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →