X (Twitter) 推荐系统架构设计深度解析
X (Twitter) 推荐系统架构设计深度解析
本文基于
xai-org/x-algorithm开源仓库的源码分析,系统性地解读了 X 平台 "For You" 信息流的推荐算法架构、核心机制与设计哲学。
目录
- 系统架构总览
- 召回阶段:双塔模型
- 精排阶段:Transformer 排序模型
- 特征工程:Embedding 与 Hash Trick
- 关键设计决策
- 工程实践要点
- 总结与思考
1. 系统架构总览
1.1 核心组件
X 的推荐系统由三个核心模块构成:
| 组件 | 技术栈 | 职责 |
|---|---|---|
| Home Mixer | Rust | Pipeline 编排层,负责组装整个推荐流水线 |
| Phoenix | JAX/Python | ML 模型层,提供召回 (Retrieval) 和精排 (Ranking) 模型 |
| Thunder | Rust | 实时数据层,提供 In-Network(关注的人)推文流 |
1.2 推荐漏斗
┌─────────────────────────────────────────────────────────────────┐
│ 全量推文库 │
│ (~数亿条推文) │
└───────────────────────────┬─────────────────────────────────────┘
│
▼ Thunder (关注) + Phoenix Retrieval (推荐)
┌─────────────────────────────────────────────────────────────────┐
│ 候选集 │
│ (~1000 条) │
└───────────────────────────┬─────────────────────────────────────┘
│
▼ Pre-Scoring Filters (初筛)
┌─────────────────────────────────────────────────────────────────┐
│ 过滤集 │
│ (~500 条) │
└───────────────────────────┬─────────────────────────────────────┘
│
▼ Phoenix Scorer (精排 Transformer)
┌─────────────────────────────────────────────────────────────────┐
│ 精排集 │
│ (~32 条) │
└───────────────────────────┬─────────────────────────────────────┘
│
▼ Weighted Scorer + Selection
┌─────────────────────────────────────────────────────────────────┐
│ 最终 Feed │
│ (Top K 展示) │
└─────────────────────────────────────────────────────────────────┘
2. 召回阶段:双塔模型
2.1 为什么需要双塔?
核心矛盾 :精排模型虽然精准,但计算复杂度 O(N),无法遍历数亿推文。
解决方案 :双塔模型通过解耦 (Decoupling) 实现极速召回。
2.2 双塔架构
┌─────────────────┐ ┌─────────────────┐
│ User Tower │ │ Candidate Tower│
│ (Transformer) │ │ (MLP) │
├─────────────────┤ ├─────────────────┤
│ - 用户 ID │ │ - 推文 ID │
│ - 历史行为序列 │ │ - 作者 ID │
│ - 用户特征 │ │ - 推文特征 │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
V_user [256] V_item [256]
│ │
└──────────────┬───────────────────────┘
│
▼
Similarity = V_user · V_item
│
▼
Top-K 召回
2.3 核心优势
| 特性 | 说明 |
|---|---|
| 物品向量可预计算 | 推文发布时即可离线计算 Embedding,存入向量数据库 |
| 用户向量实时计算 | 请求时只需跑一次 User Tower |
| ANN 极速检索 | 利用近似最近邻算法,毫秒级从亿级候选中检索 Top-K |
2.4 共享向量空间的学习
问题 :用户特征(年龄、性别)和物品特征(文本、图片)看似不在同一空间,为何能计算相似度?
答案 :通过监督学习强行对齐。
训练数据: (用户A, 推文B, 点赞=1) -- 正样本
(用户A, 推文C, 滑过=0) -- 负样本
Loss Function: 让 V_A · V_B ↑, 让 V_A · V_C ↓
结果: 神经网络自动学会把"喜欢篮球的用户"和"NBA推文"映射到同一区域
3. 精排阶段:Transformer 排序模型
3.1 与召回的区别
| 维度 | 召回 (双塔) | 精排 (Transformer) |
|---|---|---|
| 处理规模 | 亿级 → 千级 | 百级 → 十级 |
| 交互方式 | 独立编码,仅点积 | 全交互 Attention |
| 精度 | 粗糙 | 精准 |
| 计算复杂度 | O(1)(配合索引) | O(N times S) |
3.2 输入序列构造
精排模型将所有信息拼接成一个长序列:
输入序列 = [用户特征 | 历史记录1 | ... | 历史记录S | 候选推文1 | ... | 候选推文C]
形状: [Batch, 1 + 128 + 32, 256] = [Batch, 161, 256]
3.3 Candidate Isolation (候选隔离)
问题 :如果候选推文之间可以互相 Attention,评分会受批次内其他推文影响,不稳定。
解决方案 :特殊的 Attention Mask。
Keys (被看的位置)
─────────────────────────────────────────────▶
│ 用户 │ 历史 │ 候选 │
┌─────┼──────┼──────────────────┼──────────────────┤
Q │ 用户│ ✓ │ ✓ ✓ ✓ ✓ ✓ ✓ ✓ │ ✗ ✗ ✗ ✗ ✗ ✗ │
u ├─────┼──────┼──────────────────┼──────────────────┤
e │ 历史│ ✓ │ ✓ ✓ ✓ ✓ ✓ ✓ ✓ │ ✗ ✗ ✗ ✗ ✗ ✗ │
r ├─────┼──────┼──────────────────┼──────────────────┤
i │ │ │ │ 只能看自己 │
e │ 候选│ ✓ │ ✓ ✓ ✓ ✓ ✓ ✓ ✓ │ ✓ ✗ ✗ ✗ ✗ ✗ │
s │ │ │ │ ✗ ✓ ✗ ✗ ✗ ✗ │
│ │ │ │ ✗ ✗ ✓ ✗ ✗ ✗ │
└─────┴──────┴──────────────────┴──────────────────┘
规则:
- 候选 → 用户/历史: ✓ (可以看)
- 候选 → 其他候选: ✗ (不能看)
- 候选 → 自己: ✓ (可以 Self-Attention)
3.4 多目标预测
模型不输出单一分数,而是预测 所有交互行为的概率 :
输出: [B, Num_Candidates, Num_Actions]
= [32, 32, 15]
Actions:
├── P(favorite) # 点赞
├── P(reply) # 评论
├── P(repost) # 转发
├── P(click) # 点击
├── P(video_view) # 视频观看
├── P(share) # 分享
├── P(follow_author) # 关注作者
├── P(not_interested) # 不感兴趣 (负面)
├── P(block_author) # 屏蔽 (负面)
├── P(mute_author) # 静音 (负面)
└── P(report) # 举报 (负面)
3.5 加权打分
WeightedScorer 将多目标概率组合为最终分数:
text{Score} = sum_{i} w_i times P(text{action}_i)
- 正面行为(点赞、转发):正权重
- 负面行为(屏蔽、举报):负权重
优势 :业务层可通过调整权重快速改变推荐策略,无需重新训练模型。
4. 特征工程:Embedding 与 Hash Trick
4.1 三张 Embedding Table 架构
X 的 Phoenix 模型使用了 三张独立的 Embedding Table :
┌─────────────────────────────────────────────────────────────────┐
│ Embedding Table 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ │
│ │ User Table │ 用户 ID → Embedding │
│ │ [N_user, D] │ 例如: [10,000,000, 256] │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │
│ │ Post Table │ 推文 ID → Embedding │
│ │ [N_post, D] │ 例如: [100,000,000, 256] │
│ │ │ (历史推文和候选推文共用此表) │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │
│ │ Author Table │ 作者 ID → Embedding │
│ │ [N_author, D] │ 例如: [50,000,000, 256] │
│ │ │ (历史作者和候选作者共用此表) │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
代码依据 (recsys_model.py):
class RecsysEmbeddings(NamedTuple):
user_embeddings: ... # 来自 User Table
history_post_embeddings: ... # 来自 Post Table
candidate_post_embeddings: ... # 来自 Post Table (同一张表)
history_author_embeddings: ... # 来自 Author Table
candidate_author_embeddings: ... # 来自 Author Table (同一张表)
注意 :用户和作者虽然本质上都是 UserID,但使用的是 不同的表 。User Table 代表"读者的兴趣画像",Author Table 代表"作者的内容风格/人设"。
4.2 Hash Trick 完整流程
4.2.1 配置参数
@dataclass
class HashConfig:
num_user_hashes: int = 2 # 用户 ID 用 2 个 Hash 函数
num_item_hashes: int = 2 # 推文 ID 用 2 个 Hash 函数
num_author_hashes: int = 2 # 作者 ID 用 2 个 Hash 函数
4.2.2 从原始 ID 到 Embedding 的完整流程
以推文 ID 为例:
┌─────────────────────────────────────────────────────────────────┐
│ 推文 ID → Embedding 完整流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 原始推文 ID: 1757483920193847296 (Snowflake 格式) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Hash 函数 (2 个不同的 seed) │ │
│ │ hash1 = xxhash64(tweet_id, seed=1) % TABLE_SIZE │ │
│ │ hash2 = xxhash64(tweet_id, seed=2) % TABLE_SIZE │ │
│ │ │ │
│ │ 假设 TABLE_SIZE = 10,000,000 │ │
│ │ hash1 = 888 │ │
│ │ hash2 = 1024 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ post_hashes = [888, 1024] # 形状: [2] │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Embedding Table 查表 │ │
│ │ │ │
│ │ Post_Table[888] → V1 = [0.12, -0.34, ..., 0.56] [256]│ │
│ │ Post_Table[1024] → V2 = [0.78, 0.23, ..., -0.11] [256]│ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ post_embeddings = [V1, V2] # 形状: [2, 256] │
│ │
└─────────────────────────────────────────────────────────────────┘
4.3 Reshape 操作:多向量合并
查表得到多个向量后,需要通过 Reshape (展平) 合并为单个向量:
# 查表后的原始形状
history_post_embeddings.shape = [B, S, 2, 256]
# ^ ^ ^ ^
# | | | └── Embedding 维度 D
# | | └── num_item_hashes = 2
# | └── 历史序列长度 S (如 128)
# └── Batch 大小 B
# Reshape: 把最后两维 [2, 256] 展平成 [512]
history_post_embeddings_reshaped = embeddings.reshape((B, S, 2 * 256))
# 结果形状: [B, S, 512]
本质 :把两个 256 维向量首尾相接成一个 512 维向量。
V1 = [0.12, -0.34, ..., 0.89] # 256 维
V2 = [0.78, 0.23, ..., 0.45] # 256 维
V_combined = [V1 | V2] = [0.12, -0.34, ..., 0.89, 0.78, 0.23, ..., 0.45]
# 512 维
所有实体的 Reshape :
| 实体 | Reshape 前 | Reshape 后 |
|---|---|---|
| 用户 | [B, 2, 256] |
[B, 512] |
| 历史推文 | [B, S, 2, 256] |
[B, S, 512] |
| 历史作者 | [B, S, 2, 256] |
[B, S, 512] |
| 候选推文 | [B, C, 2, 256] |
[B, C, 512] |
| 候选作者 | [B, C, 2, 256] |
[B, C, 512] |
4.4 单条推文的特征构建
Reshape 后,还需将多个维度的特征 拼接并投影 :
┌─────────────────────────────────────────────────────────────────┐
│ 单条推文特征向量 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────┐ │
│ │ 推文 ID │ │ 作者 ID │ │ 用户行为 │ │ 场景 │ │
│ │ (Hash x2) │ │ (Hash x2) │ │ (Multi-Hot) │ │(枚举)│ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──┬───┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ [512] [512] [256] [256] │
│ │ │ │ │ │
│ └────────┬────────┴────────┬────────┴──────┬───────┘ │
│ │ │
│ ▼ Concatenate │
│ [1536] │
│ │ │
│ ▼ Linear Projection │
│ [256] ────▶ 进入 Transformer │
│ │
└─────────────────────────────────────────────────────────────────┘
4.5 OOV (Out-of-Vocabulary) 问题的解决
4.5.1 Hash Trick 本身就是 OOV 解决方案
核心洞察 :使用 Hash Trick 后, 不存在真正的 OOV 。
def get_embedding(any_id: int, table_size: int) - > int:
# 无论 ID 是什么,总能映射到表中的某一行
return hash(any_id) % table_size
任何新 ID 都会被 Hash 函数映射到 [0, TABLE_SIZE) 范围内,不会出现"找不到"的情况。
4.5.2 冲突带来的"隐式 OOV"
虽然技术上没有 OOV,但新 ID 会 继承旧 ID 的 Embedding (Hash 冲突):
新推文 X (刚发布,从未训练过)
│
▼ Hash
槽位 888 (之前被老推文 A 训练过)
│
▼ 查表
Embedding = 老推文 A 的语义向量 (可能完全不相关)
4.5.3 X 的三层防御机制
层 1:Multi-Hash 组合唯一性
# 新推文 X
hash1(X) = 888 # 撞了老推文 A
hash2(X) = 5001 # 撞了老推文 B
# 组合 (888, 5001) 是全新的,提供了区分度
层 2:Author Embedding 兜底
# 新推文 X 的特征
post_embedding = 噪声 (推文 ID 是新的)
author_embedding = 准确 (作者是老用户)
# Transformer 会自动 attend 到更可靠的信号
层 3:在线学习快速修正
t=0: Embedding 是噪声
t=5min: 收集到 1000 次反馈
t=10min: 梯度下降更新 Table[888] 和 Table[5001]
t=15min: Embedding 已包含推文 X 的真实语义
4.6 Hash 冲突的容忍性
关键洞察 :推荐是 概率排序任务 ,不需要精确语义匹配。
| 对比 | LLM 词向量 | 推荐系统 ID Embedding |
|---|---|---|
| 映射方式 | 一对一 | 多对一 (Hash 冲突) |
| 是否允许冲突 | ❌ 不允许 | ✅ 可容忍 |
| 任务类型 | 精确生成 | 概率排序 |
| 错误代价 | 句子完全错乱 | Top-3 变 Top-5,可接受 |
冲突为何不致命 :
- Multi-Hash 组合冲突率极低 ($10^{-12}$)
- 冲突是随机噪声,统计上被正确信号淹没
- 有正则化效果,防止过拟合单条推文
4.7 开源代码中的特征缺失
重要发现 :X 开源的代码中 没有显式的内容特征 (文本 Embedding、图片特征)。
模型主要依赖:
- 协同过滤信号 :用户行为(点赞、转发等)
- 隐式语义学习 :相似内容被相似用户喜欢 → Embedding 自动靠近
冷启动兜底 :依赖 Author Embedding 。新推文虽然 ID 是噪声,但作者是老用户,可提供初始语义。
4.8 完整的 Embedding 流程图
┌─────────────────────────────────────────────────────────────────────────────┐
│ X Phoenix Embedding 完整架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 原始输入 │
│ ┌──────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐│
│ │ UserID │ │ History TweetIDs │ │ History AuthorIDs│ │ Candidate IDs ││
│ └────┬─────┘ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘│
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Hash 函数 (xxhash64, 2个seed) ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ user_hashes post_hashes author_hashes post_hashes │
│ [B, 2] [B, S, 2] [B, S, 2] [B, C, 2] │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ User Table │ │ Post Table │ │Author Table│ │ Post Table │ │
│ │ [N_u, 256] │ │ [N_p, 256] │ │ [N_a, 256] │ │ (同一张表) │ │
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ [B, 2, 256] [B, S, 2, 256] [B, S, 2, 256] [B, C, 2, 256] │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Reshape → 展平最后两维 ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ [B, 512] [B, S, 512] [B, S, 512] [B, C, 512] │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Concat (拼接多维度特征) + Linear Projection → [256] ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Transformer ││
│ │ [User | History_1 | ... | Candidate_1 | ...] ││
│ │ 形状: [B, 1 + S + C, 256] = [B, 161, 256] ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5. 关键设计决策
5.1 纯模型驱动,无手工特征
X 的 README 明确声明:
We have eliminated every single hand-engineered feature and most heuristics from the system.
- 优势 :简化数据 Pipeline,减少特征工程负担
- 代价 :完全依赖用户行为,冷启动问题严重
5.2 技术栈分工
| 层 | 技术 | 原因 |
|---|---|---|
| Pipeline 编排 | Rust | 高并发、低延时、内存安全 |
| ML 模型 | JAX | 高性能自动微分、TPU 原生支持 |
| 实时数据流 | Rust + Kafka | 处理百万级 TPS |
5.3 召回与精排的边界
- 召回 (Retrieval) :极致效率,用双塔"粗筛"
- 精排 (Ranking) :极致精度,用 Transformer"细选"
- 关键约束 :精排只处理 ~32 个候选,严格控制计算量
6. 工程实践要点
6.1 Embedding Table 管理
| 策略 | 说明 |
|---|---|
| 表大小 | 百万到亿级,平衡冲突率与内存 |
| 热门物品专属行 | 高频访问的推文/作者可分配独立索引 |
| 在线学习 | 新推文几分钟内根据反馈修正 Embedding |
6.2 分布式 ID 生成 (Snowflake)
64-bit ID 结构:
┌───────┬───────────────────┬────────────┬──────────────┐
│ 1 bit │ 41 bits │ 10 bits │ 12 bits │
│ sign │ timestamp (ms) │ machine ID │ sequence │
└───────┴───────────────────┴────────────┴──────────────┘
特性: 全局唯一、时间有序、高性能、去中心化
6.3 开源代码的局限
以下模块被排除,未开源:
pub mod clients; // 后端服务通信
pub mod params; // 模型权重参数
pub mod util; // 工具函数 (含 Hash 实现)
7. 总结与思考
7.1 架构设计要点
- 分层漏斗 :召回→粗排→精排→混排,逐层收窄,逐层精细
- 效率与精度的权衡 :双塔换速度,Transformer 换精度
- 协同过滤为主 :依赖用户行为信号,而非内容理解
- 工程约束驱动设计 :候选数量、历史长度严格受限
7.2 开放问题
| 问题 | 思考 |
|---|---|
| 内容特征缺失 | 生产系统是否有额外的 Content Embedding 通道? |
| 冷启动 | 新作者 + 新推文如何处理?是否有探索机制? |
| 实时性 | 在线学习的更新频率?Embedding 漂移如何控制? |
7.3 对实践的启示
- 不必追求完美匹配 :推荐是概率任务,容错性高
- Hash Trick 是性价比之选 :用可控的精度损失换取巨大的空间节省
- 用户行为是最强信号 :协同过滤在工业界依然有效
- Transformer 适合精排 :在候选集收窄后才能发挥威力
附录:核心代码文件索引
| 文件 | 路径 | 职责 |
|---|---|---|
| 精排模型 | phoenix/recsys_model.py |
PhoenixModel 定义 |
| 召回模型 | phoenix/recsys_retrieval_model.py |
双塔模型定义 |
| Transformer | phoenix/grok.py |
注意力机制与 Mask |
| Pipeline | home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs |
流水线编排 |
| 打分器 | home-mixer/scorers/weighted_scorer.rs |
多目标加权 |
本文档基于 xai-org/x-algorithm 开源代码分析,结合推荐系统工程实践整理。
审核编辑 黄宇
