Skip to content

Phoenix 召回:双塔检索架构

生产版双塔召回:用户塔 transformer、语义 ID 候选塔、对比学习训练、索引随 checkpoint 一起发布;10-13 起加训“停留达标”头

Updated

这一页回答什么

推荐系统在排序之前,先从千万级全站候选池中筛选出数百至上千条候选帖子。Phoenix 召回阶段采用双塔模型执行粗筛:双塔模型分别将用户历史与候选帖子映射至同一向量空间。用户塔把用户的近期浏览与交互记录编码为单位向量;候选塔把每条候选帖子编码为单位向量。在线检索通过大矩阵乘法计算相似度,并提取 top-K 候选。

本页解析 2026-08 起开源的生产版召回系统,涵盖双塔输入结构、训练目标、索引生成与加载机制以及在线打分流程。类与方法的详细拆解见实体页 RecsysTwoTowerModel(召回模型);召回索引收录的帖子范围由 Phoenix 召回索引:rankall 与事件层 决定。

旧版文档基于 phoenix/recsys_retrieval_model.py 与 run_pipeline.py 的演示代码编写,两份文件现已删除。当前生产召回模型类为 RecsysTwoTowerModel(phoenix/xrex/models/recsys_two_tower_model.py:1610)。

核心结论

  1. 双塔分离计算:系统为每个在线请求计算一次用户塔。系统对全量语料离线预计算候选塔,将结果保存为 post_embeddings 表并写入 checkpoint。在线服务启动时直接加载该表(phoenix/README.md:116-119、phoenix/xrex/train/trainer_recsys.py:4195-4235)。
  2. 候选特征解耦帖子 ID 哈希:生产配置设置 use_post_embedding=False,候选塔仅输入“作者哈希 + 语义 ID(SID)”。同主题帖子共享 SID 前缀,新发帖子无需积累交互即可获得有效向量表征(phoenix/xrex/configs/xrecsys_two_tower.py:463-465、:330-332,phoenix/README.md:109-115)。
  3. 用户塔不使用用户 ID 嵌入:系统配置 use_user_embedding=False,仅通过交互历史序列和单张画像 token(包含国家、语言等)表征用户(xrecsys_two_tower.py:463、:330,phoenix/README.md:101-108)。
  4. 训练采用对比学习:模型默认将“点赞”作为正样本。负样本包含批次内其他用户的候选帖子,以及每例 64 个全局负样本,同时执行 log-Q 校正。用户主动划走、举报或拉黑的候选帖子同样计入分母(recsys_two_tower_model.py:1035-1380)。
  5. 2026-10-13 新增“停留达标”预测头:在 combined 与 home_only 配置中,系统新增第三个候选头 ld,以 CLIENT_TWEET_RECAP_DWELLED 为正样本单独训练。在线服务打分公式为 1.0 × 点赞头得分 + 0.75 × ld 头得分(xrecsys_two_tower.py:293-296、recsys_two_tower_model.py:2969-2992)。上游 README 称该指标为“读完整篇”;源码中该头对应停留达标事件,生产环境判定阈值在源码中未包含。
  6. 打分机制支持非点积模式:combined 配置启用 4 个用户查询头和 MoL(mixture of logits)打分机制。小门控网络混合多个余弦相似度计算单条候选帖子的得分(xrecsys_two_tower.py:313-320、recsys_two_tower_model.py:710-880)。旗舰配置 xrecsys_two_tower 与 nano 配置仍沿用向量点积。

双塔总览

flowchart TB
    subgraph UT["用户塔 · 每请求在线"]
        UF["画像 token"] --> CAT["拼接 用户前缀 与 历史"]
        HH["历史 ×1023<br/>作者哈希+SID+动作+停留+上下文"] --> CAT
        CAT --> TF["Transformer 8 层"]
        TF --> POOL["掩码平均池化<br/>或 4 个查询头"]
        POOL --> N1["L2 归一化"]
    end
    subgraph CT["候选塔 · 离线建索引"]
        CP["作者哈希 ×2 + SID token"] --> PROJ["concat→MLP<br/>或 各自投影再相加"]
        PROJ --> N2["L2 归一化"]
    end
    N2 --> IDX["post_embeddings 表<br/>写进 checkpoint"]
    N1 --> SC["点积 或 MoL 打分"]
    IDX --> SC
    SC --> MASK["按语料类型、可用掩码、话题过滤"]
    MASK --> TOPK["top-K"]

