抖音推荐架构

每天有超过 7 亿人打开抖音。每一次滑动屏幕,系统都在 100 毫秒内决定:给你看什么视频?

这个决定背后,是一个极其复杂的推荐系统。它需要:

  • 实时性:用户行为(点赞、评论、完播)需要秒级反馈到推荐模型
  • 个性化:两个用户看到的视频完全不同,每个人的推荐都是「私人定制」
  • 海量内容:每天有数千万新视频上传,需要被分发到感兴趣的用户面前
  • 准确性:推荐不准确,用户会流失;推荐太精准,用户会腻

这就是抖音推荐系统面临的工程挑战。这篇文章从架构视角,解析字节跳动推荐系统的设计与实现。

推荐系统的核心挑战

推荐系统不是「把用户喜欢的视频推给用户」这么简单。在抖音这个体量下,每个「简单」的问题都变得极其复杂。

挑战一:实时性要求

用户刚看完一个视频,系统需要在几秒内调整后续推荐。如果推荐延迟太久,用户会觉得「不跟手」。

实时性分层:

100ms 级:视频推荐排序(从候选池中选出一个视频)
1s 级:用户行为上报(点赞、评论写入推荐系统)
1min 级:实时特征更新(用户的实时兴趣向量)
1h 级:短期兴趣模型更新
1day 级:长期兴趣模型、用户画像更新

延迟超过 200ms,用户的滑动体验会明显下降。

挑战二:规模问题

抖音的规模让几乎所有常规方案失效。

量化数据(截至 2024 年):

日活用户:7 亿+
日均视频上传:数千万
推荐系统每日处理数据量:PB 级
模型特征维度:千亿级
单次推荐请求特征数:万级
每日推荐次数:千亿级

如果用传统方式,每个用户存储 1MB 用户画像:
7 亿用户 × 1MB = 700TB

这只是静态数据,还有实时行为流、特征向量……

挑战三:冷启动问题

新用户没有行为数据,新视频没有曝光记录。推荐系统必须在「信息不足」的情况下做出决策。

冷启动分类:

用户冷启动:新用户没有任何历史行为
- 解决方案:利用外部数据(设备信息、地理位置)、热门内容兜底

视频冷启动:新视频没有任何曝光记录
- 解决方案:流量扶持计划、创作者激励、CPC 竞价保底

特征冷启动:新特征(如新标签)没有足够样本
- 解决方案:迁移学习、特征降维

用户流失召回:用户长期未访问后回来
- 解决方案:长期兴趣模型、周期性重置短期兴趣

推荐系统的三层架构

抖音推荐系统分为三个核心层:召回层(Recall)、精排层(Ranking)、重排层(Re-ranking)。

推荐系统架构图:

┌──────────────────────────────────────────────────────────────┐
│                       推荐请求入口                           │
│                    用户打开抖音,刷新推荐流                    │
└──────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────┐
│                         召回层                               │
│     从千万级内容池中召回千量级候选视频(多路召回)            │
│                                                               │
│     ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐    │
│     │ 行为召回 │  │ 兴趣召回 │  │ 协同过滤 │  │ 热点召回 │    │
│     └─────────┘  └─────────┘  └─────────┘  └─────────┘    │
└──────────────────────────────────────────────────────────────┘

                              ▼ (千量级候选)
┌──────────────────────────────────────────────────────────────┐
│                         精排层                               │
│     对千量级候选进行 CTR/CVR 预估,排序选出百量级            │
│                                                               │
│     ┌─────────────────────────────────────────────────┐      │
│     │           深度学习模型(数百个特征)              │      │
│     │                                                   │      │
│     │  user Embedding + content Embedding → Score     │      │
│     └─────────────────────────────────────────────────┘      │
└──────────────────────────────────────────────────────────────┘

                              ▼ (百量级)
