# Personalized Reco｜后端个性化推荐算法模块（解耦版）｜Spec（索引版）

> 阶段：高层规范（spec）
>
> 目标：把「个性化推荐算法」做成后端**独立模块**，输入用户画像与曝光历史，模块内部从数据库拉取文案候选并排序，输出推荐句子（支持 Feed / Push / Widget 三种场景的可调参策略）。
>
> 依据：
>
> - `设计说明文档/個性化推薦算法規則.md`
> - `设计说明文档/句子文案打分規則.md`（risk_flags 命名与语义必须严格对齐）
> - `spec_kit/User Profile Scoring/spec.md`（用户画像输入契约，V1.2）

---

## 1. 背景与动机（摘要）

需要在后端实现一套跨场景一致、可调参、可观测、可回退的推荐流程，并将其做成独立模块以便复用（API / Celery 均可调用），降低 Push 等高风险场景的翻车概率。

已确认约束：

- 数据库当前未设计且为空：本需求内新增 DB 设计与迁移子模块。
- risk_flags 等内容画像语义：严格按 `句子文案打分规则` 执行。
- 推荐请求：由客户端在请求时传入 `user_profile` 与历史集合。
- Widget 情绪区间（0.4~0.8）：采用软降权。
- Push/Widget：需要频控与冷却；且 API 与 Celery 两种调用方式都需要。

---

## 2. 目标（Goals）

- **模块解耦**：推荐逻辑以独立包/子模块方式存在，暴露稳定接口，不与 FastAPI 路由/ Celery 任务强绑定。
- **统一 Pipeline**：Hard Filter → Soft Scoring → Rerank（多样性/新鲜度/疲劳），并支持候选不足的回退梯度（L0~L3）。
- **输入明确**：输入用户画像（允许字段缺失）、已推荐的文案 ID、已触达/浏览的文案 ID（用于去重/疲劳/频控）。
- **从 DB 取候选**：模块内部负责根据画像与场景召回候选，并从数据库读取文案画像/风险标记/元信息。
- **输出可直接下发**：输出推荐句子（含 content_id、文本、排序分与必要的解释字段用于打点/调参）。
- **可观测**：对候选规模、过滤原因、回退层级、 served_k 等关键指标提供统一事件结构，便于落点与报表。

---

## 3. 非目标（Non-goals）

- **不在本阶段定义**：内容生产（生成句子）、人工审核工作流、embedding/向量检索（V2 扩展）。
- **不绑定具体表结构**：仅规定“必须能读到的字段契约”，具体表名/索引/迁移由后续 plan 细化。
- **不负责推送/Widget 排程**：模块只负责“给定场景与约束，返回推荐结果”，调度由上层系统决定。

---

## 4. 术语与数据对象（Definitions，摘要）

### 4.1 场景（Scene）

- **feed**：探索优先（Exploration-first），返回序列 TopK（例如 30 条）。
- **push**：精确与安全优先（Precision-first），通常返回 Top1（或 TopN 供上层挑选）。
- **widget**：稳定与品牌一致性优先（Consistency-first），返回 Top1（每日一句）。

### 4.2 用户画像 U（UserProfile）

用户画像结构以客户端问卷输出为准（V1.2），详见：

- `spec_kit/User Profile Scoring/spec.md`（字段与缺失约定）
- `modules/reco-engine/spec.md`（本模块如何消费画像与缺失判定）

### 4.3 文案画像 C（ContentProfile）

模块需能从 DB 读到（或通过视图/派生字段得到）：

- **content_id**（稳定主键）
- **text**（句子文本）
- **stage**（或可推导的 stage 标签）
- **emotion_score**（0~1 或标记为 general）
- **need_suitability / context_suitability**（离散适配表/JSON）
- **personalization_power**（0 / 0.5 / 1）
- **risk_flags**（集合/数组/位图均可，语义一致即可）
- （可选）**author_id / template_id / review_confidence**

---

## 5. 模块边界与接口（API Contract）

### 5.1 入口函数（推荐引擎接口）

推荐模块对外暴露一个主入口，建议形式（不限定语言结构，但需表达同等信息）：

- **输入**：
  - `scene`: `"feed" | "push" | "widget"`
  - `user_profile`: `UserProfile`
  - `already_recommended_ids`: `list[int|str]`
  - `touched_or_viewed_ids`: `list[int|str]`（触达/浏览/点击等均归一为“已曝光”集合）
  - `k`: int（feed 默认 30，push/widget 默认 1）
  - `now`: 时间戳（用于时间衰减/冷却窗口）
  - `constraints`（可选）：如黑名单 author/template、最大风险等级、频控窗口天数等
