X (Twitter) 推薦系統架構設計深度解析
本文基于
xai-org/x-algorithm開源倉庫的源碼分析,系統性地解讀了 X 平臺 "For You" 信息流的推薦算法架構、核心機制與設計哲學。
目錄
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 開源代碼分析,結合推薦系統工程實踐整理。
審核編輯 黃宇
-
算法
+關注
關注
23文章
4784瀏覽量
98038 -
源碼
+關注
關注
8文章
685瀏覽量
31317 -
系統架構
+關注
關注
1文章
73瀏覽量
24217
發布評論請先 登錄
汽車電子電氣架構設計及優化措施
影響RF系統架構設計的參數特性探討
基于ARM架構設計的M1芯片
對嵌入式系統中的架構設計的理解
系統架構設計的詳細講解
深度:嵌入式系統的軟件架構設計!資料下載
商城庫存系統中心架構設計與實踐案例
GPU架構深度解析
X (Twitter) 推薦系統架構設計深度解析
評論