想象你走进一家巨型商场:门口有导购员根据你的穿着气质推荐当季新款,这是“推荐系统”;你走到服务台说“我要找一款降噪耳机”,客服几分钟就帮你定位到货架,这是“搜索系统”;电梯口滚动播放的品牌广告,则是“广告系统”。这三者看起来天差地别,背后却共享同一套“导购大脑”——一套能回答“用户此刻最该看到什么”的机器学习系统。 面试官让你设计这样一套系统时,真正想看的不是你会不会调库,而是你有没有“商场主管”的全局观:知道先派人去仓库海选(召回),再让资深买手精挑(排序),最后让陈列师巧妙摆放(重排);整个过程还需要用户档案(特征)、业绩报表(指标)、应急预案(挑战处理)。本文就用“经营一家导购团队”这个比喻,把 ML 系统设计面试题拆解得明明白白。

岗位关联

掌握系统设计的结构化思维,是从“会调包”到“能架构”的分水岭。大厂算法岗面试的压轴题,考的不是模型代码,而是这套“导购大脑”的搭建逻辑。

目录
  1. 可视化演示
  2. 系统架构
  3. 召回策略
  4. 排序模型
  5. 重排策略
  6. 特征数据
  7. 评估指标
  8. 系统挑战
  9. 面试策略
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示推荐系统召回-排序-重排分层架构、搜索多路召回通道与广告CTR预估模型演进,理解ML系统设计面试的核心框架。

本节配有交互动效(进入视口后自动加载,约 3KB)。

推荐系统 召回/排序/重排分层 千万→千级候选 召回层从千万级候选快速筛选千级候选。

系统架构

想象你是一家大商场的运营总监,手下有一支金牌导购团队:前台负责接单登记,海选专员冲进仓库把所有可能相关的货都搬出来,资深买手根据顾客口味精挑细选,货架陈列师最后调整摆放位置。如果让买手直接从几百万件商品里大海捞针,顾客早就等得不耐烦走了——分工,才是效率的第一来源。

推荐系统、搜索引擎、广告系统的设计哲学正是如此。一次用户请求到达服务器后,系统不会直接扫描全量商品库,而是像导购团队一样分层处理,每一层只干一件事、只缩小一次范围。经典的流水线是:召回层把全量物品从百万级快速缩到几百,排序层精打细算缩到几十,重排层结合业务规则输出十几个。这个“漏斗”结构是所有系统设计的骨架。

在三层逻辑之上,还有工程上的横向分层。在线服务层负责实时响应请求,通常要求延迟在几十毫秒内;近线层处理特征拼接、缓存刷新等亚实时任务,比如把用户最新一次点击立即写进特征;离线层负责训练模型、构建索引、统计报表,是系统“知识”的来源。面试时你要先画出这三层,再讲清请求如何穿越它们。

回到商场:导购团队之所以高效,正是因为每个角色边界清晰、顺序固定。你在面试中把这个“漏斗 + 分层”讲清楚,面试官就知道你具备主管视角,而不是只会搬砖的店员。

request→召回Ccand→排序Rtop→重排response\text{request} \xrightarrow{\text{召回}} C_{\text{cand}} \xrightarrow{\text{排序}} R_{\text{top}} \xrightarrow{\text{重排}} \text{response}
这就是推荐系统的“交通路线图”:请求从入口进来,依次经过三个站点,每个站点负责削减候选规模,最终生成响应。
  1. 请求输入一次推荐由用户 ID 和上下文信息构成
    req=(u,c,t)\text{req} = (u, c, t)
  2. 召回筛选从全量物品集 $\mathcal{I}$ 中按粗粒度规则快速筛出候选
    Ccand={i∣i∈I, ϕ(u,i)>θ}C_{\text{cand}} = \{ i \mid i \in \mathcal{I}, \ \phi(u, i) > \theta \}
  3. 排序打分精排模型对候选逐个计算点击概率
    Rtop=topk i∈Ccand y^uiR_{\text{top}} = \text{topk}_{\, i \in C_{\text{cand}}} \ \hat{y}_{ui}
  4. 重排输出最终按策略调整顺序后返回
    response=Rerank(Rtop)\text{response} = \text{Rerank}\left(R_{\text{top}}\right)

召回策略

