想象你是一位老班长,要在国庆节办一场 20 周年同学聚会。全班 50 人,微信群里常年活跃的只有十来位,剩下的老同学散落在天南海北,换了号码、换了城市。你会怎么办?你肯定不会只盯着通讯录挨个发微信——而是同时用好几招:在班级群里吼一嗓子“聚会啦,看到的帮忙转发”(热门召回)、问当年玩得好的死党有没有其他人的联系方式(协同过滤)、对着泛黄的毕业照努力辨认“这个像不像咱班谁”(向量召回)、顺着 A 认识 B、B 认识 C 的朋友链一路打听(图召回)。最后,你把所有渠道找回来的名单合并在一起,划掉重复的人名,给每个渠道分配不同的精力,等聚会结束再复盘哪个招最管用。这套“广撒网、多渠道找人”的打法,正是工业界推荐系统里大名鼎鼎的多路召回。 推荐系统的本质,就是从数以亿计的商品里,为每个用户挑出他最可能喜欢的那几个。它就像一场漏斗式的筛选:先广撒网捞出候选(召回),再精挑细选排出名次(精排)。而多路召回,就是这张网的第一层——用多种策略并行打捞,保证谁的“漏网之鱼”都不落下。这篇文章,我会带着你从零搭建这套“找人系统”,看懂每一路召回的原理、如何融合去重、怎么分配配额,以及如何评估它到底好不好用。
多路召回是工业推荐系统的地基,掌握它,你就掌握了从千万级物品到百级候选的第一道闸门。这是推荐、搜索召回工程师的核心竞争力,也是面试中最高频的架构题。
可视化演示
动态演示多路召回的核心机制,通过开始/执行/监控/完成四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
多路召回:推荐召回 推荐召回层定位与核心挑战 协同过滤与内容召回策略 核心机制可视化 多路召回整体架构与关键组件
召回层概述
老班长筹备聚会的第一个决定,不是马上打电话,而是先想清楚:我到底要用哪几种方式找人? 只发微信,那些从不看群的同学就永远失联;只靠死党转告,覆盖范围又太窄。所以你需要先画一张“找人地图”——把通讯录、朋友圈、朋友链、老照片这些渠道都列出来,每个渠道派一组人去执行。在推荐系统里,这一步叫做召回层设计:明确用哪几路策略、每路负责打捞什么样的候选。
推荐系统的完整流程是一个漏斗:全量物品可能有上千万,但用户一屏只看得完十几条。系统不可能对每个物品都做精细打分,所以要分阶段缩量。召回层就是这个漏斗的最上端,它的任务是在几十毫秒内,从上千万物品中捞出几百上千个“有可能喜欢”的候选,交给下游的粗排、精排去细算。这个过程叫候选生成,它在整个架构中处于最前哨的位置,直接决定了推荐效果的上限——召回层漏掉的物品,后面再精细也救不回来。
为什么必须“多路”而不是“一路”?因为每一种召回策略都有自己的盲区。协同过滤能找到“和你口味相似的人喜欢的物品”,但对冷门新物品束手无策;向量召回能捕捉语义相似,却可能忽略强共现关系。单一策略就像只用微信找人,覆盖率必然有限。多路并行、各展所长,才能保证候选的多样性和覆盖率。把 K 路召回得到的候选集合合并起来,候选池的总规模就是各路候选数的累加:
回头再看老班长的“找人地图”,你就会明白:地图上每条渠道都是一路召回,渠道越多,找到的人越全;但渠道太多,名单也会越来越长,必须有人负责合并、去重和筛选。这就是召回层在推荐系统中扮演的角色——用最少的算力,网住最多的可能。
- 候选集合符号$\mathcal{C}$ 表示融合后进入下一阶段的候选物品集合,两侧的竖线表示“集合的大小”,也就是候选总数。
- 多路求和$\sum_{k=1}^{K}$ 表示从第 1 路加到第 K 路,把每一路召回通道产出的候选数量逐一累加。
- 单路候选数$|\mathcal{C}_k|$ 表示第 k 路召回通道产出的候选物品个数,比如协同过滤路找回了 300 个,向量路找回了 500 个。
协同过滤召回
老班长最信任的方法,是给当年玩得最好的几个死党打电话:“你还和谁有联系?”死党 A 说他有张三的微信,死党 B 说李四上周还和他吃过饭。这些信息都是从“和你关系近的人”那里打听来的——这就是协同过滤的核心思想:人以群分,物以类聚。和你相似的人喜欢的东西,你也大概率喜欢。
协同过滤分成两派。用户协同(User-based CF)的思路是:找到和你行为相似的一群“邻居用户”,把他们喜欢而你没见过的物品推荐给你——好比让死党们帮你回忆“咱们共同的朋友还有谁”。物品协同(Item-based CF)的思路是:如果你喜欢物品 A,那就找和 A 一起被很多人喜欢的物品 B 推荐给你——好比你说“我喜欢吃火锅”,死党立刻说“那家烧烤你肯定也爱,上次我们几个吃完火锅就去撸串了”。工业界因为用户数量远大于物品数量,物品协同更常用,它依赖一张共现矩阵:统计每对物品被多少用户共同消费过。
共现次数高不等于真的相似——爆款商品几乎和所有商品都“共现”过。所以计算物品相似度时要做热度惩罚,常用的公式是余弦相似度,把两个物品被用户评分的向量拉出来算夹角:
老班长打了一圈电话,名单上多了十几个名字。这些名字不是从通讯录里翻出来的,而是从“关系近的人”嘴里问出来的。协同过滤也是一样,它不关心物品本身长什么样,只看用户的集体行为——用群众的眼光替你发现宝藏。它的短板也很明显:一个全新的物品没人消费过,没有任何共现数据,协同过滤对它就是“查无此人”,这时候就得换一路召回了。
- 分子共现加和$\sum_{u \in U} r_{ui} \times r_{uj}$ 对所有同时和物品 i、j 发生过交互的用户,把两个交互值相乘再求和。共同交互的用户越多,分子越大。
- 分母长度归一$\sqrt{\sum_{u \in U} r_{ui}^2}$ 和 $\sqrt{\sum_{u \in U} r_{uj}^2}$ 分别是两个物品交互向量的长度,做归一化,避免热门物品因交互多而虚高。
- 相似度取值$\text{sim}(i, j)$ 结果落在 -1 到 1 之间,越接近 1,说明两个物品的“粉丝画像”越一致。
向量召回
光靠死党打听还不够。老班长翻出毕业照,盯着照片里一张张青涩的脸努力辨认:“这个戴眼镜的,是不是咱班那个学霸?”这种“看长相找相似”的思路,在推荐系统里就是向量召回:把用户和物品都映射到一个向量空间里,用向量的距离来衡量“像不像”。
怎么把一个人变成一串数字?这就是 Embedding(嵌入) 的功劳。我们可以用一个深度学习模型,把用户的历史行为、画像特征压缩成一个几百维的向量,比如用户向量 $\mathbf{u} = [0.2, 0.8, 0.1, \dots]$;同样把物品的属性、描述也压成物品向量 $\mathbf{p}$。双塔模型就是最经典的实现:左边一座塔编码用户,右边一座塔编码物品,两座塔互不干扰,各自产出向量,然后在顶层用相似度把它们拉近——用户喜欢的物品,向量就会“贴”在一起。
有了向量,怎么快速找出和用户向量最接近的物品?如果对全量物品逐个算相似度,耗时太长。工业界普遍用 ANN(近似最近邻)检索,比如 Facebook 开源的 FAISS、商用的 HNSW 索引。它们像图书管理员给书按索书号排架:不需要一本本翻,顺着索引路径就能快速定位到“相邻书架上”的候选。向量之间最常用的亲密度指标是余弦相似度:
老班长对着毕业照认出了几个老同学,靠的是“长得像、气质像”这种模糊的直觉。向量召回也一样,它不需要精确规则,只要语义上“神似”就能捞出来——哪怕这个物品从来没和用户有过交集,只要能投其所好,向量就会把缘分牵起来。向量召回尤其擅长解决冷启动和语义泛化问题:新的短视频没被看过,但只要它的画面语义和用户偏好契合,照样能被捞进候选池。
- 向量点积$\mathbf{q} \cdot \mathbf{p}$ 把用户向量和物品向量的每一维相乘后求和,衡量两个向量在同一方向上的共鸣程度。
- 向量模长$\|\mathbf{q}\|$ 和 $\|\mathbf{p}\|$ 分别是两个向量的长度。点积除以长度可以消除向量尺度的影响,防止某个向量“天生就长”导致分数虚高。
- 余弦相似度$\cos(\mathbf{q}, \mathbf{p})$ 最终取值在 -1 到 1 之间,越接近 1 表示两个向量方向越一致,也就是用户越可能喜欢这个物品。
图召回
电话打了一圈,老班长发现一个规律:张三联系上了李四,李四认识王五,王五又跟赵六住同一个小区……人际关系就像一张巨大的网,顺着一条线能摸出一串人。这就是图召回的直觉:把用户和物品都看成节点,把他们的行为关系看成边,在这张图上沿着关系链扩散,把多跳之外的潜在兴趣也捞回来。
第一步是行为构图。把“用户 u 点击过物品 i”变成一条边,把“用户 u 和用户 v 是好友”也变成一条边,一张融合了用户与物品的异构图就建好了。图上藏着协同过滤看不到的“间接关系”:你和某个人虽然不认识,但你们都连着同一个物品节点,中间只隔一跳;顺着边再走两步,还能发现更隐蔽的关联。要让机器学会利用这些关系,常用的方法是随机游走:从一个节点出发,按边的权重随机走向邻居,走出一串节点序列——这就像老班长从张三出发,随机问“你认识谁”,再顺着新朋友继续问。游走出来的序列喂给 Node2Vec 这类模型,就能把每个节点学成一个向量嵌入,节点的“社交地位”和“连接模式”都被编码进去。
随机游走每一步怎么选方向?答案是按边的权重算概率。边上累积的共现次数越多,被走到的可能性越大:
老班长的“朋友链打听法”最大的价值,是找到了那些既不在通讯录里、也没人主动提起的“边缘同学”。图召回的多跳扩展能力正是如此:它不满足于“你直接喜欢过什么”,而是沿着行为图一路探索“你喜欢的东西,被谁喜欢过,那个人还喜欢什么”。这种长程的关联,是协同过滤和向量召回都容易遗漏的,所以图召回在多路召回里是不可或缺的一路。
- 边权重$w_{ij}$ 表示节点 i 到节点 j 这条边的权重,权重越大,说明两个节点关系越密切,越容易被随机游走选中。
- 归一化分母$\sum_{k \in N(i)} w_{ik}$ 把节点 i 的所有邻居 $N(i)$ 的边权全部相加,作为分母让概率分布合法。
- 转移概率$P(v_j \mid v_i)$ 表示从节点 i 一步走到节点 j 的概率,随机游走就按这个概率在图上“散步”采样。
融合去重
聚会前夜,老班长把所有渠道的名单摊在桌上:微信群接龙 12 人、死党提供 8 人、毕业照认出 5 人、朋友链打听出 10 人。仔细一核对,发现张三被三个渠道重复登记了,李四也在两个名单里。他拿出红笔,把重复的名字一个个划掉,最后得到一份干净的总名单。这个过程,就是召回系统的融合去重。
融合的第一步是去重。同一件商品可能同时被协同过滤、向量召回和图召回捞到,它在候选池里只能出现一次。去重策略很简单:以物品 ID 为键,多个候选只保留一条记录,同时标记它被哪几路命中——这个“命中次数”是很有价值的信号,被越多路同时看中的物品,通常越值得推荐。第二步是分数归一化。各路的打分尺度完全不同:热门召回打的是播放量(可能是几十万),向量召回打的是余弦值(0 到 1 之间),如果不做归一化直接相加,热门路会完全碾压其他路。就像混着人民币和日元算总账,必须先统一汇率。常见做法是 min-max 归一化或除以本路最大值。
归一化之后,才能谈权重融合。给每路分配一个可信度权重,做加权求和。权重怎么定?可以靠人工经验,也可以靠线上数据学习。融合得分用公式表示就是:
老班长划掉重复名字的那一刻,就是在做去重;他给“死党提供”的名单打了更高的信任分,就是在调权重。融合去重不是简单的“合并同类项”,而是一场有主次的统筹:先统一口径,再论功行赏,最后按总分排座次。经过这一步,候选池从各路候选的大杂烩,变成了一份有序、无重复的精炼名单。
- 归一化分数$\hat{s}_k(i)$ 是物品 i 在第 k 路的归一化得分——每路先除以自己的最大值,让各路的分数都落在同一量纲。
- 融合权重$w_k$ 是第 k 路召回的权重,代表这一路的可信程度,所有权重之和通常约定为 1。
- 加权求和$s_{\text{fuse}}(i)$ 把各路归一化分数加权相加,得到物品 i 的最终召回分,最后按这个分数排序,取 Top N 进入精排。
配额分配
名单整理好了,但老班长遇到了新问题:聚会场地只能容纳 40 人,可候选名单有 60 个。给每个渠道最多几个名额?他想了想:死党提供的名单最可靠,给 20 个名额;微信群接龙水分大,只给 8 个;毕业照和朋友圈渠道各给 6 个。这就是配额分配——在候选池总预算固定的前提下,决定每路召回各占多少名额。
为什么要做配额?因为推荐系统的算力和时延是硬约束。精排阶段要对每个候选做复杂的深度模型打分,候选越多,耗时越长,机器成本越高。所以召回层的产出必须控制在预算以内,比如 500 个候选。候选预算 $B$ 就是整个候选池的上限。最简单的配额策略是按固定比例切分:第 k 路分到的名额由比例 $p_k$ 决定,但同时要照顾到该路实际产出的候选数——如果某路只找回来 3 个,给它 20 个名额就是浪费;反过来,如果某路特别能干,也要允许它“超额完成任务”的名额被截断。公式化表达就是:
老班长的配额不是一成不变的:听说死党那边又联系到几个老同学,他马上把微信群的名额挪了两个过去。推荐系统也是这样,配额需要动态调整——当某路召回的效果变好(命中率提升),就应该给它更多名额;当热点事件爆发,热门召回路要临时扩容。工业界常用线上反馈实时调整配额,让有限的算力流向最“能打”的召回路。配额分配的本质,就是在算力约束和召回效果之间找平衡:既不让任何一路饿死,也不让任何一路浪费粮食。
- 候选预算$B$ 是候选池的总预算,比如 500 个名额;$p_k$ 是分给第 k 路的比例,所有路的比例之和等于 1。
- 向下取整$\lfloor \cdot \rfloor$ 把预算乘比例的结果向下取整,保证名额是整数,不会出现“半个人”。
- 取较小值$\min(|\mathcal{C}_k|, \dots)$ 当该路实际候选不足配额时,全量放入不浪费;当该路候选充足时,按配额截断。
评估优化
聚会圆满结束,40 位老同学久别重逢,场面感人。但老班长没有就此收工——他拿出一张纸,复盘每个渠道的“战绩”:死党提供的 20 个名额里,来了 18 个;微信群接龙的 8 个名额里,只来了 3 个;毕业照认出的 5 人,来了 4 个。下回再办聚会,他肯定把微信群的配额砍掉,把死党的配额加大。这套“事后算账、按战绩调兵”的流程,就是召回系统的评估优化。
离线阶段,最常用的指标是召回率(Recall@K):在召回的前 K 个候选里,有多少是用户真正感兴趣的物品。它的分母是测试集中用户真实产生过行为的物品总数——相当于“所有本该到场的老同学”,分子是候选池里真正被用户点击的物品数——相当于“通过各渠道找到且真的来了的人”。漏掉的人越少,召回率越高:
离线指标好看,不代表线上一定好用。真正的试金石是在线 AB 实验:把用户流量随机分成对照组和实验组,一组用旧召回策略,一组用新召回策略,观察点击率、时长、转化率等业务指标。老班长如果只是自己觉得“死党名单靠谱”就下结论,那是拍脑袋;只有记录下每个渠道真实的“到场率”,才是用数据说话。迭代优化是一个永不停歇的循环:离线评估发现短板 → 新增或调整召回路 → 做 AB 实验验证 → 根据线上数据调整配额和权重 → 继续下一轮。多路召回架构从来不是一次搭好就完事,它像一张不断修补的渔网,每一轮复盘都在让网眼更密、更准。
- 命中数$|\text{Hits}@K|$ 表示在召回的前 K 个候选中,命中了多少个用户真正感兴趣的物品,分子越大越好。
- 相关物品总数$|\text{Relevant}|$ 是测试集里用户真实交互过的物品总数,即“本该被找回来的全部老同学”。
- 召回率$\text{Recall}@K$ 比值越高,说明候选池覆盖得越全,漏网之鱼越少;但召回率太高也可能牺牲精度,需要和精确率一起看。
AI 应用场景
电商平台的召回架构升级
阶段:召回层设计
解决的问题:某电商平台早期只有“热门商品 + 基于物品的协同过滤”两路召回。热门路被头部爆款垄断,长尾商品永远没有曝光机会;协同过滤路对用户没买过的新品类存在严重盲区,用户想“换个口味”时推荐结果全是老面孔。多路召回并行后,向量召回负责挖掘用户潜在品类偏好,图召回通过“买过 A 的人还看了 B”的关系链扩展新领域,候选覆盖率显著提升。
具体案例:一位用户平时只买数码配件,传统召回只会推手机壳、数据线。引入向量召回后,模型发现他的浏览向量和某个机械键盘的向量高度相似,于是键盘被捞进候选池并成功推荐。这正是“毕业照认出老同学”的电商版:不需要历史共现,只看语义契合。
短视频信息流的实时热点应对
阶段:在线服务与配额分配
解决的问题:短视频平台的推荐系统必须实时响应热点。当某个话题突然爆火,如果热门召回路的配额是固定的,新热点就挤不进候选池;如果向量召回路权重过高,又可能过度追热点,忽略用户长期兴趣。动态配额分配机制根据实时 CTR 反馈,自动给热门路临时扩容,让热点视频快速进入更多用户的候选池。
具体案例:某城市突发“晚霞刷屏”,大量用户拍摄晚霞视频上传。系统检测到这类视频的实时互动率飙升,将热门召回路的配额从 15% 临时提升到 30%,同时向量召回路根据“晚霞”“天空”等语义向量持续补充长尾相关视频,用户刷到的晚霞内容既不重复也不断流。
搜索引擎的多路语义召回
阶段:候选生成与融合
解决的问题:搜索不是只搜字面匹配。用户搜“怎么减肥”,传统倒排索引只能返回包含“减肥”字样的页面,无法覆盖“瘦身”“减脂”“热量控制”等语义相关的内容。多路召回让搜索引擎同时跑字面匹配、语义向量召回和点击关系图召回,再融合去重,保证结果的召回率。
具体案例:用户搜索“性价比高的手机”,向量召回路找出与“性价比”语义相近的“千元机推荐”“学生党手机”等内容,图召回路根据大量用户“搜了 A 又点了 B”的共现关系补充关联页面,最终候选池里既含字面匹配,又有语义扩展,搜索结果明显更丰富。
Python 演示
# -*- coding: utf-8 -*-"""多路召回演示:热门 + 协同过滤 + 向量 三路召回,归一化融合去重"""import mathfrom collections import defaultdict def cosine(a, b): dot = sum(x * y for x, y in zip(a, b)) na = math.sqrt(sum(x * x for x in a)) nb = math.sqrt(sum(y * y for y in b)) return dot / (na * nb + 1e-9) class RecallPipeline: def __init__(self, items, sim_item, user_hist, user_vec, k=20): self.items = items self.sim_item = sim_item self.user_hist = user_hist self.user_vec = user_vec self.k = k def hot(self): top = sorted(self.items, key=lambda it: -it['pop'])[:self.k] return {it['id']: it['pop'] for it in top} def cf(self, uid): score = defaultdict(float) for iid in self.user_hist.get(uid, []): for jid, w in self.sim_item.get(iid, []): score[jid] += w top = sorted(score, key=lambda x: -score[x])[:self.k] return {cid: score[cid] for cid in top} def vector(self, uid): vec = self.user_vec[uid] cands = [(it['id'], cosine(vec, it['vec'])) for it in self.items] cands.sort(key=lambda x: -x[1]) return {cid: s for cid, s in cands[:self.k]} def merge(self, routes, weights, quota, budget=40): norm = {} for name, cands in routes.items(): mx = max(cands.values()) if cands else 1.0 q = max(1, int(budget * quota[name])) norm[name] = (q, [(cid, s / mx) for cid, s in cands.items()]) fused = defaultdict(float) hit = defaultdict(set) for name, (q, nlist) in norm.items(): for cid, ns in nlist[:q]: fused[cid] += weights[name] * ns hit[cid].add(name) cands = sorted(fused, key=lambda x: -fused[x]) multi = sum(1 for v in hit.values() if len(v) > 1) return cands, multi if __name__ == '__main__': items = [{'id': i, 'pop': 100 - i, 'vec': [i * 0.1, round(math.sin(i), 3)]} for i in range(100)] sim = {i: [(j, 1.0 / (1 + abs(i - j))) for j in range(max(0, i - 2), min(100, i + 3)) if j != i] for i in range(100)} pipe = RecallPipeline(items, sim, {7: [3, 5, 8]}, {7: [0.6, 0.4]}) routes = {'hot': pipe.hot(), 'cf': pipe.cf(7), 'vec': pipe.vector(7)} cands, multi = pipe.merge(routes, {'hot': 0.2, 'cf': 0.4, 'vec': 0.4}, {'hot': 0.3, 'cf': 0.4, 'vec': 0.3}) print('候选池大小:', len(cands)) print('Top5 候选:', cands[:5]) print('同时被多路命中的物品数:', multi)
def merge(self, routes, weights, quota, budget=40):融合入口,收下各路候选的字典 {物品ID: 分数},并带上每路的权重和配额。mx = max(cands.values()) if cands else 1.0找本路最高分作为归一化分母,等价于“先统一汇率再算总账”。q = max(1, int(budget * quota[name]))按候选预算和配额比例,算出这一路能进候选池的名额上限。norm[name] = (q, [(cid, s / mx) for cid, s in cands.items()])把每路候选的分数都除以本路最大值,完成分数归一化。fused[cid] += weights[name] * ns加权融合得分:归一化分数乘以该路权重,累加到物品的总分上。hit[cid].add(name)记录这个物品被哪几路命中,用于统计“多路命中”的交叉信号。multi = sum(1 for v in hit.values() if len(v) > 1)数一数同时被多路召回的物品数,这是衡量各路互补性的直观指标。
常见误区
误区既然叫“广撒网”,那每路召回的候选数应该尽可能多,反正后面还有精排层把关,多召回几个总没坏处。
正解召回数量受算力和时延的硬约束。候选池越大,精排层要打分的样本越多,响应时间越长、机器成本越高。而且低质量的候选会稀释精排的注意力,拉低整体推荐质量。正确的做法是像老班长定 40 人会场一样:先定预算,再按配额精打细算,让每一路都“优中选优”地贡献候选。
为何常见:人容易直观地认为“覆盖越全越好”,而忽略了工业系统的资源瓶颈。召回层的设计本质是在覆盖率、精度和算力三者之间做权衡。
误区双塔向量召回这么强大,语义泛化能力又强,协同过滤这种“老古董”应该淘汰掉了。
正解两类召回捕捉的信号完全不同。协同过滤直接利用用户群体的共现行为,对强关联、高频场景极其精准,比如“买过尿不湿的人常买奶粉”;向量召回擅长语义泛化,但对行为稀疏的长尾物品效果不稳定。就像老班长既要靠死党打听(协同),也要靠毕业照辨认(向量),两条腿走路才稳。工业界的主流做法是让它们互补共存,而不是互相替代。
为何常见:因为向量召回是更“新潮”的技术,学习者容易产生“新技术替代旧技术”的线性思维,忽略了不同信号源在工程上往往是互补关系。
误区多路召回融合时,把每路给出的分数直接加起来排序,谁分高谁进候选池。
正解各路的分数尺度完全不同——热门召回可能是几十万的播放量,向量召回是 0 到 1 的余弦值。直接相加相当于“人民币加日元”,热门路会完全碾压其他路,融合结果退化成单路召回。必须先做归一化(除以本路最大值或 min-max),再乘以权重做加权求和。老班长不会把“死党提供的 20 个名字”和“微信接龙的 20 个名字”直接混在一起数人头,他会先区分来源可信度。
为何常见:初看“融合”二字,直觉上就是把数加起来。只有意识到各路分数不可比,才会理解归一化的必要性。
误区离线评测里新召回策略的 Recall@K 提升了 5 个百分点,那肯定可以全量上线,没必要再做实验。
正解离线指标和线上业务指标之间存在系统性偏差。离线数据是历史行为,无法反映用户面对新推荐时的即时反馈;线上还有时延、多样性、疲劳度等离线测不到的因素。正确做法是先做小流量 AB 实验,用真实点击率、转化率等业务指标验证,再逐步放量。就像老班长在聚会上记下每个渠道的“到场率”,而不是聚会前凭感觉断定哪招最灵。
为何常见:离线评测做起来简单、反馈快,容易让人产生“数据都证明了”的错觉;但推荐系统的终极裁判永远是线上真实用户的行为。
面试题
Q1. 推荐系统里“召回(Recall)阶段”的主要职责是?
- A.从全量物品中快速挑出候选集← 正确答案
- B.对候选物品进行精细打分排序
- C.为用户生成个性化标题文案
- D.统计物品的点击率做报表
答案:A。召回层处于推荐漏斗最前端,负责在几十毫秒内从千万级物品中捞出千级候选,为下游精排缩小范围。B 是精排层的职责,C 是文案生成,D 是数据统计,都与召回阶段的核心目标不符。
Q2. 协同过滤召回的核心假设是?
- A.相似的物品会被相似的人喜欢← 正确答案
- B.热度高的物品一定值得推荐
- C.新物品应该获得更多曝光量
- D.用户只喜欢看同类的内容
答案:A。协同过滤基于“人以群分、物以类聚”的假设,利用用户群体的历史交互行为推断偏好。B 是热门召回的逻辑,C 是冷启动策略,D 过度绝对化,忽略了用户兴趣的多样性。
Q3. 向量召回一般用什么技术加速海量物品的相似度检索?
- A.全表顺序扫描计算相似度
- B.ANN(近似最近邻)索引← 正确答案
- C.Redis 缓存所有物品分数
- D.把物品随机打散成小批
答案:B。向量召回面对千万级向量,全表扫描太慢,工业界用 FAISS、HNSW 等 ANN 索引做近似最近邻检索,在不显著损失精度的情况下大幅提速。A 不可行,C 缓存的是标量分数而非向量检索能力,D 随机打散不解决相似度搜索问题。
Q4. 对多路召回得分做加权融合前,必须先做哪一步?
- A.对每路分数做归一化← 正确答案
- B.删除得分最低的一路召回
- C.把分数全部乘以同一个系数
- D.把各路由高到低硬拼接
答案:A。各路的分数分布差异极大,热门路是数量级大的原始计数,向量路是 0 到 1 的相似度,必须先归一化到同一量纲才能加权,否则热门路会碾压其他路。B 会丢失信号,C 乘同一系数仍不改变量纲差异,D 硬拼接不是融合。
Q5. 图召回中“随机游走”的直接用途是?
- A.在图上采样节点序列用于学嵌入← 正确答案
- B.给每个节点计算精确的最短路径
- C.统计全网节点的入度出度总和
- D.把图的邻接矩阵转成稀疏存储
答案:A。随机游走从起点按边权概率游走,产生节点序列,这些序列作为训练样本喂给 Node2Vec 等模型学习节点向量。B 是图论最短路径问题,C 是度数统计,D 是存储优化,都不是随机游走的目的。
Q6. 推荐漏斗中,召回层和精排层的关系是?
- A.召回先缩量,精排再细排← 正确答案
- B.召回和精排互相替代
- C.精排先选,召回后补充
- D.两层处理完全相同的集合
答案:A。推荐漏斗是逐级缩量的流水线:召回层负责从全量快速缩到候选池,精排层在候选池上做精细打分排序。B 两层职责不同不可替代,C 顺序反了,D 两层处理的集合规模完全不同。
Q7. 多路召回中“配额分配”主要解决什么问题?
- A.控制每路召回进入候选池的预算← 正确答案
- B.决定采用哪种存储引擎建索引
- C.自动为用户生成历史行为标签
- D.替代精排阶段的所有排序逻辑
答案:A。配额分配在候选池总预算固定的前提下,决定每路召回各占多少名额,并支持动态调整,本质是算力与效果的权衡。B 是存储选型,C 是特征工程,D 越权了,配额分配不参与精排排序。
Q8. 下列哪些属于常见的主流召回策略?
- A.协同过滤召回← 正确答案
- B.向量召回← 正确答案
- C.图召回← 正确答案
- D.价格预测召回
答案:ABC。协同过滤、向量(双塔)、图召回(Node2Vec)都是工业界主流召回路,它们分别利用共现行为、语义向量和图关系。价格预测属于精排层的预估模型,不用于候选生成,D 错误。
Q9. 关于多路召回的融合去重,下列说法正确的有?
- A.需要去除多路召回的重复物品← 正确答案
- B.各路分数需要归一化再融合← 正确答案
- C.加权融合的权重需实验调优← 正确答案
- D.融合后候选池越大效果越好
答案:ABC。去重保证候选唯一,归一化让分数可比,权重通过实验调优找到最优组合,这三者都是融合去重的核心环节。D 错误,候选池受算力约束,不是越大越好。
Q10. 评估多路召回系统时,通常会使用哪些方法?
- A.离线计算 Recall@K 指标← 正确答案
- B.在线 AB 实验对比新旧策略← 正确答案
- C.分析各召回路的命中贡献占比← 正确答案
- D.仅凭工程师主观判断效果好
答案:ABC。离线 Recall@K 衡量候选覆盖,在线 AB 验证真实业务效果,分析各路的命中贡献用于指导配额调整,三者构成完整的评估闭环。D 主观判断违背数据驱动原则,明显错误。