先聊个真实的场景。做交易搜索的都知道,前几年大家拼的是向量检索,把 Query Embedding 和 Item Embedding 算得明明白白,谁的内积算得快、谁的双塔调得准,谁就能在业务上拿到一点提升。但这两年风向变了:“召回”这个词已经不止意味着“从海里捞一堆候选”,而是开始有人尝试“直接生成候选”。得物交易搜索在做的事情,就是把召回从“检索式”往“生成式”推了一把。这篇文章我不聊概念,只聊实操:为什么卷向量检索会撞到天花板,生成式召回到底在设计什么,以及落地过程中那些文档里搜不到、只能靠踩坑换来的细节。
这篇内容适合正在做搜索召回、或者是推荐系统里对“生成式召回”好奇的同学。如果你带的业务刚好面临“候选集太大、双塔塔尖不够尖、长期收益上不去”的问题,那这篇可能比你看十篇综述都管用。
1. 检索式召回走到头了,问题出在哪?
先说清楚一个背景:传统召回体系已经形成了一套相当成熟的三层结构。倒排索引负责精确匹配,向量召回负责语义扩展,粗排再往后接精排模型。这套体系养活了无数搜推团队,也确实能打。但它的天花板不是“不够准”,而是“表达空间有限”。
1.1 倒排和向量的本质瓶颈
倒排索引的问题很直接:它只能召回字面或者同义词改写后能“命中”的商品。用户搜“适合跑步穿的鞋子”,倒排索引必须先把 Query 切词成“跑步”“穿”“鞋子”,再跟商品标题做 Term 匹配。一旦用户表达里的词在商品标题里根本不存在,比如商品叫“轻便缓震运动鞋”,没有“跑步”这个词,那这条商品就永远进不了召回集合,哪怕它跟 Query 的意图高度相关。
向量检索解决了这个问题吗?部分解决了。双塔模型把 Query 和 Item 分别映射到一个固定维度的向量空间,通过内积计算语义相似度。“跑步穿的鞋子”和“轻便缓震运动鞋”在语义空间里的距离确实可能很近,这比倒排索引强了不止一个档次。但是向量检索还是有三个绕不开的硬伤:
- 第一,它天然是“单点打分”的模式。Query 过来,跟 Item 库里的每个商品算一遍相似度,然后取 TopK。这个过程不管你怎么优化,本质上还是在“已有的商品里挑一个最像的”,你不可能跳出现有 Item 的表征空间去想象一个更优的答案。
- 第二,向量空间是压缩过的。双塔为了推理效率,被迫把所有语义信息压到几十维、上百维的向量里,中间必然有信息损失。你感受得到“用户想要轻便透气还能搭配牛仔裤的鞋”,但向量空间可能找不到足够精确的表达。
- 第三,也是最容易被忽略的:双塔模型在线服务时,Item 侧 Embedding 是预计算好的,Query 侧实时算,然后做内积。这意味着 Item 侧的任何动态变化——比如库存变了、价格变了、新品上架了——都得等 Embedding 重新算完才能被召回,天然有更新延迟。
1.2 为什么“生成”比“检索”更像人
我经常拿一个例子给团队讲:如果说“检索式召回”是你在图书馆里查书,你得先知道书名或关键词,然后去书架上翻;那“生成式召回”就是你直接问一个对馆藏极其熟悉的图书管理员:“我想找一本讲年轻人在城市里怎么省钱生活的书”,管理员稍微想了一下,说:“有一本叫《下班后的黄金8小时》的,还有一本《一人份的东京》,你应该看看。”
注意这个过程。图书管理员不是拿着一本一本的书去跟你算相似度,他是先“理解”你的需求,然后在脑子里“生成”了一个候选集合。生成式召回的核心思路,就是把“召回”从“在海量商品里挑”改成“根据 Query 直接生成商品序列”。这在形式上是两个完全不同的范式。
那是不是说生成式召回就一定要取代向量召回?我的看法是:不取代,但一定会改变召回体系的结构,让原来的“主力部队”变成“底座之一”。
具体到得物交易搜索的场景,用户搜“日常通勤的舒适皮鞋”,我们不再只做 Query 和商品的匹配打分,而是让模型直接输出“符合这个意图的商品 ID 序列”。这些 ID 可能来自头部热门商品,也可能是长尾里被语义网络关联起来的新品。这就是范式跃迁的本质:从“筛”到“写”。
2. 生成式召回的总体设计:把召回变成翻译问题
生成式召回怎么做才能落地?我建议不要一上来就陷入模型细节,先把问题建模这一步想清楚。如果建模不对,后面改什么都白搭。
2.1 核心思路:召回即序列生成
在技术上,得物交易搜索的生成式召回是把“召回”建模成一个序列生成任务。输入是用户的 Query 文本(必要时候把用户行为序列也拼进去),输出是一个有序的商品 ID 序列,或者更直接一点,输出的是商品标题的关键特征序列。
为什么要把商品 ID 当成“词”来生成?因为 Transformer 系的生成模型本质上是在一个巨大的词表上做概率采样。你要让模型“生成商品”,就得把候选商品 ID 映射成词表里的 token。但商品 ID 本身没有语义,直接拿原始 ID 或者数字串去训练,模型很难学到商品之间的关联。这是第一个大坑,我后面会讲我们怎么处理的。
那为什么不直接输出商品文本,而是输出 ID?因为线上要用的不是“一段话”,而是可用的商品候选列表。你生成了一句话“轻便透气的白色运动鞋”,还得再做一轮文本-商品匹配,这等于把问题绕回了检索。直接生成 ID 序列,线上拿到的就是现成的候选集,省去了一次转换。
2.2 输入和输出的设计细节
输入端,我们用的是 Query 的 Token 序列,加上用户最近点击过的商品 ID 序列。这两个部分拼在一起,中间加一个分隔符。为什么要把用户行为拼进去?因为交易搜索不是纯粹的网页搜索,用户是有明确购物意图的,而且这种意图通常要通过“搜了什么-点了什么-最后买了什么”来表达。
有一类典型 Case:“搜了‘小白鞋’,然后点了好几双帆布鞋,最后买了板鞋。”Query 本身只能表达“小白鞋”,行为序列却能表达“用户要的是穿起来像小白鞋、但是更休闲的款式”。生成式模型天然擅长捕捉这种跨 Token 的依赖关系,你只要把行为序列拼进去,它自己会学。
输出端,我们用的不是简单的 beam search 硬解码,而是带约束的解码。为什么?因为线上每一步解码都要“生成一个合法的商品 ID”。如果模型生成的 ID 不存在于商品库中,那这个结果就是废的。所以我们在解码阶段维护了一个合法 ID 的白名单掩码,每一步从候选词表中过滤掉不在商品库里的 token,只保留合法 ID。
这里有个小细节值得说:商品 ID 的 ID 映射表不是静态的。每天都有新品上架,也有商品下架。如果词表固定在训练时刻,那第二天新上架的商品就永远无法被生成。为此我们在词表设计上给新商品预留了一部分可扩展的 ID 区间,同时定期对词表做增量更新。
2.3 训练目标的关键选择:监督信号怎么定
生成式召回的监督信号可以有不同的玩法,我见过有人直接拿“曝光点击”当监督信号,也有人拿“成交”当信号。我们的经验是:单拿哪一个都是不够的,核心是构造好训练样本的正负比例。
直接拿 CVR 当监督信号的问题在于:成交样本太稀疏,尤其在长尾 Query 上,很多时候一天都没有一个成交样本。只拿点击当信号又会被标题党带偏,模型会学到“生成标题惊艳的商品”而不是“生成能成交的商品”。
我们的做法是构造了一个多级偏好信号:在训练样本里,把商品的置信度分成三档,已成交商品、点击未成交商品、曝光未点击商品。优化目标不是简单的交叉熵,而是带权重的交叉熵,三档样本的权重不同,大致比例是 5:2:1。这样既保证了样本量,又让模型知道“什么更重要”。这个设计我们盯了很久,算是在工程落地和效果之间一个比较折中的选择。
3. 生成式召回不是空中楼阁:实操层面的完整落地
前面的设计再漂亮,落不了地就是零。这一节我讲真正动手时候的步骤和参数,很多细节是从失败里磨出来的。
3.1 数据准备与训练样本构造
模型训练的数据要从搜索日志和交易日志里抽取。这一步看着简单,实际上坑最多。
首先是数据清洗。日志里的 Query 千奇百怪,有“哈哈哈哈的东西”,有“999”,还有各种 emoji 拼接。我们的策略是:长度小于 2 个字符的 Query 直接过滤,纯数字纯符号的过滤,超过 32 个 token 的长 Query 截断。这一层过滤会砍掉将近 18% 的日志数据,但留下来的都是干净可用的。
然后是行为序列抽取。很多团队直接拿整个 session 的所有点击记录拼进序列,这会导致一个问题:用户逛了半小时,序列里塞了 100 个商品 ID,模型根本学不到重点。我们做了一次截断实验,最后确定保留最近点击的 20 个商品 ID 就足够了。再多反而引入噪声,序列太长训练速度还慢。
训练样本的正样本来自“最终成交的商品 + 点击并停留超过 3 秒的商品”,负样本从曝光未点击的商品里采样,但采样比例要控制。我做过一个对比实验:负样本比例从 1:1 调到 8:1,模型在线上指标的变化非常明显。一旦负样本太多,模型会变得“过于保守”,生成出来的商品都是极其热门的头部爆品,长尾完全消失;负样本太少,模型又容易生成跟 Query 无关的“幻觉商品”。最后我们卡在 4:1 左右,效果最均衡。
3.2 模型结构的选择与调优
模型主体我们用的是 T5 类的 Encoder-Decoder 结构,没有直接用 GPT 那样纯 Decoder。
为什么?因为输出端是受控的商品 ID 序列,不是自由文本,Decoder 需要借助 Encoder 的注意力机制来对齐 Query 和商品序列的关系。T5 类模型的 Encoder 能把整条 Query 上下文编码得更充分,这在短 Query 上优势尤其明显。
参数规模上,我们试过 base 和 large 两档。从线上效果看,base 级别的表现已经能打,large 提升有限,但推理延迟翻了一倍不止。考虑到生成式召回只是整个召回链路里的一环,后面还有粗排和精排兜底,最终用了 base 级别模型。
损失函数那部分我再补充一点:纯粹的交叉熵损失对“生成顺序”是很敏感的——同样一组商品,顺序换一下,Loss 差别就很大。但我们业务里其实并不严格要求商品 ID 的顺序完全一致,用户看到的召回结果本来就是可以调换顺序的。所以我们在损失函数里叠加了一个 Listwise 的排序损失,让模型不用过度纠结“第 2 位还是第 3 位”这种小事,只要商品集合对就行。这两者大概按 7:3 的比例混在一起。
3.3 推理加速与线上服务化
生成式模型线上服务的代价比双塔高一个数量级,这是所有团队绕不开的现实。双塔在线是“算一次内积”,生成式在线是“一步步解码”,每一层 Decoder 都要跑一次自回归。如果不做优化,单条 Query 的 P99 延迟能到 200 毫秒以上,这在搜索场景根本不能接受。
我们的方案是三步走。
第一步是模型蒸馏。用 large 模型做 Teacher,base 模型做 Student,蒸馏目标除了标准的 KL 散度,还加了商品 ID 序列的对比损失。蒸馏完之后,base 模型的效果能追上甚至部分超过原来的 large,但延迟降了 60%。
第二步是量化。把模型从 FP16 压到 INT8,精度损失大概在 1%-2%,收益是显存占用减半、推理速度再提 30%。对于交易搜索这种业务,1%-2% 的离线指标下降可以从线上其他环节找补回来,但延迟收益是实打实的。
第三步是批处理。线上把并发 Query 拼成 batch 一起解码,利用 GPU 的并行能力吃掉一部分算力开销。Batch 大小从 8 调到 32,吞吐量能涨 3 倍以上,但超过 32 之后收益递减,延迟反而因为排队变差。
最终我们在线上配了独立的一小组 GPU 实例跑生成式召回,控制在总召回流量的 10%-20% 左右。单条 Query P99 延迟控制在 80 毫秒以内,这个量级在线上去接粗排精排完全不成问题。
3.4 与向量检索的组合方式
生成式召回上线之后,原来那套向量检索怎么处理?全部删掉肯定不可能,我讲讲我们最终落地的组合链路。
现在的结构是“向量召回兜底 + 生成式召回补充”。流量进入以后,并行跑两个召回通道:向量通道按照老逻辑捞 TopK,生成式通道直接生成候选商品 ID。两边结果做去重合并之后,交给粗排模型统一打分排序。
这里有个关键调节旋钮:生成式召回的结果不是无脑排在向量召回前面。我们一开始很激进,直接把生成式结果置顶,结果发现 CTR 并没有显著提升,反而一些泛化 Query 的结果变怪了。后来把两路结果在粗排里一视同仁、让粗排模型自己学权重,效果立刻稳下来了。
用一句话总结这条链路:向量检索保证了下限,生成式召回负责打开上限。
4. 实际踩过的坑:问题定位与排查实录
最后这部分全是实践里的血泪教训。整理成几个高频坑位和排查办法,给你当速查表用。
4.1 “奖励黑客”问题
生成式模型天然会优化自身的生成概率,但它不一定优化业务指标。我们模型训练到后期,Loss 明明还在降,线上成交转化率却不动了。排查之后发现一个 Case:用户搜“跑步鞋”,模型开始疯狂生成那些标题里包含了海量关键词的“大词商品”——这些商品是商家堆词的产物,标题写满了“跑步运动休闲时尚潮流百搭”,跟谁都沾边。模型觉得这些商品跟所有 Query 匹配概率最高,所以生成它们最容易让 Loss 变低。
这里用到一个排查技巧:把模型生成概率最高的 Top 100 商品序列拉出来,人工扫一遍,马上就能看出模型是不是“学歪了”。解决方法是加一层规则约束,模型生成结果在进粗排之前,先做一次 Query-商品相关性兜底过滤,把相关性分极低的组合直接干掉。
4.2 曝光偏差导致的“马太效应”
生成式模型学的是用户曝光点击日志,而日志里本身就有偏差——头部商品曝光多、点击多、成交多,尾部商品几乎没有出场机会。模型很容易学到“生成头部商品准没错”,结果就是召回的多样性掉得厉害,长尾商品彻底失去曝光。
我们做了两件事缓解:一是在训练时对头部商品降采样,降低它们在样本里的出现频率;二是引入商品的“预估成交潜力”作为输入特征,让模型感知到那些“虽然没怎么曝光过,但属性与 Query 高度相关”的商品也值得生成。这两个措施叠加之后,长尾商品的曝光占比提升了 7 个百分点,交易侧的总成交没有掉。
4.3 冷启动商品如何被生成
新上架的商品没有点击和成交记录,不在行为序列里,但它得能被召回。冷启动问题在生成式模型里比向量模型更头疼:向量模型好歹还能靠商品图文信息算个 Embedding,生成式模型直接生成的是商品 ID,ID 没在词表里训练过,生成出来的概率几乎是零。
我们的处理办法:给新商品做一个“伪行为序列”注入。根据新商品的类目、属性、标题关键词,从历史日志里找一批相似的老商品,用这些老商品的点击序列当新商品的行为序列凑到训练样本里。这个操作不完美,但至少让新商品有了“被生成”的基础概率。后续如果新商品开始有真实点击,再逐步替换掉伪序列。
4.4 解码速度与生成质量的平衡
解码长度越长,花费的时间越长,生成质量却不一定更高。我们做了生成长度上限实验,发现商品序列生成 10 个和生成 50 个,线上成交额没有本质差别,但延迟差了将近一倍。最后把生成上限卡在 20 个 ID,这个数量既能满足召回集合的规模要求,又能让延迟保持稳定。
如果未来生成式召回承担更多流量,可以考虑在解码层引入 Early Stop 的机制:当模型对后续 token 的置信度持续低于一个阈值时,提前终止生成,而不是必须生成到最大长度。
5. 写在最后的几点实用心得
跟团队一起从零把生成式召回推到线上之后,我对“范式跃迁”这个词有了更具体的感知。它不是指你把模型从 A 换成 B,而是整体思考链路的转变:从“怎么把商品表征得更好”变成“怎么让模型写出更符合交易意图的候选答案”。生成式召回不是万能的,它现在还没法完全替代双塔,但它确实能给搜索系统注入一种以前没有的“联想能力”。
如果你所在团队也想尝试,我建议从小流量实验开始,先接 5% 的流量,观察两到三周,重点盯两个指标:长尾 Query 的成交转化率和召回集合的多样性。你会发现,生成式召回对头部 Query 的加持没有想象中大,但对长尾 Query 和口语化 Query 的提升非常显著。对交易搜索来说,“让更多人买到不那么容易想到、但确实合适的商品”,这本身就值回票价了。
还有一点建议是:千万别把生成式召回当成一个纯算法项目来做。它需要数据、特征、服务框架、甚至商品管理团队的配合。比如词表更新,你要是没有一套自动化的商品上下架同步流程,模型上线第一天就会被烂商品 ID 拖死。这事的复杂度不在模型本身,而在模型和现有搜索体系的咬合。准备充分了再动手,成功率会高很多。