做了一段时间交易搜索召回,最直观的感受就是:这个赛道已经卷到“向量检索”四个字都快被说烂了。双塔、对比学习、ANN、量化、图索引、模型蒸馏…… 大家默认只要把语义向量做对了,召回就解决了一切。但真到了得物这种交易场景里,你会发现一个很实际的问题:向量检索只是让“长得像”的文本撞在一起,距离“用户到底想买什么”这个意图,中间还隔着类目、品牌、属性和一堆交易约束。所以当“生成式”这个概念进入召回领域时,我反而觉得这才是真正值得关注的范式变化,不是又一种向量模型的变体,而是把召回从“检索匹配”变成了“条件生成”。
这篇文章不打算聊太多理论名词,就围绕我们在得物交易搜索里的“生成式召回”落地实践展开,讲清楚它和传统向量检索的本质区别、为什么交易场景需要这种跃迁、具体怎么训练怎么上线,以及实践过程中踩过的那些坑。适合正在做搜索召回、推荐召回,尤其是对“生成式模型怎么进工业级pipeline”感兴趣的同学参考。
1. 别再只卷向量检索了:召回这条赛道的真实瓶颈
1.1 向量检索这套玩法,卷到最后剩下什么
先聊聊现状。现在大家提起召回,基本默认就是“双塔 + ANN”的组合:query 编码成一个向量,item 编码成一个向量,然后在高维空间里做近邻搜索,拿回最近的几百个。这套东西的优点大家都知道,离线训练简单、线上召回快、对语义相似有一定泛化能力。所以很多团队在向量检索上投入了大量精力,做特征交叉、做难负样本、做多向量交互,甚至把 BERT 级别的模型塞进 query 侧编码,让召回能力看起来越来越强。
但问题是,向量检索有一个隐性天花板:它本质上是在“已经被索引好的商品集合”里做匹配。索引是快照,商品上新如果没进索引,语义再相似也召不回;query 如果包含复杂的逻辑约束,比如“黑色 43码 高端篮球鞋 500以下送男友”,向量检索很难把每个约束都表达清楚,最后只能靠 ANN 召回一批“整体语义接近”的东西,把精确拆解交给下游排序。这个方案在通用搜索里够用,但在交易搜索里,召回层的误差会直接传导到成交转化。
我自己做了几轮实验之后,最大的体会是:向量检索解决的是“语义相似度高不高”,但交易搜索真正要回答的是“用户这笔购买意愿对应的商品集合是什么”。这两个问题看起来接近,其实差得很远。语义相似高的商品不一定具备交易条件,比如用户想买“夏季透气跑鞋”,向量检索召回来的可能是“跑步运动鞋男透气轻便”,这没问题,但如果碰到“送女友生日礼物”这种带关系、带场景、带预算结构的query,双塔把整个query压成一个向量,基本就丢掉了一半信息。
1.2 交易搜索场景的特殊性:为什么比通用搜索更挑剔
得物的交易搜索有自己的明显特点:用户带着明确的购买目的进来,搜“AJ1 低帮”“始祖鸟 冲锋衣”“情人节礼物送男友”这类query占比很高。这个场景下,召回结果必须具备几层约束:
- 类目要对:搜“篮球鞋”不能召出“板鞋”;
- 品牌/型号要对:搜“空军一号”不能召出“Dunk”;
- 交易条件要对:能买、有库存、状态可售;
- 意图场景要对:搜“送男友”和搜“情侣鞋”对应的商品集合差很多。
这些约束在向量检索的常规框架里很难被显式建模。你可以通过加特征、加多任务学习去缓解,但模型本质上还是在做“整体相似度匹配”,没法真正把query里的多个约束逐一拆解、逐一满足。我拿线上badcase看过,向量召回经常出现一种情况:query里明确写了“黑色”,召回来一堆白色商品的标题,因为从语义分布上看“黑色运动裤男”和“运动裤男”向量距离太近了。
所以说,在这个场景里,与其继续在向量相似度公式里挖参数,不如换一个思路:让模型直接“生成”一个满足约束的目标候选描述。这个思路就是生成式召回的起点。它不是不问青红皂白地把query压成向量,而是把每个query当成一个条件,让模型像写一句话一样把“用户真正想要的东西”描述出来,再通过这个描述去拉取线上商品集合。
1.3 “范式跃迁”到底跃迁在哪
很多人听到“范式跃迁”会觉得是大词,但在我理解里,它在工程上的含义其实很朴素:召回层从“表示学习 + 近邻检索”的范式,转向“条件生成 + 约束解码 + 索引拉取”的范式。前者的核心是“给定空间里找最近”,后者的核心是“根据条件直接推导目标”。
打个不严谨的比方,传统向量检索像是你在一个巨大图书馆里根据一本书的简介去找同类书,你得一本一本地比对相似度;而生成式召回更像是你直接向一个懂行的店员描述需求,店员根据你的描述直接报出几本书名,你再拿这些书名去书架上取书。后者更灵活,能处理更复杂的约束,但也更危险——因为店员可能说错,你必须有一套机制确保他报出来的书名真实存在、没有乱编。
所以整个范式跃迁的真正难点不是“用生成模型替代向量模型”,而是在生成模型之上叠一个工业级可控层。这部分我会在后面展开。
2. 生成式召回的原理拆解与关键设计
2.1 什么是生成式召回:从“检出来”变成“生成出来”
我们落地的生成式召回,技术路线可以这样概括:把“召回目标”定义为一个序列生成任务。输入是一条用户query,输出不是排序分数,而是一个“目标商品描述片段”,这个片段可以是商品标题的关键词序列、类目路径、品牌名加属性词的组合。生成模型要做的事情,就是根据query逐个预测这些输出token,生成结束后,用这段描述去线上倒排索引或者商品库中拉取一批具体商品作为召回候选。
这样做有个明显好处:生成过程天然具有“约束拆解”的能力。比如query是“黑色43码高端篮球鞋”,自回归生成过程中,模型第一步可能会生成“篮球鞋”,第二步生成“黑色”,第三步生成“43码”,第四步生成“高端”,每一步都在把query里的子约束逐步表达出来。相比之下,双塔向量模型是同时把所有信息压成一个向量,生成模型则是“逐个落子”,每个token都在显式对应一个语义约束。
当然,直接让模型输出“商品标题全文”风险很大,一是中文长文本生成容易重复、跑偏,二是生成出来的标题再和线上商品做匹配也有误差。所以我们最终选定的输出目标不是完整标题,而是一个“结构化查询序列”,形如:
[类目] 运动鞋/篮球鞋 [品牌] Nike [性别] 男 [属性] 黑色 [属性] 透气 [场景] 实战这样做的好处在于:输出是可枚举的结构化token字段,每一段都能和线上商品的结构化标签精确匹配;生成结果本身可解释,线上badcase可以直接看到模型在哪一类目或属性上做错了。
2.2 商品目标序列怎么设计:纯ID、标题还是结构化字段
这是我们在设计阶段争论最多的问题。可选方案大致有三种:
- 直接用商品ID作为生成目标:模型逐步生成某个商品ID的token,再把ID映射回商品。好处是目标明确、和曝光点击样本直接对齐;坏处是商品ID是离散无意义符号,模型学起来难,泛化到新品时几乎没有效果,而且线上解码时必须限制在“商品ID词表”,词表动辄几百万甚至上千万,约束解码代价极大。
- 用商品标题文本作为生成目标:优点是标题本身是自然语言,语义信息丰富,且可以和query形成良好的序列到序列关系;缺点是线上生成的标题片段不一定和已有商品完全一致,匹配需要阈值判断,而且标题里的无关营销词(“官方”“正品”“限量”“包邮”)会干扰生成。
- 用结构化字段序列:包含类目、品牌、性别、属性标签等,每个字段来自受控词典。优点是可控性最强、可解释性好、匹配线上标签直接;缺点是信息量有限,难以表达长尾属性。
我们最终选择了第三种,但做了一点扩展:在结构化字段序列后面追加一段“核心语义关键词”,来自商品标题分词后的关键片段,这样既保留可控字段的稳定匹配,又给生成模型留出吸收长尾语义的空间。实际效果看,这条配置在离线评估中比纯ID方案在商品命中率上高了一截,比纯标题方案在“可匹配率”上也好很多,因为受限词典把输出空间限制住了,生成出来的结果几乎都能在线上找到真实对应的商品集合。
2.3 约束解码:防止模型一本正经地生成“不存在的商品”
生成式召回最让工程同学害怕的一点,就是模型生成出来的东西听起来很合理,但线上根本没有这件商品。比如query是“耐克联名OFF-WHITE篮球鞋”,模型把结构化字段生成了类目“篮球鞋”、品牌“耐克”、系列“OFF-WHITE”,这几个字段单独看都合法,但组合起来可能没有在售商品。如果我们不去管它,这一路召回产出的候选就是空集,白白浪费一次召回机会。
所以上线前,我们做了三层约束解码:
- 词表约束:每个位置的候选token只允许来自当前field对应的受限词典。类目位置只能生成已有类目,品牌位置只能生成品牌库里的品牌,这一步直接避免生成“不存在的类目/品牌名”。
- 组合约束:解码过程中维护一个“已生成组合校验器”,每生成一个字段,就实时查一下当前组合在线上是否还有对应的在售商品集。举个例子,如果前面已生成“运动鞋/篮球鞋”和“李宁”,下一步要生成“属性/高端”,系统会先查这个组合下商品量是否大于阈值,如果为0,就强行把该token的概率置零。
- 不确定性兜底:就算生成结果经过前两层校验,最终召回返回时依然会和线上倒排索引做一次交集,同时在输出为空时自动回退到向量检索结果,确保这条recall通路永远不会“空手而归”。
这套约束机制配合下来,生成式召回的线上空结果率被压到了和我们普通向量召回同一量级。我实际测试时,一开始觉得“组合约束”会过度限制生成多样性,但统计后发现,交易搜索头部query其实高度聚焦,受控词典覆盖了绝大多数有效组合,少数长尾组合交给向量召回兜底完全合理。
2.4 生成式不是来替代向量检索的,而是来补位和分流
这里想特别点一句,免得大家理解偏了。我们在得物落地的生成式召回,并不是把向量检索换掉,而是做成并行的第二召回源,甚至更准确地说,是一种“意图路由 + 候选拉取”的机制。系统会先判断query属于什么类型:
- 强意图、多约束的query,比如“黑色43码实战篮球鞋500以内”,更适合走生成式意图解析 + 倒排拉取,召回精确度明显更高;
- 泛意图、有探索性的query,比如“好看的球鞋”,直接向量检索更合适,生成式反而容易把候选卡得太死;
- 长尾、无法匹配类目体系的query,比如用户自创的网络词、缩写,生成模型也未必理解,依然靠向量召回和同义词扩展。
从线上AB结果看,生成式召回对“精确类query”的成交转化提升贡献最明显,对泛query基本是持平。这符合预期:我们不要求它全场景通吃,只要求它把原本向量检索解决得不好的那一类query,真正解决掉一层。这也才是“跃迁”真正落地的姿势——不是革命,是新增一条有独特优势的recall路径,再和旧路径做融合。
3. 在得物交易搜索里的落地实践全流程
3.1 训练数据怎么构造:从曝光点击日志到序列目标
生成式召回的训练数据和传统向量召回不一样。向量召回需要构造(query, item)正负对,生成式召回需要构造(query, target_sequence)对,其中target_sequence就是前面说的结构化字段序列加核心关键词。
我们的数据来源主要有三个:
- 线上曝光点击数据:用户搜“AJ1低帮”,点了某个商品,我们把该商品的类目、品牌、属性、标题关键词抽出来,组装成目标序列,作为训练正样本;
- 成交数据:点击样本基础上叠加“是否成交”的权重,成交样本的loss权重调高,毕竟交易搜索最终衡量的是转化;
- 人工规则生成的合成样本:把query里的核心词替换成品牌、属性等字段的别名,构造泛化样本,缓解头部样本过拟合。
训练样本里负样本也是必需的,但生成式模型的负样本构造逻辑和对比学习不同。我们主要用两种:一种是“不匹配商品目标序列”的负样本,即给定query,把别的item组装出来的序列拿来当负例;另一种是“部分正确部分错误”的负样本,比如类目对了但品牌错的序列,训练模型学会真正理解字段间的依赖关系。这一点个人觉得比纯双塔负采样更能帮助模型学会约束拆解。
3.2 模型选型:不要一上来就上大模型
很多同学听到“生成式召回”第一反应就是上几十B的大模型。我们落地的过程中反复比较过,结论是:如果只是生成短小的结构化序列,基础规模的encoder-decoder模型就足够了,没必要为了“生成式”三个字背上大模型推理成本。
我们实际用的模型是大概1到2亿参数级别的中文预训练模型,还在上面做了一点轻量改造。为什么这么小也够用?因为任务本身不是开放文本生成,而是受限词典内的字段填充,输出长度也就十几个token,难点在约束解码和系统配合,不在模型容量。模型太大反而容易引入更多的随机性,弱化约束机制的作用。我们在离线评测时对比了同结构不同参数量版本,发现从1亿涨到3亿参数,对类目命中率只有零点几个点的提升,但推理时间翻了一倍不止,果断退回轻量版本。
训练loss方面,我们以标准的交叉熵生成loss为主,同时叠加了一个“字段级准确率”辅助loss,让模型对每个字段类型有更强的边界感。比如类目位置生成错了,辅助loss会直接影响该位置的梯度更新,而不是等到整个序列生成完才算总账。这个设计对收敛速度帮助挺明显,一轮训练后字段级准确率能快速涨到80%以上。
3.3 推理链路设计:先生成“意图”,再用倒排索引拉商品
这里要单独把推理链路拿出来讲,因为这是整个方案里最工程化的部分,也是最容易被论文带偏的部分。
最直觉的做法是:把几十万甚至几百万个商品的ID都作为生成词表,让模型在线直接生成一串商品ID。但前面说过,这个做法词表太大、约束校验太重、线上根本扛不住。所以我们换了一条路线:模型不直接生成具体商品,而是先生成“查询意图”,再把意图翻译成一次倒排索引查询。
整个过程拆成三步:
- query进入生成模型,输出结构化字段序列;
- 字段序列经过一个校验器,逐字段和线上词典匹配,并过滤组合商品量为空的序列;
- 把校验后的字段组合转成倒排索引查询条件,比如“类目=篮球鞋 AND 品牌=Nike AND 颜色=黑色 AND 价格<=500”,一次性拉取候选商品,再合并进召回列表。
这个路线最大的好处是:生成结果不是具体的商品快照,而是“意图描述”,因此线上商品索引更新后,只要意图本身不变,今天上架的新品也能被同样的意图拉取到。这一点直接解决了传统向量召回里“新品进不了索引就永远被遗忘”的痛点。我们在验证集上专门统计过新品召回率,生成式召回上线后,上架当天的新品被强势query召回的占比明显上升。
3.4 和现网召回pipeline的接入方式
接入环节我们尽量做成“独立recall通路”,避免大规模改动现有架构。线上召回流程变成三路并行:
- 向量召回:保留原有双塔 + ANN,负责泛意图和兜底;
- 规则或字符串召回:负责品牌词、型号词的精确匹配;
- 生成式召回:负责多约束、强意图query的结构化解析和候选拉取。
生成式召回的产出和其他两路结果做一个Union,送入粗排和精排。Union的时候要给生成式召回的候选打一个“来源标记”,但不额外加固定分数,让它和自然候选一起接受精排的重新打分。这么设计是为了防止人为抬高某一路权重导致精排学偏。
另外,接入时的超时控制也很重要。我们在线上给生成式召回分配了一个极短的执行预算,比如20毫秒。如果生成模型整体耗时超过预算,直接放弃这一路结果,不让它拖垮整个搜索链路的p99。实测下来,因为生成序列短 + 词表裁剪 + int8量化,线上单query平均耗时能压到十几毫秒,可以稳定纳入在线预算。
3.5 离线指标和在线AB实验怎么设计
离线评估方面,和常规向量召回只看Recall@K不同,我们对生成式召回额外看四类指标:
- 类目准确率:生成出来的类目是否等于日志里用户点击商品的实际类目;
- 品牌准确率:生成品牌和实际成交商品品牌一致的比例;
- 组合可匹配率:生成的字段组合在线上是否有在售商品集合,反映约束校验层的有效性;
- 强约束query的Recv率提升:单独圈出包含两个及以上属性的query,看生成式加入后Recall和MRR的变化。
线上实验则是经典AB分层。实验组看到的是“向量召回 + 生成式召回Union”,对照组保持原逻辑。我们设置了两个核心关注指标:无结果率(下降多少)、曝光到成交转化率(提升多少)。辅助指标看新品曝光占比和搜索badcase率。
这里提醒一句,生成式召回在线上的效果不是一上线就立刻爆发,一开始可能还因为生成结果改变了原有候选集合,导致精排需要重新适应新的分布。所以我们要求AB至少跑满两周,等精排模型对新recall来源的分布有过拟合窗口之后再下结论。第一轮我们只看头部query的成交转化,发现确实涨了几个点,但没有想象中大;后来把分析粒度拆到“多约束query”上,提升幅度才真正显出来,说明效果确实集中在目标场景。
4. 实操现场:我踩过的坑和排查技巧实录
4.1 生成结果跑偏:类目对了,品牌却开始“融会贯通”
上线初期最让我们头疼的badcase,是模型在字段生成顺序上出现问题。类目位置基本是准的,但到品牌位置,模型会基于query里的近义词、别名甚至错别字“发挥”,比如用户搜“nike af1”,模型生成了类目“运动鞋/板鞋”、品牌“Nature”而不是“Nike”。排查了很久,发现不是模型没学好,而是训练数据里把“AF1”和“空军一号”做了同义扩展,同义扩展后的query在没有明确品牌词的情况下,模型学会了从上下文“猜品牌”,猜错率高。
最终解法是调整训练策略,把品牌槽位的生成条件加强,具体做了两件事:一是在目标序列里,品牌字段前强制增加一个“[BRAND]”的token,让模型知道当前生成位置是品牌;二是对这种“品牌名缺失但商品实际属于某品牌”的样本,我们把日志里的真实商品品牌标签以更大概率的强制输入放进解码器端,而不是让模型自由猜测。
这类问题排查的技巧其实就一条:生成模型badcase一定要按字段拆解定位。先看是哪个字段错的,再去查这个字段在训练语料里对应的标记是否足够清晰。很多时候不是模型容量不够,是sequence目标和输入之间对齐关系太弱。
4.2 解码参数选择:beam size是不是越大越好
生成式模型在线解码时,我们做了一系列对比实验,可以分享一个比较省心的配置组合:
| 参数 | 我实践下来的合理范围 | 备注 |
|---|---|---|
| beam size | 4 到 6 | 再大收益很小,线上时延翻倍 |
| 重复惩罚 | 1.2 到 1.5 | 防止同一属性在序列中反复生成 |
| no repeat ngram size | 3 | 避免“黑色 黑色”连续出现 |
| 温度 | 0.7 到 0.9 | 太低容易生成保守无区分度的常见类目 |
| 最大生成长度 | 低于20个token | 我们这条链路根本不需要长输出 |
beam size这个值我特别多提一句。一开始我们以为beam越大越稳,实际把beam从4调到10后,类目准确率几乎没变,反而让模型更容易生成“概率高但组合不合法”的长序列,因为beam search会偏向那些每一步概率都不低、但前后组合违反常理的路径。后来我们发现,真正提升稳定性的是“组合约束校验器”挂在beam搜索过程中,而不是生成完再校验。这样能提前把不合法路径剪掉,效果显著好于盲目加大beam。
4.3 线上一路候选全空:生成器与倒排索引的“对齐事故”
有一个印象很深的故障:某天线上监控发现生成式召回的p99波动,而且部分query的生成结果到达倒排引擎后,返回的商品为空。刚开始以为是生成模型抽风,后来查日志发现,问题出在倒排索引的标签和训练数据里的标签版本不一致。
具体原因是训练数据里商品属性标签用的是旧的标签体系,比如“篮球鞋”的类目id是10001,而线上索引已经升级到新体系,对应id变成了10002。生成结果里的“类目id”到了线上检索时匹配了错误的词典,自然查不到商品。所以这里提醒所有做类似方案的同学:生成式模型输出的结构化字段,必须由线上配置中心统一派发,训练时用到的词典和线上索引词典必须同源同版本,否则就会出现这种看起来像模型不行、其实是“前后端词典不一致”的隐蔽事故。
从那之后我们把词典同步放进发布流程的原子环节:模型发布前,必须校验线上词典版本和训练时使用的词典版本一致,否则禁止上线。
4.4 生成结果多样性丢失:一个类目里的品牌扎堆
还有一个比较有意思的问题:生成式召回在部分query下多样性很差。比如“篮球鞋”这个query,模型生成结果里大概率是Nike、Adidas这两个头部品牌,其他品牌全被过滤掉了。从精确性看没问题,但对交易搜索来说并不是好事,因为用户可能就是想看某个二线品牌。
为了这个问题我们尝试了几种方案,最终有效的是在解码候选生成阶段引入“品牌轮换偏好”,即当query本身没有明确品牌约束时,允许模型在多个高概率品牌之间采样,而不是只挑top1。同时在组合校验器里限制一个品牌在一个query batch下最多出现次数,做输出层面的均匀化。这其实是在“生成精确”和“提升召回覆盖”之间做平衡,需要结合自己场景的搜索体验定义来调。我们在得物搜索里更偏向照顾长尾品牌曝光,所以对多样性的要求会更高一些。
4.5 内容安全与合规的一道前置闸门
这个点虽然放在后面,但非常重要。生成式模型本身有想象力,而搜索场景对内容的合规性要求极其严格。我们在生成式召回入口前就加了一道内容安全检查,对query中的非法字符、恶意诱导、敏感词做拦截,被拦截的query直接走兜底逻辑,不进入生成模型。
同时,模型生成的每个字段也要过一遍内容安全校验,尤其是用户自由输入的扩展关键词段。我们不允许生成结果中出现任何不受控的、无法通过安全词典校验的token。这块其实花了不少时间,但绝对值得做,因为一旦生成内容出合规事故,代价远高于效果提升带来的收益。
5. 个人体会与后续想法
5.1 范式跃迁不是推倒重来,而是给召回层加了一条新的“意图通道”
这段时间做下来,我最大的体会是:不要被“范式跃迁”这个词吓到。从工程视角看,我们做的事情本质上是在召回层新增了一条“生成式意图解析 + 倒排拉取”的通道,并让它和原有向量检索通道做协同。向量检索依然是地基,负责泛语义和兜底;生成式召回则负责把强约束query拆解成可检索的结构化条件,两者互不替代,而是互补。
这种形态的“跃迁”反而是最稳的,因为它没有把系统的命脉押在一个未经验证的新模型上。即使生成式召回在某类query上效果不达预期,直接摘掉这一路开关,系统就能回滚到原状。我们在上线过程中一直保留了这个开关能力,这让我在实际AB推进时有底气去实验,不怕“翻车”。
5.2 后续方向上,我更看好“多模态生成式召回”和“长期兴趣生成”
如果按这个思路继续往前延伸,我个人觉得有两个方向值得尝试。
第一个是“多模态生成式召回”。现在输出目标还是类目、品牌、属性这些文本字段,但如果把商品主图的视觉标签也纳入目标序列,生成模型就能在用户query里没有品牌/类目词时,通过风格描述来推导商品形态,比如用户搜“美拉德色系外套”,模型可以从颜色风格字段生成候选,这在纯文本向量检索里很难直接表达。
第二个是“用户长期兴趣生成摘要”。把召回触发条件从“单次query”扩展成“用户连续session行为摘要”,用生成模型先输出一段用户当下兴趣的结构化描述,再拿这段描述去拉取商品。这会更容易把兴趣漂移、场景切换这些信息带进来,和现有session向量比,生成式描述更加可解释、可干预。我们内部已经在搭这个实验框架了。
最后给所有想入这个方向的团队一个私心建议:别一上来就上大模型、做开放生成,先从小规模结构化序列生成做起,把“约束解码 + 词典同源 + 兜底回退”这三件工程基础打牢。只要这三件做到位,哪怕模型很轻量,你也能在真实的检索系统里看到效果。真正决定生成式召回能不能落地的,从来不只是模型那点事儿,而是整个工程链路能不能接住它。