# 正念 APP｜三種分發場景推薦算法方案（Feed / Push / Widget · V1）

> **目標**：
> 針對三種分發方式（Feed、App Push、Widget 每日一句）制定一套一致但可調參的推薦流程。
>
> **共用基礎**：
>
> * 用戶畫像：U（mom_stage one-hot；emotion/context/need 允许缺失/跳过；含 `profile_confidence/profile_answered`）
> * 文案畫像：Cᵢ（stage、emotion_score、context/need suitability、personalization_power、risk_flags；建議含 content_id/author_id/template_id/review_confidence）
> * 推薦骨架：Hard Filter → Soft Scoring → Rerank（多樣性/新鮮度/疲勞）

---

## 一、先說整體流程與思路（適用三種場景）

### 1) 核心理念：同一個用戶同一天需要「不同強度」的個性化

* **Push**：最容易翻車 → **高精準 + 低風險**（Precision-first）
* **Widget**：每天唯一一句 → **穩定、代表性、可長期持續**（Consistency-first）
* **Feed**：可上下滑探索 → **可試探、可擴展、多樣性**（Exploration-first）

### 2) 共用三段式 Pipeline

1. **Candidate Generation（候選集生成）**

   * 依 U 取一個「候選池」：匹配 need/context/stage 的內容 + 一部分通用內容
   * V1.2：若問卷允許跳過導致 `U.need/U.context/U.emotion_score` 缺失，候選池需自動提高通用安全內容占比，并视为至少进入 L1（限制 `personalization_power≤0.5`）
2. **Hard Filter（硬性過濾）**

   * 依 risk_flags 與產品規則剔除高風險內容
3. **Soft Scoring（軟性打分）**

   * 用加權模型計算核心匹配分
4. **Rerank（重排）**

   * 去重、作者/模板多樣性、頻控、時間衰減
5. **Serve（分發）**

   * 依場景不同策略選 TopK 或組成序列

### 3) 工程必備：候選不足回退策略 + 可觀測指標（V1.1）

#### 3.1 候選不足回退（Fallback Ladder）

> 目的：避免「過濾/頻控後候選為空」導致體驗斷崖；同時在回退時**自動降個性化與降風險**（尤其 Push）。

共用定義：

* `fallback_level`：從 0 開始遞增（0=不回退）
* 回退原則：**先放寬匹配，再放寬探索，再回退到通用安全池**；同時限制 `personalization_power` 上限

建議梯度（可按場景調整）：

* **L0（正常）**：按原候選配比取候選
* **L1（放寬匹配）**：`need/context` 由 `1 → ≥0.5`；`personalization_power` 上限降為 `≤0.5`
* **L2（回退通用池）**：加入更多 `general`/低風險句；`personalization_power` 上限降為 `0`
* **L3（兜底）**：僅從「通用安全句庫」抽取（需單獨維護白名單/安全池）

每次請求需輸出最終使用的 `fallback_level`（用於分析覆蓋率與風險）。

#### 3.2 可觀測指標（必打點）

每次生成推薦（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，記錄原因（如 hard_filter_all / freqcap_all / pool_empty / unknown）

核心監控衍生指標（報表層計算）：

* **覆蓋率**：`served_k > 0` 的比例（按場景分開）
* **回退率**：`fallback_level_final > 0` 的比例
* **頻控後候選數分布**：`candidate_pool_size_after_freqcap` 的 P50/P90/P99

---

## 二、總分結構（Score Decomposition）

對任意文案 Cᵢ：

[
\textbf{final_score}(U,Cᵢ) = \mathbb{I}[\text{pass filters}] \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}
]

* `S_need = Cᵢ.need_suitability[U.need]`（若 U.need 缺失，见 V1.2）
* `S_context = Cᵢ.context_suitability[U.context]`（若 U.context 缺失，见 V1.2）
* `S_stage`：general=1；命中=1；unknown對非unknown=0.7；其餘=0
* `S_emotion`：若文案 general→0.8；否則 `1-|U.emotion_score-Cᵢ.emotion_score|`（若 U.emotion_score 缺失，见 V1.2）

### V1.2：当问卷允许跳过题目时的打分约定（工程必读）

> 目标：用户画像字段不全时仍能产出排序；并在不确定时自动降个性化/降风险。

判定缺失：

* `U.need` 为空对象 `{}` 或不存在 → 视为缺失
* `U.context` 为空对象 `{}` 或不存在 → 视为缺失
* `U.emotion_score` 为 `null`/不存在 → 视为缺失

缺失时的保守计算：

* `S_need`：
  * 若 `U.need` 缺失 → `S_need = 0.5`
  * 否则 → `S_need = Cᵢ.need_suitability[U.need]`
* `S_context`：
  * 若 `U.context` 缺失 → `S_context = 0.5`
  * 否则 → `S_context = Cᵢ.context_suitability[U.context]`
