Skip to content

设计一个推荐系统

提出问题

推荐系统是互联网产品的核心引擎,从短视频 Feed 流、电商商品推荐到内容资讯分发,本质上都是"从海量候选集中为用户找到最可能感兴趣的内容"。面试中出现频率极高,因为推荐系统涵盖了系统设计的几乎所有核心要素:数据处理(实时/离线)、算法分层(漏斗)、性能与延迟的权衡、AB 实验平台。生产场景中,推荐系统直接关系到用户留存和商业收入,设计的好坏有明确的数据反馈。

分析问题

四阶段漏斗:召回 → 粗排 → 精排 → 重排

推荐系统的核心架构是一个逐层过滤的漏斗,每层候选集缩小一个数量级。以下是各层的数据流和关键参数:

                 ┌─────────────────────────────────────┐
                 │         物品池(千万级)              │
                 └────────────┬────────────────────────┘

                    ┌─────────▼─────────┐
                    │     召回(Recall)  │  候选从千万 → 千级
                    │  Faiss ANN 检索    │  P99: 10-20ms
                    │  协同过滤 / 热门兜底 │
                    └─────────┬─────────┘

                    ┌─────────▼─────────┐
                    │    粗排(Pre-Rank) │  候选从千级 → 百级
                    │  双塔向量内积      │  P99: 1-3ms
                    │  轻量 LR 模型      │
                    └─────────┬─────────┘

                    ┌─────────▼─────────┐
                    │    精排(Ranking)  │  候选从百级 → 几十级
                    │  DeepFM / DIN      │  P99: 20-50ms
                    │  MMOE 多目标       │
                    └─────────┬─────────┘

                    ┌─────────▼─────────┐
                    │    重排(Re-Rank)  │  最终输出 Top-N
                    │  MMR 多样性打散     │  P99: <5ms
                    │  业务规则/广告插入  │
                    └─────────┬─────────┘

                    ┌─────────▼─────────┐
                    │  用户最终看到的 Feed  │
                    └───────────────────┘

召回层(Recall)

目标是从千万级物品池中筛选出千级候选。关键在于多路召回,单一路径永远不够:

召回策略原理候选量级延迟适用场景
向量召回(双塔)用户/物品 Embedding 内积,Faiss 检索 Top-K50010-20ms泛化推荐,基础召回
ItemCF(协同过滤)物品共现矩阵,取"看了A的人也看了B"3005-10ms长尾兜底,关联推荐
热门召回按热度/时效预排序,全局 Top-N200<1ms冷启动、爆发热点
图召回(GraphSAGE)用户-物品交互图随机游走20020-50ms社交关系推荐
语义召回标签/关键词倒排匹配3003-5ms搜索场景

向量召回的生产落地细节:双塔模型一般用 Item-ID 和用户行为序列作为输入,Embedding 维度 64-128(更高维度 recall 提升边际递减,但检索延迟线性增长)。Faiss 索引选择上,HNSW 适合高精度(延迟 10ms、召回率 95%+),IVF-PQ 适合高吞吐(延迟 1-2ms、召回率 85%)。实际生产中,我用 128 维 Embedding + HNSW 32-efSearch 128 配置,在 1000 万物品集上 P99 召回延迟 15ms。

容易踩的坑:向量召回只在训练时覆盖的用户-物品交互上有表现,对新物品/新用户基本失效,所以必须配合多路召回聚合。各路召回取 Top-K 后做合并去重,再进入粗排。

粗排层(Pre-ranking)

千级候选直接进精排计算量太大(单次精排 20-50ms,千级就是 20-50 秒),所以必须用粗排快速筛到百级。

粗排模型选型对比

模型参数量单次推理耗时精度(相对精排)适用条件
双塔内积百万级0.5ms60-70%最轻量,Embedding 复用
双塔+Attention千万级2ms75-80%引入序列特征
LR(逻辑回归)万级0.1ms50-60%极简降级方案

真实场景里,我见过粗排分数和精排分数排序一致性只有 60-70% 的情况,也就是说有 30-40% 的候选在粗排阶段被误杀——但总体接受,因为粗排的目标是宁可漏掉一些好内容,也要保证精排的计算资源不被撑爆

精排层(Ranking)

精排用最复杂的模型做精确 CTR/CVR 预估。主流模型演进:

Wide & Deep (2016) → DeepFM (2017) → DIN (2018) → DCN V2 (2020) → MMOE (2018) → SIM (2020)

DeepFM 架构要点

  • Wide 侧:手工交叉特征(如"用户性别=男 & 物品类目=游戏"),用 LR 直接学习
  • Deep 侧:Dense Embedding → 多层 MLP(一般为 3 层,256→128→64),学习高阶隐式交叉
  • 输出:Sigmoid 做 CTR 预估