- **输出**：
  - `items`: 推荐结果数组（长度 ≤ k）
  - `meta`: 本次推荐的统计与解释字段（用于打点/调参）

### 5.2 输出结果项（RecommendedItem）

每条推荐至少包含：

- `content_id`
- `text`
- `final_score`
- `fallback_level_final`
- `explanations`（轻量可选）：如命中 need/context/stage、是否 general、被降个性化的原因（缺失/低置信度/回退）

### 5.3 数据访问抽象（Repository / Gateway）

为实现解耦，推荐模块**不得**直接依赖 FastAPI 的 `Depends`；应通过注入的 `ContentRepository` 读取数据：

- `fetch_candidates(scene, user_profile, limit, fallback_level) -> list[ContentProfile]`
- `fetch_contents_by_ids(ids) -> list[ContentProfile]`（用于补全曝光历史的画像信息时可用）

实现层可使用 `AsyncSession`（SQLAlchemy）完成具体查询，但算法层不感知 ORM/SQL。

---

## 6. 推荐流程（统一 Pipeline）

本节是工程必须遵守的“骨架”，参数可按场景配置。

### 6.1 Candidate Generation（候选集生成）

核心要求：

- 依据 `U` 召回一个候选池：匹配 need/context/stage 的内容 + 一部分通用内容（general）。
- 若问卷允许跳过导致字段缺失（need/context/emotion_score 缺失），候选池需自动提高通用安全内容占比，并视为至少进入 **L1**（限制 `personalization_power ≤ 0.5`）。

### 6.2 Hard Filter（硬性过滤）

基于 `risk_flags` 与产品规则做剔除（说明文档 3.x）：

- `block_health_medical`：全场景硬过滤。
- `U.stage.unknown=1`：过滤 `unsafe_for_stage_unknown`。
- `U.stage.parenting=1`：过滤 `unsafe_for_stage_parenting`。
- `U.emotion_score≤0.2`：过滤 `unsafe_for_emotion_low`。
- 跨维度规则：`U.stage.unknown=1` 且 `need_suitability[parenting_pressure]=1` 且 `personalization_power=1` → 禁推。

Hard Filter 后需输出 `candidate_pool_size_after_hard_filter` 以及若清空的 `empty_reason`。

### 6.3 Soft Scoring（软性打分）

采用说明文档的线性加权结构：

最终分：

\[
final\_score(U,C_i)=\mathbb{I}[pass]\times\Big(S_{core}+S_{personal}+S_{fresh}-P_{fatigue}-P_{repeat}-P_{risk}-P_{uncertainty}\Big)
\]

其中

\[
S_{core}=w_{need}S_{need}+w_{emotion}S_{emotion}+w_{stage}S_{stage}+w_{context}S_{context}
\]

缺失字段的保守计算（说明文档 V1.2，必须实现）：

- `U.need` 缺失 → `S_need = 0.5`
- `U.context` 缺失 → `S_context = 0.5`
- `U.emotion_score` 缺失 → `S_emotion = 0.8`（general 仍为 0.8）

个性化加成（必须受回退梯度约束）：

\[
S_{personal}=\alpha \cdot personalization\_power \cdot \max(S_{need}, S_{context})
\]

不确定性惩罚（Push 建议默认开启，模块需支持开关）：

\[
P_{uncertainty}=\beta \cdot (1-conf_U)\cdot(1-conf_{C_i})\cdot personalization\_power
\]

### 6.4 Rerank（重排：多样性/新鲜度/疲劳）

最小要求：

- **去重**：排除 `already_recommended_ids` 与 `touched_or_viewed_ids`（同一句不重复）。
- **多样性**：作者/模板/标签多样性；Feed 场景建议使用 MMR（说明文档 4.5）。
- **频控与冷却**（Push/Widget 必做）：同句/同作者/同模板在窗口 X 天内不重复（具体 X 由 plan 定参数）。

---

## 7. 回退策略（Fallback Ladder）

候选不足或过滤/频控导致 served_k 过少时，必须按梯度回退，并输出最终 `fallback_level_final`：

- **L0（正常）**：按场景默认召回配比。
- **L1（放宽匹配）**：need/context 命中阈值放宽；`personalization_power ≤ 0.5`。
- **L2（回退通用池）**：增加 general 低风险句；`personalization_power = 0`。
- **L3（兜底安全池）**：仅从通用安全句库/白名单池抽取。