资深导购有个职业习惯:顾客一开口,他不会去翻整个仓库,而是先走几条“快捷通道”——比如“跟您品味相似的老顾客常买什么”、“这个品类里卖得最好的几款”、“最近朋友圈都在晒什么”。这些通道互不重合,各自快速拎出一小批货,合并起来就是候选清单。这套动作,就是召回。

召回层的要求是“快而全”:宁可多召回一些不够精准的,也不能漏掉真正相关的商品。业内最常用的是多路召回(Multiple Recall)——每一路用一种独立的策略或索引去筛候选,最终把各路结果合并取并集。常见路径有协同过滤(与你相似的用户喜欢什么)、向量召回(Embedding 相似度检索)、热度召回(大家最近都在买什么)、类目召回(你常看的品类新品)。

多路召回的关键在于“互补性”:每路召回应该覆盖不同的兴趣侧面,合并后才有增量价值。如果两路召回结果高度重叠,其中一路就是冗余的。工程上还要为每路设置候选上限,并为整体设置延迟预算,否则召回层会拖垮整个服务。面试官提到召回时,最想听你讲清“为什么是这几路,而不是哪一路更准”。

海选专员从不纠结“这件货到底合不合适”,那是买手的活儿。他只负责用最快的速度把“有可能”的货都堆到桌上——这正是面试官想听的边界感:召回阶段别追求精确,要追求覆盖和速度。

Ccand=⋃k=1KRecallk(u)C_{\text{cand}} = \bigcup_{k=1}^{K} \text{Recall}_k(u)
多路召回公式:用户 u 的候选集,等于 K 路召回结果的并集。各路负责不同的“口味偏好”,像一个导购团队里每个人负责打听一类情报。
  1. 单路召回第 k 路用特定策略选出若干候选
    Rk(u)={i1(k),i2(k),…,ink(k)}R_k(u) = \left\{ i_1^{(k)}, i_2^{(k)}, \ldots, i_{n_k}^{(k)} \right\}
  2. 取并集所有路的候选合并成一个集合
    Ccand=R1(u)∪R2(u)∪⋯∪RK(u)C_{\text{cand}} = R_1(u) \cup R_2(u) \cup \cdots \cup R_K(u)
  3. 去重截断合并后去重,并限制候选总量
    ∣Ccand∣≤N,去重后\left| C_{\text{cand}} \right| \le N, \quad \text{去重后}

排序模型

候选商品堆了一桌,资深买手开始精挑。他会翻开顾客档案:年龄、性别、历史购买记录、最近还在犹豫什么,再结合当下的季节和场合,给每件商品打一个“这顾客会喜欢”的分。这个打分过程,就是排序。

排序层是推荐系统的“心脏”,主流方案是 CTR(点击率)预估模型。模型输入用户、物品、上下文特征,输出 0 到 1 之间的点击概率,再按概率降序排列。从早期的逻辑回归(LR)到梯度提升树(GBDT),再到 DeepFM、DIN 等深度模型,核心始终是同一件事:更准确地估计“用户是否会对这个物品产生正反馈”。

LR 虽然简单,但表达能力有限,因此要手动构造交叉特征;深度学习能自动学习高阶特征交互,但对数据量和工程要求更高。面试高频考法是:给你一个 CTR 模型,问你怎么加特征、怎么处理样本偏差、怎么做在线学习。一个可以完整复述的公式就是带交叉项的 LR。

买手打分时如果只看单品不看组合,就会闹出“给刚买完空调的人再推荐空调”的笑话。交叉特征就是让买手学会“看一眼品类、再看一眼顾客最近购买时间,组合起来判断”。讲排序时主动提到交叉,面试官会认为你理解了模型能力的边界。

y^ui=σ(w0+∑j=1nwjxj+∑a<bwabxaxb)\hat{y}_{ui} = \sigma\left( w_0 + \sum_{j=1}^{n} w_j x_j + \sum_{a<b} w_{ab} x_a x_b \right)
这是带交叉特征的逻辑回归:前半部分是常规线性加权,后半部分是特征两两组合的交叉项,让简单模型也能表达“用户爱看科技类 + 商品是数码产品”这类组合信号。
  1. 线性加权把每个特征值乘以对应权重再求和
    z=w0+∑j=1nwjxjz = w_0 + \sum_{j=1}^{n} w_j x_j
  2. 特征交叉显式构造两两特征组合项
    zcross=∑a<bwabxaxbz_{\text{cross}} = \sum_{a<b} w_{ab} x_a x_b
  3. Sigmoid 映射把得分压缩成点击概率
    y^=σ(z+zcross)=11+e−(z+zcross)\hat{y} = \sigma(z + z_{\text{cross}}) = \frac{1}{1 + e^{-(z + z_{\text{cross}})}}

