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

265 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Rerank & Freqcap重排 / 去重 / 频控Plan
> 对应规范:`spec_kit/Personalized Reco/modules/rerank-freqcap/spec.md`
>
> 规则来源(必须对齐):
>
> - `设计说明文档/個性化推薦算法規則.md`Feed 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_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 | 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_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: 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|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_score`
- `Sim(c,s)`
- `content_id` 相同:`Sim=1`
- `template_id` 相同且非空:`Sim += 0.6`
- `author_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** 作为代表标签(若为空则跳过)
> 说明:内容画像是 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_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`,实现更平滑的序列控制(而非一刀切过滤)。