规则：

- 回退时必须“降个性化与降风险”，尤其 Push。
- 每次回退都要重新统计候选规模，并记录触发原因（如 `hard_filter_all` / `freqcap_all` / `pool_empty`）。

---

## 8. 场景默认参数（V1 建议）

### 8.1 Feed（Exploration-first）

- 候选配比：匹配 60% + 通用 30% + 探索 10%
- 权重建议：`w_need=0.35, w_emotion=0.20, w_stage=0.15, w_context=0.30`
- 序列：Top1 最高分，后续使用 MMR（λ=0.7）

### 8.2 Push（Precision & Safety-first）

- 候选配比：need 强命中 70% + emotion 命中 20% + 通用安全 10%
- 权重建议：`w_need=0.45, w_emotion=0.35, w_stage=0.15, w_context=0.05`
- 默认启用 `P_uncertainty`；当 `fallback_level_final>0` 或画像缺失/低置信度时自动降个性化

### 8.3 Widget（Consistency & Brand-first）

- 候选池：以 general + `personalization_power≤0.5` 为主
- 权重建议：`w_need=0.25, w_emotion=0.25, w_stage=0.30, w_context=0.20`
- 情绪区间约束：建议 `emotion_score` 限制在 0.4~0.8（过低/过高降权或过滤，具体由 plan 定）

---

## 9. 数据库与存储契约（DB Contract）

模块需要 DB 提供以下能力（不限定表名实现方式）：

- **按标签召回**：能按 need/context/stage/general、personalization_power、risk_flags 等条件筛选候选。
- **按 ID 批量拉取**：用于补全曝光历史或在 rerank 时补字段。
- **安全池支持**：L3 兜底必须能拉到一批“通用安全句”（白名单/低风险集合）。

性能要求（V1）：

- 单次推荐（k<=30）应避免 N+1 查询；候选批量拉取 + 内存打分。
- 查询需可加索引：`stage`、`general`、`personalization_power`、（以及用于召回的标签字段）。

---

## 10. 可观测性（事件与指标）

每次推荐（Feed session / 每次 Push / 每日 Widget）必须能产出以下字段（由上层统一打点即可）：

- `candidate_pool_size_raw`
- `candidate_pool_size_after_hard_filter`
- `candidate_pool_size_after_dedup`
- `candidate_pool_size_after_freqcap`
- `fallback_level_final`
- `served_k`
- `empty_reason`（served_k=0 时必填）

建议额外输出（用于调参/排查）：

- `scene`
- `conf_U`
- `missing_fields`（need/context/emotion 的缺失情况）
- `risk_filtered_count_by_flag`（可选聚合）

---

## 11. 安全与合规

- 严禁返回 Hard Filter 禁推内容。
- 对 Push 场景，默认倾向“低风险 + 低个性化强度”；当画像不确定或字段缺失时必须进一步降个性化。
- 不在仓库写入真实数据库账号密码；连接串统一从 `DATABASE_URL` 环境变量读取（已由 `server/app/core/config.py` 支持）。

---

## 12. 验收标准（Acceptance Criteria）

- 能以统一接口在三种场景运行：输入画像 + 曝光集合 → 输出 TopK 句子。
- 画像字段缺失时仍能产出排序，并触发降个性化策略（至少 L1），不会报错。
- Hard Filter 规则生效：命中 `block_health_medical` 等风险标记的内容绝不出现在输出中。
- 候选不足时会逐级回退并输出 `fallback_level_final`，不会出现无输出且无原因字段的情况。
- 输出包含用于上层打点的 meta 统计字段，便于观测候选规模与回退率。

---

## 13. 子模块规范索引（modules/）

> 子模块规范用于实现拆分；本文件保留大需求高层约束与对外契约摘要。

- `modules/db-design/spec.md`：数据库设计与迁移（当前库为空，本需求内补齐）
- `modules/content-repository/spec.md`：数据访问层（按画像与场景拉取候选、按 ID 批量查）
- `modules/reco-engine/spec.md`：推荐引擎编排（候选→过滤→打分→重排→回退）
- `modules/scoring/spec.md`：打分与惩罚项（严格对齐规则文档；含缺失字段保守策略）
- `modules/rerank-freqcap/spec.md`：重排/去重/频控（Feed 的 MMR；Push/Widget 冷却）
- `modules/observability/spec.md`：可观测事件结构与指标（candidate_pool_size_* 等）
- `modules/integration-api-worker/spec.md`：API 与 Celery 集成（请求入参、响应、任务封装）

