第3章:推荐系统架构:召回-排序-重排经典架构、离线与实时计算、分层设计思想

聊推荐系统架构,我习惯先讲一个故事。

几年前我接手一个短视频推荐项目,用户量涨得很快,但推荐效果突然崩了。线上延迟飙到800ms,用户刷两下就卡住。我查了半天,发现是召回和排序揉在一个服务里,所有候选集一起算,服务器直接扛不住。

嗯,这就是典型的架构没分层。今天咱们就把这套经典架构掰开揉碎讲清楚。

3.1 召回-排序-重排:三层漏斗

推荐系统的核心,说白了就是一个漏斗。你想想看,全量商品可能有上亿个,但用户最终看到的只有几十个。怎么筛?靠三层:召回、排序、重排。

核心思想:每一层只做一件事,把上一层的输出作为输入,逐步缩小候选集。

3.1.1 召回层:从亿到千

召回的目标是快速从海量物品中捞出一批“可能感兴趣”的候选。速度是第一位的,精度可以放一放。

我个人习惯把召回策略分成几类:

  • 基于内容的召回:用户看过A,就找和A相似的B、C。比如你刷过篮球视频,我就把足球、羽毛球也推给你。
  • 协同过滤召回:用户A和用户B行为相似,A喜欢的东西推给B。经典算法有ItemCF、UserCF。
  • 向量化召回:把用户和物品都映射成向量,用向量相似度检索。现在主流是用双塔模型+Faiss。
  • 热点/冷启动召回:新用户没行为?直接推热门内容。新物品没曝光?用内容特征硬匹配。

我在项目中遇到过一个问题:召回策略太多,每个策略都返回500个结果,加起来好几千。但用户真正需要的可能只有几十个。后来我加了一个多路召回融合的步骤,把各路结果按权重混合,再截断到固定数量。效果好了不少。

避坑指南:我曾经把召回数量设得太大,导致排序层压力爆炸。建议召回量控制在1000-2000之间,具体看你的算力。

3.1.2 排序层:从千到百

排序层负责精排。它用更复杂的模型(比如DeepFM、DIN、DCN),对召回结果逐一点击率预估。目标是选出最可能被点击/转化的Top-N。

排序层有两个关键点:

  • 特征工程:用户特征(年龄、性别、历史行为)、物品特征(类别、标签、发布时间)、上下文特征(时间、地点、设备)。特征越多,模型越准,但训练成本也越高。
  • 模型选择:早期用LR、GBDT,现在主流是深度学习模型。我建议从DeepFM起步,效果好、实现简单。

说白了,排序层就是“精挑细选”。它不在乎速度,只在乎精度。但也不能太慢,毕竟用户等着呢。

3.1.3 重排层:从百到十

重排层是最后一道关卡。它不改变排序结果的整体顺序,但会做一些微调:

  • 多样性控制:不能全是同一类内容。比如用户刷了10个篮球视频,第11个必须换个口味。
  • 去重:同一个视频不能出现两次。
  • 业务规则:广告插入、运营强插、新人扶持等。
  • 打散策略:按类别、作者、发布时间等维度打散,避免内容扎堆。

重排层我见过最坑的事:有个团队忘了做去重,用户连续刷到3个一模一样的视频,直接骂街。嗯,这种低级错误千万别犯。

3.2 离线与实时计算:两条腿走路

推荐系统离不开数据。数据怎么算?分两条线:离线计算和实时计算。

3.2.1 离线计算:慢工出细活

离线计算处理的是历史数据。比如用户过去7天的行为、物品的全量特征。它不要求实时性,但要求准确和全面。

典型场景:

  • 训练模型:每天凌晨跑一次,用全量数据更新模型参数。
  • 生成用户画像:统计用户长期兴趣,比如“喜欢篮球”、“爱看搞笑视频”。
  • 计算物品相似度:离线算好ItemCF的相似矩阵,线上直接查。

