Large Language Model as Universal Retriever in Industrial-Scale Recommender System

Junguang Jiang, Yanwen Huang, Liu Bin, Xiaoyu Kong, Xinhang Li · Alibaba · CIKM 2026 · 发表 2026-11-03
16 分钟阅读时长 6109字 4.50综合分 第 3 名11-04 Generative 日报
目录 · 11 节1它到底在解决什么问题?2URM 的整体流程3为什么要 128 个读出位?4千万商品的大表怎么学?5怎么在千万商品里只算一小部分?6实验:一个模型能顶几路专用召回?7换一句目标描述,结果会怎么变?8线上:异步跑、存起来再用9我认为它的局限10一个推荐团队可以怎么试11结论
相关 5新颖 4实用 4证据 5时效 5
URM 的线上服务:用户的点击、购买等行为异步触发一次 URM 推理(特征处理、查表取商品向量、vLLM 推理、商品采样),结果写入远端存储;在线请求走召回、粗排、精排、重排时直接读取这份结果
URM 的线上服务:用户的点击、购买等行为异步触发一次 URM 推理(特征处理、查表取商品向量、vLLM 推理、商品采样),结果写入远端存储;在线请求走召回、粗排、精排、重排时直接读取这份结果
按 query 出现频次分组的 RQ R@1000:蓝线是训练中见过的 query,红线是没见过的 query,同一频次下两条线基本重合
按 query 出现频次分组的 RQ R@1000:蓝线是训练中见过的 query,红线是没见过的 query,同一频次下两条线基本重合

做推荐召回、手里维护着好几路专用召回模型的团队值得读。阿里淘天这篇 CIKM 论文让一个大模型同时承担点击召回、分场景召回、长尾召回、兴趣探索、带搜索词的召回等多种目标,总体效果还好过工业界常用的多任务召回模型。核心思想一句话:用户历史和这次召回的目标都写成一段输入,大模型一次前向输出几个用户向量,和数千万候选商品各自的向量算内积,取得分高的一组商品;换目标只需要换那句目标描述。论文把它叫 URM(Universal Retrieval Model)。

在工业数据的 9 种召回目标上,URM 的平均 R@1000 是 0.403,最强的多任务基线是 0.363,平均相对提升 11.0%,9 项里 6 项第一。为了让大模型在千万级候选上算得动,作者每次只给几万个商品打分,商品打分这一步的计算量从约 5000 GFLOPs 降到约 2 GFLOPs。在广告平台上线 A/B 中,核心指标广告收入提升 3.01%。真正有用的是一个模型听指令切换召回目标:多路召回不必各训各的,和已学目标相近的新目标改一句描述就能试。作者也承认,和训练目标差得很远的新目标仍要补训练数据。

它到底在解决什么问题?

线上推荐一般是多路召回:一路按点击行为召回,一路按购买召回,一路专找长尾商品,一路给用户推没逛过的新类目,每个场景(首页、详情页等)可能还有自己的一路。每一路都有自己的训练数据和模型结构,单独训练、评估、部署。目标一变,就得再收一份数据、再训一路模型。新目标的样本还常常不够。

设想一位用户(示意),最近收藏了一双跑鞋、买了运动袜、点了几件速干衣。不同目标希望召回的东西不一样:

目标(示意)希望召回什么传统做法
点击预测召回用户接下来最可能点的跑步装备一路双塔或序列模型
兴趣探索用户没逛过的新类目,比如骑行装备另一路模型,另一份样本
长尾召回销量小但相关的小店跑鞋再一路模型
带搜索词召回搜“防水”时的防水跑鞋三塔模型,单独加一个 query 塔

URM 的回答是:这四行用同一个模型,区别只在输入末尾那句目标描述,比如“请召回用户会点击的商品”“请召回新类目的商品”。

URM 的整体流程

步骤做什么产出
1. 写输入把用户属性、统计特征、各种行为序列写成一段话,历史商品用商品 ID 直接嵌在句子里;后面接一句目标描述,再接 M 个读出位一条最长 1024 个位置的序列
2. 大模型前向Qwen-7B 全参数微调,只跑一次前向M 个读出位各给出一个用户向量
3. 打分每个用户向量和商品向量做内积,一个商品取 M 个内积里最大的那个当分数商品分数
4. 采样不给全部千万商品打分,只在几轮采样圈出的几万个商品里算最终 1000 个商品

输入里的商品不是文字,而是特殊 token。商品 ID 有几十亿个,放不进大模型的词表,所以作者另建了一张分布式哈希表,按 ID 查出商品向量,再经过一层 MLP 变成和文字 token 一样长的向量。这张输入端的表要覆盖行为序列里十亿级的商品,和后面打分用的千万级候选商品表不是同一张。用户某类行为为空时,这句话直接省掉,序列只截断不补零。这比固定长度的特征输入更省算力,也更不怕特征缺失。