* `S_emotion`：
  * 若 `U.emotion_score` 缺失 → `S_emotion = 0.8`
  * 否则按原定义计算（文案 general 仍为 0.8）

额外约束（与回退梯度一致，6A）：

* 若 `conf_U` 偏低或字段缺失明显：必须启用 `P_uncertainty`
* 若进入回退梯度（`fallback_level_final>0`）：按 L1/L2/L3 限制 `personalization_power` 上限（见 3.1）

個性化加成：
[
S_{personal} = \alpha \cdot Cᵢ.personalization_power \cdot \max(S_{need}, S_{context})
]

不確定性懲罰（Push 建議啟用）：
[
P_{uncertainty} = \beta \cdot (1-\text{conf}_U) \cdot (1-\text{conf}_{Cᵢ}) \cdot Cᵢ.personalization\_power
]

> 說明：
>
> * `conf_U`：用戶畫像置信度（建議由問卷生成後隨時間衰減；見「用戶畫像」文件 V1.1）
> * `conf_{Cᵢ}`：內容標註置信度（`Cᵢ.review_confidence`；缺省可給 0.7）
> * `P_{uncertainty}` 可併入 `P_risk`（工程上更簡單），但需保留獨立打點便於調參

---

## 三、Hard Filter（共用禁推規則）

### 3.1 risk_flags（內容自帶）

> V1.1 起，risk_flags 命名/語義以「句子文案打分規則」為準（`unsafe_for_*` / `block_*` / `soft_*`）。

* `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`
* `Cᵢ.risk_flags` 含 `block_health_medical` → **全场景禁推（Hard Filter）**（仅挡「医疗/诊断/严重暗示」）

> `soft_health_sensitive` 不做 Hard Filter，改由 Soft Penalty（見 Push 的 P_risk）

### 3.2 產品規則（跨維度）

* `U.stage.unknown=1` 且 `Cᵢ.need_suitability[parenting_pressure]=1` 且 `Cᵢ.personalization_power=1` → 禁推

---

# 四、Feed 模式（App 內上下滑）— Exploration-first

## 4.1 目標

* 讓用戶快速感到「被理解」
* 同時探索更多題材，避免內容疲乏
* 允許一定程度的試探（比 Push 寬鬆）

## 4.2 流程（Feed Session）

1. **建候選池**（大）

   * 60%：匹配 need/context 的內容（含 personalization_power=0.5/1）
   * 30%：通用內容（general）
   * 10%：探索內容（與 context/need 邊緣相關）
2. Hard Filter（仍嚴格）
3. Soft Scoring（偏多樣性）
4. Rerank：用 MMR 做序列

## 4.3 Feed 打分權重（建議）

* `w_need=0.35, w_emotion=0.20, w_stage=0.15, w_context=0.30`

> Feed 更重視「語境共鳴」與內容多樣性，所以 context 權重上調。

## 4.4 Feed 序列重排（MMR）

* Top1：最高分（確保第一眼命中）
* 後續：在高分候選中，最大化與已選內容的差異（作者/模板/need/context）

### 4.5 Feed 的 MMR 定義（工程側定義，V1.1）

選擇第 t 個內容：

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

建議預設：`λ = 0.7`（更偏相關性，仍保留多樣性）

* `Rel(c)`：使用 `final_score(U,c)`（或 Soft Scoring 後的分數）
* `Sim(c,s)`：相似度定義（無 embedding 也可落地）

相似度（離散特徵版，推薦 V1 上線）：

* 若 `content_id` 相同 → `Sim=1`
* 若 `template_id` 相同且非空 → `Sim += 0.6`
* 若 `author_id` 相同且非空 → `Sim += 0.3`
* `need/context/stage` 標籤重合（Jaccard）→ `Sim += 0.1 * Jaccard(tags_c, tags_s)`
* 最終 `Sim` clamp 到 `[0,1]`

字段依賴與回退：

* 缺 `template_id/author_id` 時，只用 tags 相似度（Sim 仍可算）
* 若後續引入文本 embedding，可將 embedding cosine 加權合併（V2）

---

# 五、App Push（可設定 0–5 次）— Precision & Safety-first

## 5.1 目標

* Push 是「打擾」：錯一次就掉留存
* 追求「準、溫柔、少翻車」

## 5.2 流程（每次 Push）

1. **候選池**（小而精）

   * 70%：need 命中（S_need=1）的內容
   * 20%：emotion 命中（距離小）的內容
   * 10%：通用安全句（general + 低風險）
2. Hard Filter（最嚴格）
3. Soft Scoring（偏 need + emotion）
4. 冷卻與頻控（必做）

   * 同一句/同作者/同模板至少 X 天不重複
5. 發送