重排策略

买手选出最合适的十件商品,但陈列师并不照单全收。他会微调:如果发现五件都是同一品牌,就换掉两件换成别的牌子;如果某件商品是商场本月的重点推广位,就往前挪一挪。这些“柜面规则”,就是重排。

重排层处理的是“排序模型看不到的问题”。排序模型对每个商品独立打分,但用户看到的是一个序列——商品之间的相互影响、位置的曝光差异、品牌多样性、以及广告位等业务诉求,都需要重排层统筹。最经典的算法是 MMR(最大边际相关性),它在“相关性”和“多样性”之间找平衡,避免推荐列表清一色同品类。

MMR 的核心思想是贪心:每次从未选集合中挑一个“既相关、又与已选商品最不相似”的商品加入结果。参数 λ 控制相关性和多样性的权重。工程上重排层还常叠加业务规则,比如广告位插入、品牌保护、已购商品过滤,以及位置偏差校正——越靠前的位次曝光越多,点击统计必须校准。

陈列师每摆一件新货,都会看一眼前面已经摆了什么——如果全是蓝衬衫,下一件就该换成红围巾。这就是 MMR 的直觉:记得自己放过什么,才会让整个橱窗丰富起来。

j∗=arg⁡max⁡i∈C∖S[λ⋅rel(i)−(1−λ)⋅max⁡j∈Ssim(i,j)]j^{*} = \arg\max_{i \in C \setminus S} \left[ \lambda \cdot \text{rel}(i) - (1-\lambda) \cdot \max_{j \in S} \text{sim}(i, j) \right]
MMR 公式:每次从未入选集合里挑一个“相关性得分减去与已选集合最大相似度”最大的商品,放入已选集合。这是一个贪心的多样性控制。
  1. 候选与已选S 是已经选中的商品,C\S 是还没选的候选
    i∈C∖Si \in C \setminus S
  2. 相关性奖励商品本身与用户需求的相关分
    λ⋅rel(i)\lambda \cdot \text{rel}(i)
  3. 多样性惩罚它与已选商品中最近似者的相似度
    (1−λ)⋅max⁡j∈Ssim(i,j)(1-\lambda) \cdot \max_{j \in S} \text{sim}(i, j)
  4. 贪心挑选选分最高的商品加入结果并重复
    S←S∪{j∗}S \leftarrow S \cup \{ j^{*} \}

特征数据

导购员手里有一沓“顾客档案卡”:正面是顾客基本信息,背面是过去每一次购物的记录,右上角还写着今天的天气。推荐系统的特征,就是这张卡片——用数字化的方式记录“用户是谁、商品是什么、当下在什么场景”。

特征分为三大类:用户特征(年龄、性别、注册时长、历史点击序列)、物品特征(类目、价格、品牌、上新时间)、上下文特征(当前时间、设备、地理位置、是否周末)。这些特征经过编码(OneHot、Embedding)后拼接成一个大向量,喂给排序模型。特征的质量往往比模型结构更决定效果——业内常说“特征为王”。

特征工程的核心操作有:归一化(把价格等连续值缩放到同一量纲)、分桶(把年龄切成年龄段)、交叉组合(“女性 + 美妆类”比单独两个特征更有意义)。在深度模型里,Embedding 技术把稀疏的 ID 类特征映射为稠密向量,让模型学到语义相似性,这也是向量召回的基础。

档案卡信息越全,导购的推荐就越有依据;但卡片太厚也会让导购翻起来费劲。特征工程就是决定“哪些信息值得写进卡片、怎么写最省事”,这永远是面试中的加分话题。

x(u,i,c)=[xu⊕xi⊕xc⊕xcross]x_{(u,i,c)} = \left[ x_u \oplus x_i \oplus x_c \oplus x_{\text{cross}} \right]
特征拼接公式:最终特征向量由用户特征、物品特征、上下文特征以及交叉特征拼接而成,就像档案卡正反面和今天的天气备注被钉在一起。
  1. 用户特征来自画像与历史行为统计
    xu=[age,gender,hist7d]x_u = [\text{age}, \text{gender}, \text{hist}_{7d}]
  2. 物品特征来自商品库的类目与属性
    xi=[cat,price,brand]x_i = [\text{cat}, \text{price}, \text{brand}]
  3. 上下文特征当时的时间与场景
    xc=[hour,device,loc]x_c = [\text{hour}, \text{device}, \text{loc}]
  4. 拼接组合四段特征合并为最终向量
    x=concat(xu,xi,xc,xcross)x = \text{concat}\left(x_u, x_i, x_c, x_{\text{cross}}\right)

