系统设计:推荐系统
从召回到精排,构建千万级用户的个性化推荐架构。
推荐系统是互联网公司最核心的系统之一,直接影响用户留存和商业化收入。面试中常要求设计一个完整的推荐架构,考查候选人对算法、工程、数据三者的综合能力。
场景分析(Scenario)
需求
- 功能需求:为用户推荐个性化内容(新闻、视频、商品等)
- 用户规模:DAU 1000 万,日均请求 10 亿次
- 延迟要求:首页推荐 < 200ms,非首页 < 500ms
- 内容规模:物品池 1 亿+,日新增 100 万
核心挑战
- 海量物品如何快速召回:从 1 亿候选中选出数百个
- 实时兴趣捕捉:用户刚刚点了赞,下次刷新要体现
- 冷启动:新用户、新物品无历史数据
- 多样性与新颖性:避免信息茧房
- Exploration vs Exploitation:已知兴趣 vs 新领域探索
服务架构(Service)
┌─────────────┐ ┌─────────────────────────────────────────────┐
│ Client │───▶│ Recommendation Service │
└─────────────┘ │ ┌────────┐ ┌────────┐ ┌──────┐ ┌──────┐ │
│ │ Recall │─▶│ Pre-Rank │─▶│ Rank │─▶│ ReRank│ │
│ └────────┘ └────────┘ └──────┘ └──────┘ │
└─────────────────────────────────────────────┘
│ │ │
┌────────▼──────┐ ┌─────▼──────┐ ┌──▼─────┐
│ Feature Store │ │ Model Service │ │ Rule Engine│
└───────────────┘ └──────────────┘ └─────────┘
四阶段漏斗
| 阶段 | 输入 | 输出 | 目标 |
|---|---|---|---|
| 召回(Recall) | 1亿 → 500 | 粗筛候选 | 快、广、轻 |
| 粗排/预排序(Pre-Rank) | 500 → 100 | 初筛排序 | 平衡速度与精度 |
| 精排(Ranking) | 100 → 20 | 精准预估 CTR/CVR | 高精度 |
| 重排(Re-Rank) | 20 → 10 | 最终展示 | 体验优化 |
召回层(Recall)
召回层必须在 < 20ms 内从 1 亿候选中选出数百个。核心思想:多路召回 + 倒排索引。
多路召回策略
| 召回路 | 原理 | 适用场景 |
|---|---|---|
| 协同过滤(CF) | 相似用户/物品 | 有历史行为的用户 |
| 内容召回(CB) | 标签匹配 | 解决冷启动 |
| 向量召回(Embedding) | DNN 计算相似度 | 泛化能力强 |
| 热门召回 | 全局/分群热门 | 兜底策略 |
| 运营规则 | 运营配置 | 新品推广、活动 |
协同过滤(Collaborative Filtering)
用户协同过滤(User-CF)
找到与目标用户兴趣相似的用户,推荐他们喜欢的物品。
import numpy as np
from scipy.spatial.distance import cosine
def user_cf(ratings, target_user, k=5):
"""
ratings: user-item matrix (users x items)
target_user: index of the user
"""
user_sim = 1 - np.array([
cosine(ratings[target_user], ratings[u])
if np.sum(ratings[u]) > 0 else 0
for u in range(len(ratings))
])
# Exclude self, get top-k similar users
user_sim[target_user] = -1
top_k_users = np.argsort(user_sim)[-k:]
# Aggregate ratings from similar users
scores = np.zeros(ratings.shape[1])
for u in top_k_users:
scores += user_sim[u] * ratings[u]
# Recommend items target user hasn't seen
seen = ratings[target_user] > 0
scores[seen] = 0
return np.argsort(scores)[::-1][:10]
问题:用户矩阵往往非常稀疏,计算复杂度高 (O(U^2))。
物品协同过滤(Item-CF)
更适合用户量级远大于物品量级的场景(如电商)。
计算物品之间的共现相似度:
$$sim(i, j) = \frac{|N(i) \cap N(j)|}{\sqrt{|N(i)| \cdot |N(j)|}}$$
优势:物品相似矩阵更稳定(物品变化慢),推荐更有可解释性。
向量召回:双塔模型(Two-Tower Model)
User Tower Item Tower
┌──────────┐ ┌──────────┐
│ User ID │ │ Item ID │
│ Profile │ │ Category │
│ History │─▶ Embedding ──▶ │ Tags │
│ Context │ │ Stats │
└──────────┘ └──────────┘
│ │
└───────── Sim(Q, I) ────────┘
- 用户塔和物品塔分别输出固定维度的 embedding
- 通过内积/余弦相似度计算匹配分数
- 物品 embedding 预先计算并写入 ANN 索引库
近似最近邻(ANN)索引
向量召回需要快速搜索最相似的 K 个物品:
| 算法 | 原理 | 召回率 | 内存占用 |
|---|---|---|---|
| Faiss(IVF-FLAT) | 聚类 + 倒排 | 高 | 中等 |
| Faiss(IVF-PQ) | 聚类 + 乘积量化 | 中等 | 低 |
| HNSW | 分层可 navigable 小世界图 | 很高 | 高 |
| Milvus | 企业级向量数据库 | 高 | 高 |
精排层(Ranking)
精排层提供精确的 CTR(点击率)或 CVR(转化率)预估。
###特征体系
| 特征类别 | 示例 |
|---|---|
| 用户画像 | 年龄、性别、兴趣标签、活跃度 |
| 物品特征 | 类目、标签、热度、质量分 |
| 交叉特征 | 用户偏好类目 × 物品类目 |
| 上下文 | 时间、地理位置、设备 |
| 统计特征 | 历史 CTR、曝光数、点击数 |
| 序列特征 | 最近 50 次点击序列 |
###排序模型演进
| 阶段 | 模型 | 特点 |
|---|---|---|
| 1.0 | LR | 轻量、可解释,无法学习交叉 |
| 2.0 | FM / FFM | 自动学习二阶特征交叉 |
| 3.0 | Wide & Deep | 记忆 + 泛化双路 |
| 3.5 | DeepFM | FM + DNN,无需人工特征工程 |
| 4.0 | DIN / DIEN | 引入 Attention,序列建模 |
| 5.0 | MMoE | 多目标联合优化 |
DeepFM 简化结构
import torch
import torch.nn as nn
class DeepFM(nn.Module):
"""简化版 DeepFM 用于理解核心结构"""
def __init__(self, field_dims, embed_dim=8, mlp_dims=[64, 32]):
super().__init__()
self.num_fields = len(field_dims)
# Embedding layer (shared by FM and Deep)
self.embedding = nn.ModuleList([
nn.Embedding(dim, embed_dim) for dim in field_dims
])
# FM 一阶线性部分
self.fm_first = nn.ModuleList([
nn.Embedding(dim, 1) for dim in field_dims
])
# Deep MLP
input_dim = self.num_fields * embed_dim
layers = []
for dim in mlp_dims:
layers.extend([nn.Linear(input_dim, dim), nn.ReLU(), nn.Dropout(0.2)])
input_dim = dim
layers.append(nn.Linear(input_dim, 1))
self.mlp = nn.Sequential(*layers)
def fm_layer(self, embeddings):
"""
FM 二阶交叉: sum(sum(vi * vj)) = 0.5 * [ (sum vi)^2 - sum(vi^2) ]
embeddings: [batch, num_fields, embed_dim]
"""
square_of_sum = torch.sum(embeddings, dim=1) ** 2
sum_of_square = torch.sum(embeddings ** 2, dim=1)
fm_out = 0.5 * torch.sum(square_of_sum - sum_of_square, dim=1, keepdim=True)
return fm_out
def forward(self, x):
# x: [batch, num_fields] 每个 field 的离散 ID
# Embedding lookup
embeddings = torch.stack([
self.embedding[i](x[:, i]) for i in range(self.num_fields)
], dim=1) # [batch, num_fields, embed_dim]
# FM first-order
fm_first = torch.sum(
torch.stack([self.fm_first[i](x[:, i]) for i in range(self.num_fields)], dim=1),
dim=1
) # [batch, 1]
# FM second-order
fm_second = self.fm_layer(embeddings)
# Deep part
deep_input = embeddings.view(x.size(0), -1)
deep_out = self.mlp(deep_input)
# Final prediction (sigmoid in loss)
return fm_first + fm_second + deep_out
多目标优化(MMoE)
推荐系统往往同时优化多个目标:
- 点击(CTR):用户是否点击
- 点赞/收藏( Engagement) :浅层互动
- 停留时长(Dwell Time) :深度消费
- 分享/评论(Conversion) :深层转化
使用 MMoE(Multi-gate Mixture-of-Experts)共享底层表示,同时优化多个目标。
重排层(Re-Rank)
重排层关注用户体验,目标是:多样性 + 新颖性 + 业务规则。
多样性算法:MMR(Maximal Marginal Relevance)
$$MMR = \lambda \cdot Relevance(S_i) - (1 - \lambda) \cdot \max_{S_j \in R} Similarity(S_i, S_j)$$
- 平衡相关性与多样性
- 高相关性且与已选物品差异大的物品得分高
业务规则插入
- 必出规则:运营活动、新品扶持
- 打散规则:同类目不超过连续 3 个
- 频率控制:同一作者/品牌间隔不低于 N 个
- 负反馈过滤:用户点过"不喜欢"的物品过滤
存储设计(Storage)
数据流架构
User Behavior ──▶ Kafka ──▶ Flink ──▶ Feature Store
│ │
▼ ▼
Training Online Serving
│ │
└─▶ Model Store ◀── Train Pipeline
存储选型
| 数据 | 存储 | 原因 |
|---|---|---|
| 用户画像 | Redis Cluster | 低延迟、高 QPS |
| 物品特征 | Redis / Feature Store | 批量预加载 |
| 实时行为 | Kafka + Flink | 流式处理 |
| 离线特征 | Hive / ClickHouse | 批量计算 |
| 向量索引 | Milvus / Faiss | ANN 搜索 |
| 模型文件 | S3 / 对象存储 | 大模型文件 |
| A/B Test | MySQL / Config Center | 实验配置 |
冷启动策略
| 场景 | 策略 |
|---|---|
| 新用户 | 基于注册信息的CB召回、热门兜底、探索导向推荐 |
| 新物品 | 内容标签匹配、新品流量扶持、相似物品扩展 |
| 新系统 | 专家规则 → 简单模型 → 深度学习逐步迭代 |
Bandit 算法:解决 Exploration
- Epsilon-Greedy:以 ε 概率随机探索
- UCB(Upper Confidence Bound) :兼顾均值和方差
- Thompson Sampling:贝叶斯采样,平衡探索和利用
实时推荐
近实时特征更新
点击 ──▶ Kafka ──▶ Flink 窗口聚合 ──▶ Redis Feature Store
│
用户刷新 ──────────────────────────────▶ 推荐服务读取最新特征
- Flink 滑动窗口实时计算用户最近行为特征
- 写入 Redis TTL 7 天
- 推荐服务读取时合并实时 + 离线特征
面试答题框架
推荐系统面试回答结构
阶段一:需求澄清(2 分钟)
- 推荐什么内容?新闻/视频/商品?
- 用户规模、物品规模、QPS 预期
- 实时性要求?能否接受 T+1 离线计算?
阶段二:召回层设计(5 分钟)
- 多路召回策略(CF + CB + Vector + Hot)
- 每路召回的数量和分数归一化
- ANN 索引选型(Faiss / Milvus)
- 如何评估召回质量(Recall@K)
阶段三:排序层设计(5 分钟)
- 特征工程:用户/物品/交叉/上下文/序列
- 模型选型:从 LR → DeepFM → DIN 的演进
- 在线 serving:TensorFlow Serving / Triton
- 多目标优化是否必要?
阶段四:重排与体验(3 分钟)
- 多样性:MMR、类目打散
- 新颖性:EE 问题、Bandit 算法
- 业务规则:频率控制、负反馈
阶段五:系统与工程(3 分钟)
- 数据流:Kafka + Flink + Feature Store
- A/B Test 框架
- 冷启动处理
- 监控:CTR、多样性、覆盖率
常见追问
| 追问 | 回答要点 |
|---|---|
| “如何解决信息茧房?” | 多样性算法 + 探索机制 + 人工运营干预 |
| “新用户怎么推荐?” | 基于画像的 CB + 热门 + 快速收集反馈 |
| “推荐结果怎么离线评估?” | AUC、GAUC、NDCG、HR@K;线上 A/B Test 为准 |
| “如何实时更新用户兴趣?” | Flink 实时特征 + 在线学习 / 近实时模型更新 |
| “推荐去重怎么做?” | 用户曝光/点击写入 Bloom Filter + Redis Set |
关键知识点总结
| 模块 | 核心技术 |
|---|---|
| 召回 | Item-CF、双塔模型、ANN(HNSW/Faiss)、多路融合 |
| 排序 | DeepFM、DIN、MMoE、特征工程 |
| 重排 | MMR、规则引擎、Bandit、频率控制 |
| 工程 | Kafka + Flink、Feature Store、Model Serving、A/B Test |
| 冷启动 | CB 兜底、探索机制、新物品扶持、多臂老虎机 |
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。