9.9 KiB
9.9 KiB
Rerank & Freqcap(重排 / 去重 / 频控)|Plan
对应规范:
spec_kit/Personalized Reco/modules/rerank-freqcap/spec.md规则来源(必须对齐):
设计说明文档/個性化推薦算法規則.md(Feed MMR λ=0.7;Push/Widget 冷却口径)spec_kit/Personalized Reco/overview.md(模块边界:本模块在 Soft Scoring 之后执行)
1. 目标与交付物
1.1 目标
- 将 Soft Scoring 后的候选集变为可下发的最终排序(长度 ≤ k)。
- 实现 V1 最小集合:
- 去重:排除
already_recommended_ids ∪ touched_or_viewed_ids - Feed 序列多样性:MMR 重排(离散特征版)
- Push/Widget 频控与冷却:至少保证“同句不重复”;作者/模板按输入能力做增强
- 去重:排除
- 输出稳定的
meta统计字段,用于定位 served_k 不足的原因(dedup/freqcap 导致清空等)。
1.2 交付物
modules/rerank-freqcap/plan.md:本技术计划(本文件)。- 代码实现(tasks 阶段落地)建议位置:
server/app/features/personalized_reco/rerank_freqcap/- 包含:
- 纯函数
rerank_and_freqcap(...) -> RerankResult RerankConfig与默认参数(按 scene)Sim/Tag构造工具函数(Feed MMR)
- 纯函数
- 单元测试(tasks 阶段落地):
- 去重正确性
- Feed MMR 的 Top1 + 多样性选择
- Push/Widget 冷却规则(在给定历史集合输入下)
2. 模块职责边界(V1 约定)
2.1 本模块负责
- 从 scored_candidates 中做过滤/重排:
- Dedup:按历史集合过滤
- Freqcap:按冷却维度(句子/作者/模板)做“硬过滤或强约束”
- Feed:MMR 生成序列(保证多样性)
- 输出
ranked_items与meta(候选规模、过滤数量、缺失输入统计等)。
2.2 不在本模块实现
- 不计算 Soft Scoring 分数:只消费
final_score(或等价的 score)。 - 不做 Hard Filter:Hard Filter 发生在更早阶段,本模块只处理已通过 Hard Filter 的候选。
- 不维护服务端长期历史(V1):冷却窗口“X 天”由客户端在请求时传入对应的“最近窗口内集合”,或由未来服务端侧补齐。
说明(V1 冷却窗口语义):本模块以“输入集合代表冷却窗口内的历史”为准。
cooldown_*_days作为配置与可观测字段保留,便于未来接入服务端历史后真正按时间计算。
3. 输入/输出与数据结构(V1)
3.1 输入
scene:feed | push | widgetscored_candidates:List[ScoredCandidate],至少包含:content_id: intfinal_score: float(或score)author_id: str | Nonetemplate_id: str | Nonecontent_profile(用于 feed 标签:stage/need/context 等;缺失时可退化)
already_recommended_ids:List[str|int]touched_or_viewed_ids:List[str|int]- 可选历史(若客户端暂不传,V1 作为增强项):
recent_author_ids: List[str] | Nonerecent_template_ids: List[str] | None
k: 目标条数(feed 默认 30;push/widget 默认 1)config:mmr_lambda(Feed 默认 0.7)cooldown_sentence_days/cooldown_author_days/cooldown_template_days(按场景默认)
3.2 输出
ranked_items:List[ScoredCandidate](长度 ≤ k)meta(V1 必须字段):candidate_pool_size_after_dedup: intcandidate_pool_size_after_freqcap: intfreqcap_filtered_counts: { sentence?: int, author?: int, template?: int }(可选但建议)missing_history_fields: List[str](例如recent_author_ids未提供)
4. 关键技术决策(V1)
4.1 ID 归一化(避免 str/int 混用导致漏过滤)
由于输入历史集合可能是 str|int,V1 统一做:
- 尽量将
content_id归一化为int - 无法转换的值忽略并记录 debug(不影响主流程)
4.2 Push/Widget 的频控策略:先保证“同句不重复”,再增强作者/模板
V1 选择“安全且可落地”的策略:
- 句子冷却(必做,硬过滤):
- 若
content_id出现在历史集合中,则直接过滤
- 若
- 作者/模板冷却(增强项):
- 若
recent_author_ids/recent_template_ids有输入,则对命中者执行硬过滤 - 若无输入,则跳过该维度,但在
meta.missing_history_fields记录缺失,便于可观测
- 若
说明:规范允许作者/模板作为硬频控或强降权。V1 采用“有输入就硬过滤、无输入就跳过”的方式,避免伪实现与误杀。
4.3 Feed 的多样性:MMR(离散特征版)
V1 实现 MMR 的离散相似度(不依赖 embedding):
[ MMR(c)=\lambda\cdot Rel(c) - (1-\lambda)\cdot \max_{s\in S} Sim(c,s) ]
Rel(c):使用final_scoreSim(c,s):content_id相同:Sim=1template_id相同且非空:Sim += 0.6author_id相同且非空:Sim += 0.3- 标签重合(Jaccard):
Sim += 0.1 * Jaccard(tags_c, tags_s) - clamp 到
[0,1]
标签集合 tags_* 的 V1 落地定义(必须可算、且对缺字段鲁棒):
stage:<stage>(例如stage:general/expecting/parenting/unknown)need:<key>:从need_suitability中取 最大值的 key 作为代表标签(若为空则跳过)context:<key>:从context_suitability中取 最大值的 key 作为代表标签(若为空则跳过)
说明:内容画像是 suitability(0/0.5/1)结构;V1 取 argmax 能保证标签集合小且稳定,便于测试。后续可扩展为“取所有 ≥0.5 的 key”以增强多样性。
5. 具体算法流程(V1)
5.1 Dedup(必做,三场景共用)
输入:
seen_ids = already_recommended_ids ∪ touched_or_viewed_ids
处理:
- 过滤
content_id ∈ seen_ids的候选
输出:
candidate_pool_size_after_dedup = len(filtered_candidates)
5.2 Freqcap(Push/Widget 必做;Feed 可选)
V1 频控实现顺序(先句子,再作者/模板):
- 句子冷却:过滤
content_id ∈ seen_ids - 作者冷却(若提供
recent_author_ids):过滤author_id ∈ recent_author_ids - 模板冷却(若提供
recent_template_ids):过滤template_id ∈ recent_template_ids
输出:
candidate_pool_size_after_freqcapfreqcap_filtered_counts(按维度统计被过滤数量)
5.3 Feed:MMR 序列重排(建议实现)
步骤:
- Top1:直接取
final_score最高者 - 对后续位置 t=2..k:
- 对每个未选候选 c 计算
MMR(c) - 选择
MMR最大者加入序列
- 对每个未选候选 c 计算
性能与实现约束(V1):
- 候选数 N(例如 200~500)时,朴素 (O(kN^2)) 仍可能偏大;V1 可采用:
- 先截断到
top_n_for_mmr(例如 200)再做 MMR - 或缓存
Sim(c,s)的最大值并增量更新(实现复杂度更高,V1 可不做)
- 先截断到
5.4 Push/Widget:选 TopK
在 dedup+freqcap 后:
- 按
final_score降序取前 k 条作为ranked_items
6. 默认参数(V1 建议)
6.1 Feed
mmr_lambda = 0.7top_n_for_mmr = 200(避免候选过大导致重排过慢)
6.2 Push(冷却窗口口径来自算法规则的工程默认)
cooldown_sentence_days = 14(同句 14 天不重复)cooldown_author_days = 7(同作者 7 天不重复,需输入recent_author_ids才能执行)cooldown_template_days = 7(同模板 7 天不重复,需输入recent_template_ids才能执行)
6.3 Widget
cooldown_sentence_days = 7cooldown_author_days = 7cooldown_template_days = 7
说明:V1 冷却天数在本模块主要用于配置与可观测字段;真正“按天”判断需要历史带时间戳或服务端持久化,后续迭代补齐。
7. 可观测与 meta(V1)
本模块建议输出(供 observability 子模块汇总):
candidate_pool_size_after_dedupcandidate_pool_size_after_freqcapfreqcap_filtered_counts(sentence/author/template)missing_history_fields:- 例如客户端未提供
recent_author_ids→ 记录author - 未提供
recent_template_ids→ 记录template
- 例如客户端未提供
目标:当 served_k 过少时,能快速判断是 dedup/freqcap 导致,还是上游候选不足。
8. 测试计划(V1)
8.1 单元测试(纯函数)
- Dedup:
- 输入历史包含某些
content_id,输出必须不包含这些 id str/int混用能正确归一化
- 输入历史包含某些
- Freqcap:
- 仅提供
content_id历史时:句子冷却生效 - 提供
recent_author_ids时:作者维度过滤生效;未提供时meta.missing_history_fields正确 - 提供
recent_template_ids时:模板维度过滤生效;未提供时meta.missing_history_fields正确
- 仅提供
- Feed MMR:
- Top1 恒等于最高分
- 后续序列在候选足够时避免连续同作者/同模板(可用统计阈值断言)
tags缺失时仍能稳定运行(只使用可得字段)
8.2 最小集成验证(与 reco-engine 串联时)
- 输入一批 scored_candidates + 历史集合:
- Feed:输出长度 ≤ k,且 meta 规模统计正确
- Push/Widget:在历史命中时能过滤掉重复句子
9. 风险与后续演进
9.1 已知风险
- V1 冷却窗口“按天”无法严格执行:因为历史输入缺少时间戳或服务端持久化。本模块已通过“输入集合代表窗口内历史”做可落地实现,但需要在产品/客户端侧保证窗口裁剪正确。
- Feed MMR 的性能:候选过大时重排可能变慢;V1 用
top_n_for_mmr截断兜底。
9.2 V1.1+ 演进方向
- 服务端侧持久化冷却历史(按用户维度记录 sentence/author/template 的最近触达时间),真正按
cooldown_*_days判定。 - 将“作者/模板冷却”从硬过滤升级为“强降权 + 允许破例”,并在 meta 中记录“破例原因”(候选不足等)。
- 将
P_repeat/P_fatigue由rerank-freqcap产出并注入scoring的external_terms,实现更平滑的序列控制(而非一刀切过滤)。