┌──────────────────────────────────────────────────────────────┐
│                         重排层                               │
│     业务规则调整、多样性控制、频控,最终展示                  │
│                                                               │
│     • 频控:一个作者的视频 24h 内不超过 3 条                │
│     • 多样性:穿插不同类型内容                                │
│     • 运营干预:置顶、屏蔽、敏感过滤                         │
└──────────────────────────────────────────────────────────────┘

召回层详解

召回层是推荐系统的「第一道关卡」。它需要从千万级内容池中,快速筛选出用户可能感兴趣的千量级视频。

多路召回策略:

┌─────────────────────────────────────────────────────────┐
│                     用户请求                             │
└─────────────────────────────────────────────────────────┘

        ┌─────────────────┼─────────────────┐
        ▼                 ▼                 ▼
   ┌─────────┐      ┌─────────┐      ┌─────────┐
   │ 行为召回 │      │ 兴趣召回 │      │ 协同过滤 │
   │         │      │         │      │         │
   │ 基于用户│      │ 基于内容 │      │ 基于相似│
   │ 历史行为│      │ 标签匹配 │      │ 用户推荐│
   └────┬────┘      └────┬────┘      └────┬────┘
        │                 │                 │
        └─────────────────┼─────────────────┘

                   ┌─────────────┐
                   │   并集去重   │
                   └─────────────┘

行为召回:根据用户历史观看、点赞、评论的视频,找到「相似」的视频。

# 行为召回示例:基于用户 last_n 行为召回相似内容
class BehaviorRecall:
    def recall(self, user_id: str, last_n: int = 20) -> List[str]:
        # 1. 获取用户最近 N 次交互的视频 ID
        watched_videos = self.user_behavior.get_recent(user_id, last_n)

        # 2. 获取这些视频的内容 ID
        video_ids = [v.id for v in watched_videos]

        # 3. 通过协同过滤找到相似视频
        similar_videos = self.cf_model.get_similar(video_ids)

        # 4. 过滤用户已看过的
        return self.filter_watched(user_id, similar_videos)

兴趣召回:基于用户的兴趣标签,从内容池中召回匹配的视频。

# 兴趣召回示例:基于用户兴趣标签召回
class InterestRecall:
    def recall(self, user_profile: UserProfile, limit: int = 200) -> List[str]:
        # 1. 获取用户兴趣标签及权重
        interest_tags = user_profile.get_top_interests(weight_threshold=0.3)

        # 2. 按标签召回视频
        results = []
        for tag, weight in interest_tags.items():
            # 召回该标签下的新视频(7 天内发布)
            videos = self.content_index.search(
                tag=tag,
                filters=["publish_time > 7_days_ago"],
                limit=limit
            )
            # 加权排序
            for v in videos:
                v.score *= weight
            results.extend(videos)

        # 3. 合并去重
        return deduplicate_and_sort(results, limit)

协同过滤召回:找「相似用户」,把相似用户喜欢的内容推荐给你。

# 协同过滤召回示例
class CFRecall:
    def recall(self, user_id: str, limit: int = 200) -> List[str]:
        # 1. 找到与该用户最相似的 N 个用户
        similar_users = self.user_similarity.find_top_k(user_id, k=100)

        # 2. 获取相似用户喜欢但该用户未看过的视频
        candidate_videos = set()
        for sim_user_id, similarity_score in similar_users:
            sim_user_videos = self.user_behavior.get_liked(sim_user_id)
            for video_id in sim_user_videos:
                if not self.user_behavior.has_watched(user_id, video_id):
                    candidate_videos.add((video_id, similarity_score))

        # 3. 按相似度加权排序
        return sorted(candidate_videos, key=lambda x: x[1], reverse=True)[:limit]

精排层详解

精排层对召回层返回的千量级候选进行 CTR/CVR(点击率/转化率)预估,给每个视频打一个分数。

精排模型架构(简化版):

