本文内容撰写存在 AI 辅助
第 1 章:全景概览
本章目标:快速建立 X 推荐系统的模块边界、职责分工与核心设计哲学。
1 四大子系统角色与关系
| 子系统 | 主语言 | 主要职责 | 在请求链路中的位置 |
|---|---|---|---|
home-mixer |
Rust | 在线编排:接收请求、组织流水线、返回最终 Feed | 请求入口与总调度 |
candidate-pipeline |
Rust | 可复用流水线框架(Trait 抽象) | 执行引擎 |
thunder |
Rust | Kafka 事件消费 + 内存索引,提供站内候选 | in-network 召回 |
phoenix |
Python (JAX/Haiku) | 召回(双塔)与排序(Transformer)模型 | out-of-network 召回 + 预测打分 |
[!tip] 术语首现
- For You 编排层(For You Orchestration Layer):这里对应
home-mixer。- 站内候选(In-Network):来自关注关系内容。
- 站外候选(Out-of-Network, OON):由模型发现的非关注内容。
2 系统架构总览

3 核心设计哲学
执行与策略解耦(Execution-Policy Decoupling)
- 执行顺序由
candidate-pipeline提供,业务策略由home-mixer装配。 - 好处:策略变更不改引擎;多业务线复用同一框架。
- 若不解耦:业务逻辑会散落在执行代码里,演进成本高。
- 执行顺序由
Trait 可插拔(Pluggable Traits)
Source/Hydrator/Filter/Scorer/Selector/SideEffect统一接口。- 好处:新增策略仅需实现 Trait + 注册组件。
- 若不抽象:阶段逻辑硬编码,A/B 试验成本高。
全链路可降级(Graceful Degradation)
- Query Hydrator / Source / Scorer 失败多为“记录错误并继续”;Filter 失败回退到备份候选。
- 好处:高可用优先,避免局部依赖抖动导致全请求失败。
- 若无降级:单点依赖故障会直接打穿 SLA。
本章结论:系统通过“编排层 + 可插拔流水线 + 降级策略”实现了高可演进、可用性优先的在线推荐架构。
第 2 章:一次请求的完整旅程
本章目标:按真实执行顺序串起一次 For You 请求,从输入到返回形成主线心智模型。
1 用户上下文补全(Query Hydration)
candidate-pipeline 首先执行 hydrate_query(),并行拉取用户上下文,再顺序合并。
1.1 补全内容
- 用户行为序列(User Action Sequence, UAS):
UserActionSeqQueryHydrator(典型动作包括:点赞、回复、转发/引用、点击、个人页点击、视频观看、分享、停留、关注作者、不感兴趣、拉黑、静音、举报等) - 用户特征(User Features):
UserFeaturesQueryHydrator(关注/屏蔽/订阅/屏蔽词)
1.2 补全方式
- 并行请求:
join_all扇出,同时触发多个下游调用 - 顺序合并:各 hydrator 只回写自己负责字段 ,然后合并(update)
- 失败降级:单模块失败不终止请求,推荐场景能够容忍个别字段的缺失
[!question] 为什么“先部分补全,再 update”?
- 并发安全:避免多个任务同时写同一
query。- 失败隔离:某个 hydrator 失败不污染其他结果。
- 职责清晰:
hydrate=取数,update=合并。
2 候选获取(Source)
Source 阶段并行拉候选,当前核心是 Thunder(站内)+ Phoenix(站外)。
- Thunder 在内存中维护“可推荐帖子池”,并根据关注关系和发帖排序来进行站内召回候选
- Phoenix 计算用户行为向量,在全网内容向量库(离线计算)中进行相似检索,召回站外候选
in_network_only=true:只跑 Thunder,禁用 Phoenix。in_network_only=false:两路并行拉取并合并候选池。
[!tip] 设计动机 并行拉取将总耗时逼近
max(T_thunder, T_phoenix);若串行,延迟近似相加。
3 候选补全(Candidate Hydration)
Source 只给“轻候选”,Candidate Hydrator 再补推文/作者/视频等细节:
InNetworkCandidateHydrator:标记是否 in-networkCoreDataCandidateHydrator:推文核心数据(作者、文本、转推/回复关系)VideoDurationCandidateHydrator:视频时长SubscriptionHydrator:订阅作者信息GizmoduckCandidateHydrator:作者资料(粉丝数、用户名)
[!question] 为什么不在 Query 阶段一次补齐? Query 阶段还没有候选
tweet_id列表;候选级补全必须等 Source 产出候选后才能做。
4 过滤(Filter)
Filter 串行执行,每个过滤器输出 kept/removed;失败则回退到该过滤器前的备份候选。
常见规则:
- 去重:
DropDuplicatesFilter、RetweetDeduplicationFilter - 内容有效性:
CoreDataHydrationFilter、AgeFilter - 用户侧约束:
SelfTweetFilter、AuthorSocialgraphFilter、MutedKeywordFilter - 会话状态约束:
PreviouslySeenPostsFilter、PreviouslyServedPostsFilter - 订阅权限约束:
IneligibleSubscriptionFilter
5 打分(Scorer)
按顺序执行:
PhoenixScorer:多行为概率预测WeightedScorer:行为加权聚合AuthorDiversityScorer:同作者重复衰减OONScorer:站外分数系数调整
加权核心公式: $$ \text{weighted\_score}=\sum_i w_i \cdot P(\text{action}_i) $$
作者衰减公式: $$ \text{multiplier}=(1-\text{floor})\cdot \text{decay}^{\text{pos}}+\text{floor} $$
[!note] OON 调整语义
OONScorer是分数倾斜(乘系数),不是硬配额;是否入选仍取决于整体排序结果。
[!warning] 参数裁剪 行为权重与 OON 系数来自
home-mixer/params,该模块未公开。 OON 系数默认保持站内外的帖子占比为 1:1<!-- TODO: 具体权重常量 -->
6 选择与后过滤
先由 TopKScoreSelector 按分数排序截断,再执行后置 Hydrator/Filter:
- Post-selection Hydrator:
VFCandidateHydrator,可见性审查结果(安全/违规/删除等) - Post-selection Filter:
VFFilter、DedupConversationFilter,同一会话链路里只保留更优那条,避免刷屏式重复;同一会话链路,可以理解为围绕同一个对话线程(conversation)展开的一串推文关系(回复,转推等)
[!question] 为什么部分过滤放在 Selector 之后? VF 等检查成本高,放后置只处理入围 Top-K,显著降低全量候选开销。
本章结论:一次请求的主路径是“并行取数 + 串行决策”,并通过阶段化降级在稳定性与效果之间取得平衡。
第 3 章:流水线框架(candidate-pipeline)
本章目标:解释通用流水线如何用 Trait 抽象执行机制,并让业务模块可插拔。
1 Trait 体系与 enable() 条件启用
框架定义了六类核心 Trait:
QueryHydrator<Q>Source<Q, C>Hydrator<Q, C>Filter<Q, C>Scorer<Q, C>Selector<Q, C>
每个 Trait 默认有 enable(),支持基于 query 条件启停(如 in_network_only)。
每个 Trait 的含义,可参考第二章的每小节内容
2 标准执行顺序
candidate_pipeline.rs 的固定顺序:
- QueryHydrator 查询补齐
- Source 粗召回(站内/站外)
- Hydrator 候选补齐
- Filter 过滤
- Scorer 评分
- Selector 排名
- PostSelectionHydrator 后补齐
- PostSelectionFilter 后过滤
- SideEffect(异步,不阻塞主返回)
这使“执行机制稳定、业务策略可替换”成为可能。
3 业务组装示例
home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs 展示了如何把策略装入框架:
- Query Hydrators:用户行为序列 + 用户特征
- Sources:Thunder + Phoenix
- Filters/Scorers/Selector:策略链
本章结论:candidate-pipeline 用固定执行骨架承载可插拔策略,降低了功能迭代与实验成本。
第 4 章:站内召回(Thunder)
本章目标:拆解 Thunder 的“离线准备 + 在线检索”双链路,以及三层过滤细节。
1 数据准备链路:Kafka → 反序列化 → 内存索引
核心流程:
- 消费推文创建/删除事件(
tweet_events_listener*.rs) - 将 kafak 消息按约定协议反序列化为可读的事件(
deserializer.rs) - 映射为
LightPost(发帖事件) /TweetDeleteEvent(删帖行为) - 更新
PostStore三类索引,每个作者可配置不同的“索引上限”,来控制延迟和开销:original_posts_by_user:每个作者的“原创推文时间线”secondary_posts_by_user:每个作者的“次级内容时间线”(回复/转推)video_posts_by_user:每个作者的“视频时间线”(包含转推,但不包含回复)
2 在线召回链路:限流 → 查索引 → 过滤 → 排序截断
thunder_service.rs::get_in_network_posts:
Semaphore并发限流- 参数约束(列表截断、
max_results默认) - 读取
PostStore - 按
created_at倒序 + Top-N 返回
3 三层过滤逐层展开
3.1 第一层:请求入口层(Service)
- 限制
following_user_ids、exclude_tweet_ids输入规模 - 可选从 Strato (在线特征与状态数据的读写后端)拉关注列表
- 早期限流,避免过载
3.2 第二层:PostStore 查询层(核心)
- 仅扫描关注作者
- 每作者扫描上限
MAX_TINY_POSTS_PER_USER_SCAN - 过滤
exclude_tweet_ids、已删除推文 - 过滤“转推自己内容”
- secondary 帖子附加会话约束(回复链合法性)
- 每作者返回上限(original/reply/video 分别配置)
3.3 第三层:排序截断层
- 全量候选按时间倒序
- 截断到
max_results
[!tip] 设计动机 把高频过滤前置到内存索引层,可在“低延迟 + 高并发”下维持可控资源消耗。
[!warning] 缺失模块
thunder/config.rs、thunder/metrics.rs、thunder/strato_client.rs在当前快照缺失。<!-- TODO: Thunder 配置与指标完整定义 -->
本章结论:Thunder 通过事件驱动内存索引和分层过滤,实现了高吞吐、低延迟的站内候选供给。
第 5 章:站外召回与双塔模型(Phoenix Retrieval)
本章目标:说明 Phoenix 站外召回触发条件、双塔机制与 Top-K 质量保障。
1 PhoenixSource 触发流程
触发条件(home-mixer/sources/phoenix_source.rs):
in_network_only == falseuser_action_sequence可用(否则报错并由上游降级)
2 双塔原理(Two-Tower)
双塔模型是目前最主流的向量召回方法:

设用户向量为 $\mathbf{u}$,候选向量为 $\mathbf{v}$,相似度可写为: $$ \text{score} = \mathbf{u}\cdot\mathbf{v} $$
本项目代码中:
- 用户塔:
PhoenixRetrievalModel.build_user_representation()(Transformer 编码 user+history) - 候选塔:
CandidateTower(MLP 投影 + L2 归一化) - 检索:
_retrieve_top_k()计算相似度并取 Top-K
测试保证(test_recsys_retrieval_model.py):
- 索引合法性:
0 <= index < corpus_size - 分数有序性:Top-K 分数单调不升
3 工程保障(离线/增量/ANN/分片)
[!note] 代码与生产差异 当前开源代码在检索示例中使用全量矩阵乘 +
top_k,属于可读性优先实现。
生产级常见保障通常包括:
- 候选向量离线全量重算 + 近实时增量
- ANN 索引(HNSW/IVF/PQ 等)
- 分片并行检索与多路 merge
本章结论:Phoenix 站外召回在理论上采用双塔 + Top-K,开源代码展示了核心算法形态,生产工程细节留待补全。
第 6 章:排序模型(Phoenix Ranker)
本章目标:完整拆解排序模型输入、结构、掩码机制、输出语义与测试方法。
1 模型架构总览