评估指标

商场月度复盘会上,运营总监盯着三张报表:话术培训得分(离线演习)、当日接待转化率(在线效果)、月度总销售额(业务大盘)。单看任何一张都会误判团队实力,推荐系统的评估同样是“三维指标体系”。

离线指标在历史数据上评估模型,最常用的是 AUC——它衡量“正样本得分高于负样本得分”的概率,对排序质量非常敏感。在线指标则看真实流量中的反馈:CTR(点击率)、CVR(转化率)、停留时长。最终还要落到业务指标:GMV、日活、留存。三层指标共同组成完整的评估闭环。

面试中常被追问“AUC 涨了线上会涨吗”?答案是“不一定”。离线数据存在位置偏差和选择偏差,离线表现好不等于线上效果好。所以所有模型改动都需要上线 A/B 实验,用统计显著性判断胜负。业内有一句口头禅:离线 AUC 决定“要不要做人”,A/B 实验决定“要不要上线”。

话术培训成绩再漂亮,也要看明天顾客是否买单;同理 AUC 再高,也要等 A/B 实验给出最终裁决。面试时把这“三层报表”逻辑讲全,面试官会觉得你既有算法能力又有业务嗅觉。

AUC=1n+n−∑(i,j)∈DI ⁣(y^i>y^j)\text{AUC} = \frac{1}{n^{+} n^{-}} \sum_{(i, j) \in D} \mathbb{I}\!\left( \hat{y}_i > \hat{y}_j \right)
AUC 公式:把所有正样本和负样本两两配对,计算“正样本预测分 > 负样本预测分”的比例。AUC=0.5 相当于瞎猜,AUC=1 是完美排序。
  1. 配对每条正样本 i 与每条负样本 j 组成一对
    D={(i,j)∣labeli=1, labelj=0}D = \{ (i, j) \mid \text{label}_i = 1, \ \text{label}_j = 0 \}
  2. 计数正样本分高于负样本分则计 1
    I ⁣(y^i>y^j)\mathbb{I}\!\left( \hat{y}_i > \hat{y}_j \right)
  3. 归一化除以总对数得到 0 到 1 的概率
    AUC=#{y^i>y^j}n+⋅n−\text{AUC} = \frac{\#\{ \hat{y}_i > \hat{y}_j \}}{n^{+} \cdot n^{-}}

系统挑战

再强的导购团队也有头疼时刻:来了个从没逛过商场的新顾客,档案卡一片空白(冷启动);老顾客三个月才来一次,记录稀疏得可怜(数据稀疏);网红爆款一晚上就卖断货,昨天的畅销榜今天失效(实时性)。这些问题不是边角料,而是系统设计的核心考点。

冷启动是推荐系统最经典的问题,分为用户冷启动、物品冷启动和场景冷启动。解决思路是“先普适、再个性化”:对新用户先用热门物品兜底,同时利用注册信息(性别、年龄、兴趣标签)做粗粒度推荐,等用户产生少量交互后立刻切换到个性化策略。这就像导购对新顾客先递上大众好评榜,再观察目光停留来调整话术。

针对冷启动的经典模型是“基于相似用户的加权平均”:用户 u 对物品 i 的预估分,等于用户自身平均分与相似邻居打分加权平均的混合。难点在于缺乏交互时如何评估相似度——这时要借助内容特征(如注册的行业、浏览的类目)来建立“伪相似”。

新顾客走进商场,老练的导购会先递上热门榜单,同时观察顾客的目光流连在哪类橱窗,再逐步调整话术——这就是冷启动的完整策略:先兜底、再试探、后个性化。面试官想听到的正是这种有层次的方案。

