别再只卷向量检索了,得物交易搜索如何用“生成式”实现召回范式跃迁?
这两年做搜索召回的同学,几乎人人都在聊向量检索。从双塔到 ANN,从 HNSW 到量化压缩,大家把 Faiss、Milvus 调得越来越溜,召回率也一点点往上磨。但说实话,我越来越觉得这条路有点“卷”错方向了——向量检索解决的是“相似怎么算”,而搜索真正难的是“需求怎么被理解”。最近我得物交易搜索团队做了一次新的尝试,把召回从“判别式匹配”切换到“生成式构造”,这个过程让我重新理解了什么叫检索范式的跃迁。
先说一下背景。得物交易搜索的场景比通用搜索要复杂不少:用户搜“篮球鞋 实战 耐磨”,背后可能对应的是品牌、品类、材质、场景、价格带多个维度的组合条件;搜“送男友 千元 礼物”这种 Query,压根没有明确品类,完全是意图推断。传统的向量召回在这里暴露了一个很尴尬的问题:它擅长找“长得像”的东西,但不擅长“生成”一个用户脑子里还没说全的答案。
这篇文章不准备讲太多理论,重点聊聊我们为什么从向量检索转向生成式召回、生成式召回具体是怎么做的、落地时踩了哪些坑,以及这套思路对交易搜索乃至更广的搜索场景有什么启发。如果你正在做搜索、推荐或者任何跟“理解用户需求”相关的系统,这篇文章应该能给你一些不一样的抓手。
1. 向量检索的天花板:召回率不是唯一指标,召回“正确的东西”才是
1.1 向量召回的本质是“近似”,不是“理解”
我们先回到最基础的问题:向量检索到底在做什么?
把商品、Query 各自编码成向量,然后用内积或者余弦相似度找最近邻。这个流程大家都很熟了。本质上,它是在一个高维空间里做近似匹配——Query 和商品的语义距离足够近,就把商品捞出来。
但这里有个很微妙的偏差:“语义距离近”不等于“需求满足”。尤其是交易搜索里,用户的需求往往是组合式的、条件化的,甚至是带排除语义的。
举个例子。用户搜“黑色 羽绒服 中长款”,在向量空间里,它确实能召回一堆黑色羽绒服,但“中长款”到底怎么定义?衣长到哪个位置算中长?不同类目对“中长款”的标注标准还不一样。向量模型很难精确表达这种条件约束,它更像是在做一个“模糊联想”。你问它“中长款”,它给你返回的可能是“稍微有点长”的、也可能是“长到膝盖”的,取决于训练语料里怎么共现。
这还不是最麻烦的。更麻烦的是否定语义和条件组合。比如“不要白色的”、“200-300 之间”、“适合外场”这种带过滤性质的意图,向量检索天然就不擅长。你可以说,这类需求交给后面的精排去过滤不就行了?但问题是,如果召回层根本没把足够多的候选集捞出来,精排再强也巧妇难为无米之炊。
1.2 向量召回的一个隐蔽缺陷:Query 稀疏性与长尾表达
另一个我在实践中体会很深的问题,是Query 稀疏性。
向量模型是靠“见过足够多相似的 Query-Item 对”才能学好映射的。但真实线上 Query 是极度长尾的,尤其是交易搜索这种场景,促销词、口语化描述、新款代号、网红叫法,每天都在冒出来。
“秋天的第一杯奶茶”这种 Query,如果只看字面,向量模型大概率会把它当成饮品搜索,但实际上用户想搜的可能是一个可以发朋友圈的礼物。这类语义如果没有在训练数据里充分出现,向量召回就只能靠运气。
说白了,向量检索的本质是“记忆相似模式”。它把所有语义都压到一个固定维度的向量空间里,然后用距离衡量相关性。它学到的是一种静态的、平均化的语义分布,一旦用户的需求表达超出了它的记忆范围,召回质量就会断崖式下跌。
1.3 召回率 91.3% 的错觉:当我们讨论召回率时,到底在讨论什么
最近看到华为云码道检视修复智能体提到“召回率 91.3%”,很多人觉得这数字很漂亮。但我在实际做搜索系统时养成了一个习惯:先看召回集里到底有没有“正确的东西”,再看召回率数字。
因为召回率本质上是一个“相对指标”,它取决于你怎么定义 Ground Truth。如果 Ground Truth 只包含“跟 Query 字面匹配的商品”,那召回率很容易做高。但如果 Ground Truth 定义成“用户真正想买的东西”,那大多数向量召回系统可能连 50% 都到不了。
这不是说向量检索没用。它依然是我们系统里最基础、最重要的召回通道之一。但我们必须承认:向量召回解决的是“语义相似性召回”,而交易搜索真正需要的是“需求满足性召回”。这两个目标之间有 gap,而这个 gap 靠继续调向量模型是填不满的。
2. 从“匹配”到“生成”:生成式召回的核心思路与架构设计
2.1 生成式召回的基本思想:先构造答案,再匹配答案
那怎么填这个 gap?我们当时的想法是:与其让模型去“猜”用户要什么,不如让模型直接“生成”用户可能想要的商品描述,然后再拿生成的结果去做检索。
这就是生成式召回的核心逻辑——把召回问题从“Query 到 Item 的匹配”转变成“Query 到查询表达的生成,再基于生成的表达去匹配 Item”。
用大白话说:传统方法是用户说“我要一双实战篮球鞋”,系统直接去商品池里找“实战篮球鞋”;生成式方法是系统先自己脑补出“Nike LeBron 系列、缓震好、抓地强、适合外场、中帮”,再拿这一串扩展出来的属性去商品池里捞。
这里的关键突破在于:生成式模型不是一个“匹配器”,而是一个“需求补全器”。它不直接回答“哪个商品最相关”,而是回答“用户到底在说哪些条件、哪些条件没说但可以推断出来”,然后把推断结果变成一个中间表示——我们叫它Query 结构化描述——再基于这个结构化描述做检索。
2.2 中间表示怎么设计:从“自然语言”到“逻辑表达式”
中间表示是整个生成式召回的地基。如果第一步就生成错了,后面全白搭。
我们参考了业界比较成熟的 NL2SQL 思路,但针对交易搜索做了定制化改造。我们把每个用户 Query 解析成一个由条件簇组成的逻辑表达式:
- 精确条件:品牌、类目、价格区间、颜色等能够直接映射到商品结构化字段的条件。
- 模糊条件:风格、场景、人群、功能等需要模型推断的软性条件。
- 关系条件:条件之间的逻辑关系,包括 AND、OR、NOT,以及一些特殊的约束(比如“不要某个品牌”)。
举个例子:
Query:“送男友的千元实战篮球鞋,不要白色的”
解析结果:
{ "场景": "送礼", "对象": "男友", "类目": "篮球鞋", "价格带": "约1000元", "功能": "实战", "排除": ["白色"] }这个结构化描述跟原始 Query 的区别是:它把隐含的信息显式化了。“送男友”这个短语,字面没有任何一个词是“性别男 + 礼物场景”,但生成模型会根据常识和用户行为模式,把它推断成“礼物场景 + 男性偏好”。
这一步非常关键。因为一旦信息被显式化,后面无论是走 ES 倒排、还是走向量召回,都非常好办——因为查询条件已经是“可执行”的了。
2.3 生成模型的选型:为什么我们没有一上来就上大模型
可能有人会问:既然叫“生成式”,是不是直接上 LLM 就完事了?
说实话,我们在最初做技术选型时确实考虑过直接用大模型。调研一轮之后,发现几个现实问题:
- 延迟不可控。大模型推理一次至少几百毫秒,到了大促峰值流量下,搜索链路根本等不起这一轮生成。
- 成本太高。交易搜索的 Query 量级很大,全部走 LLM 的话,推理成本会是传统检索的好几倍。
- 可解释性不足。LLM 生成的结果是一个黑盒,一旦生成出离谱的解析结果,排查问题的难度非常大。
所以我们最终选了分层生成策略:
- 第一层:轻量 NLU 模型(基于预训练模型微调),负责处理高频、明确、常见的 Query 模式。这层大约覆盖 80% 的流量,延迟控制在 10ms 以内。
- 第二层:LLM(规则约束 + 小模型兜底),负责处理长尾、复杂、口语化的 Query。这层只覆盖剩余 20% 的流量,但恰恰是这 20% 贡献了绝大部分的召回增益。
- 第三层:规则兜底,负责处理模型完全搞不定的极短 Query 或者纯泛搜词,保证不跌穿底线。
这套分层设计的好处是:用 80% 的高频流量摊薄了成本,用 20% 的长尾流量换取了效果上限。而且因为高频流量走的是轻量模型,整个系统的稳定性和可解释性都有了保障。
2.4 生成结果怎么用:两路召回合并的实操细节
生成式模型产出的结构化描述,本身不直接是一个商品集合,它还需要过一道“召回执行”的工序。
我们的做法是:同时走两路。
- 第一路:结构化查询召回。把生成的结构化描述转成 ES 的 Bool Query,去做精确匹配。这一步的作用是“保准”,只要是品牌、价格、颜色这类能精确命中的条件,都尽量命中。
- 第二路:扩展向量召回。把结构化描述里的模糊条件(风格、场景、人群)拼接成一段“伪自然语言”,再走向量召回。这一步的作用是“扩量”,把那些无法精确映射到结构化字段的商品也捞回来。
两路结果做合并、去重、粗排之后,进入精排阶段。
这里有个实操细节可以分享一下:两路结果不是简单做 Union,而是要给两路分别设定“置信度权重”。结构化查询召回的结果权重高(因为它精确),向量召回的结果权重稍低(因为它模糊)。如果两路都命中了同一商品,那这个商品会获得一个较高的加权得分,相当于在精排阶段天然获得了优势。
这么做的好处是:生成式召回不是“替代”传统的向量召回,而是给向量召回加了一层“条件约束”的上限。它让向量召回不再完全靠“猜”,而是有了一个生成式模型给出的方向性指引。
3. 实战中的系统落地:从离线验证到线上 A/B 的完整链路
3.1 离线评测怎么做:先别急着看线上指标,重点看“召回集构成”
任何新召回方案上线前,最怕的就是“自我感觉良好”。我们当时的离线评测思路,跟传统做法做了点区分。
传统做法:拿历史日志里的曝光点击数据,切成训练集和测试集,然后算 Recall@K、NDCG@K 这些指标。
这种做法的问题在于:它只能衡量“模型是否记住了历史行为”,不能衡量“模型是否理解了用户意图”。也就是说,你只是在检验旧系统能不能被更准确地复现,而不是在检验新系统能不能做得比旧系统更好。
我们的做法是:除了常规指标,还额外标注了一份“需求满足集”。
具体做法是:从线上日志里抽了一批长尾 Query,由业务同学逐个标注“如果这个 Query 让我去买,我最想看到哪 3-5 个商品”。这里的商品不一定在历史点击里出现过,甚至可能是完全没曝光过的商品。然后用这个标注集去算“需求满足率”。
这一步非常关键。因为它衡量的是系统的上限能力,而不是“对历史行为的拟合能力”。我印象很深刻,当时我们传统的 Recall@K 只提升了不到 2 个点,但需求满足率提升了接近 9 个百分点。这两个数字放在一起,才能真实反映生成式召回的价值。
3.2 线上 A/B 实验的坑:别拿“点击率”当唯一决策指标
线上 A/B 我们做了大概三轮,每轮跑一周以上。第一轮结果出来的时候,团队内部差点吵起来——点击率几乎没涨,但搜索成交转化率涨了。
这就很有意思了。后来一分析,原因是:生成式召回确实把更符合用户需求的商品捞出来了,但这些商品不一定是用户最想“点”的。用户可能看到商品列表第一眼,还是会先去点最熟悉的品牌、最常买的款式;但真正决定“买不买”的,是列表里有没有那个“我想要的”。如果列表里出现了那个“对的商品”,用户就会搜得更深、买得更果断。
所以如果你只盯着 CTR 做决策,很可能会得出“生成式召回没用”的错误结论。搜索系统的最终目标不是让人点得更多,而是让人更快地找到想要的东西。CTR 只是过程指标,转化率和成交才是结果指标。
3.3 稳定性保障:生成式模型也会“抽风”,怎么办
生成式模型跟传统检索模型最大的不一样,就在于它的输出不确定性。同样是“实战篮球鞋”,今天生成出来的结构化描述可能是“外场、耐磨、缓震”,明天可能就变成“包裹性、透气、抓地力”。
这带来的直接问题是:同一 Query 在不同时段召回的候选集可能不一样,这会导致用户体验上的抖动。
我们当时做了三个措施来缓解:
- 温度参数调低:让生成结果的随机性降到最低,尽量保持同一 Query 的输出稳定。
- 结果缓存:对高频 Query 的生成结果做缓存,设置一个合理的过期时间(比如 10 分钟),避免短时间内反复生成产生抖动。
- 最小改动原则:模型生成的字段里,如果某些字段的置信度比较低,就不让它进入结构化查询条件,只作为扩展召回的参考词。这样即使生成结果有波动,主路径也不会受太大影响。
3.4 一个印象深刻的 Case:长尾 Query 带来的真实增量
说一个让我印象特别深刻的 case。
有个用户搜的是“打球穿啥鞋不心疼”。这个 Query 如果走传统向量召回,几乎必死——因为向量模型根本不知道怎么把“不心疼”映射到商品属性上。但我们生成式模型解析出了几个维度:
- 场景:打球/运动
- 价格偏好:偏低/性价比高
- 心理特征:耐磨耐操、不心疼
基于这个解析,系统把一批“耐磨、性价比高、适合外场实战”的篮球鞋召回到了列表里。最后这个 Query 的成功转化率,比传统方案高了 15%。
这类 Query 在日志里占了很大比例:用户根本不会像写搜索词一样说“耐磨外场篮球鞋 300 元以内”,而是会用非常口语化、情绪化的方式表达需求。传统向量检索的问题就在这里:它能理解字面,但理解不了潜台词。而生成式召回,恰恰是把潜台词翻译成了可执行的检索条件。
4. 生成式召回踩过的坑与应对方案:每一条都是真实教训
4.1 坑一:生成结果太“发散”,结构化字段满天飞
第一版模型上线后,我们发现它特别喜欢“脑补”。比如用户搜“卫衣”,它能生成出“宽松版型、美式复古、重磅棉、落肩设计、男女同款”一堆属性。单个看都没错,但把这些属性全部当成硬条件去检索,召回集瞬间就窄了——因为市场上并没有那么多同时满足所有条件的卫衣。
解决方案是给生成式模型加一个**“条件置信度约束”**。每个生成字段必须附带一个置信度打分,只有分数超过阈值的字段才进入硬性检索条件;其余字段则当作软性扩展,只提升该商品的排序权重,不参与过滤。
这个改动的效果立竿见影:硬条件平均从 5 个降到了 2-3 个,召回池的覆盖率恢复到了正常水平,同时排序质量反而提升了。
4.2 坑二:排除条件太容易误伤
生成式模型能理解“不要白色”,这是好事。但问题是,如果它对“不要”的理解稍有偏差,比如把“不要太贵”理解成了“不要 1000 以上”,就会把用户原本可能接受的高价商品全部过滤掉,造成召回质量的断崖式下跌。
我们在第二轮实验里就吃过这个亏。后来加了一条规则:排除条件必须有 0.9 以上的置信度才能进检索条件。同时,模型生成的所有排除条件,在粗排阶段还要再做一次“宽松化”——不是直接过滤,而是给带排除属性的商品降权,让它在精排阶段再被真正淘汰。
这套“先降权、后过滤”的机制,大大降低了一句话理解错导致全盘皆输的风险。
4.3 坑三:生成式召回和粗排之间的“理解断层”
生成式模型解析得再好,如果下游的粗排模型不能理解这个结构化描述,效果也会打折扣。
我们的粗排模型原本是基于向量相似度的,它只知道“Query 编码向量”和“Item 编码向量”之间的夹角。但生成式模型产出的结构化描述,不是单纯的文本,它是一组有逻辑关系的条件簇。直接把条件簇拼成文本丢给粗排模型,等于把已经结构化的信息又重新打散成了模糊的序列,损失非常大。
所以我们调整了粗排模型的结构:在输入层增加了结构化特征。我们把生成式模型输出的品牌、类目、价格带、风格、场景等字段,单独编码成特征向量,跟原有的文本向量拼在一起,再输入粗排模型。
这个改造让粗排模型能够“看到”生成式模型的分析结论,而不是只看到一句话。上线之后,粗排到精排的流转率明显提升。
4.4 坑四:线上延迟抖动,超时导致大量请求走兜底
生成式模型不管怎么轻量化,都会增加一部分延迟。特别是第二层 LLM,虽然只覆盖 20% 的流量,但碰上峰值时段,还是会在 P99 延迟上产生明显抬升。
我们的应对思路是“降级不等于放弃”:LLM 调用一旦超过 50ms,就直接放弃生成,让请求降到第一层轻量模型;如果轻量模型也超时,再降到最后一级纯规则兜底。这样保证 P99 延迟不穿底线,同时长尾 Query 在非峰值时刻仍能享受生成式召回的增益。
这里有个经验值得强调:在做生成式召回时,一定要给链条每一层都设计降级路径。因为生成式模型跟传统检索模型不一样,它天然带有不确定性,超时、异常、幻觉都是常态,不是偶发。
4.5 踩坑小结:一遍遍调整之后,我们沉淀的四条原则
经过几轮迭代,我们把踩坑的经验沉淀成了四条“军规”,现在团队里任何新同学上手都得先过一遍这个:
- 生成结果必须有置信度,不能用“我觉得”代替“数据说”。每个字段、每个条件都要可量化、可过滤、可降级。
- 生成式召回是“增强”,不是“替代”。它给传统召回提供方向性指导,而不是试图包办一切。
- 结构化的中间表示是灵魂。只有把生成结果结构化,才能让它真正变成可执行的检索条件,而不是另一段模糊的自然语言。
- 线上稳定性优先于单点效果。任何一个想上线的模型,先证明自己“不会闯祸”,再证明自己“很有用”。
5. 不止于搜索:生成式召回对推荐、广告等场景的复用思路
5.1 推荐场景:从“猜你喜欢”到“替你表达需求”
生成式召回这套思路,其实不止能用在搜索。推荐系统里也有类似的痛点:用户没有主动输入 Query,系统只能靠行为历史来猜。但行为历史是稀疏的、多义的、充满噪声的。
比如一个用户最近看了很多篮球鞋,但可能只是在帮朋友挑礼物。如果推荐系统直接按照“篮球鞋”这个品类去推,大概率推不到点子上。但如果我们用生成式模型去推断用户的**“当前潜在意图”**,生成一个类似“送礼场景、预算千元、偏实战”的结构化描述,再基于这个描述去召回商品,推荐的准确率会高很多。
这块我们还没有完全落地,但已经在做方向性验证了。初步结果看,用生成式召回替代一部分纯行为序列召回,在挖掘新意图方面的优势非常明显。
5.2 广告场景:创意文案与定向条件的自动生成
广告系统里,生成式召回还可以做一件很有意思的事:自动生成定向条件。
传统广告定向是广告主自己设定人群标签、地域、年龄段、兴趣分类。但很多中小广告主根本说不清自己的目标人群是谁。这时候,可以用生成式模型把广告主的落地页内容、商品信息、历史转化数据作为输入,自动生成一串“目标人群描述”,再把这串描述转成定向标签。
这跟搜索里的“Query 解析成结构化条件”几乎是一个逻辑。本质都是把人的模糊表达翻译成机器可执行的精确条件,只是输入和输出的形式换了一下而已。
5.3 供应链与运营场景:商品标签的自动补全
还有一个比较意外的复用方向:商品标签自动补全。
生成式召回里有一个基础环节,是把商品的原始信息(标题、描述、图片识别的结果)转化成一个完整的“商品特征描述”。这个特征描述不光能用于检索,还能反哺商品的标签体系。
比如得物上有大量潮流单品,标题可能只写了“某某品牌联名鞋款”,但生成式模型可以根据它的详情页描述、社区内容,补全出“街头风、复古感、限量、高帮、深色系”这些标签。这些自动补全的标签,不仅能提升召回效果,还能让运营在做专题、做活动时更容易选品。
5.4 但别急着照搬:三个前提条件必须具备
如果你想把这套方案迁移到你自己的场景里,我觉得有三个前提条件必须具备:
- 你的场景里存在“结构化可枚举的约束维度”。比如品牌、类目、价格、颜色这些字段,在商品数据里是存在的、可查询的。如果商品数据本身就是一堆无结构的文本,生成式召回的优势发挥不出来。
- 你的用户表达里存在“隐含需求”。如果用户的搜索词都写得很直白,比如“耐克篮球鞋”,那生成式召回带来的增量有限,传统向量召回基本够了。
- 你有能力承担一部分不确定性的调试成本。生成式模型不是确定性算法,它需要你有一套完整的置信度、降级、监控机制去兜底。如果你连基本的 A/B 实验能力都没有,建议先把地基打好再上这个方向。
6. 效果数据与下一步规划:生成式召回的进化路径
6.1 目前拿到的结果:增量是实打实的,但不是“颠覆式”的
到目前为止,我们的生成式召回方案已经在得物交易搜索全量上线一段时间了。拿到的核心结果如下:
- 长尾 Query 的需求满足率提升约 9 个百分点。
- 整体搜索成交转化率相对提升约 6%。
- P99 延迟控制在要求范围内,核心链路无明显劣化。
- 覆盖线上约 20% 的长尾流量,这类流量的召回相关性提升尤其明显。
说实话,这个增量没有到“颠覆式”的程度,但它是实打实的增量,而且主要来自于过去向量检索完全覆盖不到的那部分长尾流量。对于搜索这种已经很成熟、提升空间极度收窄的系统来说,这个量级已经相当可观了。
6.2 技术上最大的转变:从“Embedding 一切”到“结构化一切”
我个人感受最深的技术转变,是团队对“向量检索”这件事的认知发生了根本性的变化。
前两年大家都信奉“Embedding 一切”,觉得万物皆可向量化,所有语义都能映射到向量空间里。这个思路确实解决了很多问题,但也把很多问题“糊”在了向量距离里——你说不清模型为什么觉得这个商品相关,也说不好哪些维度贡献了相关性。
生成式召回的思路,逼着我们回到“结构化”这个老路上来。不是因为结构化更简单,而是因为结构化更可控、更可解释、更容易优化。向量负责模糊联想,结构化负责精确约束,两者结合,效果远好于只押注任何一边。
6.3 下一步规划:从“生成召回”到“生成排序”再到“生成交互”
当前我们做的还是“生成召回”,下一步有三条线在规划里:
- 生成排序:利用生成式模型产出 Query 的核心需求点,在 LTR 阶段直接作为特征输入,让排序模型更清楚用户真正关心什么。
- 生成摘要:在召回结果里自动生成商品的“推荐理由”,沿着“你搜的‘送男友’→ 这款鞋的‘实战性能’→ 适合场景”的逻辑链去解释为什么推荐这个商品。
- 生成交互:用生成式模型做“多轮澄清”。当系统不确定用户意图时,主动生成澄清问题,“你是想要实战款还是穿搭款?预算大概多少?”用提问的方式把模糊 Query 变成明确 Query。
这几条线,本质都是在把“生成”这件事从召回层往整个搜索链路延伸。我个人的判断是,未来两三年里,搜索系统的核心竞争力,会从“谁能把相似度算得更准”转向“谁能把用户意图理解得更透”,而生成式方法大概率会是这个转变的主要抓手。
6.4 想对做搜索的同行说:别被热门概念绑架,回到问题本身
最后,我想说点跟技术无关但很重要的体会。
现在整个行业有一个倾向:什么热就卷什么。向量检索热,大家都去卷向量;大模型热,大家都去卷大模型。但我觉得,真正高效的做法是回到问题本身——先搞清楚你的系统到底在哪里漏掉了需求,再选择合适的技术去补。
对我们来说,问题出在长尾 Query 的需求理解上,所以生成式召回的思路天然合适。如果你的问题出在高频 Query 的精度上,那优化的重点可能是精排,而不是召回。如果出在商品冷启动上,那要解决的问题是内容理解,而不是检索策略。
技术永远是为了解决问题而存在的。当你能把一个模糊的、口语化的、带潜台词的 Query,变成一个精确的、可执行的、结构化的检索条件时,你就已经完成了一次范式的跃迁——不是因为你用了生成式,而是因为你真正理解了用户想要什么。
这个方向我会继续做下去,后续如果有了新的阶段性成果,再回来跟大家分享细节。