关键分工是:大模型负责“理解这个用户和这次的目标”,一张分解过的商品大表负责“把理解变成千万商品里的分数”,采样负责“只算可能有用的那一小部分”。下面拆开讲。

为什么要 128 个读出位?

生成式召回最直接的做法,是让大模型在最后一个位置输出一个用户向量,再和所有商品比。问题在于一个向量很难同时代表一个用户的几类兴趣:跑鞋和骑行装备在商品空间里可能离得很远,一个向量只能落在中间。

URM 在输入末尾加 M 个固定的占位 token(论文叫 query token),本文叫它“读出位”。每个读出位有自己可学的 embedding,大模型在每个读出位输出一个用户向量,一次前向就得到 M 个。因为是单向注意力,后面的读出位能看到前面读出位的结果,所以不同读出位会学到不同侧面的兴趣。一个商品的分数取它和 M 个用户向量内积中的最大值。作者说试过把这些内积加权求和,取最大值效果更好。

小例子(示意):两个读出位,一个偏跑步,一个偏骑行。跑鞋和这两个用户向量的内积是 0.9 和 0.1,骑行头盔是 0.2 和 0.8。取最大值后两者分别是 0.9 和 0.8,都能进前排。如果只有一个折中的用户向量,两者可能都只有 0.5 左右,被更“平均”的商品挤掉。

附录的对照说明好处不只来自维度更高。在点击预测召回(CPR)上,32 个读出位、每个 128 维时 R@1000 是 0.248。只用 1 个读出位输出 4096 维、再切成 32 段 128 维向量,总维度一样,却只有 0.163;把商品向量也加到 4096 维,也只有 0.203。正文默认用 128 个读出位,读出位越多效果越好。

千万商品的大表怎么学?

最后一步“用户向量 × 商品表”里,商品表 W 每个商品一列,候选有上千万,直接学参数太多、也太难学。以往的生成式召回用 Semantic ID 把商品编成几层短码来回避这张大表,但那样要多次调用大模型逐层生成。一个 1.5B 的模型在 1024 长度上跑一次就要约 50 毫秒,这已经是作者线上召回的延迟上限,所以 URM 坚持只跑一次前向,改为把大表拆小。

做法是 W = U·Vᵀ。U 是一个小的投影矩阵,把 4096 维的大模型输出压到 128 维;V 是每个商品一个 128 维的向量。商品向量又由两部分相加: Hoot 说

Hoot 说老商品和新商品,商品向量怎么来收起 ✕
老商品两部分相加,新商品只靠文本这一路:两类商品最后都和同一组用户向量算内积,差别只在商品向量由哪几部分组成。

训练里见过的商品

区分部分 V_dis
每个商品一行可学参数,用双塔 ID embedding 初始化
迁移部分 V_trans
商品文本经文本 embedding 大模型编码,再经可学线性层压到 128 维
商品向量
两部分相加

只用区分部分时全部商品 R@1000 是 0.256,相加后是 0.263。

训练里没见过的商品

区分部分 V_dis
不用:论文写明这类商品按 W = U·V_transᵀ 打分
迁移部分 V_trans
同一条文本路径照样能算出来
商品向量
只靠迁移部分

表 3:加上迁移部分,没见过商品上的 R@1000 从只用区分部分的 0.116 提到 0.130。

这篇论文的贡献是让一个大模型听目标描述做召回;这张表拆开的是它为了只跑一次前向而改写的商品表 W = U·Vᵀ。数字是论文 CPR 目标上的消融 R@1000。 出处: 论文 §3.2 商品表分解与表 3

  • 区分部分(V_dis):每个商品一行独立的可学参数,用双塔模型的 ID embedding 初始化。训练样本多的商品靠它把相似商品分开。
  • 迁移部分(V_trans):把商品的标题、类目、价格、店铺、近 7 天销量和点击率写成一段话,交给一个文本 embedding 大模型编码,再经过一层可学的线性层压到 128 维。训练里没见过的新商品只有这一部分,靠它也能被召回。

消融结果说明两部分各管一头:在 CPR 上,只用区分部分全部商品的 R@1000 是 0.256,没见过的商品是 0.116;只用迁移部分分别是 0.152 和 0.101;两者相加是 0.263 和 0.130。也就是说,区分部分撑起整体效果,迁移部分补上新商品,相加后两项都最好。

训练时也不在全部商品上算 softmax,而是让正样本只和抽样出来的负样本比:每个 batch 按出现频次的 3/4 次方采 10000 个负样本,每个商品的分数同样取 M 个读出位里的最大内积。