r^ui=α⋅rˉu+(1−α)⋅∑v∈N(u)sim(u,v)⋅rvi∑v∈N(u)∣sim(u,v)∣\hat{r}_{ui} = \alpha \cdot \bar{r}_u + (1-\alpha) \cdot \frac{\sum_{v \in \mathcal{N}(u)} \text{sim}(u, v) \cdot r_{vi}}{\sum_{v \in \mathcal{N}(u)} \left| \text{sim}(u, v) \right|}
冷启动混合预估公式:当用户行为极少时,多信任用户自身的先验均值;当相似邻居足够多时,多参考邻居的打分。α 就是这两者的信任分配旋钮。
  1. 用户均值用户对自己的历史行为取平均
    rˉu=1∣Iu∣∑i∈Iurui\bar{r}_u = \frac{1}{|I_u|} \sum_{i \in I_u} r_{ui}
  2. 邻居加权用最相似邻居的打分做加权估计
    r^uiCF=∑v∈N(u)sim(u,v)⋅rvi∑v∈N(u)∣sim(u,v)∣\hat{r}_{ui}^{CF} = \frac{\sum_{v \in \mathcal{N}(u)} \text{sim}(u,v) \cdot r_{vi}}{\sum_{v \in \mathcal{N}(u)} |\text{sim}(u,v)|}
  3. 动态混合α 控制先验与邻居的信任比例
    r^ui=α⋅rˉu+(1−α)⋅r^uiCF\hat{r}_{ui} = \alpha \cdot \bar{r}_u + (1-\alpha) \cdot \hat{r}_{ui}^{CF}

面试策略

最后,把镜头拉回面试间。面试官提出“请你设计一个推荐系统”,就像一位顾客走进商场只说了一句“我想买点东西”。新手导购会直接冲向货架;金牌导购会先确认:“送人还是自用?预算多少?什么场合?”——这就是面试的第一步:澄清需求。

系统设计面试的答题框架,业内常总结为四步:Scoping(明确规模、延迟、指标)→ Architecture(画出召回/排序/重排的分层架构)→ Deep Dive(深入某个模块谈模型选型与优化)→ Trade-off(对比方案优缺点并给出建议)。80% 的候选人输在第一步:不澄清就开始画图。

面试官高频追问点包括:候选集规模多大?QPS 多少?延迟预算多少?离线与在线的一致性如何保证?如何评估每层的增量贡献?回答时要给出量级估计和取舍逻辑,而不是背概念。例如“召回候选从 500 提到 1000,排序延迟增加 80ms,但覆盖提升 12%,可以接受”——这种有数字的权衡才显功力。

面试和导购一样,是一场“沟通设计”而不是“背诵产品手册”。你越是先问清楚顾客想要什么,再展示你的货架布局和选品逻辑,对方就越相信你能把系统真正搭起来。

Score=0.3⋅Clarity+0.4⋅Depth+0.2⋅Tradeoff+0.1⋅Summary\text{Score} = 0.3 \cdot \text{Clarity} + 0.4 \cdot \text{Depth} + 0.2 \cdot \text{Tradeoff} + 0.1 \cdot \text{Summary}
面试打分权重:澄清需求占 30%,深入内核占 40%,权衡取舍占 20%,总结复盘占 10%——这就是面试官心里的评分卡。
  1. 澄清需求问清用户规模、延迟、核心指标
    Clarity: DAU∼108, 延迟<100ms, 指标=CTR\text{Clarity}: \ \text{DAU} \sim 10^8, \ \text{延迟} < 100\text{ms}, \ \text{指标} = \text{CTR}
  2. 深入内核把模型选型和特征设计讲透
    Depth: 特征→模型→评估→迭代\text{Depth}: \ 特征 \rightarrow 模型 \rightarrow 评估 \rightarrow 迭代
  3. 权衡取舍给出多个方案并量化对比
    Tradeoff: 精度↑↔延迟↓\text{Tradeoff}: \ 精度 \uparrow \leftrightarrow 延迟 \downarrow
  4. 总结复盘用一分钟回顾整个设计
    Summary: 需求→架构→细节→权衡\text{Summary}: \ 需求 \rightarrow 架构 \rightarrow 细节 \rightarrow 权衡

AI 应用场景

电商首页猜你喜欢(完整链路)

阶段:召回 → 排序 → 重排 → A/B 实验验证

解决的问题:过去电商首页靠运营人工挑选商品,无法应对千万级商品量和亿万用户的个性化需求。AI 系统通过三段式流水线,让每个用户看到不同的商品流,同时把决策耗时从人工小时级压缩到请求毫秒级。

