新功能:个性化推荐算法
This commit is contained in:
264
spec_kit/Personalized Reco/modules/rerank-freqcap/plan.md
Normal file
264
spec_kit/Personalized Reco/modules/rerank-freqcap/plan.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# 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 | 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 默认 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: 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** 作为代表标签(若为空则跳过)
|
||||
|
||||
> 说明:内容画像是 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 频控实现顺序(先句子,再作者/模板):
|
||||
|
||||
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 Feed:MMR 序列重排(建议实现)
|
||||
|
||||
步骤:
|
||||
|
||||
- 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. 可观测与 meta(V1)
|
||||
|
||||
本模块建议输出(供 `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`,实现更平滑的序列控制(而非一刀切过滤)。
|
||||
|
||||
Reference in New Issue
Block a user