怎么在千万商品里只算一小部分?

推理时如果每个用户都和 1000 万个商品算内积,计算量太大。最简单的办法是随机抽一部分商品来算,但用户真正想要的商品很可能根本没被抽到。URM 的做法有点像顺藤摸瓜:先在商品表上建好近邻索引,然后反复执行“打分、按概率抽、扩到近邻”。

算法从一批随机挑出的候选商品出发,之后每一轮做三件事:

  1. 用同一组用户向量给当前这批商品算概率(温度 0.07)。
  2. 按概率抽 1000 个商品(不是直接取前 1000,这样排在第 1000 和 1001 名的商品机会更公平)。
  3. 把这 1000 个商品各自最多 32 个近邻并进来,成为下一轮要打分的这批商品。

这样共做 4 轮,返回最后一轮抽到的 1000 个。

这样做成立的前提是:在商品表里挨得近的商品,对同一用户的得分也相近。每一轮都复用同一组用户向量,不会多跑大模型。

手算一下规模:从第二轮起,每轮最多 1000 ×(32 + 1)= 33000 个商品要打分,而全量是约 1000 万个,也就是说每轮只碰约 0.3% 的商品。

论文把同样的节省换算成了计算量。按它的参数(128 个读出位、4 轮),投影加采样打分约为 128 × 128 ×(4096 + 4 × 1000 × 32)≈ 2.2 × 10⁹ 次运算,也就是约 2 GFLOPs。全量打分约为 128 × 4096 × 10⁷ ≈ 5.2 × 10¹²,约 5000 GFLOPs,所以打分这一步和大模型本身比起来变得很小。

采样轮数的实验说明 4 轮基本够了。作者离线用“采样后的 R@1000 ÷ 全量打分的 R@1000”衡量采样找回了多少:1 轮只有 0.2%,2 轮 2.1%,3 轮 41.7%,4 轮 91.0%,5 轮 91.1%。我的理解是起点随机,前两轮还在往用户的兴趣区域靠,所以几乎找不回东西。

实验:一个模型能顶几路专用召回?

工业数据集来自线上日志,有上亿条样本,候选商品数千万。每个用户一天的行为合在一起当目标集合,用前几天训练、后一天测试。基线是工业界常用的双塔、Transformer 序列模型和 Attention-DNN,每个都有只训一个目标(单任务)和同时训多个目标(多任务)两种版本,表现最好的 Attention-DNN 还加了 Shared Bottom、MMoE、PLE 三种多任务结构。下表摘录 R@1000(场景 B 和购买预测两列略去,URM 在这两项也是第一):

模型CPR场景 A场景 C兴趣探索 SR长期兴趣 LR长尾 LIR搜索词 RQ9 项平均
双塔 单任务0.1290.2710.1290.0690.0660.1170.3550.161
Attention-DNN 单任务0.2530.4770.2600.1060.2130.2510.6510.323
Attention-DNN 多任务0.2380.4560.2770.0620.3360.2650.6710.351
Attention-DNN Shared Bottom0.2430.4420.2700.0720.3370.2240.7450.357
Attention-DNN PLE0.2560.4510.2740.0620.3270.2240.7610.363
URM0.2630.5300.3620.0930.2850.2400.8350.403

这张表说明两件事。第一,传统多任务有跷跷板:Attention-DNN 从单任务换成多任务后,长期兴趣从 0.213 涨到 0.336,兴趣探索却从 0.106 掉到 0.062。第二,URM 没有全面通吃:它在点击、购买、三个场景和带搜索词这 6 项第一,兴趣探索、长期兴趣、长尾三项仍输给最好的基线。带搜索词的召回尤其值得注意:专门加了 query 塔的三塔模型是 0.822,URM 不加任何专门结构做到 0.835,两者相当。

在 4 个公开数据集(Amazon 的 Sports、Beauty、Toys 和 Yelp)上,URM 的 HR@5 和 NDCG@5 平均比最强基线(包括 TIGER 等 Semantic ID 方法)高 46% 和 29%。这些数据集规模小,主要说明方法本身可用,工业表更能说明问题。

换一句目标描述,结果会怎么变?

