# 子模块：Rerank & Freqcap（重排 / 去重 / 频控）｜Spec

## 1. 目标描述

对 Soft Scoring 后的候选集做最终的“可下发排序”，解决：

- **去重**：避免同一句在短期内反复出现。
- **多样性**：Feed 场景需要序列多样性（作者/模板/标签）。
- **频控与冷却**：Push/Widget 必做，按句子/作者/模板维度做窗口期不重复。

> 当前确认：历史集合由客户端在请求时传入（已推荐 ID、已触达/浏览 ID）。

---

## 2. 输入 / 输出定义

### 2.1 输入

- `scene`: `feed | push | widget`
- `scored_candidates`: `List[{ content_id, score, content_profile... }]`
- `already_recommended_ids`: `List[str|int]`
- `touched_or_viewed_ids`: `List[str|int]`
- （可选）`recent_author_ids` / `recent_template_ids`（若客户端暂不传，可由后续服务端侧补齐）
- `k`: 目标条数
- `config`：
  - Feed：`mmr_lambda`（默认 0.7）
  - 冷却窗口：`cooldown_sentence_days / cooldown_author_days / cooldown_template_days`

### 2.2 输出

- `ranked_items`: 排好序的候选（长度 ≤ k）
- `meta`：
  - `candidate_pool_size_after_dedup`
  - `candidate_pool_size_after_freqcap`
  - `freqcap_filtered_counts`（可选，按维度统计）

---

## 3. 规则与算法（V1 最小集合）

### 3.1 去重（必做）

- 过滤 `content_id` ∈ `already_recommended_ids ∪ touched_or_viewed_ids` 的候选。
- 去重键定义（对齐算法规则的工程约定）：
  - `sentence_key = content_id`
  - `author_key = author_id`（可空）
  - `template_key = template_id`（可空）

### 3.2 Feed：MMR 序列重排（建议）

- Top1：最高分，确保第一眼命中。
- 后续：使用 MMR 选择序列，最大化与已选内容的差异（作者/模板/need/context/stage）。

MMR 定义：

\[
MMR(c)=\lambda\cdot Rel(c) - (1-\lambda)\cdot \max_{s\in S} Sim(c,s)
\]

其中：

- `Rel(c)`：可直接使用 `final_score`
- `Sim(c,s)`：离散特征版（V1 推荐）：
  - content_id 相同：1
  - template_id 相同：+0.6
  - author_id 相同：+0.3
  - 标签重合（Jaccard）：+0.1 * Jaccard(tags_c, tags_s)
  - clamp 到 [0,1]

### 3.3 Push/Widget：频控与冷却（必做）

最小要求：

- 同一句在窗口 X 天内不重复（句子冷却）。
- 同作者/同模板在窗口期内尽量不重复（可作为硬频控或强降权，plan 阶段定）。

> 当前 V1 输入侧只确认有 content_id 历史；若 author/template 历史暂不具备，可先对 content_id 强硬去重，并将 author/template 频控作为“可选增强”（meta 仍需统计缺失原因）。

---

## 4. 验收标准（可验证）

- **去重正确**：输出不包含 `already_recommended_ids` 与 `touched_or_viewed_ids`。
- **Feed 序列多样**：在候选足够时，序列中不会出现大量同作者/同模板连续重复（可用统计阈值验收）。
- **Push/Widget 频控生效**：在冷却窗口内同一句不会再次被推荐（基于传入历史集合验证）。
- **可观测**：输出 meta 中包含 `after_dedup/after_freqcap` 的候选规模，便于排查 served_k 不足原因。