┌────────────────────────────────────────────────────────────┐
│                       精排模型                              │
│                                                            │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐   │
│  │ 用户特征     │    │ 视频特征     │    │ 上下文特征   │   │
│  │ (千维度)     │    │ (千维度)     │    │ (百维度)     │   │
│  └──────┬──────┘    └──────┬──────┘    └──────┬──────┘   │
│         │                   │                   │          │
│         ▼                   ▼                   ▼          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              Deep Neural Network                     │   │
│  │                                                       │   │
│  │   Embedding Layer ──► MLP ──► Output (CTR Score)    │   │
│  │                                                       │   │
│  └─────────────────────────────────────────────────────┘   │
│                            │                               │
│                            ▼                               │
│                      ┌─────────┐                          │
│                      │排序得分  │                          │
│                      └─────────┘                          │
└────────────────────────────────────────────────────────────┘

精排层的核心是特征工程和模型结构。

# 精排特征示例
class RankingFeatures:
    def extract(self, user_id: str, video_id: str) -> Dict[str, float]:
        return {
            # 用户特征
            "user_active_days": self.get_user_active_days(user_id),
            "user_follow_count": self.get_user_follow_count(user_id),
            "user_video_like_rate": self.get_user_like_rate(user_id),

            # 视频特征
            "video_like_count": self.get_video_likes(video_id),
            "video_comment_count": self.get_video_comments(video_id),
            "video_complete_rate": self.get_video_complete_rate(video_id),
            "video_publish_hour": self.get_video_publish_hour(video_id),

            # 交叉特征
            "user_video_match_score": self.get_match_score(user_id, video_id),
            "user_author_interaction": self.get_user_author_interaction(user_id, video_id.author_id),

            # 实时特征
            "user_realtime_interest_1h": self.get_realtime_interest(user_id, "1h"),
            "user_realtime_interest_24h": self.get_realtime_interest(user_id, "24h"),
        }

精排模型的典型结构是 Deep & Cross Network (DCN) 或 DeepFM:

# 简化的 DCN 模型结构
class DCNModel:
    def __init__(self, feature_dim: int):
        # Cross Network:自动特征交叉
        self.cross_layers = [
            CrossLayer(input_dim=feature_dim)
            for _ in range(3)
        ]

        # Deep Network:隐式特征学习
        self.deep_layers = [
            Linear(input_dim=feature_dim, output_dim=512),
            Linear(input_dim=512, output_dim=256),
            Linear(input_dim=256, output_dim=128),
        ]

        # 输出层
        self.output_layer = Linear(input_dim=128 + feature_dim, output_dim=1)

    def predict(self, features: Tensor) -> float:
        # Cross Network
        x_cross = features
        for cross_layer in self.cross_layers:
            x_cross = cross_layer(x_cross, features)

        # Deep Network
        x_deep = features
        for deep_layer in self.deep_layers:
            x_deep = deep_layer(F.relu(x_deep))

        # 合并
        x = torch.concat([x_cross, x_deep], dim=-1)

        # 输出
        return torch.sigmoid(self.output_layer(x))

重排层详解

重排层在精排结果基础上,应用业务规则进行最终调整。

重排层核心逻辑:

1. 频控(Exploitation Control)
   - 同一个作者的视频:24h 内不超过 3 条
   - 同一个话题的视频:24h 内不超过 5 条
   - 用户已看过的视频:直接过滤

2. 多样性(Diversity)
   - 内容类型穿插:舞蹈、美食、知识、搞笑……
   - 避免连续推送同一类型内容
   - MMR(Maximal Marginal Relevance)算法

3. 运营干预
   - 敏感内容过滤
   - 置顶内容
   - 热点事件加权
   - 新用户引导内容

4. 最终展示
   - 按调整后的分数排序
   - 拼接成推荐流返回