离线计算我用得最多的是Spark和Hive。数据量大?没关系,慢慢跑。跑完存到Redis或HBase,线上直接读。

3.2.2 实时计算:快刀斩乱麻

实时计算处理的是用户当下的行为。比如你刚点了一个视频,系统立刻知道“哦,他对这个感兴趣”,然后调整推荐结果。

典型场景:

  • 实时特征更新:用户点击后,特征立刻更新到线上。
  • 实时召回:用户行为触发新的召回策略,比如“看了A,立刻召回和A相似的B”。
  • 实时排序:模型在线预测,毫秒级返回结果。

实时计算我推荐用Flink或Spark Streaming。注意,实时计算不能太重,否则延迟会爆炸。我一般把实时计算控制在100ms以内。

注意:离线计算和实时计算不是二选一,而是互补。离线算长期兴趣,实时算短期意图。两者结合,效果最好。

3.3 分层设计思想:解耦的艺术

分层设计,说白了就是把系统拆成独立的模块,每个模块只干一件事。这样做的好处是:

  • 可维护:改召回不影响排序,改排序不影响重排。
  • 可扩展:想加一个新召回策略?直接加一路,不用动其他代码。
  • 可测试:每一层可以单独测试,出问题能快速定位。

我见过一个反面教材:所有逻辑写在一个大函数里,召回、排序、重排混在一起。后来想加一个“去重”功能,改了三天,还引入了新bug。这就是不分层的代价。

分层设计的具体做法:

  • 数据层:统一管理用户画像、物品特征、行为日志。用Redis做缓存,用HBase做存储。
  • 召回层:多个召回策略并行,结果合并后传给排序层。
  • 排序层:接收召回结果,用模型打分,输出Top-N。
  • 重排层:对排序结果做后处理,输出最终推荐列表。
  • 接口层:对外提供API,封装内部逻辑。

你想想看,如果每一层都独立部署,出了问题可以单独回滚。我曾经因为排序模型上线了一个bug,导致所有用户推荐结果都是空的。但因为分层设计,我直接切回旧模型,5分钟就恢复了。要是没分层,估计得通宵修代码。

3.4 架构实战:一个简化示例

光说不练假把式。我写一个简化版的推荐系统架构代码,帮你理解各层怎么协作。

# 伪代码:推荐系统主流程
def recommend(user_id, request):
    # 1. 召回层:多路召回
    recall_results = []
    recall_results += recall_by_content(user_id)   # 基于内容
    recall_results += recall_by_collaborative(user_id)  # 协同过滤
    recall_results += recall_by_hot(user_id)       # 热点
    recall_results = dedup_and_merge(recall_results, top_k=1000)

    # 2. 排序层:模型打分
    scored_results = []
    for item in recall_results:
        score = rank_model.predict(user_id, item)
        scored_results.append((item, score))
    scored_results.sort(key=lambda x: x[1], reverse=True)
    top_100 = scored_results[:100]

    # 3. 重排层:后处理
    final_list = []
    seen_categories = set()
    for item, score in top_100:
        if item.category not in seen_categories:
            final_list.append(item)
            seen_categories.add(item.category)
        if len(final_list) >= 20:
            break

    return final_list

这段代码虽然简单,但把三层架构的核心逻辑都体现出来了。实际生产环境会更复杂,但思路是一样的。

个人经验:我建议新项目从这套架构起步。别一上来就搞微服务、分布式,先把三层逻辑跑通。等用户量上来了,再考虑拆分和优化。

3.5 总结

推荐系统架构,说白了就是“分而治之”。召回负责广撒网,排序负责精挑细选,重排负责最后把关。离线计算和实时计算各司其职,分层设计让系统更健壮。

我记得刚入行时,总觉得架构是虚的,代码才是实的。后来踩了无数坑才明白:好的架构能让你少加一半的班。嗯,这句话你记着,以后会懂的。