精排特征输入构成(以 DeepFM 为例):

python
# 特征拼接伪代码
class DeepFMRanker:
    def predict(self, user_features, item_features, context_features):
        # 稀疏特征:用户 ID、物品 ID、类目 ID → Embedding 层
        sparse_inputs = concat(
            self.user_embedding(user_features['user_id']),      # 64-dim
            self.item_embedding(item_features['item_id']),     # 64-dim
            self.cate_embedding(item_features['category_id'])  # 16-dim
        )
        # 稠密特征:价格、CTR_7d、时长均值等 → 直接拼接
        dense_inputs = concat(
            normalize(item_features['price']),       # 1-dim
            normalize(item_features['ctr_7d']),      # 1-dim
            normalize(item_features['duration_avg']) # 1-dim
        )
        # Wide 侧:手工交叉特征
        wide_features = self.cross_features(user_features, item_features)
        # DeepFM 前向
        deep_out = self.deep_mlp(sparse_inputs, dense_inputs)  # 256→128→64
        wide_out = self.wide_lr(wide_features)
        return sigmoid(deep_out + wide_out)

面试会问的坑:特征交叉爆炸。50 个特征做两两交叉就是 1225 个组合,全部塞进 Wide 侧不现实。实践上只挑高频共现对,或者用 FM 自动学习交叉。另外,精排模型线上推理必须控制特征获取时间——每个特征从 Redis 取花 1ms,30 个特征就 30ms,全链路延迟会超 50ms 的 P99 目标。

重排层(Re-ranking)

精排给出的是"最可能点击"的排序,但实际业务需要更多约束:

  • MMR(最大边际相关性)打散:每选一个物品,不仅考虑相关性,还要考虑与已选物品的相似度。公式:Score = λ * 相关性 - (1-λ) * max(已选物品相似度)。λ 通常取 0.5。
  • 广告插入:从第 4 位开始,每 7-10 个自然结果插入一个广告位,广告本身也经过精排 CTR 预估,但会乘以出价系数。
  • 运营提权:特定节日、活动商品,加权 +20% 排序分。
  • 硬性过滤:已购买、已举报、未成年人不适内容直接删除。

特征工程与特征平台

推荐系统的效果上限(注意不是下限,下限靠模型架构)取决于特征质量。特征体系分为三类:

  • 用户特征:长期画像(性别、年龄、城市、兴趣标签)、短期行为(最近点击/购买/搜索序列、时长分布)
  • 物品特征:内容属性(类目、标签、价格、发布时间)、统计特征(近 7 天曝光/点击/转化率、好评率)
  • 上下文特征:时间(工作日/周末、凌晨/午间)、设备(手机型号、网络环境)、位置(GPS 经纬度)

特征平台承担特征的生产、存储、在线获取:

python
# 真实线上特征服务代码(简化版)
class FeatureService:
    def __init__(self):
        self.redis = RedisCluster('redis://rec-feature:6379')
        self.kv_store = S3AwareKVStore('s3://rec-feature-store/')

    def get_features(self, user_id: str, item_ids: list[str]) -> dict:
        start = time.time()

        # 实时特征(Flink 每 30 秒窗口写入 Redis,过期 5 分钟)
        realtime_key = f"rec:user:{user_id}:realtime"
        realtime = self.redis.mget(realtime_key, fields=['click_seq', 'last_category', 'duration_30s'])

        # 离线特征(Hive T+1 产出,写入 KV 存储,更新时全量替换)
        profile = self.kv_store.get(f"rec:user:{user_id}:profile",
                                     keys=['age', 'gender', 'city', 'tag_weights'])

        # 物品特征(批量从 Redis 拉取,用 pipeline 避免一次取一个)
        pipe = self.redis.pipeline()
        item_keys = [f"rec:item:{item_id}:features" for item_id in item_ids]
        for k in item_keys:
            pipe.hgetall(k)
        item_features = dict(zip(item_ids, pipe.execute()))

        # 耗时打点,超过 20ms 打印警告
        elapsed = time.time() - start
        if elapsed > 0.02:
            logger.warning(f"feature_service latency {elapsed*1000:.1f}ms user={user_id}")

        return merge_features(user=realtime, profile=profile, items=item_features)

特征一致性坑:离线训练和在线推理用的特征值必须一致,否则会有"训练时特征 A 是 0.8,在线变成 0.2"的偏差,导致 CTR 预估偏低。解决方法:在线特征和离线特征用同一套 SQL/Pipeline 产出,且特征日志落盘回头用于离线训练。

在线/离线/近线架构与冷启动

推荐系统有三条数据流并行执行:

时间粒度:   天级  ←——————————————→  毫秒级
            ┌──────────┐   ┌──────────┐   ┌──────────┐
            │ 离线流    │   │ 近线流    │   │ 在线流    │
            ├──────────┤   ├──────────┤   ├──────────┤
            │ Hive/Spark│   │ Flink    │   │ 实时推理  │
            │ 全量训练  │   │ 增量更新  │   │ 特征拼接  │
            │ 模型产出  │   │ Embedding │   │ 规则过滤  │
            │ 用户画像  │   │ 实时特征  │   │ 打分排序  │
            └──────────┘   └──────────┘   └──────────┘

生产事故经验:曾有一次离线模型训练完推送后,特征 schema 不兼容(新模型需要 128 维 Embedding,在线 KV 里存的还是 64 维),导致线上召回的向量维度不匹配,直接零召回持续 20 分钟。之后加了一道 schema 校验:模型上线前,用线上样本集跑一遍推理,对比旧模型输出,差异超过 5% 就自动拦截。

冷启动是推荐系统最棘手的工程问题之一:

冷启动类型问题解决方案上线效果参考
用户冷启动新用户无行为注册画像映射 + 热门兜底 + Bandit 探索新用户 7 日留存提升 15%
物品冷启动新内容无曝光内容属性向量召回 + 小流量探索(1% 流量)新内容曝光量提升 3 倍
系统冷启动新系统无数据内容 Embedding 用预训练模型(如 Sentence-BERT)初始化冷启阶段 CTR 达到成熟期 60%

EE(Explore-Exploit)的具体实现:给每个用户分配一个探索概率 ε,初始为 0.2,随曝光量增加线性衰减到 0.05。探索时随机选非热门物品展示,但限制探索产生的曝光不超过总曝光 5%,避免影响收入。

AB 实验与评估

推荐系统必须用数据说话,AB 实验是核心基础设施。每个推荐策略变更都需要在流量桶上做分桶实验。

实验分层架构

流量入口(100%)
  ├── Layer 1: 召回策略实验(10% = 实验组,90% = 对照组)
  │     ├── 实验组:新向量召回
  │     └── 对照组:旧向量召回
  ├── Layer 2: 精排模型实验(10% / 90%)
  │     ├── 实验组:DeepFM → DCN V2
  │     └── 对照组:DeepFM
  └── Layer 3: 重排规则实验(10% / 90%)
        ├── 实验组:新 MMR 打散 λ=0.6
        └── 对照组:旧 MMR 打散 λ=0.5

核心指标跟踪

  • 核心指标:CTR(点击率,一般提升 0.1% 就算显著)、CVR(转化率,提升 0.05% 就有商业价值)、用户停留时长、人均曝光数
  • 业务指标:GMV(电商)、广告收入、次日留存
  • 质量指标:多样性(Simpson 指数,低于 0.1 说明内容太单一)、覆盖率(长尾内容曝光比例,低于 20% 说明马太效应严重)、新颖性

统计显著性要求:p-value < 0.05,且实验至少运行 7 天,避免周末和工作日行为差异导致的偏差。我之前踩过的坑:一个模型上线实验跑了 3 天 CTR 提升 2%,紧急全量上线,结果第 5 天 CTR 回落到 -0.5%,最后发现是前 3 天赶上促销活动,实验组和对照组的样本分布不一致导致的虚假提升。

总结

推荐系统设计面试的关键产出是一张清晰的分层漏斗图,搭配每条数据流的处理方式。可以用以下话术组织回答:

"我的推荐系统设计分为四层:召回用双塔向量 + 协同过滤,粗排用轻量级双塔筛到百级,精排用 DeepFM 做 CTR 预估,重排加多样性和业务规则。离线走 Hive 训练,近线用 Flink 更新特征,在线用 Redis 提供特征服务。冷启动用热门兜底 + Bandit 探索。AB 实验平台承接所有策略变更验证。"

说给面试官听的工程细节话术

  • 特征实时性保障:Flink 30 秒窗口写入 Redis,特征过期时间 5 分钟,避免数据膨胀
  • 向量检索:128 维 HNSW 索引,P99 延迟 15ms,召回率 95%
  • 全链路 P99 延迟预算:召回 15ms + 粗排 3ms + 精排 50ms + 重排 5ms + 网络 10ms = 83ms,需要压到 50ms 以内的话,精排就得降级模型或减少特征数
  • 特征一致性:离线在线同源产出,线上推全前做 schema 校验和输出一致性校验

参考

参考:《深度学习推荐系统》王喆;Google Wide & Deep / YouTube DNN 论文;Faiss 向量检索库文档;美团推荐系统技术博客。

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。