这部分最能体现“通用召回”的含义:同一个模型,只换输入里那句目标描述。

  • 分场景:同一批用户,用“场景 A”的描述召回,场景 A 的 R@1000 是 0.530;用通用点击召回的描述只有 0.440。三个场景的相对提升在 20.5% 到 31.0% 之间。这说明模型确实在读那句话,并按场景调整了输出。
  • 兴趣探索:换成“请召回新类目的商品”,结果里新类目商品的占比从 18.8% 升到 46.2%,兴趣探索的 R@1000 从 0.051 升到 0.093。代价是点击召回从 0.263 降到 0.213,说明目标之间确实在取舍,而不是白拿。
  • 没训过的组合:训练里只有“带搜索词”和“长尾”两种单独的目标,测试时把两句描述拼在一起。带搜索词召回的 R@1000 是 0.836,和只用带搜索词目标时的 0.835 持平;结果里长尾商品的占比则从 79.6% 升到 82.4%。也就是说,加上长尾要求没有牺牲和搜索词的匹配。

对训练中没见过的搜索词,作者把 query 按自身有多常见(出现频次,图的横轴取 log2)分组画图(上面第二张图)。“见过”指训练数据里出现过这个 query,和它常不常见是两回事:一个不常见的 query 可能恰好没进训练数据。同一频次下,见过和没见过的 query 召回率基本一样。没见过的 query 多半是长尾词,分布也和常见词不同,按频次对齐后再比才公平。

线上:异步跑、存起来再用

7B 模型在 1024 长度上的 P99 延迟是 233 毫秒,不可能放进在线请求里同步跑。作者的办法是把 URM 挪出请求链路(上面第一张图):用户的点击、购买等行为异步触发一次 URM 推理,10 分钟内的行为合并成一次触发;推理服务查表取商品向量、用 vLLM 做一次 prefill、再跑采样,结果写进远端存储。在线请求来了,召回阶段直接读这份结果。作者说这样做的算力开销能压到和普通模型召回相当。

2025 年 4 月 28 日到 5 月 14 日的线上 A/B 中,广告收入提升 3.01%,CTR 提升 0.78%,CVR 提升 1.24%,长尾商品数量提升 2.23%。作者还提到,因为目标可以用文字直接改,线上可以直接 A/B 不同的目标描述和特征组合来调效果,不用每次重训模型。

我认为它的局限

第一,结果不是实时的。召回结果在用户行为后异步算好再存起来,10 分钟内的行为合并成一次触发,所以在线读到的结果可能落后用户最新的行为几分钟。摘要里说的“延迟控制在几十毫秒”,对默认的 7B 模型并不成立:附录表里它在 1024 长度上的 P99 是 233 毫秒。能上线靠的是异步设计把推理挪出了请求链路,作者也明说这是用时效换算力。

第二,训练成本高。在 64 张 H20 上,Qwen-7B 训 5000 万条样本要 25 小时。为了让每天的数据在 24 小时内训完,作者把训练数据抽样到 5%,只训一轮。对数据量大的团队,这意味着大模型召回每天只能学到一小部分日志。

第三,并非每个目标都更好。9 项里有 3 项输给专用基线,长期兴趣差得最多(0.285 对 0.337)。用指令把兴趣探索调上去,点击召回也会掉约 19%。“一个模型替代多路召回”在汇总指标上成立,但具体某一路换掉之前,要先看那一路自己的指标。

一个推荐团队可以怎么试

  1. 先选两三个已有专用召回的目标,用一个较小的大模型(比如 1.5B)做 URM 式的统一召回,在离线 R@K 上逐路对比,判断统一模型是否会伤到某一路。
  2. 读出位数量从 8、32 逐步加到 128,看召回率是否持续上升;同时比较“区分部分 + 迁移部分”和只用 ID embedding 在新商品上的差距。
  3. 采样轮数从 2 到 5 扫一遍,用“采样后召回 ÷ 全量召回”确认几轮够用,再定线上成本。
  4. 线上先用异步触发、结果落存储的方式接入一路召回,A/B 不同的目标描述,观察收入、CTR 和长尾占比是否同向。

结论

最关键的启发是:大模型做召回的价值不只在效果,而在于把“召回目标”从模型结构里拿出来,变成一句可以随时改的输入。

维度评价
问题重要性高。多路召回的维护和新目标冷启动是推荐系统的常见负担
方法创新性中高。读出位、商品表分解、近邻采样都不算全新,但组合起来解决了大模型在千万候选上一次前向出结果的问题
实验证据较强。有 9 个目标的工业离线对比、各部件消融和线上 A/B,但 9 项里有 3 项输给基线
工程成本高。7B 模型全参数微调、分布式哈希表、vLLM 推理服务和异步结果存储都要新建
是否值得尝试值得。已有多路召回、目标经常变化的团队,可以先在离线上用小模型验证统一召回不伤主路

读完这篇,你觉得

点一下即可,无需登录;再点一次撤回。只统计匿名次数,不记录你是谁。

讨论

欢迎补充实验细节、复现经验或不同看法。需 GitHub 登录。

正在加载讨论…