Files
mindfulness/spec_kit/Personalized Reco/modules/rerank-freqcap/plan.md
2026-02-02 16:47:37 +08:00

9.9 KiB
Raw Blame History

Rerank & Freqcap重排 / 去重 / 频控Plan

对应规范:spec_kit/Personalized Reco/modules/rerank-freqcap/spec.md

规则来源(必须对齐):

  • 设计说明文档/個性化推薦算法規則.mdFeed MMR λ=0.7Push/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按冷却维度句子/作者/模板)做“硬过滤或强约束”
    • FeedMMR 生成序列(保证多样性)
  • 输出 ranked_itemsmeta(候选规模、过滤数量、缺失输入统计等)。

2.2 不在本模块实现

  • 不计算 Soft Scoring 分数:只消费 final_score(或等价的 score
  • 不做 Hard FilterHard Filter 发生在更早阶段,本模块只处理已通过 Hard Filter 的候选。
  • 不维护服务端长期历史V1冷却窗口“X 天”由客户端在请求时传入对应的“最近窗口内集合”,或由未来服务端侧补齐。

说明V1 冷却窗口语义):本模块以“输入集合代表冷却窗口内的历史”为准。cooldown_*_days 作为配置与可观测字段保留,便于未来接入服务端历史后真正按时间计算。


3. 输入/输出与数据结构V1

3.1 输入

  • scene: feed | push | widget
  • scored_candidates: List[ScoredCandidate],至少包含:
    • content_id: int
    • final_score: float(或 score
    • author_id: str | None
    • template_id: str | None
    • content_profile(用于 feed 标签stage/need/context 等;缺失时可退化)
  • already_recommended_ids: List[str|int]
  • touched_or_viewed_ids: List[str|int]
  • 可选历史若客户端暂不传V1 作为增强项):
    • recent_author_ids: List[str] | None
    • recent_template_ids: List[str] | None
  • k: 目标条数feed 默认 30push/widget 默认 1
  • config
    • mmr_lambdaFeed 默认 0.7
    • cooldown_sentence_days/cooldown_author_days/cooldown_template_days(按场景默认)

3.2 输出

  • ranked_items: List[ScoredCandidate](长度 ≤ k
  • metaV1 必须字段):
    • candidate_pool_size_after_dedup: int
    • candidate_pool_size_after_freqcap: int
    • freqcap_filtered_counts: { sentence?: int, author?: int, template?: int }(可选但建议)
    • missing_history_fields: List[str](例如 recent_author_ids 未提供)

4. 关键技术决策V1

4.1 ID 归一化(避免 str/int 混用导致漏过滤)

由于输入历史集合可能是 str|intV1 统一做:

  • 尽量将 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_score
  • Sim(c,s)
    • content_id 相同:Sim=1
    • template_id 相同且非空:Sim += 0.6
    • author_id 相同且非空:Sim += 0.3
    • 标签重合JaccardSim += 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 作为代表标签(若为空则跳过)

说明:内容画像是 suitability0/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 FreqcapPush/Widget 必做Feed 可选)

V1 频控实现顺序(先句子,再作者/模板):

  1. 句子冷却:过滤 content_id ∈ seen_ids
  2. 作者冷却(若提供 recent_author_ids):过滤 author_id ∈ recent_author_ids
  3. 模板冷却(若提供 recent_template_ids):过滤 template_id ∈ recent_template_ids

输出:

  • candidate_pool_size_after_freqcap
  • freqcap_filtered_counts(按维度统计被过滤数量)

5.3 FeedMMR 序列重排(建议实现)

步骤:

  • Top1直接取 final_score 最高者
  • 对后续位置 t=2..k
    • 对每个未选候选 c 计算 MMR(c)
    • 选择 MMR 最大者加入序列

性能与实现约束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.7
  • top_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 = 7
  • cooldown_author_days = 7
  • cooldown_template_days = 7

说明V1 冷却天数在本模块主要用于配置与可观测字段;真正“按天”判断需要历史带时间戳或服务端持久化,后续迭代补齐。


7. 可观测与 metaV1

本模块建议输出(供 observability 子模块汇总):

  • candidate_pool_size_after_dedup
  • candidate_pool_size_after_freqcap
  • freqcap_filtered_countssentence/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_fatiguererank-freqcap 产出并注入 scoringexternal_terms,实现更平滑的序列控制(而非一刀切过滤)。