ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

生成式召回:当向量检索卷不动时,交易搜索如何换道增量

2026/10/2 8:42:09 拓冰建站 浏览量
生成式召回:当向量检索卷不动时,交易搜索如何换道增量 上周我们团队在复盘交易搜索的召回效果时一个技术同学突然抛出一个问题“现在向量召回的比例都快拉到40%了剩下的20%还能怎么榨”这个“怎么榨”背后的潜台词其实是过去三年里整个搜索行业都在卷同一个方向——把文本、图片、甚至用户行为全塞进同一个向量空间然后用ANN索引比谁更快用双塔模型比谁更准。这种内卷已经卷到了一个相对收益很低的平台期。而当时我们内部正在测试的一个方向恰好能回答这个问题——既然大家都在卷“表征式召回”那能不能直接跳出来用“生成”的思路来做召回这篇文章就围绕得物交易搜索里一个比较特殊的尝试来展开我们把召回从“匹配”换成了“生成”用生成式召回Generative Retrieval把原本依赖向量检索和倒排索引的交集空间往前推了一大步。在讲具体方案之前先把这个技术选择的适用边界说清楚这套方法适合那些query意图高度复杂、商品语义偏非标、以及传统检索很难覆盖长尾口语化请求的场景。如果你正在做电商搜索、交易撮合、甚至站外内容检索这篇文章里踩过的坑和一套可落地的工程框架多少能给你提供一点参考。1. 先看清边界向量检索到底在什么环节“卷不动了”1.1 向量检索解决的旧问题与留下的新盲区向量检索解决的是一个非常直观的问题两段文本各自被encoder映射成一个稠密向量然后用内积或者余弦距离来度量它们的语义相似度。这也是为什么“黑武士”能匹配到“黑色全黑运动鞋”因为这些词在语义空间里的位置很接近。这套体系在过去的五六年里确实很强它把搜索从“字面匹配”推进到了“意图匹配”也把召回率从倒排索引时代的20%左右一路拉到接近40%甚至更高。但当我们把真实用户query全部铺开看的时候会发现向量检索有一个容易被忽视的边界它仍然是“检索”而不是“推理”。所谓“检索”意思是它必须从一个已经存在的候选集中通过距离计算去“找”最接近的东西。如果用户的需求在候选集里没有一个足够近的邻居向量检索再准也没用。我举一个我们在得物场景里真实遇到的情况。有用户搜“一千块以内送男朋友的生日礼物”这种query在传统倒排里几乎无法处理——因为没有任何商品标题会写“生日礼物”。它靠向量检索也很难做因为“一千块以内”“送男朋友”“生日礼物”这三个限定条件组合在一起编码器很难通过一个全局向量把这三个子意图同时保存下来。语义空间里的表示会互相稀释。这其实是向量检索一个比较深的痛点组合约束越复杂编码损耗越大召回质量掉得越快。除此之外向量检索还有一个工程层面的问题。它的召回效果高度依赖embedding质量而embedding的质量又高度依赖训练数据。如果某个商品在日志里出现的次数很少、或者query本身是极端口语化的长尾表达embedding很容易退化成“稀疏散点”在ANN检索时几乎不可能被远距离命中。所以我在团队内部经常说一句话“向量检索把头部做得很香但腰部以下基本靠天吃饭。”1.2 得物交易搜索里特殊的“非标品”挑战可能有些人不太熟悉得物的商品结构这里稍微展开一下。得物的商品池有一个显著特征大量非标品、强版本差异、潮流属性极强。同一款鞋可能有几十种配色版本不同版本在标题里的描述差异非常小但价值和稀缺度完全不同。与此同时用户搜索时用的词汇高度“黑话化”比如“AJ1芝加哥”“黑武士”“倒钩”“小闪电”这些词在商品标题里根本不一定是原文而是某个社区广泛使用的昵称。这个特性给传统召回带来一个连锁反应倒排索引靠字面匹配基本失效向量检索靠语义匹配能覆盖一部分但仍然会撞上“语义相近、期望不同”的场景。比如“黑武士”既可以指全黑的Yeezy 350也可以指某个品牌的全黑跑鞋。如果query的意图不进一步细化向量空间里它们就是同一个点。这种情况下继续优化向量模型其实是在做“用一个向量表达多重意图”这件事效果注定会有瓶颈。所以当我们讨论“生成式召回”的时候不是因为它听起来更前沿而是因为它在底层逻辑上绕开了上面这两个死结它不再去“找”一个接近的向量而是把“query到商品ID”的关系当成一个条件生成问题直接学习从query到target的映射。这个切换很关键它把“近似检索”变成“条件生成”等于把召回从“大海捞针”换成了“按图索骥”。2. 生成式召回的核心原理把召回从“匹配”改成“翻译”2.1 一个关键思路转变放弃距离计算改为条件生成生成式召回这个概念其实在学界已经有几年的积累。它最初的代表性工作是Differentiable Search IndexDSI思路非常大胆把整个语料库“记忆”在一个Transformer模型的参数里用户query输入模型模型直接输出对应的文档ID。也就是说召回不再需要外挂索引索引本身被模型内化了。后续的SEAL、NCI等工作进一步把它扩展到更大规模的语料也引入了更多可控性设计。但在得物交易搜索的场景里我们不是照搬DSI而是借了它的核心思想把query和商品之间的关系建模成“条件概率生成问题”。输入是用户query一段文本输出是一个商品ID序列。模型的训练目标是让这种条件概率最大化在给定某个商品ID被历史用户点击/购买的前提下让模型学会“见过这个query→翻译出这个商品ID”的映射关系。这样做的好处在于模型学到的不是“语义相似”而是“意图到商品的直接关联”。前者依赖表示空间里的距离后者依赖序列解码时积累的条件信息。打个不太严谨但容易理解的比方传统向量检索像是你查地图找“离当前位置最近的咖啡馆”它永远只能告诉你附近有什么生成式召回则更像是你直接告诉一个老司机“我要去一个适合聊天、不太吵、有手冲咖啡的地方”老司机直接说出那家店的名字。前者是地理距离的度量后者是需求的理解与翻译。2.2 目标端编码商品ID必须做语义离散化刚开始做生成式召回最容易犯的错误是直接把商品原始ID拿来做生成的target。这个坑我们踩过效果非常差。原因很好理解——原始商品ID是一个随机分配的编号它本身不携带任何语义信息。你让模型直接去“生成”一串随机的数字本质上是在逼模型死记硬背几百万个无规律的映射这和让一个学生背圆周率前十万位没有区别。模型根本没有足够的归纳能力去捕捉ID与商品之间的内在规律。所以真正落地生成式召回的第一步是给商品ID做一层语义离散化编码。我们参考了VQ-VAE的思路做了个简化版用商品的多层属性构建一个层次化的codebook。以得物场景为例一个商品的语义ID结构大致如下层级含义示例L1一级类目球鞋 / 服饰 / 箱包 / 配饰L2品牌或系列Nike / AJ系列 / Yeezy系列L3风格/属性高帮 / 低帮 / 黑武士 / 全黑L4叶子款具体商品款号如DD1391-100把原始ID替换成这种结构化的语义ID之后模型学到的就不是“ID 123456对应某个query”而是“在某品牌、某风格、某款号组成的语义路径上这个query的概率更高”。这直接让生成过程变得可解释解码每一步其实是在逐步缩小候选空间。同时它还带来了一个额外的好处——因为同一家族的SKU共享了前面的层级编码模型天然具备了一定的泛化能力即使某个具体款号在训练数据里没有出现只要它的类目和风格路径与某个高频query匹配依然有一定概率被生成出来。2.3 损失函数与训练目标不是简单做分类当output空间是语义ID序列模型训练目标看起来就是一个典型的sequence-to-sequence文本生成任务用交叉熵做loss就行。但实际落地时有两个问题会严重影响效果。第一个问题是商品空间极度不平衡。电商场景里头部爆款商品占据了绝大多数点击和成交长尾商品天然稀少。如果直接用原始日志训练模型很快就会退化成一个“只会背热门答案的复读机”不论用户搜什么它生成的top结果永远集中在几十个爆款ID上。为了解决这个问题我们做了两件事一是对训练样本做了按商品ID的频率降采样控制单个ID的最大出现次数二是在计算loss时给热门商品加一个惩罚权重逼着模型把概率质量分配到更细的语义路径上而不是一味“抄近路”。第二个问题是decode空间太大直接用全量词表算softmax不现实。几百万商品每个商品对应一个语义ID序列序列长度即使压缩到4层编码词表也在几十万以上。我们采用的方案是分level解码第一层先解码类目第二层基于类目约束再解码品牌/风格逐层缩小候选集合。每一层的预测都是一个独立的softmax头层与层之间用mask做约束。这样既控制了计算量又让语义ID的层级结构真正发挥了解码过程中的先验作用。3. 得物交易搜索中的落地架构生成式召回不是替代而是叠加3.1 整体链路设计多路召回并行生成式单独走一条线我们最终上线的架构并不是用生成式召回替代倒排或向量检索而是在这两者之外新增一条“生成式召回通路”。整个链路是这样组织的用户输入query先经过统一的中文分词、拼写纠错、意图识别模块。意图识别模块会对query做一个分流凡是高频大词、短查询、明确型号词直接走传统的倒排和向量双路召回保证速度和精度凡是长query、口语化表达、意图复杂或者包含“预算”“场景”这类软约束的query除了传统双路还会额外触发一条生成式召回。多条召回结果经过一个统一的“融合分配模块”先做去重再按比例分配展示坑位最后送入精排模型排序。这种做法的好处很直接传统召回提供“保底”生成式召回提供“增量”。不会出现生成式模型效果波动导致整条搜索体验崩盘的情况。我在这里尤其要强调一点——不要一上来就把生成式召回作为唯一召回通路那会让你在排查badcase时非常痛苦。先用小流量验证增量价值再逐步扩大生成式通路的流量配额这个节奏感非常重要。3.2 意图归一化生成式通路前的关键前置处理生成式模型本身对输入噪声是有容忍度的但容忍度不代表完全不在意。如果我们把非常口语化、带着语气词甚至错别字的query直接丢给生成模型它一样会被干扰。所以在这条通路里query在进入生成器之前还会多做一个处理我们内部管它叫“意图归一化”。意图归一化做的事情主要有三件清除无意义语气词和修饰成分。例如“我想看看那种就是黑色的耐克鞋”会被规整为“黑色的耐克鞋”。将口语化的品牌昵称映射到标准品牌词。例如“AJ”“乔1”会被规范为“Air Jordan 1”的语义路径。预算词、场景词与商品类目的关联映射。例如“送男朋友”会被映射到“男款、配饰/潮流鞋服”的类目先验。这个前置处理本身就是一个小模型我们可以基于规则加一个小型翻译模型来实现不必做得太重。但它带来的训练数据增益非常明显生成式模型吃到的输入是规整过的query学习难度直线下降输出质量也随之提升。3.3 Beam Search生成候选可控参数与解码细节生成式召回的解码阶段我们采用了Beam Search策略。相比贪心解码Beam Search能够在每一步保留多个候选路径最后输出多条商品语义ID序列正好满足“召回K个候选”的业务要求。这里给出一组我们在实际调参过程中验证过的参数经验Beam Size通常取4到8。太小召回多样性不足太大解码耗时明显上升而且容易在后期坍缩到同一条路径上。我们在得物的实验里beam size6时效果与成本最均衡。每层解码都设置一个最小置信度阈值。例如某个层级的最大概率低于0.15则判定为“低置信query”该条候选直接丢弃。这是防止生成式幻觉的关键手段。最终生成候选数量控制在50到200之间。比向量召回通常fetch上千个候选要少得多但胜在精准度高不需要精排消耗太多算力去“捞”它。解码完成后会有一个“语义ID翻译回真实商品ID”的过程。如果多条候选语义ID路径映射到同一个商品只保留一个。这个模块逻辑不复杂但漏掉了它会产生大量重复坑位影响用户体验。3.4 与向量检索、倒排索引的融合从“三分天下”到“按需调配”生成式召回上线之后我们面对一个很现实的问题三条召回通路并行时每个通路在最终结果里占多少坑位合适一开始我们采用的是“固定配额”策略——倒排、向量、生成式各自分配固定的坑位数。生成式分配20%的坑位但上线后发现有些简单query它插进来的结果并没有比倒排更好反而稀释了头部精准结果。后面我们改成了“按意图复杂度动态调配”的策略在意图识别阶段就给该query打一个“复杂度分”。复杂度分低则极少分配生成式结果复杂度分高则生成式通路占的坑位可以提升到30%到40%。这个策略上线后整体成交转化率提升了不少因为复杂意图query的召回质量上去了而简单query的精准度没有受损。这条经验非常重要我要再强调一次生成式召回的价值不是替换传统检索而是在传统检索注定失位的场景里补位。它的优势场景明确我们不要试图让它去做短query精准匹配的活。4. 实操实录五个绕不开的坑与对应的解法4.1 商品ID不稳定今天训练的模型明天可能失效生成式召回最折磨人的一个问题是商品ID的动态变化。电商商品池和文档库不一样它不是静态的每天都有新品上架、老品下架、款式微调。如果你把语义ID完全绑定在一个动态变化的商品维度上比如L4层用SKU编号那么只要商品的SKU发生变化对应的语义路径就跟着变了模型学到的映射关系立刻失效。我们的解决思路是在编码时区分“稳定位”和“增量位”。稳定位包含类目、品牌、风格等长期不变的属性增量位则包含款式编号、发售批次等易变信息。训练时我们对增量位做随机扰动增强让模型不要过度依赖具体增量值而是学会通过稳定位加部分增量信息来定位商品。上线后即使某些L4增量位发生变化模型依然能通过稳定位生成正确的商品簇再由商品簇内的补充规则映射到最新的SKU。4.2 生成式幻觉生成了几个“不存在”的商品大家可能听说过LLM的幻觉问题生成式召回同样存在这个现象只是表现形式不一样。我们遇到的情况是模型在某些长尾query下会生成一个结构完整但没有对应商品的语义ID。例如它生成了一个“球鞋/Nike/AJ/黑武士”路径但这个路径对应的商品已经下架或者在商品库里根本不存在这个款号。这个问题在离线评测里不容易暴露因为离线评估用的是历史点击数据模型只需要复现历史行为即可但线上运行时商品池每天都在变幻觉问题会被放大。我们的对策分两道防线。第一道是解码后的“存在性校验”任何语义ID必须映射到当前商品库中真实存在的商品否则直接丢弃第二道是训练阶段引入“商品库扰动”每次训练迭代时随机丢掉一小部分商品让模型学会在商品库不完整的情况下仍然尽量输出有效路径减少对“死记硬背”的依赖。两道防线叠加后线上无效生成的比例降到了可接受范围。4.3 长尾商品的冷启动日志里没有它的记录模型如何认识它长尾商品在点击日志里的样本非常稀疏甚至完全为零。生成式召回的模型如果不做特殊处理对这些商品的生成概率几乎是零。但大家别忘了我们做这套方案的核心目的恰恰就是为了解决长尾和复杂意图。所以“冷启动”这件事必须正面解决。我们在实践中验证有效的方式是“teacher模型蒸馏数据增强”。先用一个表达能力更强的重模型teacher在离线数据集上生成一批“query到语义ID”的伪标注然后蒸馏到线上轻量模型。同时我们利用语义ID的层次结构做平滑某个叶子款即使没有直接训练样本只要它所属的品牌风格路径与历史query有足够高的相关概率就给它这些路径上的先验概率作为初始值。这相当于让模型“举一反三”——见过同品牌的兄弟产品就敢对同品牌的新品给出一定概率。4.4 算力与延迟seq2seq不是免费的午餐生成式召回的延迟成本是它在工程落地上最不受欢迎的一点。一个常规的seq2seq模型在CPU上解码即使序列长度只有5个token也要10到20毫秒在峰值流量下如果每个query都触发一条生成式通路算力消耗会非常可观。我们的优化方向有三个层次模型层面把Teacher模型蒸馏成小规模的student模型解码层从6层压缩到3层语义ID序列长度压到4个token以内保证单次解码速度。触发层面并不是所有query都走生成式通路而是先经过一个轻量分类器判断“是否有必要”意图简单的query直接跳过生成式避免浪费算力。工程层面对解码结果做缓存同一query在短时间内直接读缓存。电商搜索里有很多高频重复query比如“AJ1 芝加哥”一天之内被搜索几万次缓存命中率非常高。这一套组合拳打下来生成式通路的平均延迟控制在整体搜索延迟的20%以内算力成本也在可控范围内。4.5 评估指标离线Recall涨了线上成交却跌了最后讲一个我们比较惨痛的经验。第一版生成式召回上线前离线评测里Recall50提升了近5个百分点大家觉得胜券在握。结果小流量上线后整体成交转化率不但没涨反而掉了0.4%。问题出在哪离线评估用的是历史点击数据它反映的是“模型能不能召回用户此前点过的商品”但生成式召回召回出来的商品往往是用户没见过但确实相关的商品。用户看见之后有一部分会买账也有一部分会因为“不是我想要的”而离开。换句话说离线Recall衡量的是“相关性”线上转化衡量的是“成交效率”这两者之间隔着一个“用户预期管理”的gap。后来我们在融合层加了一个“新意控制”机制生成式召回的候选里与用户近期浏览/购买历史差异过大的商品会被降权同时生成式召回的候选在进入精排前会额外经过一个轻量转化率预估模型过滤掉那些极低概率成交的候选。调整之后整体效果才真正转正。5. 效果复盘与范式反思这套方案适合谁不适合谁5.1 上线后的指标变化生成式召回在得物交易搜索中跑了一段时间后我们观察到的核心指标变化大致是这样的指标变化情况无结果率下降约12%长尾query成交转化提升约18%复杂意图query的点击率提升约9%精排后置成交转化提升约4%其中最让我们意外的其实是无结果率这个指标。我们原本以为复杂的意图query更多是“召回不准”没想到真实用户里存在大量“根本召不到”的情况。生成式召回把一批原本零结果的query变成有结果可出这带来的体验提升是整个搜索链路里最直接的。5.2 什么场景适合生成式召回什么场景别硬上经过这段时间的实践我逐渐形成了一套判断标准分享出来供大家参考。适合上的场景query以长句、口语化、场景化表达为主例如“适合秋冬穿的黑色卫衣”“送人比较有面子的礼物”。商品池存在大量非标品或强属性差异品牌昵称多、黑话多。传统检索难以覆盖“组合约束”“预算”“场景”等软条件。不适合硬上的场景精确型号查询为主比如“iPhone 15 Pro Max 256G”传统倒排已经能给出完美结果。商品池规模极大且每天剧烈变化且没有稳定的属性体系来支撑语义ID。算力预算紧张且无法接受额外多出的召回延迟开销。5.3 我个人几点经验体会最后说几句偏个人感受的东西。这次做生成式召回最让我触动的一点是它让我重新理解了“召回”这个环节的定位。过去我们把召回当成一个漏斗核心是“别漏掉好东西”生成式召回的思路更像是给漏斗装了一个“翻译器”它把用户说不清的需求翻译成具体的商品路径。这个视角的转变带来的不仅是指标的提升也让整个搜索架构在思考用户意图时多了一层表达空间。另外踩过几次坑之后我现在的态度会更加务实召回层所有炫技方案最终都要回答“相比多路召回你多抓回了哪部分用户、哪部分商品”。如果答不上来哪怕离线指标再好看也不值得上线。生成式召回真正的价值是在那些传统检索注定失位的场景里做增量而不是把原有的匹配逻辑推翻重来。如果你正在纠结要不要上生成式召回我建议你先做两件事一是拉出全部无结果query统计里面有多大比例是长query、口语化表达二是梳理商品池里是否有一批语义上相关但文本上完全不匹配的商品。如果这两个问题的数据都足够扎眼那生成式召回大概率值得一试。相反如果这两个场景都很少建议还是把精力放在优化现有向量召回和精排模型上更实在。