---
title: "过滤流水线"
description: "两道过滤：21 个预打分过滤器与 5 个选后过滤器，按源码顺序逐个说明；含巴西选举过滤器的加入与删除"
---

> Documentation Index
> Fetch the complete documentation index at: https://x-algorithm.vergego.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 过滤流水线

## 这一页回答什么

本页回答四个问题：`PhoenixCandidatePipeline` 在哪两个阶段过滤候选；每个阶段有哪些过滤器；每个过滤器按什么条件删帖；哪些过滤器在开源默认值下不运行。可见性过滤服务本身的规则见 [可见性过滤](https://x-algorithm.vergego.ai/concepts/visibility-filtering/)。本页只说明 home-mixer 怎样使用它的结果。

## 核心结论

1. **两道过滤**：打分前 21 个预打分过滤器（`home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs:429-453`），选择后 5 个选后过滤器（`:511-517`）。旧版是 14 个加 3 个。
2. **过滤器顺序执行**：框架把上一个过滤器的 `kept` 交给下一个（`candidate-pipeline/candidate_pipeline.rs:379-397`）。被删的候选汇入 `filtered_candidates`，供日志与统计使用。
3. **7 个预打分过滤器在开源默认值下不运行**：`ZeroFavCodivertFilter`、`NewUserSoftNsfwFilter`、`NewUserMinEngagementFilter`、`InventoryHoldoutFilter`、`FavHoldoutFilter` 的开关默认关闭；`VideoFilter`、`TopicIdsFilter` 只在请求带对应参数时运行。
4. **`Brazil2026ElectionFilter` 已删除**。它在 2026-08-14 加入，2026-10-28 的上游提交删除了它。删除前它位于 `AuthorSocialgraphFilter` 与 `VideoFilter` 之间。
5. **流水线在选后阶段才使用可见性结果**。`VFCandidateHydrator` 只对入选候选查询可见性。`VFFilter` 删掉 Drop、Tombstone、NotEvaluated 三类结果（`home-mixer/filters/vf_filter.rs:22-27`）。

## 两道过滤的位置

```mermaid
flowchart LR
    SRC[9 个候选源] --> HYD[候选水合 ×15]
    HYD --> PRE["预打分过滤 ×21"]
    PRE --> SCORE[打分 ×5]
    SCORE --> SEL[TopKScoreSelector]
    SEL --> PH["选后水合 ×10<br/>含 VFCandidateHydrator"]
    PH --> POST["选后过滤 ×5"]
    POST --> OUT[截断到 35 条]
```

过滤器实现 `Filter<ScoredPostsQuery, PostCandidate>` trait。`enable()` 决定本次请求是否运行，`filter()` 返回 `FilterResult { kept, removed }`（`candidate-pipeline/filter.rs:29-64`）。多数过滤器用 `FilterResult::partition` 按谓词拆分候选，拆分保留输入顺序（`filter.rs:14-27`）。

## 预打分过滤器全表

下表按装配顺序排列（`phoenix_candidate_pipeline.rs:430-452`）。“默认”列表示该过滤器在开源默认值下是否运行。

| # | 过滤器 | 删掉什么 | `enable()` 条件 | 默认 |
|---|--------|----------|-----------------|------|
| 1 | `DropDuplicatesFilter` | 同一 `tweet_id` 的后续重复；被删条目的召回来源合并到保留条目 | 总是 | 运行 |
| 2 | `ZeroFavCodivertFilter` | 只由 Phoenix ZeroFav 召回带来的候选，除非作者在实验组 | `ViewerZeroFavCodivertIsTreatment` | 不运行 |
| 3 | `CoreDataHydrationFilter` | `author_id == 0`，即核心数据水合失败 | 总是 | 运行 |
| 4 | `NewUserSoftNsfwFilter` | 新用户看到的站外软色情标签帖 | For You 非话题请求、开关开启、账号小于 7 天、打分序列短于 100 | 不运行 |
| 5 | `AgeFilter` | 发布超过 48 小时的帖子 | 总是 | 运行 |
| 6 | `SelfTweetFilter` | 用户自己的帖子 | 总是 | 运行 |
| 7 | `OONRetweetReplyFilter` | 站外转发、站外回复、没有祖先信息的回复 | 总是 | 运行 |
| 8 | `OONNsfwSimclustersFilter` | SimClusters 召回的站外帖，且作者带成人内容标记 | `EnableSimclustersSource` | 运行 |
| 9 | `RetweetDeduplicationFilter` | 指向同一原帖的重复转发或原帖 | 总是 | 运行 |
| 10 | `IneligibleSubscriptionFilter` | 用户未订阅作者的订阅专属帖 | 总是 | 运行 |
| 11 | `PreviouslySeenPostsFilter` | `seen_ids` 或客户端 Bloom 过滤器命中的帖子 | 总是 | 运行 |
| 12 | `PreviouslySeenPostsBackupFilter` | `impressed_post_ids` 命中的帖子 | 总是；列表为空时全部放行 | 运行但无效果 |
| 13 | `PreviouslyServedPostsFilter` | `served_ids` 命中的帖子 | `EnableServedFilterAllRequests`，或下拉加载且非 ForegroundTruncate | 运行 |
| 14 | `ViewerMutedKeywordFilter` | 正文命中静音词的帖子 | 总是；无静音词时全部放行 | 运行 |
| 15 | `AuthorSocialgraphFilter` | 屏蔽、静音、被作者屏蔽等社交图冲突 | 总是 | 运行 |
| 16 | `VideoFilter` | 自身或引用帖带视频的帖子 | 请求带 `exclude_videos` | 随请求 |
| 17 | `TopicIdsFilter` | 话题外的帖子、排除话题内的帖子 | 话题请求或有排除话题 | 随请求 |
| 18 | `NewUserMinEngagementFilter` | 新用户看到的低互动站外帖 | 开关开启，且账号或复活时间在 30 分钟内 | 不运行 |
| 19 | `InventoryHoldoutFilter` | 按帖子类型配置的百分比，确定性抽掉 | 开关开启且任一百分比大于 0 | 不运行 |
| 20 | `FavHoldoutFilter` | 按点赞数分档的百分比，确定性抽掉 | `EnableFavHoldout` | 不运行 |
| 21 | `FairShareRankingFilter` | 候选超过 2800 条时，按召回来源等比例截断 | 开关开启、无缓存、有打分序列、Phoenix 排序 kill switch 未开 | 运行 |

README 的预打分过滤表缺少第 2、4、21 行，并把第 14 行写作 `MutedKeywordFilter`。上表以源码为准。

### 去重类（1、9）

`DropDuplicatesFilter` 按 `tweet_id` 去重（`filters/drop_duplicates_filter.rs:14-31`）。`RetweetDeduplicationFilter` 按 `retweeted_tweet_id.unwrap_or(tweet_id)` 去重：原帖与它的转发只保留第一次出现的那条（`filters/retweet_deduplication_filter.rs:19-30`）。两者都把被删条目的 `retrieval_sources` 合并进保留条目。因此下游仍能知道一条帖子被几个源同时召回。

### 数据完整性与时效（3、5）

`CoreDataHydrationFilter` 保留 `author_id != 0` 的候选（`filters/core_data_hydration_filter.rs:13`）。`AgeFilter` 从雪花 ID 解析发布时间，删除超过 `MAX_POST_AGE` 的帖子（`filters/age_filter.rs:16-30`）。`MAX_POST_AGE` = 48 × 60 × 60 秒，开源默认值（`params/config.rs:39`）。ID 无法解析时，过滤器也删除该帖子（`age_filter.rs:19`）。

### 作者与关系（6、7、15）

`SelfTweetFilter` 删掉 `author_id == user_id` 的帖子（`filters/self_tweet_filter.rs:13-14`）。

`OONRetweetReplyFilter` 是新增过滤器（`filters/oon_retweet_reply_filter.rs:10-29`）。它删除三类候选：

- 站外（`in_network == Some(false)`）的转发。
- 站外的回复。例外是 `EnablePhoenixOonReplies` 打开且候选来自 Phoenix 首页召回；`PhoenixOonRepliesRootOnly` 打开时还要求回复直接挂在根帖下。两个开关的开源默认值都是 false（`params/param.rs:67-78`）。
- 任何 `ancestors` 为空的回复，无论站内站外。

`AuthorSocialgraphFilter` 在候选命中任一条件时删除它（`filters/author_socialgraph_filter.rs:24-48`）：作者被用户静音或屏蔽；作者屏蔽了用户；引用帖作者屏蔽了用户；用户屏蔽了引用帖作者；用户屏蔽了被转发的作者。

### 内容偏好（10、14、16、17）

`IneligibleSubscriptionFilter`：候选有 `subscription_author_id` 时，只有用户订阅了该作者才保留（`filters/ineligible_subscription_filter.rs:21-26`）。

`ViewerMutedKeywordFilter` 替代了旧的 `MutedKeywordFilter`。它用 `TweetTokenizer` 把静音词和正文分词，再用 `MatchTweetGroup` 匹配。匹配在 `tokio::task::block_in_place` 中执行，避免阻塞异步运行时（`filters/viewer_muted_keyword_filter.rs:20-47`）。

`VideoFilter` 只在 `exclude_videos` 时运行，删除 `min_video_duration_ms` 或 `quoted_max_video_duration_ms` 有值的帖子（`filters/video_filter.rs:8-25`）。旧版只检查帖子自身的视频。新版在引用帖带视频时也删除该帖子。

`TopicIdsFilter` 分两步（`filters/topic_ids_filter.rs:15-105`）：

1. **话题请求**：把请求话题经 `TopicIdExpansion` 展开成子话题后匹配候选的话题标签。批量话题请求（话题数大于 6）改为“排除补集”：只删掉全部话题都落在未请求话题里的候选（`:28-41`）。
2. **排除话题**：展开排除话题，删掉命中的候选。**没有话题标签的候选在这一步也被删除**（`:93-99` 的 `_ => false` 分支）。

`TopicIdExpansion` 内置话题分类（`topic_ids_filter.rs:108` 起）。例如， `SPORTS` 展开为足球、篮球、NFL、MLB 等子话题。旧版的 `NewUserTopicIdsFilter` 已删除。

### 已看与已服务（11、12、13）

三个过滤器都用 `related_post_ids_iter()` 检查帖子本身、被转发的原帖、被回复的父帖（`home-mixer/util/candidates_util.rs:13-17`）。因此**父帖已看过的回复也会被删除**。

| 过滤器 | 数据来源 | 说明 |
|--------|----------|------|
| `PreviouslySeenPostsFilter` | `seen_ids` 与客户端 Bloom 过滤器 | Bloom 由 `bloom_filter_entries` 重建，`may_contain` 判定（`filters/previously_seen_posts_filter.rs:16-31`） |
| `PreviouslySeenPostsBackupFilter` | `impressed_post_ids` | 列表为空时直接放行（`filters/previously_seen_posts_backup_filter.rs:15-20`）。开源装配没有注册填充它的 `ImpressedPostsQueryHydrator`（`phoenix_candidate_pipeline.rs:350-352`） |
| `PreviouslyServedPostsFilter` | `served_ids` | `EnableServedFilterAllRequests` 默认 true（`params/param.rs:1027-1032`），所以每次请求都运行（`filters/previously_served_posts_filter.rs:14-19`） |

`ThunderSource` 已在请求里排除 `seen_ids`。其他源不排除，这三个过滤器负责兜底。

### 新用户保护（4、18）

`NewUserSoftNsfwFilter`（`filters/new_user_soft_nsfw_filter.rs:13-57`）删除 `in_network == Some(false)` 且 `soft_nsfw == Some(true)` 的候选。运行条件：For You 非话题请求；`EnableNewUserSoftNsfwFilter` 打开；账号年龄小于 `NewUserSoftNsfwFilterMaxAccountAgeSecs` 604800 秒（7 天）；打分序列长度小于 100，或该上限设为 0。开关默认 false（`params/param.rs:881-898`）。`soft_nsfw` 标签由 `NewUserSoftNsfwLabelHydrator` 在候选水合阶段写入。

`NewUserMinEngagementFilter`（`filters/new_user_min_engagement_filter.rs:31-117`）只作用于站外候选。可选指标是点赞、互动总数、浏览量，也可以除以浏览量取比率。过滤器删除低于阈值的候选。所有候选都缺指标时，过滤器放行整批。运行条件是开关打开，且账号年龄或账号复活后时长小于 1800 秒。开源默认值：关闭、指标 `fav`、阈值 0.0（`params/param.rs:851-880`、`:899-904`）。

### 实验抽样（2、19、20）

`InventoryHoldoutFilter` 用 `(post_id, viewer_id)` 的确定性哈希得到 0-99 的桶号，桶号小于配置百分比就删除（`filters/inventory_holdout_filter.rs:31-48`）。原创、回复、转发三类各有百分比，开源默认值都是 0（`params/param.rs:1034-1057`）。同一用户对同一帖子的抽样结果每次相同。

`FavHoldoutFilter` 复用同一个哈希，百分比由点赞数分档决定（`filters/fav_holdout_filter.rs:10-47`）：点赞数 ≤ 1 不抽；大于 1 抽 2%；大于 3000 抽 15%；中间按表逐档上升。开关默认 false（`params/param.rs:1059-1064`）。

`ZeroFavCodivertFilter` 实现 ZeroFav 召回的作者分桶实验（`filters/zero_fav_codivert_filter.rs:59-110`）。它只在用户处于实验组时运行。它只删“仅由 ZeroFav 召回带来”的候选；作者处于 Treatment 桶时，候选保留。

### 来源配额（21）

`FairShareRankingFilter`（`filters/fair_share_ranking_filter.rs:10-110`）把候选按 `served_type` 分组。总数不超过预算时全部保留；超过时按各组数量等比例分配名额，余数按小数部分大小补齐，每组保留输入顺序中的前 N 条。预算是 `PhoenixScoresResultSize` = 2800，开关 `EnableFairShareRanking` 默认 true（`params/param.rs:1357-1368`）。它是最后一个预打分过滤器，用于控制送进 Phoenix 打分的候选数量。

## 选后过滤器

选后过滤器在 `TopKScoreSelector` 与 10 个选后水合器之后运行（`phoenix_candidate_pipeline.rs:511-517`）。单元测试保证 `VideoCarouselFilter` 是最后一个（`:1184-1193`）。

| 过滤器 | 删掉什么 | 源码 |
|--------|----------|------|
| `VFFilter` | 可见性结果为 Drop、Tombstone、NotEvaluated 的帖子 | `filters/vf_filter.rs:8-27` |
| `AncillaryVFFilter` | 祖先、引用帖或被转发原帖被判 drop 的帖子 | `filters/ancillary_vf_filter.rs:7-18` |
| `AncestorVideoFilter` | `exclude_videos` 时，祖先带视频的回复 | `filters/ancestor_video_filter.rs:8-22` |
| `DedupConversationFilter` | 同一会话中分数较低的分支 | `filters/dedup_conversation_filter.rs:8-49` |
| `VideoCarouselFilter` | 选择阶段为视频轮播额外选入的候选；它们被移入轮播模块 | `filters/video_carousel_filter.rs:14-53` |

### VFFilter 与 AncillaryVFFilter

可见性结果由 `VFCandidateHydrator` 写入（`candidate_hydrators/vf_candidate_hydrator.rs:53-130`）：

- 站内候选与被转发原帖用 `TimelineHome` 安全等级查询；站外候选、祖先帖、引用帖用 `TimelineHomeRecommendations` 查询（`:70-106`）。同一帖子对关注者与非关注者可能得到不同结果。
- `EnableXaiVfClient` 默认 true，水合器调用 Rust 版可见性服务。旧的 Strato 版保留作回退（`:60-65`、`params/param.rs:1350-1355`）。
- `drop_ancillary_posts` 在任一未墓碑化的祖先、引用帖或被转发原帖被判 drop 时置真（`:140-170`）。

`VFFilter` 的判定（`vf_filter.rs:22-27`）：

```rust
match action {
    Action::Allow | Action::Interstitial | Action::Avoid | Action::Downrank => false,
    Action::Drop(_) | Action::Tombstone | Action::NotEvaluated => true,
}
```

旧版按 `visibility_reason` 判定，删除任何非 SafetyResult 原因的帖子。新版按 `visibility_action` 判定，保留 Interstitial、Avoid、Downrank。README 说明：interstitial 与 notice 的帖子留在信息流里，本仓库不负责绘制遮罩。

源码推断：可见性查询失败时，水合器对该候选返回 `Err`（`vf_candidate_hydrator.rs:119-121`），框架不更新字段，`visibility_action` 保持 `None`，`VFFilter` 不删除它（`vf_filter.rs:16`）。线上是否另有兜底，源码未包含。

可见性服务的规则、四种动作与标签来源见 [可见性过滤](https://x-algorithm.vergego.ai/concepts/visibility-filtering/) 与 [标签规则](https://x-algorithm.vergego.ai/concepts/labeling-rules/)。

### DedupConversationFilter

会话 ID 取 `ancestors` 的最小值；没有祖先时取原帖 ID（`dedup_conversation_filter.rs:42-49`）。旧版的兜底是 `tweet_id`，新版是 `get_original_tweet_id()`。因此同一原帖的转发也算同一会话。同一会话出现更高分候选时，过滤器用它替换已保留的那条（`:19-36`）。

### VideoCarouselFilter

只在 For You、`EnableVideoCarousel` 打开、未处于轮播疲劳期、非下拉加载时运行（`home-mixer/util/video_carousel.rs:16-21`）；开关默认 false（`params/param.rs:1217-1222`）。它删掉带 `video_carousel_extra` 标记的候选，再从这些候选与排在第 35 名之后的候选里挑视频，写入 `query.video_carousel`（`video_carousel_filter.rs:24-51`）。它默认去掉 HighRisk 品牌安全等级的视频（`params/param.rs:1277-1282`）。

## 历史说明：Brazil2026ElectionFilter

| 时间 | 事件 |
|------|------|
| 2026-08-14 | 上游提交 c65aa17 加入 `Brazil2026ElectionFilter`，位于 `AuthorSocialgraphFilter` 与 `VideoFilter` 之间 |
| 2026-10-13 | 按巴西选举法院文书，名单缩减为 10 月 25 日第二轮的候选人（过滤器文件注释，commit 17c96bf） |
| 2026-10-26 | README Notable Updates 宣布删除 |
| 2026-10-28 | 上游提交 5ed7963 删除过滤器文件与装配 |

依据是巴西选举决议 23.610 第 28 条 § 1º-A：使用推荐系统的平台须从结果中排除向选举法院报告的频道与账号的内容（付费推广除外）。过滤器内置一份账号 ID 名单，删除作者、被转发作者、被引用作者或任一祖先作者在名单里、且用户没有关注该账号的候选（commit 17c96bf 的 `home-mixer/filters/brazil_2026_election_filter.rs:80-98`）。用户关注的账号不受影响。在 2e2295b 中，该文件与装配都已不存在（`phoenix_candidate_pipeline.rs:429-453`）。

## 设计决策

| 决策 | 理由 |
|------|------|
| 预打分与选后两道过滤 | 打分前删掉不合格候选，省下 Phoenix 打分开销；可见性查询成本高，只对入选候选执行 |
| 过滤器顺序执行 | 前序过滤缩小集合；每个过滤器的删除数独立记录，便于归因 |
| 去重时合并召回来源 | 保留“一条帖子被几个源同时召回”的信息，供统计与下游使用 |
| 已看检查覆盖父帖 | 避免把用户看过的帖子以回复的形式再推一次 |
| 抽样用确定性哈希 | 同一用户对同一帖子结果稳定，实验可复现 |
| FairShare 放在最后 | 先删掉所有不合格候选，再按来源分配打分名额，避免大源挤掉小源 |
| 可见性按动作而非原因判定 | Interstitial、Avoid、Downrank 只改变展示方式或排序，不应在这里删除 |

## FAQ

**Q：`enable()` 返回 false 的过滤器会怎样？**
A：框架跳过该过滤器，只在阶段统计里记录总数与启用数（`candidate-pipeline/candidate_pipeline.rs:386-388`）。

**Q：为什么关注的人的转发能出现，没关注的人的转发不能？**
A：`OONRetweetReplyFilter` 只删 `in_network == Some(false)` 的转发（`oon_retweet_reply_filter.rs:26`）。站内转发不受影响。

## 源码锚点

- `home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs:429-453` —— 21 个预打分过滤器装配
- `home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs:511-517` —— 5 个选后过滤器装配
- `home-mixer/candidate_hydrators/vf_candidate_hydrator.rs:53-170` —— 可见性查询与附属帖判定
- `home-mixer/filters/vf_filter.rs:22-27` —— 可见性动作到删除的映射
- `home-mixer/filters/fair_share_ranking_filter.rs:68-110` —— 等比例配额
- `home-mixer/filters/topic_ids_filter.rs:15-105` —— 话题过滤
- `home-mixer/util/candidates_util.rs:13-17` —— 已看检查的关联 ID

## 相关页面

- [可见性过滤](https://x-algorithm.vergego.ai/concepts/visibility-filtering/) —— `VFFilter` 背后的规则与四种动作
- [标签规则](https://x-algorithm.vergego.ai/concepts/labeling-rules/) —— 可见性规则读取的标签从哪来
- [candidate-pipeline 框架](https://x-algorithm.vergego.ai/concepts/candidate-pipeline-framework/) —— `Filter` trait 与顺序执行
- [home-mixer 编排层](https://x-algorithm.vergego.ai/concepts/home-mixer-orchestration/) —— 过滤器在内层流水线中的位置
- [打分与排序](https://x-algorithm.vergego.ai/concepts/scoring-and-ranking/) —— 过滤之后的打分
- [候选选择与成型](https://x-algorithm.vergego.ai/concepts/candidate-selection/) —— 选后过滤所在的选择阶段
- [SimClusters](https://x-algorithm.vergego.ai/concepts/simclusters/) —— `OONNsfwSimclustersFilter` 针对的召回源
- [限流与 shadowban](https://x-algorithm.vergego.ai/guide/visibility-and-shadowban/) —— 从创作者视角解读这些过滤器

Source: https://x-algorithm.vergego.ai/concepts/filtering-pipeline/index.mdx
