如何设计推荐系统(如今日头条)
如何设计推荐系统(如今日头条)
📌 一、题目解读与需求澄清
这道题的本质是:如何在海量内容和海量用户之间,实时、精准地完成个性化匹配。核心挑战不在于单点技术,而在于多个子系统的协同:特征工程、模型召回、排序、实时更新、冷启动。
功能性需求(Functional Requirements)
| # | 需求 | 说明 |
|---|---|---|
| 1 | 用户打开 App 时,获取个性化 Feed | 首屏加载 ≤ 10 条,支持下拉刷新和上拉分页 |
| 2 | 系统根据用户行为实时更新兴趣画像 | 点击、停留时长、点赞、分享、不感兴趣 |
| 3 | 新内容能在发布后短时间内进入推荐池 | 内容冷启动,新文章 ≤ 5 分钟可被推荐 |
| 4 | 支持多种内容类型 | 图文、视频、短视频、直播(可先聚焦图文) |
| 5 | 支持屏蔽、举报、「不感兴趣」反馈 | 负反馈需快速生效,不应在本次 session 内再出现 |
非功能性需求(Non-Functional Requirements)
| # | 需求 | 量化目标 |
|---|---|---|
| 1 | 低延迟 | 推荐接口 P99 ≤ 200ms |
| 2 | 高吞吐 | 支撑亿级 DAU,峰值 QPS 10w+ |
| 3 | 高可用 | SLA 99.99%,推荐降级不应导致白屏 |
| 4 | 最终一致性 | 用户行为反馈可延迟数分钟生效,不要求强一致 |
| 5 | 可解释性与安全 | 内容需过审,屏蔽违规内容和敏感话题 |
容易被忽略的隐藏需求
- 新用户冷启动:没有历史行为的用户如何初始化画像(年龄、地域、注册兴趣标签)
- 内容时效性衰减:3天前的新闻不应与今天的突发新闻竞争相同的曝光位
- 结果多样性:如果全是科技内容,用户会审美疲劳(Diversity vs. Relevance 的 trade-off)
- 推荐去重:用户刷过的内容不能在短时间内重复出现
- 马太效应:热门内容越推越热,长尾创作者没有曝光机会
📐 二、规模估算
关键假设
| 参数 | 假设值 | 理由 |
|---|---|---|
| DAU | 1亿 | 对标头条量级 |
| 人均每日打开次数 | 10次 | 每次刷新获取 10 条 |
| 人均每日行为事件 | 50条 | 点击、曝光、停留等 |
| 每日新增内容 | 500万篇 | 含 UGC、PGC、转载 |
| 内容平均大小(元数据) | 1KB | 标题、作者、标签、封面URL |
QPS 计算
Feed 请求 QPS(读):
1亿用户 × 10次/天 / 86400秒 ≈ 11,574 QPS
峰值(早晚高峰 × 5倍) ≈ 60,000 QPS
行为上报 QPS(写):
1亿用户 × 50条/天 / 86400秒 ≈ 57,870 QPS
峰值 ≈ 300,000 QPS
存储计算
内容元数据:
500万篇/天 × 1KB = 5GB/天
保留1年 = 5GB × 365 ≈ 1.8TB(索引可用 Elasticsearch)
用户行为日志:
1亿用户 × 50条 × 200字节 = 1TB/天
保留90天 ≈ 90TB(冷存储 → HDFS / S3)
用户特征向量(Embedding):
1亿用户 × 256维 × 4字节 = 100GB(可放 Redis)
内容向量:
每天 500万内容 × 256维 × 4字节 = 5GB/天
有效期7天 ≈ 35GB(可放 Faiss / Milvus)
带宽计算
Feed 响应包(10条内容 × 1KB)= 10KB/请求
带宽:60,000 QPS × 10KB = 600 MB/s ≈ 4.8 Gbps(需 CDN 分摊)
关键结论
这是一个写多读也多的混合型系统,但读的延迟要求极严苛(P99 200ms)。推荐不能在用户请求时实时计算完整结果,必须大量依赖预计算和缓存。行为写入量巨大,需要异步消息队列削峰。
🏗️ 三、高层架构设计
核心组件分为五层:数据采集层 → 特征层 → 模型层(召回+排序) → 服务层 → 内容管控层。### 各组件职责说明
API Gateway:统一入口,做限流(令牌桶)、用户鉴权、A/B 实验分流,将请求路由到推荐服务。
推荐服务(Recommendation Service):核心编排者,串联召回→粗排→精排→重排四个阶段,超时则走降级策略返回热门兜底列表。
召回层:从亿级内容池中筛出候选集(~1000条)。多路并行执行,用 Scatter-Gather 模式聚合结果。
粗排(Pre-rank):用轻量模型(如线性模型或小型 DNN)将 1000 条缩减到 200 条,核心目标是快速剪枝。
精排(Ranking):用重型深度模型(如 MMOE、DeepFM)对 200 条打分,同时预测多个目标(CTR、完读率、分享率)。
重排(Re-rank):在精排结果基础上做最后一道业务处理:多样性打散、去重、广告位插入、违规内容过滤。
核心数据流
用户下拉刷新
→ API GW(鉴权 + A/B 分流)
→ 推荐服务(从 Redis 获取用户实时特征)
→ 并行触发 3 路召回(User-CF / 向量召回 / 热门召回)
→ 候选集合并去重(~1000条)
→ 粗排(LR/小DNN,~5ms)→ 200条
→ 精排(MMOE深度模型,~30ms)→ 200条打分
→ 重排(多样性/安全过滤)→ 取 Top-30
→ 返回客户端
→ 客户端行为(曝光/点击/停留)→ Kafka → Flink实时更新用户特征 → Redis
核心数据库表结构
-- 内容表(MySQL + Elasticsearch 双写)
CREATE TABLE articles (
article_id BIGINT PRIMARY KEY,
author_id BIGINT NOT NULL,
title VARCHAR(200),
category VARCHAR(50), -- 类别标签
tags JSON, -- ["AI","科技","创业"]
quality_score FLOAT DEFAULT 0.0, -- 内容质量分(离线计算)
publish_time TIMESTAMP,
status TINYINT DEFAULT 1, -- 1=正常 2=审核中 3=下架
INDEX idx_publish_time (publish_time),
INDEX idx_category (category)
);
-- 用户画像表(Redis Hash 存储实时部分,HBase 存储历史)
CREATE TABLE user_profiles (
user_id BIGINT PRIMARY KEY,
category_pref JSON, -- {"科技":0.8,"体育":0.3}
keyword_pref JSON, -- 兴趣关键词权重
embedding BLOB, -- 256维用户向量(定期更新)
updated_at TIMESTAMP,
INDEX idx_updated_at (updated_at)
);
-- 用户行为表(写 Kafka → 落地 HBase,按 user_id+time 分区)
CREATE TABLE user_actions (
user_id BIGINT,
article_id BIGINT,
action_type TINYINT, -- 1=曝光 2=点击 3=完读 4=分享 5=不感兴趣
dwell_time_ms INT, -- 停留时长(毫秒)
event_time TIMESTAMP,
PRIMARY KEY (user_id, event_time, article_id)
);
-- 内容向量表(存 Milvus,主键映射存 Redis)
-- article_id → embedding[256]
🔬 四、深入细节
子问题1:向量召回(Two-Tower Model)
问题描述:协同过滤依赖历史共现数据,对新内容和新用户效果差。需要一种能泛化的实时召回方式。
解决方案分析:Two-Tower(双塔模型)是目前工业界最主流的方案。User Tower 和 Item Tower 分别独立编码用户和内容特征,训练完成后可以离线预计算内容向量,在线只需实时计算用户向量,然后做 ANN(Approximate Nearest Neighbor)检索。
推荐方案:Two-Tower + Faiss(HNSW 索引)
# Two-Tower 模型核心结构(PyTorch 伪代码)
class TwoTowerModel(nn.Module):
def __init__(self, user_feat_dim, item_feat_dim, embed_dim=256):
super().__init__()
# 用户塔:将用户特征压缩为 256 维向量
self.user_tower = nn.Sequential(
nn.Linear(user_feat_dim, 512),
nn.ReLU(),
nn.LayerNorm(512),
nn.Linear(512, embed_dim),
)
# 内容塔:将内容特征压缩为 256 维向量
self.item_tower = nn.Sequential(
nn.Linear(item_feat_dim, 512),
nn.ReLU(),
nn.LayerNorm(512),
nn.Linear(512, embed_dim),
)
def forward(self, user_features, item_features):
user_emb = F.normalize(self.user_tower(user_features), dim=-1)
item_emb = F.normalize(self.item_tower(item_features), dim=-1)
# 点积相似度作为 logit
score = torch.sum(user_emb * item_emb, dim=-1)
return score
# 在线推理:用户向量实时计算,内容向量预存 Faiss
def recall_by_vector(user_id: str, top_k: int = 1000):
# 1. 从 Redis 获取用户实时特征
user_features = redis.get(f"user_feat:{user_id}")
# 2. User Tower 推理得到用户向量(< 1ms)
user_emb = user_tower.forward(user_features) # shape: [256]
# 3. Faiss HNSW 近邻检索(< 10ms,候选池 5000万内容)
distances, article_ids = faiss_index.search(
user_emb.reshape(1, -1), k=top_k
)
return article_ids.flatten().tolist()
关键细节:内容向量每天批量更新(新内容增量更新),用户向量在每次 Feed 请求时实时计算,或以 5 分钟为周期写入 Redis 做缓存。
子问题2:实时特征更新与用户画像
问题描述:用户刚点了一篇科技文章,系统应该立刻意识到这个信号,而不是等到次日离线训练才反映。这要求特征系统具备秒级响应能力。
解决方案分析:将用户行为分为「实时特征」和「历史特征」两层。实时特征用 Flink 流式消费 Kafka,做窗口统计后写入 Redis;历史特征用 Spark 批处理每日更新,存入 HBase。推荐时融合两层特征。
# Flink 实时特征计算(Java 伪代码,此处用 Python 描述逻辑)
# 数据流:Kafka 消费用户行为事件
# 每条事件格式:{user_id, article_id, action_type, category, tags, timestamp}
def process_user_action_stream(event_stream):
"""
滑动窗口聚合:最近 1 小时内,用户各 category 的点击次数
"""
return (
event_stream
.filter(lambda e: e.action_type in ["click", "share"]) # 正向行为
.key_by("user_id")
# 1小时滑动窗口,每 5 分钟触发一次计算
.sliding_window(size=60*60, slide=5*60)
.aggregate(CategoryCounter())
.map(lambda agg: update_redis(
key=f"user_realtime:{agg.user_id}",
field="category_click_1h",
value=agg.category_counts, # {"科技": 5, "体育": 1}
ttl=3600 # 1小时过期
))
)
def get_merged_features(user_id: str) -> dict:
"""
推荐时融合实时特征 + 历史特征
实时特征权重更高,覆盖历史特征
"""
# 实时特征:最近1小时行为(来自 Redis)
realtime = redis.hgetall(f"user_realtime:{user_id}")
# 历史特征:长期偏好画像(来自 HBase)
history = hbase.get(f"user_profile:{user_id}")
# 融合策略:实时信号衰减加权
merged = {}
for category in ALL_CATEGORIES:
rt_score = float(realtime.get(f"category_click_1h:{category}", 0))
hist_score = float(history.get(f"category_pref:{category}", 0))
# 实时信号权重 0.7,历史权重 0.3
merged[category] = 0.7 * min(rt_score / 10, 1.0) + 0.3 * hist_score
return merged
子问题3:精排模型设计(多目标优化)
问题描述:推荐不能只优化点击率(CTR),否则会出现「标题党」大量被推荐的问题。实际上需要同时优化 CTR、完读率(Read Rate)、分享率(Share Rate)等多个目标。
解决方案分析:MMOE(Multi-gate Mixture-of-Experts)是目前工业界多目标排序的主流架构。它使用多个共享 Expert 网络,每个目标通过独立的 Gate 网络动态选择 Expert 组合,有效解决了多目标之间的跷跷板问题。
class MMOE(nn.Module):
"""
Multi-gate Mixture-of-Experts
同时优化 CTR、完读率、分享率 三个目标
"""
def __init__(self, input_dim, num_experts=8, expert_dim=256, num_tasks=3):
super().__init__()
# 8个共享 Expert(每个是一个小MLP)
self.experts = nn.ModuleList([
nn.Sequential(nn.Linear(input_dim, expert_dim), nn.ReLU())
for _ in range(num_experts)
])
# 每个任务独立的 Gate(决定用哪几个 Expert 的组合)
self.gates = nn.ModuleList([
nn.Linear(input_dim, num_experts)
for _ in range(num_tasks)
])
# 每个任务的预测头
self.task_heads = nn.ModuleList([
nn.Sequential(
nn.Linear(expert_dim, 64),
nn.ReLU(),
nn.Linear(64, 1),
nn.Sigmoid() # 输出概率
)
for _ in range(num_tasks)
])
def forward(self, x):
# 计算所有 Expert 输出
expert_outputs = torch.stack(
[expert(x) for expert in self.experts], dim=1
) # [B, num_experts, expert_dim]
task_outputs = []
for i, (gate, head) in enumerate(zip(self.gates, self.task_heads)):
# Gate 计算每个 Expert 的权重
gate_weights = F.softmax(gate(x), dim=-1) # [B, num_experts]
# 加权聚合 Expert 输出
mixed = torch.sum(
gate_weights.unsqueeze(-1) * expert_outputs, dim=1
) # [B, expert_dim]
task_outputs.append(head(mixed))
# 返回:[ctr_prob, read_rate_prob, share_rate_prob]
return task_outputs
# 综合分计算(加权融合多目标)
def compute_final_score(ctr, read_rate, share_rate):
"""
这个权重配置直接影响推荐生态,需要精心调整
完读率权重高于CTR,抑制标题党
"""
return (
0.3 * ctr +
0.5 * read_rate +
0.2 * share_rate
)
子问题4:内容冷启动(新文章如何快速进入推荐池)
问题描述:一篇新文章刚发布时没有任何用户交互数据,向量召回(依赖内容 Embedding)和协同过滤(依赖共现)都无法正常工作。
解决方案分析:
- 阶段1(发布后0-30分钟):基于内容本身的文本特征做召回。用 BERT 对文章标题和摘要做 Embedding,与用户偏好标签匹配,挂到「新内容」召回通道。
- 阶段2(30分钟~数小时):用「探索流量」(小比例,约5%)主动曝光给兴趣匹配的用户,收集初始点击信号。
- 阶段3(数小时后):内容有了足够的点击/完读数据,通过置信度判断是否进入主推荐通道。
def cold_start_score(article) -> float:
"""
内容冷启动置信度评估
综合作者历史、内容质量信号,决定给多少探索流量
"""
author_score = get_author_quality_score(article.author_id) # 作者历史CTR
content_score = content_quality_model.predict(article) # 文章质量分
category_heat = get_category_trending_score(article.category) # 品类热度
# 初始曝光量(探索流量配额)
initial_traffic = int(
1000 * author_score * 0.4 + # 知名作者给更多初始流量
500 * content_score * 0.4 +
300 * category_heat * 0.2
)
return initial_traffic
# 冷启动毕业判断:当内容有足够信号后,进入正式排序
def should_graduate_from_cold_start(article_id) -> bool:
stats = get_article_stats(article_id)
return (
stats.impressions >= 500 and # 至少 500 次曝光
stats.ctr >= 0.02 and # CTR > 2%
stats.avg_dwell_time_ms >= 30000 # 平均停留 > 30秒
)
子问题5:推荐结果多样性(Diversity)
问题描述:精排后 Top-30 可能全是同一个 Category,或者同一作者的多篇文章。用户体验会非常糟糕。
解决方案:MMR(Maximal Marginal Relevance)算法,在相关性和多样性之间做 trade-off。
def mmr_rerank(candidates: list, lambda_param: float = 0.5, top_k: int = 30):
"""
MMR 重排:每次选择「与已选集合差异最大 & 精排分最高」的候选
lambda_param: 0=纯多样性, 1=纯相关性
"""
selected = []
remaining = candidates.copy()
while len(selected) < top_k and remaining:
best_item = None
best_score = float('-inf')
for item in remaining:
# 相关性分(精排输出)
relevance = item.ranking_score
# 与已选列表的最大相似度(同类别/同作者 → 相似度高)
if selected:
max_similarity = max(
compute_similarity(item, s) for s in selected
)
else:
max_similarity = 0
# MMR 分 = λ × 相关性 - (1-λ) × 与已选的最大相似度
mmr_score = lambda_param * relevance - (1 - lambda_param) * max_similarity
if mmr_score > best_score:
best_score = mmr_score
best_item = item
selected.append(best_item)
remaining.remove(best_item)
return selected
def compute_similarity(item_a, item_b) -> float:
"""简单规则:同Category相似度0.8,同作者相似度0.9,其他0.1"""
if item_a.author_id == item_b.author_id:
return 0.9
if item_a.category == item_b.category:
return 0.8
return 0.1
⚖️ 五、技术选型与 Trade-off 讨论
| 决策点 | 选项A | 选项B | 本题推荐 & 理由 |
|---|---|---|---|
| 召回策略 | 纯协同过滤 | 向量召回 + 规则召回 + CF 多路并行 | ✅ 多路并行。单一召回有明显盲区(新用户/新内容),多路互补覆盖率更高 |
| 排序模型 | 单目标 LR/GBDT | 多目标深度模型(MMOE) | ✅ MMOE。单目标优化 CTR 会劣化生态;深度模型能捕捉特征交叉 |
| 特征更新 | 纯离线日增量 | 实时流(Flink)+ 离线批处理双轨 | ✅ 双轨。纯离线无法感知用户当次 Session 内的行为转变 |
| 向量检索 | 暴力全量搜索 | ANN(Faiss HNSW / Milvus) | ✅ ANN。5000万内容全量点积不可接受(秒级延迟),HNSW 可在10ms内完成 |
| 一致性 | 强一致性(同步更新所有副本) | 最终一致性(异步) | ✅ 最终一致性。推荐系统允许几分钟延迟,强一致大幅增加写延迟,得不偿失 |
| 内容存储 | 纯 MySQL | MySQL(主数据)+ ES(全文检索)+ Redis(热点缓存) | ✅ 分层存储。不同查询模式用不同存储,MySQL 做源头真实,ES 做搜索/过滤 |
| 推送模式 | 推模式(写扩散) | 拉模式(读扩散) | ✅ 拉模式为主。头条是信息流,用户关系不如微博强,读时拉取更灵活。大 V 内容可混合推 |
❓ 六、常见面试追问点
Q1:召回阶段为什么不直接用精排模型?
精排模型(MMOE等)推理一次需要5-30ms,候选集如果有5000万内容,全量精排根本不可能。召回的本质是在可接受延迟内将5000万缩小到1000,牺牲精度换效率;精排在小候选集上追求精度。这是一个典型的漏斗思想。
Q2:用户 Embedding 和内容 Embedding 怎么保持在同一个向量空间?
Two-Tower 训练时使用同一套对比损失(通常是 In-batch Softmax 或 Sampled Softmax),用用户的正向行为(点击、完读)作为正样本对,负样本从 batch 内随机采样。两个 Tower 在相同的优化目标下训练,因此 Embedding 天然在同一空间中,点积可以度量匹配程度。
Q3:如何处理热门内容的马太效应?
核心手段有三:① 在曝光分配时给新内容和长尾内容预留 5-10% 的「探索配额」;② 精排时对已曝光次数过多的内容施加惩罚(曝光量惩罚项);③ 指标监控层面,追踪「基尼系数」衡量内容分发不均程度,阈值触发时调整策略。
Q4:用户 A/B 实验如何正确划分,避免网络效应干扰?
推荐系统 A/B 实验的标准做法是按 user_id 取模(而非按请求),保证同一用户在实验期内始终在同一桶。复杂场景下(如社交关系密集的产品),需要用 Graph Partition 做「图划分实验」,但头条这种信息流产品用户间关系弱,user-level 分桶基本够用。
Q5:如何评估推荐系统的效果?离线指标和在线指标的关系?
- 离线指标:AUC(分类)、NDCG(排序质量)、Recall@K(召回率)。问题是离线好≠线上好,因为存在「曝光偏差」(只有被推荐的内容才有反馈)。
- 在线指标:CTR(点击率)、Session Duration(用户停留时长)、Retention(次日留存)。这是真实业务目标,但反馈周期长。
- 实践中用离线指标做快速筛选,显著改进才上 A/B 实验,以在线指标做最终决策。
Q6:Faiss 索引如何应对内容的实时增删(新内容发布 / 内容下架)?
Faiss 本身不支持高效的增量更新(删除需重建索引)。工业界通常的做法是:内容 Embedding 每小时做一次全量重建,同时维护一个「黑名单」布隆过滤器(Bloom Filter)。召回结果返回后,立刻过滤黑名单(已下架内容),实现准实时的内容管控。更新频率要求极高时可以考虑 Milvus,它原生支持实时 CRUD。
Q7:推荐系统的降级策略怎么设计?
降级是生产环境必备能力,分三层:① 局部降级:精排超时(>100ms)时跳过精排,直接用粗排结果;② 召回降级:向量召回服务不可用时,只用热门召回兜底;③ 全量降级:推荐服务整体不可用时,直接从 Redis 返回「全局热门列表」(每5分钟预计算一次)。每个降级层都需要埋点监控,方便排查问题。
Q8:如何防止「信息茧房」(Filter Bubble)?
技术层面:① 在 Re-rank 阶段强制引入一定比例的跨领域内容;② 定期对用户兴趣向量做「正则化」,防止某个类别权重无限增大;③ 监控用户兴趣分布的熵值,熵值过低时主动增加多样性。更根本的是产品设计层面,提供用户可感知的兴趣管理入口。
Q9:内容安全过滤放在哪个环节最合适?
分两道:① 发布时过滤(Pre-filter):内容入库前过审,违规内容不进推荐池,这是第一道防线;② 召回后过滤(Post-filter):推荐链路中在 Re-rank 阶段再次过滤,防止漏网之鱼以及针对特定用户的定向违规(如对未成年人过滤特定内容)。两道过滤缺一不可,但 Post-filter 要保持轻量,避免引入延迟。
Q10:如何设计推荐系统的监控体系?
监控分三层:
- 业务层:CTR、DAU、Session Duration、内容分发的基尼系数,每分钟粒度大盘看板;
- 服务层:推荐接口 P50/P99 延迟、召回率、各召回通道的候选数量、降级触发次数;
- 模型层:特征分布漂移(Feature Drift)、预测分布漂移(Score Drift)。
一旦线上 CTR 相对前一周同期下跌超过 5%,立刻触发告警并回滚最近的模型更新。
🗺️ 七、学习路线图
延伸阅读方向
1. 工业界论文(必读)
- Deep Interest Network (DIN)(阿里,2018):用户历史行为序列建模的 Attention 机制
- MMOE: Modeling Task Relationships in Multi-task Learning(Google,2018):多目标排序的基础架构
- Sampling-Bias-Corrected Neural Modeling(Google,2019):Two-Tower 训练时负样本偏差修正
- 字节跳动技术博客:ByteDance AI Lab 有多篇关于头条推荐系统演进的公开技术文章
2. 开源项目
Faiss(Facebook):向量检索核心库,深入阅读 HNSW 和 IVF 两种索引Milvus:云原生向量数据库,对比 Faiss 理解其工程化差异RecBole:推荐系统算法库,涵盖从 BPR 到 BERT4Rec 的完整实现,适合对比学习
3. 系统设计扩展
- 重点理解 Lambda Architecture(批处理 + 流处理)在特征工程中的应用
- 阅读 Flink 官方文档中 Streaming Window 和 State Backend 的设计
知识复用关系
| 题目 | 复用的核心知识 |
|---|---|
| 设计 YouTube 视频推荐 | Two-Tower、多目标排序、冷启动,几乎完全复用;区别在于视频有更长的 Dwell Time 信号 |
| 设计 Twitter/微博 信息流 | 推拉结合模型(大 V 推模式)、时效性排序,是本题的社交版变体 |
| 设计搜索引擎排序 | 精排模型(BERT Reranker)、点击率模型与本题深度相关;区别是搜索有明确 Query |
| 设计广告系统 | 召回→粗排→精排漏斗几乎完全相同;区别在于广告有出价约束,需要 eCPM = bid × pCTR |
| 设计 Spotify 歌单推荐 | 协同过滤、Embedding 检索复用;区别在于音乐有更强的序列性(下一首歌预测) |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)