这一页回答什么
candidate-pipeline 是 home-mixer 所有流水线共用的框架 crate。本页回答五个问题:它定义了哪些 trait;每个 trait 的职责与默认行为;框架怎样调度它们(哪些并行、哪些顺序);组件出错时框架怎样应对;框架怎样记录每次执行的追踪。执行器 trait CandidatePipeline 的逐方法说明见 CandidatePipeline trait。
核心结论
- 7 个组件 trait 加 1 个执行器 trait。每个组件 trait 负责一类工作:
QueryHydrator、Source、Hydrator、Filter、Scorer、Selector、SideEffect。执行器CandidatePipeline把它们排成固定顺序。crate 根导出 10 个模块(candidate-pipeline/lib.rs:6-15),比旧版多了pipeline_summary。 - 框架刻意区分并行与顺序。Query 水合、候选源、候选水合、选后水合、side effect 并行执行;过滤器与打分器顺序执行(
candidate-pipeline/candidate_pipeline.rs:239-445)。 - 组件失败不会中断请求。Query 水合器、候选源、水合器、打分器返回
Err时,框架跳过这份结果,其余组件照常生效。水合器与打分器返回的条数不对时,框架作废整批结果(hydrator.rs:42-58、scorer.rs:29-45)。 - 每个组件有
enable()门,默认值为 true。框架在每个阶段先筛掉未启用的组件,并记录总数与启用数(candidate_pipeline.rs:242-243)。 - 新增流水线追踪。框架每次执行生成一份
PipelineTrace,记录各阶段与各组件的耗时、条数、删除数。嵌套调用的流水线挂在父流水线当前阶段下(candidate-pipeline/pipeline_summary.rs:30-55)。这份追踪供日志与 Under the Hood 使用。
框架在系统里的位置
flowchart TB
subgraph FW["candidate-pipeline crate"]
T["7 个组件 trait"]
E["CandidatePipeline trait<br/>execute_stages 编排"]
PS["pipeline_summary<br/>阶段与组件追踪"]
end
subgraph HM["home-mixer"]
P1[PhoenixCandidatePipeline]
P2[ForYouCandidatePipeline]
P3["其余 4 条流水线"]
C["约百个组件实现"]
end
C -->|实现| T
P1 -->|实现| E
P2 -->|实现| E
P3 -->|实现| E
E --> PS
业务代码只做两件事:实现组件 trait;在流水线结构体里把组件装进列表。编排、并行、错误应对、统计都由框架完成。
泛型参数 Q 与 C
每个组件 trait 都带泛型 Q(查询)与 C(候选)。约束定义在 candidate_pipeline.rs:73-79:
pub trait PipelineQuery: Clone + Send + Sync + 'static {
fn params(&self) -> &xai_feature_switches::Params;
fn decider(&self) -> Option<&xai_decider::Decider>;
}
pub trait PipelineCandidate: Clone + Send + Sync + 'static {}
impl<T> PipelineCandidate for T where T: Clone + Send + Sync + 'static {}PipelineQuery要求查询能提供 feature switch 参数与 decider。组件的enable()通常读取这两项。PipelineCandidate有 blanket impl,任何满足Clone + Send + Sync + 'static的类型都自动是候选。home-mixer 用了两种候选:PostCandidate与FeedItem。
七个组件 trait
所有组件 trait 都有 enable()(默认 true)和 name()(默认取类型名最后一段,util.rs:1-3)。每个 trait 的 run() 是框架调用的包装方法。它接收当前 PipelineStage,在业务方法前后记录组件统计。
1. QueryHydrator:补全查询
// candidate-pipeline/query_hydrator.rs:34-36
async fn hydrate(&self, query: &Q) -> Result<Q, String>;
fn update(&self, query: &mut Q, hydrated: Q);hydrate() 返回一个新查询,只填本组件负责的字段。update() 把这些字段拷回主查询。多个水合器并行读取同一份查询,互不干扰。失败时,run() 打印错误并返回 Err(query_hydrator.rs:21-32),框架跳过它的 update()。
2. Source:拉候选
// candidate-pipeline/source.rs:37
async fn source(&self, query: &Q) -> Result<Vec<C>, String>;run() 记录拉到的条数(source.rs:22-35)。框架把所有成功结果拼成一个列表。失败的源贡献零条(candidate_pipeline.rs:296-302)。
3. Hydrator:补全候选
// candidate-pipeline/hydrator.rs:21, 61
async fn hydrate(&self, query: &Q, candidates: &[C]) -> Vec<Result<C, String>>;
fn update(&self, candidate: &mut C, hydrated: C);- 返回值是与输入一一对应的结果列表。条数不一致时,
run()把整批替换成Err(hydrator.rs:42-58)。 update_all()只对Ok的条目调用update()(hydrator.rs:63-69)。单条失败不影响其他候选。hydrate_for_stage()(hydrator.rs:23-30)是新增钩子,让同一个水合器在候选水合与选后水合两个阶段分别记统计。
CachedHydrator:带缓存的水合器
CachedHydrator(hydrator.rs:82-134)不是一个独立阶段,而是 Hydrator 的一种实现方式。框架为它提供 blanket impl(hydrator.rs:136-222),执行逻辑如下:
already_hydrated()为真的候选直接从自身取值(新增,:105-107、:166-169)。- 其余候选用
cache_key_for(query, candidate)查CacheStore。cache_key_for是新增钩子,缓存键可以包含查询字段(:97-99)。 - 未命中的候选一次性交给
hydrate_from_client()远程拉取。成功结果写回缓存。 - 按阶段记录命中与未命中数(
:115-133)。
CacheStore<K, V>(hydrator.rs:76-80)只有异步的 get 与 insert。业务代码提供具体存储。
4. Filter:过滤候选
// candidate-pipeline/filter.rs:9-12, 64
pub struct FilterResult<C> { pub kept: Vec<C>, pub removed: Vec<C> }
fn filter(&self, query: &Q, candidates: Vec<C>) -> FilterResult<C>;filter()是同步方法,取得候选的所有权。- 新增
FilterResult::partition(candidates, keep)(filter.rs:14-27):它按谓词原地拆分候选,保留输入顺序,对每个候选只调用一次谓词。home-mixer 的多数过滤器使用它。 run()记录kept_count、removed_count、filter_rate,并发出{name}.run指标(filter.rs:46-84)。
5. Scorer:打分
// candidate-pipeline/scorer.rs:48-50
async fn score(&self, query: &Q, candidates: &[C]) -> Vec<Result<C, String>>;
fn update(&self, candidate: &mut C, scored: C);接口形状与 Hydrator 相同,也有条数校验(scorer.rs:29-45)。区别在执行方式:打分器顺序执行,后一个打分器能读到前一个写入的分数(candidate_pipeline.rs:408-411)。
6. Selector:选择
// candidate-pipeline/selector.rs:9-12, 52, 68, 80
pub struct SelectResult<C> { pub selected: Vec<C>, pub non_selected: Vec<C> }
fn select(&self, _query: &Q, candidates: Vec<C>) -> SelectResult<C>; // 默认:sort 后按 size 截断
fn score(&self, candidate: &C) -> f64; // 必须实现
fn size(&self) -> Option<usize> { None }默认 select() 调用 sort() 按分降序排序,再按 size() 截断(selector.rs:52-78)。SelectResult::is_empty() 要求两边都为空(:19-21)。选择器的 enable() 为 false 时,框架把全部候选视为选中(candidate_pipeline.rs:416-431)。用法见 候选选择与成型。
7. SideEffect:副作用
// candidate-pipeline/side_effect.rs:7-12, 33
pub struct SideEffectInput<Q, C> {
pub query: Arc<Q>,
pub selected_candidates: Vec<C>,
pub non_selected_candidates: Vec<C>,
}
async fn side_effect(&self, input: Arc<SideEffectInput<Q, C>>) -> Result<(), String>;enable() 接收 Arc<Q>(side_effect.rs:20-22)。框架用 tokio::spawn 把全部 side effect 放进后台任务,并丢弃 join_all 的结果(candidate_pipeline.rs:433-445)。side effect 失败不影响响应,也不回报给调用方。
七个 trait 对照表
| trait | 业务方法 | 同步 / 异步 | 执行方式 | 失败处理 |
|---|---|---|---|---|
QueryHydrator |
hydrate + update |
异步 | 并行 | 跳过该组件 |
Source |
source |
异步 | 并行 | 该源贡献零条 |
Hydrator |
hydrate + update |
异步 | 并行 | 跳过失败条目;条数不对整批作废 |
Filter |
filter |
同步 | 顺序 | 无错误通道 |
Scorer |
score + update |
异步 | 顺序 | 同 Hydrator |
Selector |
score(可重写 select) |
同步 | 单次 | 无错误通道 |
SideEffect |
side_effect |
异步 | 后台并行 | 结果丢弃 |
执行顺序
execute_stages()(candidate_pipeline.rs:126-179)的顺序:
flowchart TB
Q1["1 Query 水合 · 并行"] --> Q2["2 依赖型 Query 水合 · 并行"]
Q2 --> S["3 候选源 · 并行"]
S --> H["4 候选水合 · 并行"]
H --> F["5 过滤 · 顺序"]
F --> SC["6 打分 · 顺序"]
SC --> SEL["7 选择 · 单次"]
SEL --> PH["8 选后水合 · 并行"]
PH --> PF["9 选后过滤 · 顺序"]
PF --> TR["截断到 result_size_for · finalize"]
TR --> SE["10 side effect · 后台"]
- 并行阶段使用
join_all。Query 水合器之间不能互相依赖。需要读取前序结果的水合器放进dependent_query_hydrators(),这批水合器在第一批之后运行(:263-284)。home-mixer 的六条流水线都没有用这个阶段。 - 过滤按顺序执行:
run_filters把上一个过滤器的kept交给下一个(:390-394)。前序过滤器缩小集合,后序过滤器可以假定“某类候选已被清掉”。 - 打分按顺序执行:每个打分器
await完成并写回后,下一个才开始(:408-411)。 - 候选水合与选后水合共用
run_hydrators(:331-348)。两者只有组件列表与阶段标签不同。 - 截断使用
result_size_for(query),默认等于result_size()(:100-102、:154-158)。截掉的候选并入non_selected。
流水线追踪
pipeline_summary 模块是新增的可观测性层。
execute_traced()用pipeline_summary::scope包住整次执行,并给所有指标加上pipeline标签(candidate_pipeline.rs:109-119)。- 各阶段开始时调用
StageStats::begin,结束时写入耗时、条数、保留与删除数(pipeline_summary.rs:66-141)。各组件的run()用ComponentStats写入自己的耗时与条数(:143-193)。 - 框架把追踪存放在 tokio 任务局部变量里。嵌套执行的流水线不单独输出追踪,而是把追踪挂到父流水线最后一个阶段的
nested下(:30-55)。For You 的追踪因此在“候选源”阶段下包含整条PhoenixCandidatePipeline的追踪。 emit()在每次执行结束时输出一行汇总日志,包含各源拉取数、各过滤器删除数(:57-64、:266-303)。- home-mixer 把追踪转成 proto,在 Under the Hood 请求里返回,并为每个组件附上 GitHub 源码链接(
home-mixer/util/under_the_hood.rs:11-60)。
tracing span 的级别由 quiet-spans feature 控制:开启时为 DEBUG,否则为 INFO(lib.rs:1-4)。
具体流水线怎样插入组件
业务流水线是一个持有组件列表的结构体。它在 impl CandidatePipeline 中实现各 getter,返回这些列表。以 PhoenixCandidatePipeline 为例(home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs:224-234、:1112-1152):
pub struct PhoenixCandidatePipeline {
query_hydrators: Vec<Box<dyn QueryHydrator<ScoredPostsQuery>>>,
sources: Vec<Box<dyn Source<ScoredPostsQuery, PostCandidate>>>,
hydrators: Vec<Box<dyn Hydrator<ScoredPostsQuery, PostCandidate>>>,
filters: Vec<Box<dyn Filter<ScoredPostsQuery, PostCandidate>>>,
scorers: Vec<Box<dyn Scorer<ScoredPostsQuery, PostCandidate>>>,
selector: TopKScoreSelector,
post_selection_hydrators: Vec<Box<dyn Hydrator<ScoredPostsQuery, PostCandidate>>>,
post_selection_filters: Vec<Box<dyn Filter<ScoredPostsQuery, PostCandidate>>>,
side_effects: Arc<Vec<Box<dyn SideEffect<ScoredPostsQuery, PostCandidate>>>>,
}不需要某个阶段时,getter 返回 &[]。ForYouCandidatePipeline 的候选水合、打分、选后水合采用这种做法(for_you_candidate_pipeline.rs:336-354)。components()(candidate_pipeline.rs:181-229)按阶段列出全部组件名,供自检与调试使用。
设计决策
| 决策 | 理由 |
|---|---|
| 组件 trait 与执行器分离 | 业务只写“做什么”,框架统一“怎么调度” |
| 独立工作并行,有依赖的工作顺序 | 水合彼此独立,并行省延迟;过滤与打分依赖前序结果 |
| 组件失败只跳过自己 | 单个下游服务故障不让整次请求失败 |
| 水合与打分校验返回条数 | 防止结果错位写到别的候选上 |
| side effect 放后台 | 写日志、写缓存不进入请求延迟 |
| 追踪按任务局部变量收集并支持嵌套 | 内外两层流水线的追踪合成一棵树,不需要显式传递上下文 |
result_size_for 钩子 |
结果条数可按请求变化,无需改框架 |
FAQ
Q:一个 Query 水合器失败,下游会怎样?
A:该水合器负责的字段保持默认值,其他水合器的结果照常写入(candidate_pipeline.rs:250-254)。下游组件要自行应对字段缺失。例如, PhoenixSource 在缺少召回序列时返回错误(home-mixer/sources/phoenix_source.rs:113-116)。
Q:过滤器能不能并行?
A:框架不支持。run_filters 是同步循环(candidate_pipeline.rs:379-397)。过滤器本身多是内存计算,Filter trait 也没有异步方法。
源码锚点
candidate-pipeline/lib.rs:1-15—— crate 根,10 个模块与 span 级别candidate-pipeline/candidate_pipeline.rs:23-35——PipelineStage十阶段candidate-pipeline/candidate_pipeline.rs:73-104—— 泛型约束与执行器 gettercandidate-pipeline/candidate_pipeline.rs:126-179——execute_stages()candidate-pipeline/candidate_pipeline.rs:239-445—— 各阶段的执行方法candidate-pipeline/query_hydrator.rs:10-41——QueryHydratorcandidate-pipeline/source.rs:10-42——Sourcecandidate-pipeline/hydrator.rs:11-222——Hydrator、CacheStore、CachedHydratorcandidate-pipeline/filter.rs:9-85——FilterResult与Filtercandidate-pipeline/scorer.rs:9-63——Scorercandidate-pipeline/selector.rs:9-107——SelectResult与Selectorcandidate-pipeline/side_effect.rs:7-38——SideEffectInput与SideEffectcandidate-pipeline/pipeline_summary.rs:30-327—— 流水线追踪
相关页面
- CandidatePipeline trait —— 执行器方法逐个说明
- home-mixer 编排层 —— 六条流水线怎样装配组件
- 过滤流水线 ——
Filter的具体实现 - 打分与排序 ——
Scorer的具体实现 - 候选选择与成型 ——
Selector的具体实现 - Under the Hood 透明度工具 —— 流水线追踪的对外用途
- 系统架构总览 —— 框架在系统中的位置