ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

生成式召回:交易搜索从向量检索到条件生成的范式跃迁

生成式召回:交易搜索从向量检索到条件生成的范式跃迁 别再只卷向量检索了得物交易搜索如何用“生成式”实现召回范式跃迁说实话这几年聊电商搜索召回大家开口闭口就是双塔、向量、ANN、MIPS仿佛把query和商品各自编码成一个embedding然后做最大内积搜索就是出厂配置一样。我理解这种路径依赖毕竟双塔向量检索确实解决了很多语义匹配问题工程生态也成熟从FAISS到各种HNSW优化线上效果来得又快又稳。但如果你在得物这种交易搜索场景里待过一段时间你会明显感觉到一个天花板向量检索本质上是在做“相似度打分”它并不能真正理解和推理用户查询背后那一串组合约束。我举个例子用户搜“AJ1黑红脚趾 42.5码 漆皮版本”这里面有品牌、配色、尺码、材质版本四个硬属性。双塔模型就算你把query整体编码得再好最终向量空间里找一个最接近的商品embedding也很难确保四个属性同时精确命中。倒排索引倒是能精确过滤但它对“复杂语义意图”几乎无能为力比如用户想要“和这个鞋款风格接近但更冷门的选择”倒排就抓瞎了。所以最近得物交易搜索的方向开始转向“生成式召回”——把召回从“检索匹配”变成“条件生成”。一句话描述就是给定query和用户上下文模型直接生成一串商品ID序列不再走“召回候选集→打分排序”的传统路径。这个改动看起来只是实现形式换了实际上意味着整个召回范式的跃迁。这篇就聊聊我们踩坑踩出来的思考、架构设计、训练细节和那些线上才会遇到的破事。1. 先想明白一件事向量检索解决的是“相似”不是“成立”1.1 双塔模型的本质把复杂匹配压成一个点积双塔召回的原理不复杂左边塔把query序列编码成一个向量右边塔把商品侧信息标题、类目、品牌、价格、图片特征编码成另一个向量训练时让点击样本的query向量和item向量在空间里靠近不点击的样本拉远。上线时用向量索引做近似最近邻检索把topK个商品灌给下游排序。这个范式最大的优点大家都很熟悉query和item的语义相似度可以被统计在向量空间里哪怕query里没有出现商品标题的任何一个词也能通过语义拉近召回。这是倒排索引做不到的。所以我并不是否定向量检索的价值它上线后确实把搜索的召回丰富度拉高了一大截。但你要意识到双塔其实是把所有信息“压扁”成一个固定长度的向量。query侧“AJ1黑红脚趾 42.5码 漆皮”也好“AJ1红黑配色高帮男鞋”也好最后都变成128维或者256维的稠密向量。商品侧也一样标题、属性、价格、销量全挤在一个向量里。这中间有多少信息损耗绝大部分团队是不去深究的只看最终离线指标涨不涨。我这么说吧双塔模型在“只要求跟query相似”的任务上表现很好比如找长尾同义词、跨层级泛化、模糊归因但交易搜索里面很多query根本不是“找相似的”而是“满足条件”的。用户说“白水泥配色 40码 只要一千二以下”这个query的正确答案不是“和这个query长得像的商品”而是在价格、配色、尺码三个约束下同时成立的商品集合。你让双塔去区分“白水泥配色但40码断货”和“白水泥配色且有40码”它很难学到这种精确约束关系因为embedding空间里这两个商品被压得非常接近。1.2 交易搜索的硬约束恰恰是向量模型的死穴更麻烦的是交易搜索和普通内容搜索有个本质差异商品是“有状态”的。库存有没有、发货快不快、价格是不是区间内、是否参与某个活动、是不是正品保障范围、尺码是否齐全……这些约束条件在以分钟甚至秒级变化。你把商品编码成embedding这个embedding里面包含的“库存状态”是过时的甚至根本不该编码进去——因为库存是动态的。所以你会看到很多团队的做法是向量召回前面套一堆过滤条件比如“库存0 AND 价格区间 AND 类目匹配”先过滤再进向量检索。但这样问题就来了如果你过滤得严向量模型能用上的候选空间就小了召回率掉得厉害如果你过滤得松向量模型又容易把那些不满足条件的商品给召回来下游排序还要花很大力气去纠正。倒排索引在精确匹配上反而稳妥尤其是对SKU级别、spu级别的结构化属性。用户搜“AJ1 42.5”倒排直接精确匹配尺码字段既快又准。但倒排对query的自然语言变体、同义改写、组合语义就太弱了比如“科比实战鞋 包裹好 启动快”倒排基本无能为力。这就形成了一个两难局面倒排查得准但理解不了向量理解得了但查不准。得物交易搜索在做的生成式召回本质上就是为了打破这个两难用一个模型同时兼顾“语义理解”和“约束满足”。1.3 别急着否定向量真正的问题是“召回的推理深度”我得把话说透生成式召回不是要干掉向量检索而是把召回这件事的“推理深度”往上提一层。向量检索是单步匹配生成式是多步推理。就拿“搭配场景”来说用户搜“Dunk熊猫”的同时系统如果想做连带推荐传统向量召回的做法是拿当前query的embedding去商品库里找相似但用户刚刚浏览过一件“米白色卫衣”现在搜“黑色工装裤”这个“卫衣配套裤装”的横向语义关系双塔很难表达。因为你没有办法在编码query的时候把整个用户session的浏览序列作为上下文塞进一个query向量里——就算你硬塞信息也被压扁了。生成式模型不一样。它的输入可以是“query 用户短期行为序列 长期偏好 场景上下文”输出的是商品ID序列。这个结构天然允许你在生成过程中做推理模型看到用户近期浏览过米白卫衣就会把“黑色工装裤”里那些与米白色协调的款式概率抬高。这个能力不是靠“相似度”能简单刻画的这是模型在学条件分布时自己涌现出来的组合推理能力。2. 生成式召回到底是什么从“检索匹配”到“条件生成”的范式翻转2.1 核心思想让模型直接“吐”商品ID生成式召回这个概念最早被大规模讨论是在雅虎团队的DSIDifferentiable Search Index工作里。论文的思路很直接传统搜索引擎是“索引构建—召回匹配”两个阶段DSI试图把这些合并成一个可微分的索引也就是把documents放进模型参数里给定query模型直接生成相关doc的ID。放到电商交易搜索里简单实现就是用商品库里的所有商品为每个商品分配一个“语义ID”然后训练一个encoder-decoder或者LLM。输入是用户query和上下文输出是一个ID序列比如“31405-782-1234”这个ID对应一个具体商品。解码时用beam search保留多个候选ID每个ID去商品库里验证就能得到召回结果。这个思路听起来有点反直觉因为你会问模型怎么知道“31405-782-1234”是谁万一生成一个不存在的ID怎么办这两个问题正是生成式召回落地最核心的技术挑战后面我会详细讲。先说清楚为什么值得冒这个风险。因为一旦模型能直接生成商品ID那么query和商品之间的关联就不再依赖向量空间里那个固定相似度函数了模型可以在生成过程中灵活地对输入上下文进行推理、组合、约束满足这是静态双塔做不到的。2.2 从“判别式打分”到“生成式分布”差的不只是目标函数双塔召回训练目标通常是一个判别式目标给定query(q)和商品(d)模型输出一个相关性分数 s(q,d)然后用softmax或者采样负样本的NCE loss去拉近正样本、推开负样本。这个目标本质上在学一个匹配函数模型是“判别器”。生成式召回训练目标变成了最大似然给定query和上下文最大化生成正确商品ID序列的概率。这就是把他当“生成器”来训练了。这两个目标从数学形式到信息利用方式都有本质差别。判别式目标下你为了训练一个query和千万级商品之间的分类器负采样策略非常关键选不好负样本整个模型都很容易崩。生成式目标下模型是在学习一个条件概率分布 p(item_id | query, context)你不需要显式地构造那么多负样本只要用正样本序列去拟合即可。模型可以在训练过程中通过attention机制自己学会区分confusable的商品。而且生成式目标有一套很自然的“排序属性”解码时beam search出来的候选序列天然带有概率排序。也就是说你不需要再额外接一个打分器模型输出的生成概率本身就是序。这是一个巨大的工程便利——你可以把召回和粗排的活同时接到一个模型上。2.3 为什么交易搜索比内容搜索更适合先做生成式召回很多人觉得生成式召回是个锦上添花的学术概念离工业落地还很远。但交易搜索恰恰是生成式召回最能发挥价值的土壤原因有三。第一交易搜索的候选空间是“有限商品集合”不是开放域内容。商品ID是离散的、可枚举的、有边界的。你不需要像大模型生成文本那样面对无限词表只需要cover住商品库里的数百万或者上千万个ID。这对模型来说是个可控的生成任务。第二交易搜索天然是多约束满足任务。用户会加各种筛选词、属性词、价格区间生成式模型可以直接把这些约束编码进输入上下文生成结果自然满足约束比先做向量召回再filter要自然得多。第三电商搜索query通常很短但商品结构信息极其丰富。生成式模型可以通过层次化的ID编码方式把商品类目、属性、品牌等结构化信息织入ID序列里生成时按结构逐层解码精度会比直接生成扁平ID高不少。3. 得物交易搜索的落地架构不是替代而是叠加和协同3.1 三层混合召回生成式有主有权其他路径兜底我们实际搭建的架构不是“只用生成式召回”而是三层并行倒排索引、双塔向量、生成式召回。三个路径都产出候选集然后在融合层做合并去重再交给后续的精排。为什么不直接完全依赖生成式因为生成式召回有一个客观风险可能存在“覆盖不到”的样本比如训练语料中从未出现过的ID组合或者特别冷门的商品。而且生成式模型有一定的幻觉概率生成结果不一定100%可信。为了不把召回率做坏我们必须让倒排和向量作为兜底路径保证用户在最差情况下也能找回基本候选。但主路径是谁这个在迭代中逐渐变了。早期主路径是倒排双塔生成式只作为一个“增量召回”尝试到后面我们把生成式作为主召回路径之一倒排退到属性精确过滤双塔则承担模糊语义扩展。这个调整的依据不是离线指标而是线上bad case的分布——大量跟“组合约束”相关的bad case只有生成式能解。3.2 商品ID怎么编不能直接用原始ID要搞“语义ID”和“层次ID”这是生成式召回绕不过去的第一道坎。你得给每个商品一个ID作为生成目标但直接用商品原始ID一串无意义的数字训练模型模型很难学因为它无法从ID本身推断任何信息。我们采用的是层次化的语义ID方案。通过一个量化模型类似RQ-VAE的思路把商品的语义向量由商品标题、类目、品牌、属性等组成的embedding压缩成若干个离散编码层。比如一个商品最终被表示为“l1-l2-l3-l4”这样的4个token序列每个token代表一层语义。第一层对应粗粒度类目比如“鞋靴”第二层对应品牌/系列比如“Air Jordan”第三层更细对应配色或者型号第四层逼近到具体SKU级别的尺码和库存单位。这样做的最大好处是模型生成的时候是“按结构逐层解码”的。它先生成“鞋靴”再生成“Air Jordan”再生成“黑红脚趾”最后生成“42.5码”。这个生成过程就和人类搜索的意图拆解高度一致准确率比直接生成扁平ID高非常多。而且一旦中间某层生成错了后续层次可以通过beam search回退纠正容错性也更好。3.3 受限解码生成出来的ID必须是合法商品模型生成文本可以让AI自由发挥但生成商品ID不能让模型自由发挥——如果你生成了一串“无中生有”的ID线上用户看到的就是一个不存在的商品这是绝对不允许的。所以我们在推理解码阶段加了受限beam search维护一个合法的商品ID集合每次解码时用这个集合对应的token来mask掉词表中所有非法的token保证整个生成序列一定是合法商品。这个方案在实现上不算复杂但效果至关重要。如果不加受限解码生成式召回的准确率会惨不忍睹用户随便搜点什么都能看到一堆幻觉商品。具体实现上我们维护了“商品ID合并索引”把所有ID序列以及ID序列的前缀放到一个快速查询表里。解码时每一步只要查一下当前前缀是否存在于合法ID前缀集合中不在就概率置零。这样解码的每一步都天然合法相当于把倒排索引“焊死”进了生成过程里。延迟方面用batch推理加缓存优化线上p99可以控制在15ms以内完全在可接受范围。3.4 多约束注入价格库存这种动态约束该进prompt还是进mask这部分是我们在实际迭代里反复调整的地方。生成式模型输入管线至少包含三块query文本、用户上下文、场景约束。问题在于像价格区间、库存状态这些动态约束是放进输入让模型学着遵守还是放进受限解码的mask里强制过滤我的经验是硬约束必须放进受限解码层不能指望模型自觉。因为模型学的是行为概率价格和库存的变化太快了模型如果以训练时的静态库存为准线上一定会出乱子。所以线上把价格段、发货地区、库存状态、黑白名单全部放到mask逻辑里解码时直接把这些不满足条件的商品ID候选从合法集合中剔掉。软约束则放进prompt输入里比如用户近期在浏览高价位潮品还是平价单品用户是偏好某个固定品牌的粉丝还是广撒网型甚至当前场景是来自首页搜索框还是来自“您可能想找”的关联搜索位。这些信息以token序列或者额外embedding的方式注入生成模型模型真正学会的是“根据上下文调整生成偏好”的能力而不是一套死规则。4. 训练与推理的高频踩坑数据、损失函数、解码效率一个都不能少4.1 训练数据的造法不要把“曝光点击”直接当“生成目标”生成式召回的训练数据构造和双塔完全不一样。双塔需要构造“正样本对负样本对”生成式召回需要构造“query→ID序列”的样本。最直观的构造方法是把用户点击、加购、购买的商品ID对应的语义ID序列作为生成目标排在前面的是用户最后成交/加购的商品后面的是点击过的商品。但如果只这么做会遇到一个严重的面试送命题一个query在日志里可能对应好多个商品而生成式模型每个样本只能有一个目标序列。你怎么决定把哪个商品作为唯一监督目标我们的做法是构造“多目标样本”对于一个query把所有正反馈商品放到一个“候选目标集合”里训练时每个step随机采样一个目标ID去计算loss。配合课程学习策略先让模型学主要高频关联商品再逐渐增加长尾目标。这个训练方式和多标签分类有点像但保持了生成式模型的序列化优势。另一个重要细节是训练数据里要加入“不可召回”的query样本。有些query在线上就是没有明确意图或者对应的商品都是垃圾商品这种样本如果不特别处理模型会把生成概率散布到一堆低质商品上。我们单独标了一个“低质query”类别让模型对这类query生成一个特殊的空ID token表示放弃生成进一步减少垃圾召回。4.2 损失函数设计光有交叉熵远远不够开头我们用的就是标准序列交叉熵效果还行但你会发现模型很快就过拟合到高频商品上长尾商品的生成概率被严重压缩。这不仅是样本不均衡问题更是损失函数没有显式地对“检索质量”建模。后来我们叠加了两个辅助loss。第一个是集合式检索loss一个query对应的不是单个商品而是用户反馈过的多个商品我们希望生成器给这些商品的概率总和最大化。具体的做法是在解码空间里找到所有正样本ID序列可以通过前缀共享高效计算最大化整个集合的对数概率和。这个loss假设是“用户对多个商品都有意图”比总是押一个目标ID更符合交易搜索实际。第二个是对比排序loss对于每个训练batch把query的生成正确ID概率和错误ID概率做对比拉大两者差距。这相当于给生成器一个“判别能力”的辅助监督特别能抑制幻觉ID的产生。两个辅助loss和主loss按比例混合线上效果比单用交叉熵掉了不少bad case。4.3 推理效率优化15毫秒的生成召回是怎么抠出来的生成式推理天然有个劣势要一步一个token地解码比双塔那种一次前向出所有向量要慢得多。但我们实测发现在语义ID只有4层、beam size控制在32~64的前提下一次batch推理在GPU上其实非常快瓶颈反而不是模型计算而是ID合法性检查。合法性检查不能每个step都去查一次千万级ID哈希表那样太慢了。我们做了两个优化第一把beam search的每一步剪枝提前——不是所有生成候选都留着而是先根据softmax概率阈值砍掉低概率分支再走合法性mask第二把ID前缀树预加载到GPU的显存常量里mask运算直接在GPU上做避免每个step都走CPU哈希查询。线上我们还做了一个非常工程化的策略热门query的生成结果会缓存起来缓存时效通常设置为60秒左右。因为交易搜索的热门query非常集中缓存命中率可以做到接近一半这极大地缓解了生成式召回并发最高峰的GPU压力。冷门query、个性化强、约束频繁变化的query则走在线生成路径。4.4 冷启动与新商品生成式模型不认识新ID就是召回黑洞交易搜索是所有电商搜索都要面对的痛新品上架的速度远大于模型重训的速度。生成式模型又比双塔更敏感——双塔好歹能把新商品的embedding通过某个快速通道加进索引里生成式模型如果不知道新商品的语义ID序列它就永远不会生成这个新ID。我们的解法是“两段式接入”新商品先进入倒排索引和向量索引用于兜底召回和常规召回同时用新商品的商品侧向量去做一个“伪语义ID”初始化具体来说把这个新商品的向量喂进预训练好的量化器得到四层离散编码如果编码结果和某个已有老商品完全一致那就用老商品的ID序列作为初始化保证模型可以把它和相似老商品挂钩。然后不是所有新商品都立刻进入生成式召回而是等它积累了一定的点击曝光数据进入下一轮增量训练后才真正加入生成词的合法mask集合。这里宁可慢一点也不能让生成式模型对“没见过的新品”胡编ID。5. 评估与实验离线指标全是幻觉线上体感才是答案5.1 离线评估该看哪些指标很多人做召回评估只看RecallK和NDCGK但生成式召回加两个专属指标特别关键。第一个是“合法生成率”所有生成的ID序列里有多少比例能对应到当前线上合法可售的商品。如果这个指标低于95%说明模型幻觉很严重上线大概率出问题。第二个是“约束满足率”生成的候选商品里有多少满足query中文硬约束类目、品牌、尺码等。这个是判断你软约束注入到底有没有被模型学到手的关键。这两个指标都很操蛋的是离线测得好不代表线上真的好但离线测不好线上一定不好。所以我们的策略是把这两个指标作为“门槛指标”而不是“优化目标”——达标了再去看RecallK和NDCG提升。5.2 在线AB实验的坑用query分桶而不是session分桶搜索召回在线实验里最常犯的错误是用用户ID分桶。因为同一个用户同一个query可能在两个桶里都会出现如果把生成式召回的生成结果只给一部分用户看另一部分用户看到的是旧结果那同一个query反馈数据就互相污染了跑一段时间后指标会变得很奇怪。我们最后用的是query关键词分桶每个query哈希后固定进入实验桶或者对照桶。这样同一个query在实验期间只会展示同一套召回策略反馈数据是干净的。但要小心两个问题一是query分布不均会导致两个桶流量差异很大需要对热门query做分层抽样二是新query没哈希过的默认进对照组保证线上稳定性。核心指标上除了常规的CTR、CVR、GMV我们特别关注曝光多样性p90商品曝光频次、曝光商品总数、搜索页翻页率。因为生成式召回很容易“变聪明到过于自信”——直接把最相关的几个商品反复推给用户导致曝光聚集度上升长期来看对生态是伤害。5.3 线上bad case实录生成式召回看起来“很有道理”但用户不买账我在项目中期被一个bad case搞得头皮发麻。用户搜索“AJ1”生成式召回给出的结果里有两个商品看起来非常合理——同一品牌、同一系列、配色也接近但实际上是“AJ1 Low”而不是用户大概率想要的“AJ1 High”。从文本相似度看这两者几乎一致从生成式模型的条件概率看模型也觉得它们应该一起出现。但用户去点击对比后购买行为明显低于对照组的纯倒排召回结果。原因也很简单用户搜AJ1的时候虽然可能浏览Low版本但购买决策时更倾向跟明确意图对齐。这就暴露了生成式召回的一个潜在问题模型过度学到了“相关但不完全匹配”的语义关联反而把精确匹配稀释了。我们后续在训练时增加了“精确匹配惩罚项”对用户query里带有的明确属性短语如果在生成目标商品ID里缺失对应属性这个样本的loss权重降低。线上bad case数据才开始收敛。这提醒我一件事生成式召回不是越聪明越好而是要在“语义扩展”和“语义精确”之间找到那个商业上最优的平衡点这个平衡点没有一个公式要靠bad case反复喂。6. 工程与组织上的思考范式跃迁从来不是改一个模型的事最后聊点可能技术文章不爱提但真实存在的事。生成式召回在得物交易搜索里推进过程中最大的阻力反而不是模型效果而是组织惯性和系统架构惯性。召回团队习惯用向量检索排序团队习惯消费固定字段的候选集工程团队对在线解码的延迟充满警惕——这些惯性非常合理。我们做了三件事让整个范式转换平稳落地。第一不搞一刀切替换而是“主路径兜底路径”并行先把生成式召回作为增量候选上游挂在融合层外围让下游和用户无感知。第二搭了一套完整的bad case回流闭环把生成式召回产生的线上失败case自动记录、聚类、人工标注再回流到训练数据让模型逐渐理解交易场景里的隐规则。第三把生成式召回的性能指标公开到团队的统一看板让大家看到的不只是“模型涨了”而是“搜索整体体验涨了”。我现在回头看生成式召回在交易搜索的真正意义不只是多了一个花哨的召回通道而是把“搜索引擎”从匹配工具向“意图推理器”推进了一大步。它让召回这个环节第一次有机会真正理解用户输入背后的组合意图并且用结构化的方式把意图转化为具体商品。如果你也在做交易搜索或者正在纠结要不要投入生成式召回我的建议是先不要一上来就想着全量替换双塔而是找一个被“属性组合类query”折磨最痛的业务场景下手把语义ID方案和受限解码管线搭好用bad case驱动迭代。等模型真正跑起来了你会发现它带来的不是指标涨几个点而是让你重新理解了“搜索召回”这件事还能怎么玩。
返回列表