具体案例:某头部电商 App 首页 Feeds 流采用“多路召回(协同过滤 + 向量召回 + 热度)→ DeepFM 排序 → MMR 重排”的架构。上线后首页点击率提升 23%,人均浏览深度提升 15%;每次改动都通过 A/B 实验逐步放量,实验窗口 2 周,确保指标波动在统计显著性范围内。

搜索广告中的广告排序

阶段:排序模型 + 广告竞价机制

解决的问题:搜索广告需要在“用户相关性”和“广告主出价”之间取得平衡。只按出价排序会伤害用户体验,只按相关度排序则平台收入受损。AI 排序模型将点击率预估与出价结合,实现平台、用户、广告主三方共赢。

具体案例:某搜索引擎广告系统使用“预估点击率 × 广告质量分 × 出价”的综合排序分,并引入位置偏差校正。实验结果显示,兼顾体验后广告点击率保持稳定的同时,广告收入提升 18%,用户满意度指标提升 5%。

短视频新用户冷启动

阶段:召回策略 + 特征工程 + 实时反馈

解决的问题:新用户没有历史行为,协同过滤完全失效;传统的统一热门推荐导致留存差,因为热门内容不一定匹配个体兴趣。AI 方案用“注册画像 + 热门兜底 + 实时试探”组合拳,在用户前 10 分钟快速捕捉兴趣信号。

具体案例:某短视频平台对新用户采用“三级冷启动”:打开 App 前 30 秒推全站热门,第 1 分钟开始根据停留时长切换类目,第 5 分钟引入实时更新的用户向量召回。落地后新用户次留提升 15%,观看时长提升 22%。

Python 演示

python91 行
import randomfrom collections import defaultdict  class MiniRecSys:    """极简推荐系统:召回 -> 排序 -> 重排 三步流水线演示"""     def __init__(self, users, items):        self.user_list = users        self.item_list = items        self.click_map = defaultdict(set)   # 每位用户点击过的商品        self.item_attr = {}                 # 商品所属类目        self.pop_score = {}                 # 商品热度计数     def log_click(self, user, item, category):        """记录一次用户点击"""        self.click_map[user].add(item)        self.item_attr[item] = category        self.pop_score[item] = self.pop_score.get(item, 0) + 1     def recall(self, user, top_k=50):        """多路召回:协同过滤 + 热门兜底"""        seen = set(self.click_map.get(user, set()))        recs = []         # 路1:协同过滤(相似口味用户看过的商品)        for other, items in self.click_map.items():            if other == user:                continue            if len(seen & items) > 0:                for it in items:                    if it not in seen:                        recs.append(it)         # 路2:热门兜底(全站点击量最高的商品)        for it in sorted(self.pop_score, key=lambda x: -self.pop_score[x]):            if it not in seen:                recs.append(it)         unique = []        for it in recs:            if it not in unique:                unique.append(it)        return unique[:top_k]     def rank(self, user, candidates):        """排序:偏好打分 = 类目偏好 + 热度加成"""        def score(item):            cat = self.item_attr.get(item, "unknown")            pref = sum(                1 for it in self.click_map[user]                if self.item_attr.get(it) == cat            )            return pref * 0.7 + self.pop_score.get(item, 0) * 0.3         return sorted(candidates, key=score, reverse=True)     def rerank(self, ranked, top_n=10):        """重排:限制同类目商品连续出现,保证多样性"""        result = []        last_cat = None        for it in ranked:            cat = self.item_attr.get(it, "unknown")            if cat == last_cat and len(result) < top_n + 3:                continue            result.append(it)            last_cat = cat            if len(result) >= top_n:                break        return result[:top_n]     def recommend(self, user):        """完整流程:召回 -> 排序 -> 重排"""        cands = self.recall(user)        ranked = self.rank(user, cands)        return self.rerank(ranked)  if __name__ == "__main__":    sim = MiniRecSys(users=list(range(1, 51)), items=list(range(1, 201)))    items_cat = {}    for i in range(1, 201):        items_cat[i] = random.choice(["electronics", "books", "clothes"])     for _ in range(400):        u = random.randint(1, 50)        i = random.randint(1, 200)        sim.log_click(u, i, items_cat[i])     for u in [1, 2, 3]:        print("user", u, "->", sim.recommend(u))
目的:用 70 行 Python 实现一个完整的“召回 → 排序 → 重排”推荐流水线,演示多路召回如何合并候选、排序如何打分、重排如何保证多样性。
  1. self.click_map = defaultdict(set)用集合记录每位用户点过的商品,天然去重
  2. self.pop_score[item] = self.pop_score.get(item, 0) + 1每被点一次,热度加 1
  3. for other, items in self.click_map.items():遍历其他用户,寻找口味相似者
  4. if len(seen & items) > 0:交集非空说明两人口味重叠,该用户的商品值得借鉴
  5. for it in sorted(self.pop_score, key=lambda x: -self.pop_score[x]):按热度从高到低兜底补全
  6. return pref * 0.7 + self.pop_score.get(item, 0) * 0.3打分 = 类目偏好 70% + 热度 30%
  7. if cat == last_cat and len(result) < top_n + 3:同类目连续出现就先跳过,保护多样性
  8. sim.log_click(u, i, items_cat[i])模拟产生 400 条点击日志