# 重排示例:MMR 算法实现多样性
class ReRanker:
    def rerank(self, candidates: List[Candidate], k: int = 20) -> List[Candidate]:
        selected = []
        remaining = candidates.copy()

        while len(selected) < k and remaining:
            best_score = float('-inf')
            best_item = None

            for item in remaining:
                # 相关性分数(来自精排层)
                relevance = item.score

                # 多样性分数:与已选内容的不相似度
                diversity = min([
                    1 - self.similarity(item, s)
                    for s in selected
                ], default=1.0)

                # MMR = λ * 相关性 + (1-λ) * 多样性
                mmr_score = 0.6 * relevance + 0.4 * diversity

                if mmr_score > best_score:
                    best_score = mmr_score
                    best_item = item

            if best_item:
                selected.append(best_item)
                remaining.remove(best_item)

        return selected

实时特征工程

实时特征是抖音推荐的「灵魂」。用户刚刚点了一个美食视频,系统需要在秒级内把这个信号反馈到推荐模型。

实时计算架构

实时特征计算架构:

┌──────────────────────────────────────────────────────────────────┐
│                        用户行为事件流                            │
│                                                                 │
│   播放事件   点赞事件   评论事件   分享事件                        │
│       │         │         │         │                           │
└───────┴─────────┴─────────┴─────────┴────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│                      Apache Kafka                                │
│                   消息队列(万亿级吞吐)                          │
└──────────────────────────────────────────────────────────────────┘

                    ┌─────────────┼─────────────┐
                    ▼             ▼             ▼
            ┌───────────┐ ┌───────────┐ ┌───────────┐
            │ Flink 实时│ │ Flink 实时│ │ Flink 实时│
            │ 统计计算  │ │ 特征更新  │ │ 模型触发  │
            └─────┬─────┘ └─────┬─────┘ └─────┬─────┘
                  │             │             │
                  ▼             ▼             ▼
            ┌───────────┐ ┌───────────┐ ┌───────────┐
            │  分钟级   │ │  秒级特征  │ │  在线学习 │
            │  聚合指标 │ │  写入 Redis│ │  模型更新 │
            └───────────┘ └───────────┘ └───────────┘

Flink 是字节跳动实时计算的核心引擎。它处理用户行为事件,实时更新用户特征。

// Flink 实时特征计算示例
public class UserRealtimeFeatureJob {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1000);  // 千级并行度

        // 1. 从 Kafka 读取用户行为事件
        DataStream<UserEvent> events = env
            .addSource(new KafkaSource<>("user-behavior-topic"))
            .assignTimestampsAndWatermarks(
                WatermarkStrategy
                    .<UserEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                    .withTimestampAssigner((e, ts) -> e.getTimestamp())
            );

        // 2. 按用户分组,实时统计
        DataStream<UserRealtimeFeatures> features = events
            .keyBy(UserEvent::getUserId)
            .window(SlidingEventTimeWindows.of(Time.minutes(1), Time.seconds(10)))
            .process(new RealtimeFeatureProcessFunction());

        // 3. 写入 Redis(秒级延迟)
        features.addSink(new RedisSink<>(
            redisConfig,
            (feature, context) -> {
                String key = "user:feature:" + feature.getUserId();
                String value = JSON.toJSONString(feature);
                return new RedisRequest(RequestType.HSET, key, value, "EX 3600");
            }
        ));

        env.execute("User Realtime Feature Job");
    }
}

