专业书籍精读 · ML System Design Interview · 第 6 章
Machine Learning System Design Interview · Ch 6 · Ali Aminian & Alex Xu · 2023
你打开一个视频网站,首页大约给你二十个视频。你可能以为背后是「一台很懂你的机器,从全部视频里挑出你最想看的那二十个」。这一章会告诉你,真正的难题不是挑得准,是根本来不及挑。
想象一座城里有十亿家餐馆,你饿了,站在门口,要在眨眼的工夫里被告诉去哪家吃。
「把十亿家都尝一遍再排名」这条路从一开始就不存在——不是尝得不准,是时间根本不够。所以真实的做法是分两道工序:先用一个极快、有点粗糙的办法圈出几百家「大致靠谱」的,再用一个又慢又讲究的办法在这几百家里认真排名。这一章几乎所有设计,都长自这笔时间账。
在分两道工序之前,先要回答「凭什么觉得你喜欢」。老办法有两条,各有各的死法。
所以真实系统两条都用:第二条负责发现,第一条负责兜底。
关键在一个提前量。系统提前给每个视频算好一串「方位」,意思是它在一张巨大的兴趣地图上落在哪儿;也给每个人算一串,意思是你平时待在这张地图的哪一带。你一来,系统只做一件事:在地图上找离你近的那些点——像找附近的餐馆,不需要把全城都量一遍距离。
这一步只求「大致不漏」,圈出一千来个。接着才轮到那个慢而讲究的模型认真排一遍。最后还有一道复审:这条在你的国家不让播、这条太旧、这两条其实是同一个内容、这二十条全来自同一个博主——都在这一步拿掉。
值得记住的是每一层追求的东西不一样。第一层追「别漏」——它漏掉的,后面再精的排名也救不回来;第二层追「排得对」;第三层管的是那些没法写进模型里的规矩:合规、新鲜、别重复、别太单调。把这三件事混在一层里做,是新手最常犯的错。
系统只能从「它推过的东西」里学习——你没被推到过的视频,它永远不知道你会不会喜欢,于是越推越像它昨天以为的你。
推荐系统的形状不是被「怎样更聪明」决定的,是被一笔时间账决定的:十亿量级的候选、不到一眨眼的预算,逼出了「粗筛 — 精排 — 复审」这三道工序。而它最深的毛病也来自这个形状——它只能从自己推过的东西里学到东西。
想进到具体机制、记号和示意图? → 切到精读版
这一章表面是「做一个视频推荐器」,内核是两个判断。其一,推荐必须分段——这不是精度问题而是算术问题:十亿量级的候选配上几十毫秒预算,「给每个视频都过一遍模型」从一开始就不成立,所以先用极廉价的模型把 10 亿砍到一千条(追召回),再用昂贵模型排这一千条(追精度)。其二,模型必须吃得下用户特征——只认交互矩阵的矩阵分解在老用户身上很好用,一碰到新用户、新视频就彻底哑火,而平台每天最大的增量恰恰是这两样。难点不在模型结构,在冷启动与曝光偏差:系统只能从自己推过的东西里学。
本章是《ML 系统设计面试》Part II 十道真题的第 5 道,也是全书第一道排序类题。上承 Ch5 有害内容检测——那里多路信号汇成一个判定,这里汇成一个排序;下启 Ch7 活动推荐,骨架同源但特征工程重得多。现实对应 YouTube 首页、Netflix 行式首页、抖音信息流。
书设定的场景是一个 YouTube 量级的平台:视频库十亿量级、日活数亿,要为每个用户的首页挑几十条,业务目标是用户参与度(不是点击率,这个差别后面会咬人)。首页整体响应预算通常在 100ms 量级,留给推荐的往往只有几十毫秒。
这一章的形状全在一笔账里:哪怕给一个视频打分只要 1 微秒,10 亿个也要 1000 秒——而这还只是单个用户的单次请求。所以「给全库打分再取前 20」不是慢,是根本不存在。
第二个坑同样硬:训练数据是系统自己生产的。用户只能点他被推到的东西,日志里就只有被推过的内容,一个从没被推出去的视频永远拿不到证明自己的机会。时间不够、数据自产,就是这一章真正要对付的两件事。
内容式过滤用视频自身属性配上你的历史,推「像你看过的」。它有一个别的方法给不了的能力:刚上传、零观看的视频只要有标题和标签,当天就能被推出去;小众口味也吃得住,因为它不要求「还得有别人跟你一样」。代价是要人造特征,更要命的是推不出新兴趣——只会在你已有的圈子里越绕越紧,这就是过滤气泡。
协同过滤反过来,只看那张「谁看了什么」的交互表。它不需要领域知识,而且能发现你自己都说不出口的兴趣——跨类目的关联只有它看得见。代价是冷启动:新用户那一行空、新视频那一列空,两头都无从下手,而库里随时有一大片视频是冷的。
所以书选混合,且工业上更常见的是串联而非并联——协同过滤先出候选,内容式再补它够不着的那块(新视频、冷启动用户、长尾),而不是两套各出一份榜单加权平均。一句可以带走的判据:协同过滤负责发现,内容式负责兜底。
矩阵分解是协同过滤最经典的落地。想象一张巨表,行是用户、列是视频,格子里是反馈值,99.99% 的格子是空的。矩阵分解把它拆成两张瘦表:每个用户一串数、每个视频一串数,两串数的点积就是预测分;训练就是调这两张表,让点积在已观测的格子上贴近真实反馈。
格子里填什么本身就是个选择。显式反馈(点赞、评分)准但极稀疏——绝大多数人看完什么都不点;隐式反馈(点击、观看时长)密但含噪——点进去不等于喜欢,看完可能只是当背景音。实践上两者混用、分别配权。
还有一个书专门点到的训练坑:只在已观测格子上算损失,模型会学成「什么都推」——没见过的格子从没被惩罚过,把所有点积一起调大也不变差。所以要把未观测格子当成 0 一并算进去(加权交替最小二乘 WALS 就是在两项间取权重)。
但矩阵分解有个换不掉的死穴:它只认 ID。新用户在表里没有对应那一行,系统就对他一无所知——哪怕你明明知道他 25 岁、在东京、五分钟前刚搜过「露营装备」。
双塔模型把「拿 ID 查表」换成「让一座神经网络吃一堆特征、吐出向量」。用户塔吃人口属性、上下文(时段、设备)和历史(最近看过的视频向量取平均);视频塔吃标题标签的文本向量、时长、语言。两塔各出一个向量,点积即相关性分,训练成二分类——看过的是正样本,随机采样的是负样本。冷启动因此打通:新用户虽无历史,属性和上下文这两路还在,塔照样算得出向量;新视频同理。
而双塔真能上线,靠的是一个纯工程性质:两塔推理可以解耦。视频塔离线把全库跑一遍、向量灌进 ANN 索引,线上一次请求只跑一次用户塔加一次检索。代价是训练要更多数据、索引会「偶尔找错」,且两侧特征只在最后一步点积时才相遇,交叉表达力有限——这正是排序层要补的。
把那笔时间账落成结构,就是召回(candidate generation)→ 排序(ranking)→ 重排(re-ranking)三层漏斗。每层追的东西完全不同,这是本章最该记住的一张图。
召回层:多路并行,各管一件事——相关性召回(双塔 + ANN)、热门与趋势召回、你关注频道的新片。每路各出几百到几千条,合并去重后剩 10³ 量级。这一层宁可错收,不可漏收:漏掉的后面排序再强也救不回,误收的由排序层收拾。所以评召回层要用召回率而不是精确率,这是最常见的一处用错尺子。
排序层:候选只剩一千条,终于跑得起重模型,也终于用得起召回层用不起的东西——用户与视频的交叉特征、实时统计量。目标设定是本章一个关键判断:优化参与度(观看时长、完播)而非点击率。只优化点击率,系统会稳定地把标题党顶上去——用户点了、三秒退出,指标涨了、体验塌了。
重排层:三类过滤,全是模型里写不进去的约束。合规——地域限制、错误信息、有害内容(上一章那套正接在这里);新鲜度——太旧的降权;体验——去重、多样性、同一创作者一屏限量。它常被当成装饰,其实承载的是产品意志里写不进损失函数的那部分。
特征分三路:用户(人口属性、上下文如时段与设备、历史行为序列)、视频(ID、时长、语言、标题标签的文本向量、发布时间、累计观看)、交互统计。历史怎么变成一条能喂进模型的特征?最常见是把最近看过的 N 个视频向量取平均——粗暴但很强,代价是丢了顺序。
评估两套口径。离线:precision@k、mAP,以及多样性(推荐列表两两相似度的平均,越低越多样);书特意点明多样性单独看没意义——全推随机视频多样性最高。线上:点击率、完播数、总观看时长、显式反馈。
「离线涨、线上不涨」在推荐里是常态而非意外:离线指标算在旧策略生产的日志上,而新模型想推的恰恰是旧策略没推过、日志里根本没有标签的那部分。唯一可信的裁判是线上 A/B。
表 1 · 三种推荐范式
| 内容式过滤 | 协同过滤 | 混合(书的选择) | |
|---|---|---|---|
| 新视频 | 当天可推(有元数据即可) | 推不出(没人看过) | 由内容式那一路覆盖 |
| 新用户 | 要一点历史 | 完全无解 | 热门 / 趋势召回 + 人口属性 |
| 发现新兴趣 | 弱:只在已有圈子里绕 | 强:跨类目关联只有它看得见 | 由 CF 那一路提供 |
| 代价 | 要领域知识造特征;过滤气泡 | 冷启动;长尾稀疏 | 两套都要建、都要维护 |
表 2 · 召回模型:矩阵分解 vs 双塔
| 矩阵分解 | 双塔(书的选择) | |
|---|---|---|
| 输入 | 只有交互矩阵,拿 ID 查表 | 用户特征 + 视频特征,编码成向量 |
| 冷启动 | 新用户 / 新视频彻底哑火 | 属性与上下文还在,照样出向量 |
| 训练 | 快;WALS 或 SGD 都能跑 | 慢,且要明显更多数据 |
| 服务 | 一次点积,极轻 | 一次用户塔 + 一次 ANN 检索 |
| 短板 | 只在观测格子上算损失会「什么都推」 | 近似索引会漏;特征交叉只在最后一步 |
表 3 · 三层漏斗各自追什么(最常被混为一谈的一张表)
| 召回层 | 排序层 | 重排层 | |
|---|---|---|---|
| 输入规模 | 10 亿 → 10³ | 10³ → 10² | 10² → 几十 |
| 追什么 | 召回率 / 覆盖 | 精度(参与度预估) | 合规与体验约束 |
| 能用什么特征 | 只能用可预计算、两侧解耦的 | 交叉特征、实时统计都能上 | 规则 + 业务元数据 |
| 延迟预算 | ~10 ms | ~30 ms | 个位数 ms |
| 错了会怎样 | 漏了就永远救不回 | 顺序不佳,内容还在 | 违规内容 / 一屏全同质 |
还有一条实操判据不在表里:冷启动主要不是模型问题,是产品问题。新用户靠热门加人口属性撑住第一屏,再用最初几次点击快速收敛;新视频靠元数据进召回,并从探索流量里分一点曝光去攒交互数据。
「多路召回 + 排序 + 重排」今天就是所有信息流产品的通用形状:YouTube 首页、Netflix 行式首页、TikTok 推荐流、Spotify 每周发现、电商「猜你喜欢」,乃至 RAG 里的「先向量粗召回、再交叉编码器重排」——同一个漏斗换了物料。面试高频追问三处:为什么不能一个模型打到底(当场算那笔 1000 秒的账)、新用户第一屏推什么、离线涨线上不涨怎么办。
① 形状由算术决定:10 亿候选 × 每条 1 微秒 ≈ 1000 秒,所以只能分段——召回、排序、重排。
② 内容式能推新视频、抓长尾但困在过滤气泡里;协同过滤能发现新兴趣但冷启动无解。CF 负责发现,内容式负责兜底。
③ 矩阵分解把稀疏打分表拆成两张瘦表、点积即预测分;只在观测格子上算损失会让模型「什么都推」,要把未观测格子一并算进去(WALS)。
④ 它的死穴是只认 ID;双塔把查表换成编码器,属性与上下文得以进入模型,冷启动因此打通。
⑤ 双塔真能上线是因为两塔解耦:视频向量离线算好建 ANN 索引,线上只跑一次用户塔加一次检索。代价是要更多数据、索引会漏、特征交叉弱。
⑥ 三层各追各的:召回追召回率(漏了永远救不回)、排序追精度(目标是参与度不是点击率)、重排装那些写不进损失函数的约束。
⑦ 隐式反馈密而含噪、显式反馈准而稀疏,实践上混用并分别配权。
⑧ 最深的坑是曝光偏差:训练数据是系统自己生产的,离线指标只能在旧策略的视野里打转。探索流量、位置偏差修正、线上 A/B,是这一章留下的三件标配。