用户塔

用户塔以 user_representation 为计算入口(recsys_two_tower_model.py:1990-2128),按以下步骤生成用户表征:

  1. 构造输入特征(build_user_inputs,:1621-1815)。计算路径分为两类:
    • feature_prep_enabled=True(combined 系列):复用排序模型的特征准备流程,但不包含候选片段(include_candidates=False,:1633-1645)。历史 token 字段构成与排序模型一致,详见 Phoenix 排序模型。
    • 其他配置(旗舰 xrecsys_two_tower、nano):执行旧式 block_user_reduce / block_history_reduce。系统拼接作者哈希、SID、动作类型与产品面标识后执行投影(:1647-1815;两函数分别定义于 recsys_model.py:1673、:1947)。
  2. 执行 transformer 编码:模型复用 xrex/models/transformer.py。输入序列不含候选帖子,因此无需引入候选隔离机制(详见 候选隔离注意力掩码)。训练阶段将多个用户的序列拼接为单行(use_seqpack=True),并调用变长注意力算子(:2001-2028)。
  3. 池化生成用户向量:
    • 默认模式:计算所有有效位置输出向量的平均值(:2112-2116)。
    • 查询头模式(user_query_heads > 0,combined 配置为 4):模型学习 4 个查询向量。每个查询向量对完整序列执行一次注意力加权池化。权重中扣除 gamma × 距最新位置的 token 数 / 16 作为可学习的时间衰减项(:2130-2215,QUERY_HEAD_RECENCY_SCALE_TOKENS = 16.0 见 :71),输出维度为 [B, 4, D]。
  4. L2 归一化(:2123-2125)。

训练阶段配置 empty_history_user_dropout_rate=0.1:系统随机清空 10% 用户的交互历史,以此模拟冷启动新用户(xrecsys_two_tower.py:271、:941)。

候选塔

候选塔由 RecsysCandidateTower 实现(recsys_two_tower_model.py:180-331)。模型为每条候选帖子输入一组 token,包含可选的帖子哈希嵌入、2 个作者哈希嵌入和 1 个 SID token(_stack_candidate_tokens,:1880-1892)。特征组合包含三种方式:

方式 何时用 做法
concat→MLP enable_candidate_tower_linear_proj=True(旗舰、nano) 展平后过 W → 2W → W 两层,中间 SiLU(:203-246)
各自投影再相加 feature_prep_enabled=True(combined 系列) 每个 token 一个 [W, W] 投影,结果相加(:248-320)
均值池化 两者都关 直接平均,无参数(:329-331)

两种投影开关互斥(xrecsys_two_tower.py:645-652)。每种方式的输出均执行 L2 归一化。

多头机制:当 num_candidate_heads > 1 时,每个候选头配置独立的投影参数(参数名包含 _head_{i} 后缀),同时共享同一个用户向量(:214-220、:1972-1988)。combined 配置包含 3 个预测头:

头 正样本(开源默认值) 用于哪些语料
home SERVER_TWEET_FAV(recsys_two_tower_model.py:2895-2899) 首页语料 HOME、HOME_COLD、HOME_HOT
immersive 点赞、回复、引用、转发、视频质量观看、关注作者、收藏、分享(xrecsys_two_tower.py:283-291) 视频沉浸页语料 IMMERSIVE*
ld CLIENT_TWEET_RECAP_DWELLED(xrecsys_two_tower.py:293) 不占语料,作为额外打分槽位

系统通过 head_dataset_mapping 确定各语料在构建索引时调用的预测头(xrecsys_two_tower.py:297-304、trainer_recsys.py:4207-4228)。

训练目标:对比学习