// 实时特征计算函数
public class RealtimeFeatureProcessFunction
        extends KeyedProcessFunction<Long, UserEvent, UserRealtimeFeatures> {

    // 状态:窗口内的行为计数
    private ValueState<Integer> playCountState;
    private ValueState<Integer> likeCountState;
    private ValueState<Double> avgWatchTimeState;

    @Override
    public void open(Configuration parameters) {
        playCountState = getRuntimeContext().getState(
            new ValueStateDescriptor<>("playCount", Integer.class));
        likeCountState = getRuntimeContext().getState(
            new ValueStateDescriptor<>("likeCount", Integer.class));
        avgWatchTimeState = getRuntimeContext().getState(
            new ValueStateDescriptor<>("avgWatchTime", Double.class));
    }

    @Override
    public void processElement(UserEvent event, Context ctx, Collector<UserRealtimeFeatures> out) {
        // 更新状态
        playCountState.update(playCountState.value() + 1);

        if (event.getType() == EventType.LIKE) {
            likeCountState.update(likeCountState.value() + 1);
        }

        // 计算滑动平均观看时长
        double currentAvg = avgWatchTimeState.value() != null ? avgWatchTimeState.value() : 0;
        double newAvg = (currentAvg * playCountState.value() + event.getWatchTime())
                       / (playCountState.value() + 1);
        avgWatchTimeState.update(newAvg);

        // 输出实时特征(每 10 秒输出一次)
        ctx.timerService().registerEventTimeTimer(
            ctx.timerService().currentWatermark() + 10000
        );
    }

    @Override
    public void onTimer(long timestamp, OnTimerContext ctx, Collector<UserRealtimeFeatures> out) {
        // 触发时输出特征
        out.collect(new UserRealtimeFeatures(
            ctx.getCurrentKey(),
            playCountState.value(),
            likeCountState.value(),
            avgWatchTimeState.value()
        ));
    }
}

特征存储

实时特征需要高速读写。Redis 是字节跳动特征存储的核心。

特征存储架构:

┌──────────────────────────────────────────────────────────────────┐
│                         推荐服务                                  │
│                      (毫秒级读取)                               │
└──────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│                         Redis Cluster                             │
│                                                                 │
│  Key: user:feature:{user_id}                                     │
│  Value: {                                                        │
│    "realtime_play_1h": 15,      // 过去 1 小时播放数               │
│    "realtime_like_1h": 3,       // 过去 1 小时点赞数               │
│    "realtime_interest_tags":    // 实时兴趣标签                    │
│      ["美食", "健身", "知识"],                                       │
│    "last_watch_category": "美食",                                 │
│    "last_watch_author": "李子柒",                                  │
│  }                                                               │
└──────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│                         HBase                                     │
│                    (历史特征存储)                                │
│                                                                 │
│  用户长期兴趣、每日统计、历史行为序列                                │
└──────────────────────────────────────────────────────────────────┘

抖音与今日头条的推荐差异

抖音和今日头条都属于字节跳动,但推荐系统有很大差异。

核心差异对比:

| 维度 | 抖音 | 今日头条 |
| --- | --- | --- |
| 内容形态 | 短视频(15s~10min)| 图文、长文、短视频 |
| 消费模式 | 被动推荐为主 | 搜索 + 推荐并重 |
| 完播率 | 核心指标(视频必须看完才过瘾)| 阅读时长 |
| 封面质量 | 极其重要(影响点击)| 标题更重要 |
| 沉浸感 | 强(无限下滑)| 中等(有限刷新)|

内容理解差异:

抖音需要更深的视频理解能力:
- 视频分类:舞蹈、美食、知识、生活……
- 场景识别:室内、户外、街景、演播室
- 人物识别:明星、达人、素人
- 音乐识别:BGM 是什么歌
- 特效识别:用了什么滤镜/特效

今日头条侧重文本理解能力:
- 文章分类:财经、科技、体育、娱乐……
- 关键词提取:文章讲了什么
- 作者影响力:账号权重
- 时效性:是否是热点

量化数据与性能指标

抖音推荐系统量化数据(估算,基于公开资料):

规模数据:
- 日活用户:7 亿+
- 日均推荐次数:千亿级
- 日均视频上传:数千万
- 单次推荐耗时:`<` 100ms
- 推荐系统每日处理数据量:PB 级

模型数据:
- 精排模型参数规模:千亿级
- 特征维度:千亿
- 日均模型训练样本:百亿级
- 模型更新频率:小时级