常见误区

误区做召回时恨不得把全量商品都送进排序层,觉得排序模型那么强,多给点候选总没错。宁可慢一点,也不能漏。

正解召回的目标是“够好”,不是“够全”。候选集扩大后,排序层的算力和延迟成本线性上升,而新增的候选大多是无用商品。真实系统中,几百个高质量候选就能覆盖绝大多数有效点击。这就像导购不会把整个仓库搬到顾客面前——先凭经验筛出一小批,才是专业做法。

为何常见:直觉上“越全越好”很容易被接受,但工程系统处处是预算约束,面试官就是要看你能不能认识到“全”的代价。

误区离线实验 AUC 提升 0.005 就急着写周报、准备上线,觉得模型变强了线上肯定会涨。

正解AUC 衡量的是排序质量,与线上 CTR、GMV 并不等价。离线数据带有位置偏差和选择偏差,线上还有流量分配、新鲜度等因素。AUC 只是“演习成绩单”,A/B 实验才是“实战考核”。正确姿势是:离线 AUC 决定是否值得做实验,A/B 决定是否全量上线。

为何常见:离线实验简单快捷,容易让人产生“指标好 = 效果好”的思维惯性。

误区新用户没有行为数据,那就把全站热度最高的商品推给他,反正热门内容平均点击率最高。

正解推热门只是兜底,不是策略的全部。优秀的冷启动方案会结合注册信息做粗粒度画像,再根据实时反馈快速切换候选方向,并用探索与利用(EE)机制在“试探兴趣”和“满足兴趣”之间平衡。盲目推热门会造成“看起来有留存,其实是假热闹”。

为何常见:热门商品的统计平均点击率确实最高,容易忽略个体之间巨大的兴趣差异。

误区看到大厂分享“我们有 20 路召回”,就回去把协同过滤、向量召回、热门、类目、新品全都实现一遍,认为路数越多越好。

正解多路召回的关键是“互补性”而非“数量”。如果几路召回产出的候选高度重叠,合并后有效增量很小,还白白增加延迟。每新增一路,都要评估它对整体候选的“增量覆盖”是否值得。

为何常见:大厂技术分享容易让人照搬方案,忽略了不同业务场景下各路召回的独立价值。

误区面试官刚把题读完,我就抢着在白板上画出完整的架构图,想展示自己懂得多。

正解最稳妥的节奏是先澄清需求再动手设计。用户规模、QPS、延迟预算、核心指标都没问清就画图,等于导购不问需求直接推荐。80% 的面试官会打断你说“等一下,我们先明确一下场景”——这会让印象分大打折扣。

为何常见:把面试当成考试答题,总想快点给出“标准答案”,却忘了系统设计面试本质是沟通和权衡。

面试题

Q1. 推荐系统处理一次请求时,三个核心模块的正确顺序是?

单选题 · easy

  • A.召回 → 排序 → 重排← 正确答案
  • B.排序 → 召回 → 重排
  • C.重排 → 召回 → 排序
  • D.召回 → 重排 → 排序

答案:A。一次推荐请求沿漏斗流水线自上而下:先召回海选出候选,再精排打分,最后重排调整顺序。选 B 等于让导购先精挑再海选,逻辑倒置;选 C 和 D 都打破了“缩小范围”的先后关系。

Q2. 召回层最核心的目标是什么?

单选题 · easy

  • A.精确打分离线计算
  • B.快速筛出少量候选← 正确答案
  • C.调整商品展示顺序
  • D.统计全量用户画像