系统通过 retrieval_loss 计算对比学习损失(recsys_two_tower_model.py:1035-1380)。训练流程对每个预测头分别计算损失后求和(:2767-2800):

  • 正样本定义:候选集合中包含正样本动作、且不包含任何硬负或软负动作的位置(positive_and_negative_masks,:675-702)。
  • 负样本动作分类:硬负动作包含举报、不感兴趣、少看此类、取关、拉黑、静音和不相关;软负动作包含未停留 CLIENT_TWEET_RECAP_NOT_DWELLED(:2900-2916)。
  • 分母项构成:批次内其他用户的候选(in-batch negatives)、每例 num_global_negatives_per_example=64 个全局负样本,以及当前用户带有负反馈动作的候选(叠加 own_negative_logit_offset,combined 配置为 −2.0)(:1226-1290,xrecsys_two_tower.py:262、:324)。
  • 温度系数:模型提供可学习温度参数,初始值为 0.1;combined 配置固定为 0.006(:2741-2750,xrecsys_two_tower.py:322)。
  • log-Q 校正:负样本得分增加 log(1/出现次数) × 2.0,消除高频热门帖子被频繁采样为负样本引入的估计偏差(:1202-1213,logq_correction_scale = 2.0 见 :2860)。
  • 批次假负样本屏蔽:启用 mask_in_batch_false_negatives=True 时,若其他用户的候选帖子与当前用户的正样本属于同一条帖子,算法将该样本从分母中移除(:1250-1260)。
  • 样本数加权:配置 positive_count_user_weight_power=0.5。算法将每个用户的损失乘以“正样本数的 0.5 次方”后归一化,提升高正样本数用户的损失权重(:1005-1016、:1359-1362)。

10-13 的变化:“停留达标”头

开源提交 e1a0375(2026-10-13)在 combined 基础配置中将候选头数量由 2 个扩充为 3 个,新增 ld 预测头并引入权重系数 w_ld = 0.75(xrecsys_two_tower.py:293-296)。具体机制包含三个环节:

  1. ld 头仅将 CLIENT_TWEET_RECAP_DWELLED 判定为正样本,负样本选取规则与 home 头一致(recsys_two_tower_model.py:2953-2961)。
  2. ld 属于“混合头”(BLEND_HEAD_ORDER = ("ld", "tweet_click"),:75-80)。该头不绑定独立语料,而是在每条索引行追加一段特征向量。索引表单行宽度由 W 扩展为 2W(num_table_slots = 1 + 混合头数,:2981-2983;拼接逻辑见 trainer_recsys.py:4230-4235)。
  3. 在线服务分段计算得分后加权汇总:最终分 = w_fav × 第 1 段得分 + w_ld × 第 2 段得分。开源默认权重为 w_fav = 1.0、w_ld = 0.75(score_over_slots,recsys_two_tower_model.py:150-177;权重定义见 :2985-2988)。

适用范围:ld 头仅配置于 xrecsys_two_tower_combined、xrecsys_two_tower_combined_gb300 和 xrecsys_two_tower_home_only 三组配置(xrecsys_two_tower.py:514-541)。旗舰配置 xrecsys_two_tower 与 nano 配置未包含该头(:439-513、:542-580)。线上生产环境选用何种配置,源码未包含。

生产环境对“停留达标”的具体判定阈值在源码中未包含。合成数据生成代码中将其定义为“产生交互且停留时间 ≥ 10 秒”(phoenix/reference/world.py:72、:337)。

索引:随 checkpoint 一起生成

  • 训练器在保存 checkpoint 时,调用候选塔遍历计算全量语料,将向量写入 checkpoint 中的 post_embeddings 表(phoenix/README.md:116-119、trainer_recsys.py:4195-4250)。在线服务进程启动时直接读取该表,不执行在线重算(phoenix/xrex/inference/model_runner.py:2313-2330)。
  • 索引表容量上限由 max_posts 控制:旗舰配置默认值为 10,240,000,combined 配置为 28,672,000,nano 配置为 65,536(xrecsys_two_tower.py:935、:329、:555)。
  • 语料集合由多种“语料类型”合并而成,每种类型对应一份 parquet 快照文件。例如 HOME 对应 1fav_1day.parquet,IMMERSIVE2Day 对应 video_2day.parquet,EVERGREEN 对应 video_4to14day.parquet(phoenix/xrex/data/retrieval_dataset.py:290-326)。快照由 phoenix-rankall 离线生成,详见 Phoenix 召回索引:rankall 与事件层。
  • 冷启动候选分片:系统将 HOME 语料进一步拆分为 HOME_COLD 与 HOME_HOT 两组切片。发布时间不超过 2 小时、点赞数少于 8 且浏览数少于 500 的帖子划入 HOME_COLD,其余帖子划入 HOME_HOT(retrieval_dataset.py:540-570,phoenix/xrex/data/cold_pool_filter.py:16、:27-46、:66-77,combined 配置中 cold_start_max_age_seconds=7200 见 xrecsys_two_tower.py:312)。编排层 home-mixer 对冷启动候选结果配置独立数量上限 PhoenixColdStartMaxResults = 200(home-mixer/params/param.rs:17-21)。