基础设施:
- 在线服务集群:10 万+ 容器
- Kafka 集群:单集群百万级 QPS
- Redis 集群:PB 级存储
- Flink 作业:数千并发

性能指标(目标值):
- 推荐延迟 P99:`<` 100ms
- 推荐服务可用性:`>` 99.99%
- 特征新鲜度:`<` 1 分钟

推荐系统的工程挑战

特征存储挑战

挑战:千亿特征如何存储和读取?

解决方案:分层存储

┌─────────────────────────────────────────┐
│            在线特征存储(Redis)           │
│     热点特征(活跃用户、热门视频)           │
│     延迟:`<` 1ms                         │
└─────────────────────────────────────────┘


┌─────────────────────────────────────────┐
│            近线特征存储(HBase)           │
│     中等热度特征                          │
│     延迟:`<` 10ms                        │
└─────────────────────────────────────────┘


┌─────────────────────────────────────────┐
│            离线特征存储(HDFS)            │
│     冷数据(长期兴趣、历史统计)            │
│     延迟:小时级                          │
└─────────────────────────────────────────┘

模型训练挑战

挑战:百亿样本、千亿参数的模型如何高效训练?

解决方案:分布式训练 + 在线学习

┌─────────────────────────────────────────────────────┐
│                    分布式训练架构                     │
│                                                       │
│   ┌─────────┐  ┌─────────┐  ┌─────────┐               │
│   │ Worker 1│  │ Worker 2│  │ Worker N│               │
│   │ GPU: A100│  │ GPU: A100│  │ GPU: A100│              │
│   └────┬────┘  └────┬────┘  └────┬────┘               │
│        │            │            │                     │
│        └────────────┼────────────┘                     │
│                     ▼                                  │
│              ┌─────────────┐                           │
│              │ Parameter Server│                      │
│              │  参数服务器   │                           │
│              │ 分布式存储梯度│                           │
│              └─────────────┘                           │
└─────────────────────────────────────────────────────┘

训练优化:
- 混合精度训练:FP16 加速
- 梯度压缩:减少通信开销
- 异步训练:加速迭代
- 在线学习:小时级增量更新

在线服务挑战

挑战:10 万 QPS 的推荐请求如何在 100ms 内完成?

解决方案:全链路优化

┌────────────────────────────────────────────────────────┐
│                     请求处理链路                        │
│                                                         │
│  1. 请求接入(`[` 5ms `]`)                              │
│     └─► API 网关,连接复用                               │
│                                                         │
│  2. 特征获取(`[` 30ms `]`)                              │
│     └─► Redis Cluster,本地缓存                           │
│                                                         │
│  3. 模型推理(`[` 20ms `]`)                              │
│     └─► TF Serving,Batching 优化                        │
│                                                         │
│  4. 重排逻辑(`[` 5ms `]`)                               │
│     └─► 本地计算,无远程调用                              │
│                                                         │
│  5. 结果返回(`[` 5ms `]`)                              │
│     └─► 序列化 JSON                                      │
│                                                         │
│  总计:`<` 65ms(P99 `<` 100ms)                          │
└────────────────────────────────────────────────────────┘

优化手段:
- 本地缓存:热点特征本地缓存
- Batching:将多个请求合并一次推理
- 模型蒸馏:大模型蒸馏成小模型
- 提前召回:用户滑到一半时提前加载

推荐系统对业务的支撑

推荐系统的业务价值:

1. 用户留存
   - 个性化推荐提升用户粘性
   - 抖音用户日均使用时长:120+ 分钟

2. 内容分发
   - 新视频冷启动:发布后 1 小时内获得曝光
   - 长尾内容:让小众内容找到受众

3. 广告变现
   - 推荐广告与内容融合
   - 广告 CTR:行业领先水平

4. 创作者生态
   - 公平的内容分发机制
   - 优质创作者获得更多曝光

总结