2 输入详解:RecsysBatch + RecsysEmbeddings
2.1 A. RecsysBatch(hash/离散特征)
| 字段 | 形状 | 含义 |
|---|---|---|
user_hashes |
[B, Hu] |
用户多哈希 ID(0 常作 padding) |
history_post_hashes |
[B,S,Hi] |
历史内容哈希 |
history_author_hashes |
[B,S,Ha] |
历史作者哈希 |
history_actions |
[B,S,A] |
历史行为多热向量 |
history_product_surface |
[B,S] |
历史场景离散特征 |
candidate_post_hashes |
[B,C,Hi] |
候选内容哈希 |
candidate_author_hashes |
[B,C,Ha] |
候选作者哈希 |
candidate_product_surface |
[B,C] |
候选场景离散特征 |
2.2 B. RecsysEmbeddings(查表向量)
| 字段 | 形状 | 含义 |
|---|---|---|
user_embeddings |
[B,Hu,D] |
用户 hash 查表向量 |
history_post_embeddings |
[B,S,Hi,D] |
历史内容向量 |
candidate_post_embeddings |
[B,C,Hi,D] |
候选内容向量 |
history_author_embeddings |
[B,S,Ha,D] |
历史作者向量 |
candidate_author_embeddings |
[B,C,Ha,D] |
候选作者向量 |
# phoenix/recsys_model.py
embeddings = jnp.concatenate([user_embeddings, history_embeddings, candidate_embeddings], axis=1)
padding_mask = jnp.concatenate([user_padding_mask, history_padding_mask, candidate_padding_mask], axis=1)
candidate_start_offset = user_padding_mask.shape[1] + history_padding_mask.shape[1]
[!question] 为什么 hash 和 embedding 要同时输入?
- embedding 负责语义表示;
- hash 负责身份/有效位控制(如 padding mask)、追踪与调试;
- 二者分离可兼容“离线查表/在线查表”不同工程路径。
3 Transformer 骨架(grok.py)
主干组件:RMSNorm 、MHA(多头注意力)、FFN(前馈网络)、RoPE
结构路径:DecoderLayer 里先 Attention 再 FFN,均带残差与归一化
4 推荐专用注意力掩码(Candidate Isolation Mask)
候选隔离掩码(Candidate Isolation Mask)
- 也称推荐场景专用注意力掩码,确保候选之间不能相互“泄漏信息”,让模型关注与目标用户与候选之间的信息交换与评估;该掩码能显著提高推荐模型的线上稳定性与可解释性
- 实现在
phoenix/grok.py,但其生效依赖流水线输入组织:框架先构建[user, history, candidates]序列,再将candidate_start_offset传入 Transformer。
规则:
- User+History 区域:保留因果注意力;
- Candidate 区域:每个候选可看 User+History;
- Candidate 之间互不可见,仅允许看自己(对角线)。
# phoenix/grok.py
causal_mask = jnp.tril(jnp.ones((1, 1, seq_len, seq_len), dtype=dtype))
attn_mask = causal_mask.at[:, :, candidate_start_offset:, candidate_start_offset:].set(0)
candidate_indices = jnp.arange(candidate_start_offset, seq_len)
attn_mask = attn_mask.at[:, :, candidate_indices, candidate_indices].set(1)
这样设计的收益:
- 候选分数对“同批其他候选”不敏感,在线表现更稳定;
- 批内增删候选不会引入强耦合偏差。
若不这么做,候选之间会相互影响,导致分数不可比与线上波动。
5 输出定义
模型输出:
logits形状:[B, C, A]- 推理时通常经
sigmoid转为多行为概率
# phoenix/runners.py
output = hk_forward(batch, recsys_embeddings)
probs = jax.nn.sigmoid(output.logits)
6 行为类型列表(正向 + 负向)
可见来源:
home-mixer/scorers/phoenix_scorer.rs对ActionName与ContinuousActionName的映射。
| 序号 | 行为字段 | 代码映射 | 属性 | 中文解释 |
|---|---|---|---|---|
| 1 | favorite_score |
ServerTweetFav |
正向 | 点赞 |
| 2 | reply_score |
ServerTweetReply |
正向 | 回复 |
| 3 | retweet_score |
ServerTweetRetweet |
正向 | 转推 |
| 4 | photo_expand_score |
ClientTweetPhotoExpand |
正向 | 展开查看图片 |
| 5 | click_score |
ClientTweetClick |
正向 | 点击推文详情 |
| 6 | profile_click_score |
ClientTweetClickProfile |
正向 | 点击作者头像/主页 |
| 7 | vqv_score |
ClientTweetVideoQualityView |
正向 | 视频有效播放 |
| 8 | share_score |
ClientTweetShare |
正向 | 分享(泛指) |
| 9 | share_via_dm_score |
ClientTweetClickSendViaDM |
正向 | 通过私信分享 |
| 10 | share_via_copy_link_score |
ClientTweetShareViaCopyLink |
正向 | 复制链接分享 |
| 11 | dwell_score |
ClientTweetRecapDwelled |
正向 | 停留阅读(是否超过阈值) |
| 12 | quote_score |
ServerTweetQuote |
正向 | 引用推文 |
| 13 | quoted_click_score |
ClientQuotedTweetClick |
正向 | 点击被引用的推文 |
| 14 | follow_author_score |
ClientTweetFollowAuthor |
正向 | 看完推文后关注作者 |
| 15 | not_interested_score |
ClientTweetNotInterestedIn |
负向 | 点击"不感兴趣" |
| 16 | block_author_score |
ClientTweetBlockAuthor |
负向 | 拉黑作者 |
| 17 | mute_author_score |
ClientTweetMuteAuthor |
负向 | 静音作者 |
| 18 | report_score |
ClientTweetReport |
负向 | 举报推文 |
| 19 | dwell_time |
DwellTime |
连续值 | 停留时长(毫秒级连续值) |
[!warning] 完整性声明 UAS 原始事件全集定义在外部协议与聚合模块;当前仓库可直接确认的是“排序打分中被消费的行为类型”。
<!-- TODO: UAS 完整枚举需补充自 xai_recsys_proto / 外部 schema -->
7 测试方法
掩码行为测试(
test_recsys_model.py)- 验证候选可看 user/history + 自身
- 验证候选间不可见
- 验证边界 case(单候选、几乎全候选)
Top-K 合法性测试(
test_recsys_retrieval_model.py)- 索引不越界
- 分数有序(降序)
本章结论:Phoenix Ranker 通过“结构化输入 + 候选隔离注意力 + 多行为输出”实现了可扩展且稳定的候选打分能力。
第 7 章:重排与策略
本章目标:解释模型分数如何被策略层转化为最终可上屏排序。
1 多行为加权聚合:正负行为对冲
WeightedScorer 将多行为概率线性组合:
$$
\text{combined\_score}=\sum_i w_i P_i
$$
其中负反馈行为(如 not_interested/block/mute/report)通常对应负权重,对冲“高点击但高风险”的内容。
[!note] 为什么需要对冲? 仅优化单一正向指标会导致“短期刺激、长期满意度下降”。多行为联合能更稳健地近似真实用户价值。
2 作者多样性衰减:控制信息源集中
AuthorDiversityScorer 先按 weighted_score 排序,再按作者出现次序施加衰减:
$$
\text{multiplier}=(1-\text{floor})\cdot \text{decay}^{\text{position}}+\text{floor}
$$
floor的物理意义:衰减下限,保证“强相关内容”不被彻底压没。- 不做此策略会导致头部作者占位过多,降低 feed 多样性。
3 OON 调整:发现性与社交相关性的平衡
OONScorer 对 in_network=false 候选乘系数(OON_WEIGHT_FACTOR),体现“社交强相关”与“内容发现”之间的平衡。
[!warning] 参数不可见 OON 系数具体值在
home-mixer/params。<!-- TODO: OON_WEIGHT_FACTOR -->
4 后置安全过滤与会话去重
VFFilter:基于可见性结果移除不可展示内容(安全/违规/删除等)DedupConversationFilter:同一会话链路只保留更优候选,减少重复占位
本章结论:重排策略层把“模型相关性”转成“可消费、可控风险、可持续体验”的最终排序结果。
第 8 章:工程实现
本章目标:从并发、跨语言通信、降级机制与工程权衡四个维度审视实现质量。
1 Rust 异步并发模型
tokio作为运行时join_all用于 QueryHydrator / Source / Hydrator 并发Semaphore用于 Thunder 服务入口限流spawn_blocking用于 CPU/阻塞操作(如 PostStore 重处理)
2 Rust ↔ Python 交互(gRPC/RPC)
可见事实:
home-mixer通过客户端 Trait 调用phoenix_retrieval_client与phoenix_prediction_client。tonic广泛用于 Rust gRPC 服务/客户端链路。
不可见事实:
- 客户端具体协议实现、连接池、重试、超时策略在
home-mixer/clients被裁剪。
3 全链路降级设计
| 阶段 | 失败处理策略 | 风险与收益 |
|---|---|---|
| QueryHydrator | 记录错误,继续 | 保障可用性,但个性化特征可能缺失 |
| Source | 单 Source 失败不终止 | 保证有结果,但召回覆盖下降 |
| Hydrator | 长度不一致时跳过更新 | 防止数据错位污染 |
| Filter | 出错回退到备份候选 | 降低错误放大,但可能放宽规则 |
| Scorer | 单 scorer 失败继续 | 保证链路不中断,但分数质量下降 |
| SideEffect | 异步执行,不阻塞响应 | 主链低延迟,副作用最终一致 |
4 已知局限与 Trade-off
- 配置/参数不可见:难以做精确复现实验。
- 客户端实现裁剪:网络层可靠性细节不可审计。
- 检索工程细节缺失:ANN、索引发布、增量更新流程未公开。
- 部分模块声明存在但源码缺失:如
candidate-pipeline/util、thunder/config。
5 当前代码的创新点
- 候选隔离掩码落地到排序主干:兼顾效果与线上稳定性。
- 召回/排序与策略重排解耦:模型更新与策略更新互不强耦合。
- 全链路可降级:面向生产稳定性的工程优先级明确。
- 统一 Trait 可插拔框架:降低多场景复用成本。
6 关键代码模块索引表
| 模块 | 关键文件 | 作用 |
|---|---|---|
| Pipeline 引擎 | candidate-pipeline/candidate_pipeline.rs |
执行顺序、并发、降级 |
| Query 补全 | home-mixer/query_hydrators/*.rs |
UAS 与用户特征补全 |
| 候选来源 | home-mixer/sources/*.rs |
Thunder/Phoenix 召回 |
| 候选补全 | home-mixer/candidate_hydrators/*.rs |
推文/作者/视频/VF 信息 |
| 过滤策略 | home-mixer/filters/*.rs |
去重、时效、社交图、安全 |
| 打分策略 | home-mixer/scorers/*.rs |
预测、加权、多样性、OON |
| 选择器 | home-mixer/selectors/top_k_score_selector.rs |
排序截断 |
| 站内召回服务 | thunder/thunder_service.rs |
gRPC 查询入口与限流 |
| 站内索引 | thunder/posts/post_store.rs |
内存索引与分层过滤 |
| 排序模型 | phoenix/recsys_model.py, phoenix/grok.py |
Transformer 打分 |
| 召回模型 | phoenix/recsys_retrieval_model.py |
双塔检索 |
本章结论:工程实现体现了“在线稳定性优先 + 模块可演进”的取舍,但开源裁剪使部分生产细节仍不可见。
第 9 章:总结与展望
本章目标:收敛全文结论并给出后续可推进方向。
1 总结
X 推荐系统在当前代码中呈现出清晰分层:
home-mixer负责编排candidate-pipeline负责执行骨架thunder负责站内高速召回phoenix负责站外召回与排序建模
其关键价值不只在“模型能力”,更在“模型 + 策略 + 工程降级”的协同设计。
2 展望
建议优先补齐以下内容以提升可复现性:
- 开源
clients/params/config的最小可运行替代实现; - 提供 ANN 检索与索引发布的参考流水线;
- 增加端到端压测与故障注入样例;
- 给出策略权重的可配置实验模板。
本章结论:该项目已具备完整推荐链路骨架,下一步重点应放在“可复现生产细节”的补全。
附录 A:术语表
| 术语 | 全称 | 含义 |
|---|---|---|
| For You | For You Feed | 个性化推荐信息流 |
| Query Hydration | 查询补全 | 在召回前补齐用户上下文 |
| UAS | User Action Sequence | 用户行为序列 |
| OON | Out-of-Network | 站外(非关注关系)候选 |
| VF | Visibility Filtering | 可见性/安全过滤 |
| ANN | Approximate Nearest Neighbor | 近似最近邻检索 |
| Candidate Isolation Mask | 候选隔离掩码 | 候选间互不可见的注意力掩码 |
| Strato | Strato Service | 内部特征读写/存储服务(本仓库仅见调用) |
| TES | Tweet Entity Service | 推文实体信息服务(本仓库仅见调用) |
| Gizmoduck | Gizmoduck Service | 用户资料服务(本仓库仅见调用) |