GRAM 是京东提出的电商生成式检索新范式,核心思路是用 LLM 基于 16 种结构化属性为 Query 和 Product 生成共享文本 Code,通过 Co-training + DPO Co-Alignment + JS 散度评分三层对齐,将传统「意图识别→召回→粗排」三阶段流水线整合为一个端到端的生成式检索+预排序一体化过程,在京东搜索广告线上 A/B 测试中实现广告收入 +2.46%、CTR +1.27%。
📄 原文:https://arxiv.org/abs/2504.01403
🔗 会议:WWW Companion '25 (The ACM Web Conference 2025), Sydney
📦 资源目录:~/Desktop/paper-reader-assets/2504.01403/
① 共享文本 Code:不是独立为 Query 和 Product 生成不同 ID,而是以 16 种结构化属性(品牌、品类、型号、材质等)为词汇表,用 LLM 为两端生成相同词汇空间的文本 Code,天然打通 Query-Product 的语言鸿沟;
② 双重对齐:Code Co-Alignment(DPO 对齐 Query 端和 Product 端的 Code 生成偏好)+ Query-Product Alignment(JS 散度跨 Code 评分),确保一个 Query 生成的 Code 能高效召回相关 Product;
③ 召回+粗排一体化:Code 匹配召回后直接用 Code 级别的 relevance score 排序,省掉传统粗排模型,减少漏斗信息损失。
问题背景
电商搜索的检索(Recall)环节是用户体验和商业收入的关键入口。在京东这种亿级商品规模的平台上,如何在几百毫秒内从海量商品中精准找出与用户 Query 相关的商品,是一个核心挑战。
现有方法的局限
传统方法大致分为三派:
🔤 稀疏检索 (BM25 等)
基于倒排索引的词匹配,处理同义词、专业术语、上下文语义时能力有限,对长尾 Query 效果差。
🧬 稠密检索 (DPR 等)
将 Query 和商品映射到同一向量空间做 ANN 检索。瓶颈在于缺乏 Query-Product 深度交互、存储成本高(工业级商品规模下需大量内存)、正负样本构造受限导致无法利用 LLM 的世界知识。
近年来生成式检索(Generative Retrieval)兴起,用模型直接生成文档标识符。但现有方法分两派且各有致命缺陷:
🆔 ID-Based (TIGER, LC-Rec)
用静态 ID 或通过聚类生成的语义 ID(Semantic ID),需要从零训练一套新的 ID 系统,完全浪费了 LLM 预训练中蕴含的世界知识;且 beam search 召回 $k$ 个商品的时间复杂度为 $O(k^2)$,在工业级在线系统不可行。
📝 String-Based (SEAL, GLEN)
用 n-gram 或关键词集合作为标识符,虽然利用了 LLM 的先验知识,但 Query 和 Product 的词汇分布差异巨大——用户搜索「运动鞋透气网面男款」和商品标题「XX品牌2024夏季新款透气网面跑步鞋」的关键词不完全对齐,导致生成式召回时大量漏召回。
用户搜索「华为 Mate 60 手机壳 硅胶防摔」,这是一个带多个属性的 Query。基于 n-gram 的方法可能生成「华为」「Mate」「60」「手机壳」「硅胶」「防摔」等独立 Code,但这些 Code 与商品标题「华为Mate60Pro官方硅胶保护壳全包边防摔」之间缺少系统性的对齐机制——「手机壳」和「保护壳」是同义词但不匹配,「防摔」在商品标题中没有精确出现,导致该商品可能不会被召回。
而 GRAM 通过 NER 将 Query 归一化到结构化属性空间(品牌=华为, 型号=Mate 60, 品类=手机壳, 材质=硅胶, 功能=防摔),与商品的对应属性直接匹配,大幅提升召回精度。
2.1 Code 定义与构建
GRAM 的第一步是定义一套Query 和 Product 共享的文本 Code 体系。作者从电商场景中抽象出 16 种结构化属性作为 Code 的「原子词汇」:
Code 由 1 个或多个属性通过分隔符拼接而成,按粒度分为三档:
- 粗粒度 Code:1-2 个属性(如「华为|手机壳」),覆盖面广但精度低
- 中粒度 Code:3 个属性(如「华为|手机壳|硅胶」),平衡覆盖与精度
- 细粒度 Code:超过 3 个属性(如「华为|Mate60|手机壳|硅胶|防摔」),精度高但覆盖面窄
初始 Code 构建(冷启动)
由于没有现成的 Query-Code 和 Product-Code 标注数据,GRAM 通过以下流程构建初始训练集:
- 1NER 模型训练:专家标注约 300 万条 Query 的 16 种属性 → 训练 BERT-based NER 模型
- 2属性提取:用 NER 模型从 Query 中提取关键属性集合 $Q$,从商品标题中提取对应属性
- 3点击对挖掘:基于点击日志挖掘高频 Query-Product 对,Query 的初始 Code = 自身 NER 属性 + 关联 Product 的 NER 属性;Product 的初始 Code = 自身 NER 属性 + 反向关联 Query 的 NER 属性
- 4双向扩充:最终得到 ~600 万唯一 Query、~800 万唯一 Product、~740 万唯一 Code
用户在京东搜索「夏天透气运动鞋白色」并点击了商品「Nike Air Zoom Pegasus 40 白色网面透气跑步鞋」。
Query 侧:NER 提取 → {适用时间:夏天, 功能:透气, 品类:运动鞋, 颜色:白色} → Query Code 包括「夏天|运动鞋」「透气|白色」「运动鞋|透气|白色」等组合。
Product 侧:NER 从标题提取 → {品牌:Nike, 系列:Air Zoom Pegasus, 品类:跑步鞋, 材质:网面, 功能:透气, 颜色:白色} → Product Code 包括「Nike|跑步鞋」「网面|透气|白色」等。
交叉增强:由于该 Query-Product 对有点击关系,Query 额外获得「Nike|跑步鞋」这个来自 Product 的 Code,Product 额外获得「夏天|运动鞋」这个来自 Query 的 Code。这样两端 Code 逐步趋同。
2.2 监督微调 (SFT)
2.2.1 Query-Code 生成器 SFT
基于 0.5B 开源 LLM,以 Query 文本为输入、Code 文本为目标做 SFT。训练目标为负对数似然最小化:
- $q$:用户搜索 Query
- $c$:目标 Code(由 NER 构建的 gold standard)
- $c_i$:Code $c$ 的第 $i$ 个 token
- $P_{mpt}^q$:Query 专用 Prompt(如「请为以下搜索词生成商品属性 Code:...」)
- $L_c$:Code 的长度
- $D_q^{SFT}$:Query-Code 训练数据集(约 1.85 亿条)
2.2.2 Product-Code 生成器 SFT
对称地,以商品标题为输入、Code 为目标训练 Product-Code 生成器:
- $t$:商品标题
- $P_{mpt}^t$:Product 专用 Prompt
- $D_t^{SFT}$:Product-Code 训练数据集(约 4.6 亿条)
- 其余符号含义同上
2.2.3 Co-training(协同训练)
为什么需要 Co-training
2.2.1 和 2.2.2 各自 SFT 后,模型学会了从 Query 出 Code、从 Product 出 Code。但一个致命问题:模型几乎只生成细粒度 Code(4-5 个属性),因为训练数据里高频 Query-Product 对本身就属性多。细粒度 Code 精度高但覆盖面窄——中长尾 Query/Product 可能 Code 交集为空,完全无法召回。
第一步:数据增强(论文明确描述)
- 用训练好的 Product-Code 生成器为全量活跃商品生成 Code
- 基于点击关系,找到每个 Query 关联的 Product,把这些 Product 的 Code 作为 Query 候选
- 用 Query-Code 生成器为每个 Query 额外生成 10 个新 Code
- 去重后用相关性模型过滤,通过的 Code 合并到训练集
效果:每条训练样本的 Code 从「只有 NER 标注的细粒度 Code」扩充为「粗/中/细粒度混合 Code」。如果不做这一步直接 Co-training,模型根本没见过粗 Code,训了也没用。
第二步:同时训练(论文明确描述)
论文原话:「Unlike the separate training conducted in the previous stages, this phase needs to strengthen the connection between the two models, thus training the two generators simultaneously.」
一个 training step 里具体发生了什么(论文明确描述 loss 公式,以下 batch 细节为合理推断):
- 1拼 prompt:每条样本根据类型拼不同 prompt 前缀——Query 样本拼「请为以下搜索词生成 Code:」,Product 样本拼「请为以下商品标题生成 Code:」。文本 + prompt 拼完就是模型的标准输入。
- 2forward:所有样本(Query-Code 对 + Product-Code 对)一起送进同一个 LLM,做一次前向传播。每个 token 输出词表上的概率分布。
- 3算 loss:对所有样本分别算负对数似然 loss(跟 2.2.1/2.2.2 公式一样),然后全部加在一起:batch_total_loss = Σ L_q_samples + λ · Σ L_t_samples(论文 λ=1,即等权重)。
- 4backward + 更新:batch_total_loss.backward(),梯度同时流回 LLM 的同一套参数,更新。
- $\mathcal{L}_q^{SFT}$:batch 中所有 Query-Code 样本的负对数似然 loss 之和
- $\mathcal{L}_t^{SFT}$:batch 中所有 Product-Code 样本的负对数似然 loss 之和
- $\lambda$:平衡超参(论文设为 1,Query 和 Product 端等权重)
- 关键:两个 loss 作用于同一套模型参数,一次 backward 同时受两端梯度约束
跟「各自训」的核心区别
各自训(2.2.1 + 2.2.2)
Step 1: Batch 全是 Query-Code → forward → loss → backward(只看 Query 端)
Step 2: Batch 全是 Product-Code → forward → loss → backward(只看 Product 端)
问题:两次梯度更新互不知道对方存在,参数在两个方向上各自偏移,Code 空间不一致。
Co-training(2.2.3)
Step 1: Batch 混合 → forward(同时看到两端)→ loss 求和 → backward
效果:一次梯度更新 = 同时学会「Query 怎么出 Code」+「Product 怎么出 Code」,参数被拉向「两端都能预测好」的妥协点。
Batch(假设 4 条样本,真实 batch=128):
样本 1(Query):输入「跑步鞋透气网面」→ 目标 Code「运动鞋|透气|网面」
样本 2(Product):输入「Nike Air Zoom 跑鞋」→ 目标 Code「Nike|跑步鞋|透气」
样本 3(Query):输入「苹果手机壳防摔」→ 目标 Code「苹果|手机壳|防摔」
样本 4(Product):输入「Apple 20W 快充头」→ 目标 Code「苹果|充电器|快充」
forward:4 条一起送进 LLM,同一个 Transformer 处理所有样本。
loss:loss_1 + loss_2 + loss_3 + loss_4(λ=1,不分 Query/Product 权重)。
backward:loss 对 Transformer 参数求梯度——比如某个 attention head 的权重更新方向,同时被「Query 端希望这个 token 概率高」和「Product 端希望那个 token 概率高」的梯度共同决定。
训练前:Query「跑步鞋」生成 Code「跑步鞋|透气|轻便|运动|减震」(5 属性,细),Product「Nike跑鞋」生成 Code「Nike|跑步鞋|Revolution6|黑色|网面」(5 属性,细)。两端 Code 交集只有「跑步鞋」1 个 token。
Co-training 后:因为扩充数据里有粗 Code(「跑步鞋」「运动鞋|跑步」),且两端 loss 同时约束同一套参数,模型学会了在粗/中/细粒度间平衡——Query 额外生成「跑步鞋」「运动鞋|跑步」,Product 也扩大粗 Code 覆盖。交集扩大到多个粗/中粒度 Code,召回率大幅提升。
2.3 Code 协同对齐 (Code Co-Alignment)
SFT 阶段主要关注「能不能生成 Code」,而 Co-Alignment 阶段关注「生成的 Code 能不能高效召回」。核心思路:用点击行为做监督信号,通过 DPO(Direct Preference Optimization)让模型偏好那些能召回更多相关商品的 Code。
正负样本构造
对于每个 Query-Product 点击对:
- 正例 Code ($c_w$):Query 的 Code 集合 ∩ Product 的 Code 集合 = 共享 Code
- 负例 Code ($c_l$):Query 的 Code 集合 − Product 的 Code 集合 = Query 独有但 Product 没有的 Code(或反过来)
一条 DPO 训练样本 = (Query 文本, Product 标题, 正例 Code 文本, 负例 Code 文本) 的四元组。
DPO 训练:一个样本到底发生了什么
第一步:参考模型打分(参数冻结,可以预先算好)
- 1Forward(Query 端):输入 = [Query Prompt] + Query 文本 → 模型输出对 $c_w$ 和 $c_l$ 的生成概率 $\pi_{SFT}(c_w \mid q)$ 和 $\pi_{SFT}(c_l \mid q)$
- 2Forward(Product 端):输入 = [Product Prompt] + Product 标题 → 得 $\pi_{SFT}(c_w \mid t)$ 和 $\pi_{SFT}(c_l \mid t)$
- 3取平均:$\pi_{SFT}(c_w \mid q, t) = (\pi_{SFT}(c_w \mid q) + \pi_{SFT}(c_w \mid t))/2$,$c_l$ 同理
第二步:当前模型打分(参数在训练,需要梯度)
- 4Forward(Query 端):同样输入 Query,得 $\pi_\theta(c_w \mid q)$ 和 $\pi_\theta(c_l \mid q)$
- 5Forward(Product 端):同样输入 Product,得 $\pi_\theta(c_w \mid t)$ 和 $\pi_\theta(c_l \mid t)$
- 6取平均:$\pi_\theta(c_w \mid q, t) = (\pi_\theta(c_w \mid q) + \pi_\theta(c_w \mid t))/2$
第三步:算 DPO loss → backward → 更新参数
- $D_{CA}$:偏序数据集(6740 万条),每条是 $(q, t, c_w, c_l)$ 四元组
- $\pi_{SFT}(c \mid q, t)$:参考模型对 Code $c$ 在 $(q,t)$ 下的概率 = $\frac{1}{2}[\pi_{SFT}(c \mid q) + \pi_{SFT}(c \mid t)]$(两次独立 forward 的平均,不是联合输入)
- $\pi_\theta(c \mid q, t)$:当前模型同样方式算出的概率
- $\beta_w, \beta_l$:正例/负例的权重超参
- 直观理解:让当前模型比参考模型更偏好 $c_w$(共享 Code),更不偏好 $c_l$(只有一端能生成的 Code)
样本:$q$=「跑步鞋透气」, $t$=「Nike Air Zoom 跑鞋」, $c_w$=「运动鞋|透气」, $c_l$=「跑步鞋|透气|轻便|减震」
参考模型(SFT 后的 GRAM,冻结):
Forward ①:输入 Query → 输出 $P(c_w|q)=0.35$, $P(c_l|q)=0.42$
Forward ②:输入 Product → 输出 $P(c_w|t)=0.61$, $P(c_l|t)=0.12$
平均:$\pi_{SFT}(c_w|q,t)=0.48$, $\pi_{SFT}(c_l|q,t)=0.27$
当前模型:
Forward ③:输入 Query → 输出 $P(c_w|q)=0.52$, $P(c_l|q)=0.31$
Forward ④:输入 Product → 输出 $P(c_w|t)=0.68$, $P(c_l|t)=0.15$
平均:$\pi_\theta(c_w|q,t)=0.60$, $\pi_\theta(c_l|q,t)=0.23$
DPO loss:正例比 $0.60/0.48=1.25$(偏好提升 ✓),负例比 $0.23/0.27=0.85$(偏好下降 ✓)→ loss 推动参数往这个方向继续优化。
为什么是平均值?Gram 只有一个文本输入口,不支持 Query+Product 拼接。取平均近似「两端对该 Code 的共同偏好程度」——值越大,说明 Code 既对 Query 描述得好、也对 Product 描述得好,即共享 Code 质量高。
2.4 Query-Product 对齐评分
Code Co-Alignment 解决了「Code 本身好不好」的问题,但还需要解决「同一个 Query 召回的不同商品如何排序」。为此 GRAM 引入了基于 JS 散度的 Query-Product 评分机制。
JS 散度评分
对于每个 (Query, Code, Product) 三元组,逐 token 计算 Query 端和 Product 端对该 Code 的生成概率分布的差异。差异越小(即 Query 和 Product 对同一 Code 的生成概率越接近),该 Code 对该 Query-Product 对的相关性越高:
- $P_{q_i}$:Query 端生成 Code 第 $i$ 个 token 的概率(标量,0 到 1)
- $P_{t_i}$:Product 端生成 Code 第 $i$ 个 token 的概率
- $w_i$:每个 Code 的可学习权重(按 Code 粒度不同)
- $n$:Code 的 token 数量
- 括号内是 Jensen-Shannon 散度(对称版 KL 散度),值越小说明两端对该 token 的生成概率越一致
- 注意:此阶段模型参数固定,只训练权重 $w_i$
Pairwise Ranking Loss
每个 Query 通过 Code 召回一批商品后,取 Top-K 最高分的商品作为正样本 $t_{pos}$,其余为负样本 $t_{neg}$,用 pairwise loss 训练排序:
- $t_{pos}$:通过 Code 召回的高分商品(正样本)
- $t_{neg}$:召回的低分商品(负样本)
- $\mu$:Margin 超参,要求正负样本分数差距至少为 $\mu$
- 目标:让正样本商品比负样本商品的 Code 级别 relevance 分数至少高出 $\mu$
先搞清楚 $P_q$ 和 $P_t$ 分别从哪来:
$P_q$(Query 端 token 概率)
Query 送进模型生成 Code 时,模型对每个 token 输出的概率。同一次 forward 的副产物,不额外消耗算力。
$P_t$(Product 端 token 概率)
每个商品只对它自己生成的 Code 有 $P_t$。近线系统把商品标题喂给 Product-Code Generator,生成 N 个 Code,顺便记下每个 token 的概率,写进索引。一个商品没生成过的 Code,索引里就没有这个商品的条目——自然也不会被那个 Code 召回。
具体例子:
Query「苹果手机充电器快充」→ 一次 forward,生成了 Code「充电器|快充」(它还生成了其他 Code,我们只看这一个),同时拿到:$P_{q1}=0.85$, $P_{q2}=0.91$。
Code「充电器|快充」的倒排索引里有两个商品(因为它们各自在近线系统中都生成过这个 Code):
| 商品 | 它是怎么被召回的 | 预存的 $P_t$ | JS 散度 vs Query |
|---|---|---|---|
| A: Apple 20W USB-C 快充头 | 近线处理时,Product-Code Generator 输入「Apple 20W USB-C 快充头」,生成了 Code「充电器|快充」 | $P_{t1}=0.88$, $P_{t2}=0.90$ | ≈ 0.0003 ✅ 高分 |
| B: 三星 25W 旅行充电器 | 近线处理时,输入「三星 25W 旅行充电器」,也生成了 Code「充电器|快充」(因为「充电器」和「快充」在训练中经常共现,虽然这商品本身不是快充) | $P_{t1}=0.85$, $P_{t2}=0.40$ | ≈ 0.12 ❌ 低分 |
为什么商品 B 的 $P_{t2}$ 只有 0.40?因为它的标题里没有「快充」这个词,模型强行生成 token「快充」时信心不足。JS 散度捕捉到这个差异——Query 对「快充」很有信心(0.91),但商品 B 对「快充」很勉强(0.40),所以得分低。
根本不会发生的事:Code「苹果|充电器|快充」的倒排链里不会有「三星充电器」——它从来没生成过这个 Code,索引里没有它的条目,自然不会被召回。$P_t$ 只对商品自己实际生成过的 Code 存在。
线上不需要额外 forward:$P_q$ 从 Query forward 免费获得,$P_t$ 从索引查表。JS 散度是纯数学计算,几乎不耗时。
2.5 推理与部署架构
GRAM 的在线推理流程与部署方式非常有工程价值,它做到了召回+粗排一体化:
- 1Code 生成(一次 forward):用户发起搜索 → Query-Code Generator 做一次 LLM forward,生成 N 个 Code(beam size=10)。同时免费拿到每个 token 的生成概率 $P_q$——这是 forward 的副产物,不额外消耗算力。
- 2Code 匹配召回(查索引):每个 Code 查倒排索引(Code → Product 列表),召回约 300 个广告商品。索引里除了 Code 字符串,还预存了每个商品对该 Code 的 token 级生成概率 $P_t$(近线系统提前算好写入)。
- 3Query-Product 评分(纯计算,无模型调用):对每个召回商品,用 JS 散度计算 $S_{rele} = f(P_q, P_t)$。$P_q$ 从 step 1 免费获得,$P_t$ 从 step 2 索引查表获得——全程不需要再做任何模型 forward。同一商品若被多个 Code 召回,分数累加。
- 4排序输出:按 $S_{rele}$ 降序排列,直接输出——不需要额外的粗排模型。整个流程只有 step 1 涉及一次 LLM 推理。
① $P_q$ 是 Query forward 的天然副产物——生成 Code 时模型对每个 token 都输出概率分布,取对应 token 的概率就行。
② $P_t$ 由近线系统预处理——每个商品喂给 Product-Code Generator 生成 N 个 Code,只对这些生成过的 Code 记录 token 概率,写入倒排索引。线上查表即得,不需要额外 forward。
③ 实时缓存:高频 Query 的 Code 和 $P_q$ 也会缓存,连那一次 forward 都可以省掉。
数据规模
GRAM 的实验数据来自京东真实搜索日志,规模巨大:
| 统计项 | SFT Dataset | Alignment Dataset |
|---|---|---|
| Query-Code 对 | 1.85 亿 | 6740 万 |
| Product-Code 对 | 4.60 亿 | 1.35 亿 |
| 唯一 Query 数 | 620 万 | 150 万 |
| 唯一 Product 数 | 840 万 | 1560 万 |
| 唯一 Code 数 | 740 万 | 45.3 万 |
| Query 平均字符数 | 8.0 | 7.0 |
| Product 平均字符数 | 50.3 | 51.9 |
| Code 平均字符数 | 9.9 | 7.9 |
离线评测
评测指标:Recall@k(k=10, 100, 300,衡量点击商品被召回的比例)和 RelR(Relevance Ratio,不依赖点击的 relevance 角度评估)。
| 模型 | Recall@10 | Recall@100 | Recall@300 | RelR |
|---|---|---|---|---|
| GRAM | 2.85% | 12.54% | 21.13% | 40.18% |
| BM25(稀疏) | 3.01% | 10.52% | 15.23% | 35.78% |
| DocT5Query(稀疏) | 3.13% | 10.88% | 15.88% | 35.56% |
| DPR(稠密) | 3.89% | 11.26% | 17.92% | 30.96% |
| SEAL(生成式-string) | 3.25% | 11.62% | 16.56% | 27.03% |
| LC-Rec(生成式-ID) | 4.35% | 7.16% | 7.33% | 23.94% |
① GRAM 在 Recall@10 上略低于 DPR/BM25 等传统方法,但在 Recall@100(+11.4% vs DPR)和 Recall@300(+17.9% vs DPR)上大幅领先——说明 GRAM 的 Code 覆盖面更广,不需要在 Top-10 内就精确命中,但整体召回效率远高于传统方法;
② GRAM 的 RelR(40.18%)显著优于所有 baseline,说明其召回结果的「相关商品比例」更高——这是 Code 对齐模块发挥的关键作用;
③ LC-Rec(Semantic ID 式生成召回)在 Recall@300 只有 7.33%,说明从零训练 Semantic ID 在工业级场景的泛化能力极差;其 Recall@10 最高(4.35%)可能得益于 beam search 的局部聚焦效应,但无法扩展到更大召回量。
消融实验
| 模型变体 | Recall@10 | Recall@100 | Recall@300 | RelR |
|---|---|---|---|---|
| GRAM (完整) | 2.85% | 12.54% | 21.13% | 40.18% |
| w/o Co-Alignment (CA) | 1.80% | 11.37% | 20.23% | 33.51% |
| w/o Co-training & CA | 1.57% | 7.64% | 12.89% | 33.36% |
① 去掉 Co-Alignment 后 Recall@300 降 ~1pp,但 RelR 暴跌 6.67pp——说明 Co-Alignment 对提升召回相关性质量至关重要;
② 同时去掉 Co-training 和 Co-Alignment 后 Recall@300 进一步暴跌到 12.89%(降 ~8pp),说明 Query-Product Code 的交互训练是基石。
在线 A/B 测试
GRAM 在京东搜索广告线上随机服务 5% 流量 vs 基线模型 5% 流量,持续至少一周。结果以在线模型为 baseline:
| 模型 | Ad. imp. | CTR | CPC | Ad. revenue |
|---|---|---|---|---|
| GRAM (完整) | +0.74% | +1.27% | +0.45% | +2.46% |
| GRAM w/o CA | +0.56% | +0.92% | +0.68% | +2.16% |
| GRAM w/o CT & CA | +0.37% | +0.21% | +0.79% | +1.37% |
① 完整 GRAM 广告收入 +2.46%,CTR +1.27%,所有指标 p<0.05 显著——这在工业级搜索广告系统中是非常可观的提升;
② 广告展现量 +0.74% 说明 GRAM 召回了更多相关商品,扩大了可竞价广告池;
③ 消融趋势与离线实验完全一致:Co-Alignment 贡献最大(+0.62pp CTR),Co-training 是基础前提。
亮点
- 共享 Code 空间的设计非常巧妙:不是让 Query 和 Product 各自有独立的 ID 体系,而是以 16 种结构化属性为「通用语言」,让 LLM 在同一个词汇空间里为两端生成 Code。这从根本上解决了 String-based 生成式检索中 Query-Product 词汇不对齐的核心痛点,比 TIGER/LC-Rec 的 Semantic ID 方案更充分利用了 LLM 的世界知识。
- 工程落地思维贯穿全文:从初始 Code 冷启动(NER + 点击对挖掘),到粗中细粒度 Code 的分层设计,到长度惩罚下采样平衡粗细粒度分布,再到在线缓存的延迟优化——这不是一篇纯学术 paper,而是一个真正在亿级商品规模上跑通并持续创造收入的工业系统。
- 召回+粗排一体化的工程价值:传统「意图识别→多路召回→粗排」的漏斗架构每层都有信息损失,GRAM 用一个模型同时完成这三步,减少信息损失、降低系统复杂度。在线 A/B 结果(收入 +2.46%)证明这个方向是对的。
- DPO 在检索对齐中的创新应用:将 DPO 从 LLM 对齐迁移到检索场景,用点击行为构造偏序关系来优化 Code 生成偏好——这个思路可以推广到其他生成式检索场景。
可能的不足
- 对 NER 模型的强依赖:GRAM 的 Code 体系建立在 NER 提取的 16 种属性之上。NER 模型的准确率会直接影响 Code 质量,如果 NER 出错(如将「苹果」误标为水果而非品牌),会导致系统性召回偏差。论文没有分析 NER 错误对最终效果的影响。
- Recall@10 不如稀疏/稠密方法:离线实验中 GRAM 的 Recall@10 只有 2.85%,低于 BM25 的 3.01% 和 DPR 的 3.89%。虽然在高 K 值下 GRAM 反超,但在一些对首屏结果质量要求极高的场景(如移动端搜索),Top-10 的召回质量可能比 Top-300 更重要。
- 新商品冷启动:虽然论文提到近线系统会为新商品生成 Code,但如果新商品的属性在训练数据中从未出现过(如全新品类),NER 模型和 Code 生成器可能都表现不佳。论文没有给出新商品的专项实验。
- 缺乏与最新稠密检索方法的对比:Baseline 中的 DPR 是 2020 年的方法,未与更强的稠密检索方法如 SimLM、ColBERT v2 等对比。
对我们的启发
① 属性化 Code 的思路可以借鉴:在我们的推荐系统中,商品也有丰富的属性(品牌、品类、价格段、适用人群等),可以尝试用类似的「结构化属性 Code」替代或补充现有的 Semantic ID 方案;
② 双对齐范式通用性强:Code Co-Alignment + Query-Product Alignment 的两阶段对齐思路不限于电商搜索,任何需要「用户表达 → 商品匹配」的场景(如推荐系统的召回阶段)都可以复用;
③ 召回+粗排一体化的系统简化思路:不仅减少信息损失,也降低了多模型联调的工程复杂度——可以评估在我们业务中类似思路的可行性。
一句话启发:GRAD 的核心洞见是——与其让模型从头学一套 ID,不如让 LLM 用已有的「人类语言」(结构化属性)做检索的中间表征。这种「借力 LLM 世界知识」的思路,可能是生成式检索在工业界落地的最短路径。