> Push 追加規則（V1.1）：若 `fallback_level_final > 0` 或 `conf_U` 偏低，需自動「降個性化」：
>
> * `personalization_power` 上限降為 `≤0.5`（L1）或 `0`（L2/L3）
> * 若候選不足，優先回退到「通用安全句庫」，而不是擴大高個性化內容
>
> V1.2（支持问卷跳过）补充：
>
> * 若 `profile_answered.need=false` 或 `U.need` 缺失：不得按“need 强命中”召回，候选池应提高通用安全句占比，并至少按 L1 降个性化
> * 若 `profile_answered.emotion=false` 或 `U.emotion_score` 缺失：Push 应偏向 `Cᵢ.emotion_score` 为 `general` 或 0.4–0.8 的稳定内容，并启用 `P_uncertainty`

## 5.3 Push 打分權重（建議）

* `w_need=0.45, w_emotion=0.35, w_stage=0.15, w_context=0.05`

> Push 更在乎「此刻需要」與「語氣安全」。context 權重降低。

## 5.4 Push 風險懲罰（P_risk）

* 若 `Cᵢ.personalization_power=1` 且 `S_need<1` 且 `S_context<1` → 額外扣分
* 若 content 涉及健康敏感（`soft_health_sensitive`）→ 扣分（Soft Penalty，不做禁推）
* 不確定性懲罰：`P_uncertainty`（或併入 `P_risk`），用於在 `conf_U/conf_C` 偏低時自動降權高個性化內容

## 5.5 0–5 次推送的排程策略

* 使用者設定次數 N：

  * 第 1 條：need 命中 + 安全
  * 第 2 條：emotion 修復
  * 第 3 條：self-worth / rest_balance（視 need 而定）
  * 第 4–5 條：通用句補位，避免疲勞

---

# 六、Widget（每日一句）— Consistency & Brand-first

## 6.1 目標

* Widget 是「代表你這個 App 的一句話」
* 需要：穩定、可持續、低風險、避免太私密

## 6.2 流程（每日生成）

1. 候選池：以 **general + personalization_power≤0.5** 為主
2. Hard Filter（嚴格）
3. Soft Scoring：偏 stage 安全 + emotion 中性/平靜
4. 跨日多樣性：7 日內不重複作者/模板/同一句

> 若候選不足：同樣走回退梯度（優先增加 general 安全句；不提高個性化力度）。

## 6.3 Widget 打分權重（建議）

* `w_need=0.25, w_emotion=0.25, w_stage=0.30, w_context=0.20`

> Widget 需要「身份語境安全」與「普適」，stage 權重更高。

## 6.4 Widget 的情緒區間約束

* 建議限制 `Cᵢ.emotion_score` 在 **0.4–0.8**（tired→calm）

  * 避免太低（沉重）
  * 避免太高（過嗨）

---

## 七、場景差異總結（快速對照）

| 場景     | 目標    | 候選池 | 風險容忍 | 權重重點                | 序列策略   |
| ------ | ----- | --- | ---- | ------------------- | ------ |
| Feed   | 探索+共鳴 | 大   | 中    | context↑、diversity↑ | MMR 序列 |
| Push   | 準+安全  | 小   | 低    | need↑、emotion↑      | 頻控+冷卻  |
| Widget | 穩+代表性 | 中小  | 低    | stage↑、emotion中性    | 跨日多樣性  |

---

## 八、（後段解釋預告）提及的算法/概念

* Candidate Generation（混合召回：命中 + 通用 + 探索）
* Hard Filter（規則引擎）
* Linear Weighted Scoring（加權線性模型）
* MMR（Maximal Marginal Relevance，多樣性重排）
* Time Decay（時間衰減）
* Frequency Capping（頻控）
* Cooldown Keys（去重鍵：句子/作者/模板）

> 工程側去重鍵定義（V1.1）：
>
> * `sentence_key = content_id`（若無 content_id，需生成 stable hash，但建議補 content_id）
> * `author_key = author_id`（可空）
> * `template_key = template_id`（可空）
>
> 冷卻/頻控同時支持三層 key，避免只靠單一 key 造成過度重複或過度稀釋。

---

## 九、V1 可直接上線的預設參數

* Feed：Top1 命中 + MMR 序列 30 條
* Push：每次 Top1；冷卻 7 天不重複作者，14 天不重複同句
* Widget：emotion_score 限制 0.4–0.8；7 日不重複作者/模板

V1.1 補充（工程預設）：

* Feed：MMR `λ=0.7`
* Push：啟用 `P_uncertainty`（或併入 `P_risk`）；`fallback_level>=1` 時 `personalization_power` 上限降為 `≤0.5`
* Health：`block_health_medical` 全场景 Hard Filter；`soft_health_sensitive` 走 Soft Penalty

> 一句話：
> **同一套 U×Cᵢ 打分，按場景調候選池、權重與重排約束。**