服务时的打分与过滤

RecsysTwoTowerModel.forward 执行在线检索(recsys_two_tower_model.py:2279-2600),分为四个步骤:

  1. 计算用户向量。
  2. 对索引分片执行逐段打分:未启用 MoL 时计算矩阵乘法 users @ posts.T;启用 MoL 时通过门控网络融合多个余弦相似度(支持融合 CUDA 算子与 int8 量化索引表)(:2358-2385、:924-1003)。
  3. 执行候选过滤:系统按语料类型、请求自带的候选可用掩码 eligible_mask 与话题位图进行过滤,将未通过过滤的候选得分置为 bfloat16 最小值(:2408-2445)。
  4. 调用自定义算子 top_k_by_key 筛选得分最高的 top-K 候选(:2312-2323)。

服务进程架构中,Rust 引擎中的 RecsysRetrievalPredictorServer 监听 gRPC 接口 RetrieveTopKCandidates,Python 侧 RetrievalModelRunner 运行上述前向计算(model_runner.py:4540、:5355-5370,phoenix/crates/serving/xai-recsys-proto/proto/recsys.proto:18-24)。模型加载与服务化细节详见 Phoenix 训练。

编排服务 home-mixer 通过 PhoenixSource 发起召回请求。请求召回数量以 PhoenixMaxResults(开源默认值 1000)为基准,经 quality_factor::apply 动态调整(home-mixer/sources/phoenix_source.rs:129、home-mixer/params/param.rs:5-9)。同目录下另包含通过 SID 前缀检索相似帖子的 SidSource,开源默认处于关闭状态(home-mixer/sources/sid_source.rs,EnableSidSource=false 见 param.rs:760-764)。

配置速查(开源默认值)

项 xrecsys_two_tower(旗舰) xrecsys_two_tower_combined xrecsys_two_tower_nano
emb_size / 层数 / 查询头 / KV 头 1024 / 8 / 16 / 4 1024 / 8 / 16 / 4 512 / 4 / 4 / 2
历史长度 / 候选长度 1023 / 64 1023 / 64 1022 / 64
候选组合方式 concat→MLP 各自投影再相加 concat→MLP
候选头 home home、immersive、ld home
用户查询头 / MoL 0 / 关 4 / 开 0 / 关
温度 可学习,初值 0.1 固定 0.006 可学习,初值 0.1
max_posts 10,240,000 28,672,000 65,536
帖 ID 哈希词表 / 作者词表 1 亿 / 3000 万 1 亿 / 3000 万 10 万 / 3 万

出处:xrecsys_two_tower.py:251-357、:439-580、:930-937。

设计决策

决策 理由
双塔分离,索引放进 checkpoint 在线只算用户塔;索引与模型权重同版本,不会错配(phoenix/README.md:116-119)
候选用 SID + 作者,不用帖子 ID 新帖没有互动也能落到同主题帖附近(phoenix/README.md:109-115)
用户塔无用户 ID 嵌入 用户完全由行为表示,新老用户同一套参数(phoenix/README.md:101-108)
自己的负反馈进分母 模型被直接推离用户明确拒绝过的内容
ld 作为混合头单独成段 不改动原有点赞头,服务时用权重调节两种目标的比例
冷启动单独切片 新帖不与热帖在同一池里竞争 top-K。源码未写理由,此为推测

源码锚点

  • phoenix/xrex/models/recsys_two_tower_model.py:1610 —— RecsysTwoTowerModel
  • phoenix/xrex/models/recsys_two_tower_model.py:180-331 —— RecsysCandidateTower
  • phoenix/xrex/models/recsys_two_tower_model.py:1990-2215 —— 用户塔与查询头池化
  • phoenix/xrex/models/recsys_two_tower_model.py:1035-1380 —— retrieval_loss
  • phoenix/xrex/models/recsys_two_tower_model.py:2279-2600 —— 服务前向与 top-K
  • phoenix/xrex/models/recsys_two_tower_model.py:2854-3140 —— RecsysTwoTowerModelConfig
  • phoenix/xrex/configs/xrecsys_two_tower.py:251-580 —— 预设配置
  • phoenix/xrex/train/trainer_recsys.py:4195-4250 —— 生成索引
  • phoenix/xrex/data/retrieval_dataset.py:290-570 —— 语料类型与冷启动切片

相关页面

Type to search…

↑↓ navigate↵ selectEsc close