抖音推荐系统是工程与算法的深度结合。

核心架构:三层漏斗

召回层:从千万内容池召回千量级候选
- 多路召回:行为召回、兴趣召回、协同过滤、热点召回
- 挑战:召回率和召回速度的平衡

精排层:对千量级候选打分排序
- 深度学习模型:DCN、DeepFM
- 核心:特征工程 + 模型结构

重排层:业务规则调整
- 频控、多样性、运营干预
- 挑战:用户体验与商业目标的平衡

实时特征工程:

Kafka + Flink + Redis 架构
- 秒级延迟的特征更新
- 千亿级特征的高速读写

工程挑战:

1. 规模问题:千亿特征、PB 级数据
2. 实时性:`<` 100ms 的推荐延迟
3. 冷启动:新用户、新视频的推荐
4. 效率:万级 QPS 的模型推理

思考题

问题 1:抖音推荐系统如何平衡「用户想看的内容」(短期兴趣)和「对用户长期有价值的内容」(长期兴趣)?

参考答案

这是一个推荐系统经典问题,叫「EE(Exploration-Exploitation)问题」。

短期兴趣(Exploitation):用户刚看完美食视频,给他推更多美食视频,点击率会高。

长期兴趣(Exploration):用户可能对科技内容有兴趣,但从来没看过,推给他可能流失,但长期看能拓展用户兴趣面。

平衡策略:

  1. 多兴趣建模:用 Multi-Interest Captor 等模型,把用户短期兴趣分成多个向量,分别召回不同类型内容

  2. Bandit 算法:用 epsilon-greedy 或 Thompson Sampling,在「已知好内容」和「探索未知内容」之间做概率分配

  3. 品类型多样性:硬性要求推荐结果中包含 N 个不同类型/话题的内容

  4. 用户分群:对新用户多探索(冷启动),对老用户偏重准确(数据充足)

实际做法:抖音会同时维护「短期兴趣模型」和「长期兴趣模型」,在精排阶段分别打分,最终加权融合。

问题 2:如何防止推荐系统「过度优化」导致的信息茧房问题?

参考答案

信息茧房是指系统过度迎合用户现有兴趣,导致用户视野越来越窄。

抖音的应对策略:

  1. 兴趣探索机制

    • 每隔 N 次刷新,强制插入「随机/热门」内容
    • 用户探索新兴趣时给予奖励(高权重曝光)
  2. 多样性硬约束

    • 重排层强制要求内容类型分散
    • 同一类型内容最多连续出现 3 条
  3. 负反馈建模

    • 用户点击「不感兴趣」后,强化负反馈信号
    • 避免因为一次误点击导致整个兴趣跑偏
  4. 涟漪效应抑制

    • 热门视频被大量用户喜欢,但不一定适合所有人
    • 对热点视频加「去热度」权重,降低马太效应

核心原则:推荐系统的目标不是最大化短期点击率,而是最大化用户长期留存。

问题 3:如果推荐模型出现 bug,导致所有用户的推荐都变差了,如何快速发现和止损?

参考答案

这是一个工程容错问题。

快速发现:

  1. 指标监控:实时监控全局 CTR、完播率、人均观看时长
    • 发现异常(如 CTR 下降 5%)立即告警
  2. AB 测试对照组:流量分为实验组和对照组
    • 对照组用旧模型,实验组用新模型
    • 对照组指标异常说明新模型有问题

快速止损:

  1. 一键回滚:模型支持版本切换,出问题切回旧版本
    • 字节跳动通常保留最近 10 个版本的模型
  2. 流量切换:API 网关层可以把实验组流量切回旧服务
  3. 熔断降级:推荐服务不可用时,降级为「热门推荐」兜底

预防措施:

  1. 灰度发布:新模型先上 1% 流量,观察 24 小时
  2. 金丝雀告警:检测到指标异常立即暂停
  3. 自动化测试:上线前用历史数据回测