答案:B。召回层负责在毫秒级把全量物品缩到几百个候选,讲究“快而全”。精确打分是排序层的职责,调整顺序是重排层的职责,统计画像是离线数据层的工作——它们都不属于召回的核心目标。

Q3. AUC = 0.8 的含义是?

单选题 · mid

  • A.模型预测准确率约为 80%
  • B.正样本排序超过负样本概率为 0.8← 正确答案
  • C.模型对正样本的召回率约为 80%
  • D.线上用户点击率达到百分之八十

答案:B。AUC 把正负样本两两配对,衡量“正样本分 > 负样本分”的比例,与准确率、召回率、点击率都不是一回事。AUC=0.8 表示随机抽一对正负样本,模型有 80% 概率把正样本排前面。

Q4. 重排阶段引入 MMR 算法主要为了解决什么问题?

单选题 · hard

  • A.推荐结果单一重复← 正确答案
  • B.候选物品数量不足
  • C.排序模型参数过拟合
  • D.用户特征维度太高

答案:A。MMR 通过“相关性 − 与已选集合最大相似度”的贪心公式,在保持相关的同时控制多样性,解决的是“排序后推荐列表清一色同品类”的问题。B、C、D 都不是重排层的目标。

Q5. 新用户完全没有行为数据,哪种冷启动策略最合适?

单选题 · mid

  • A.协同过滤矩阵精确求解
  • B.热门兜底结合注册画像← 正确答案
  • C.深度模型离线批量训练
  • D.全量召回后精排打分

答案:B。冷启动的核心是“先用普适信息兜底,再快速个性化”。热门兜底保底,注册画像提供粗粒度偏好,后续再用实时反馈切换。协同过滤在无行为数据时根本无法计算相似度。

Q6. 排序模型中引入特征交叉的主要目的有哪些?

多选题 · mid

  • A.增加模型整体复杂度
  • B.捕捉特征间非线性交互← 正确答案
  • C.直接替代离线特征工程
  • D.让线性模型表达组合特征← 正确答案

答案:BD。特征交叉让模型记住“女性 + 美妆”这类组合信号,这是 LR 和深度模型的共同优势。A 是副作用而非目的;C 过于绝对,真实系统中仍需要一定的人工特征工程兜底。

Q7. 为什么模型上线前必须做 A/B 实验?

单选题 · easy

  • A.因为离线指标计算耗时太长
  • B.离线与在线数据分布存在偏差← 正确答案
  • C.因为 A/B 实验实施成本最低
  • D.为了减少模型参数量与算力

答案:B。离线数据存在位置偏差、选择偏差,离线 AUC 与线上 CTR 不一定正相关。A/B 实验把新模型和旧模型放在同分布的真实流量里对比,才能得出“实战验证”的结论。

Q8. 下列哪些属于推荐系统的常见召回路径?

多选题 · mid

  • A.用户协同过滤召回← 正确答案
  • B.物品向量相似召回← 正确答案
  • C.全站热门物品召回← 正确答案
  • D.全局最优精确求解召回

答案:ABC。协同过滤、向量召回、热门兜底都是工程上常用的召回路径,讲究“快而全”。全局最优精确求解在毫秒级延迟下不可行,属于排序层的理想化目标,不是召回路径。

Q9. 位置偏差(Position Bias)指的是?

单选题 · hard

  • A.曝光位置不同导致点击统计失真← 正确答案
  • B.召回位置错误导致候选缺失
  • C.用户地理区域导致的偏好偏差
  • D.特征向量高位值导致的数值偏差

答案:A。列表靠前的商品曝光更多、点击率天然偏高,不能简单视为质量信号。位置偏差校正在排序训练和指标评估中都必须处理,否则模型会误学“位置即质量”。

Q10. ML 系统设计面试中,收到“设计推荐系统”题目后,应该先澄清哪些信息?

多选题 · extreme

  • A.用户规模与接口 QPS 上限← 正确答案
  • B.服务延迟与算力预算约束← 正确答案
  • C.核心业务指标与优化目标← 正确答案
  • D.模型初始化参数的精确数值

答案:ABC。先澄清范围是系统设计面试的第一原则。规模、延迟、指标决定了架构选型;初始化参数是实现细节,应在确定模型之后再讨论。忽略前三项直接画图,面试官会认为你缺乏主管视角。