# Observability（可观测性与打点载荷）｜Plan

> 对应规范：`spec_kit/Personalized Reco/modules/observability/spec.md`
>
> 规则来源（必须对齐）：
>
> - `设计说明文档/個性化推薦算法規則.md`（candidate_pool_size_*、fallback_level_final、empty_reason 等必打点字段）
> - `spec_kit/Personalized Reco/overview.md`（模块边界：本模块被 `reco-engine` 与 `integration-api-worker` 共同使用）

---

## 1. 目标与交付物

### 1.1 目标

- 定义推荐模块统一的 `RecoMeta`（meta/event 载荷），**随推荐结果一起返回**，由调用方负责上报/落库/打点。
- 确保每次推荐调用都能产出可用于监控与排障的关键字段：
  - 覆盖率、回退率、候选规模分布、过滤原因分布
  - `served_k=0` 时必须给出准确的 `empty_reason`（定位清空阶段）
- 与现有子模块接口对齐：
  - 候选生成（content-repository）
  - Hard Filter（引擎阶段）
  - Soft Scoring（scoring）
  - Rerank/Freqcap（rerank-freqcap）

### 1.2 交付物

- `modules/observability/plan.md`：本技术计划（本文件）。
- 代码实现（后续 tasks 阶段落地）建议位置：
  - `server/app/features/personalized_reco/observability/`
  - 包含：
    - `types.py`：`RecoMeta`（Pydantic BaseModel）
    - `builder.py`：`RecoMetaBuilder`（在 pipeline 中逐步填充）
    - `utils.py`：`empty_reason` 判定工具函数
- 单元测试（后续 tasks 阶段落地）：
  - `served_k=0` 时 empty_reason 必填且阶段一致
  - 计数口径一致性（after_* 的单调性与非负）

---

## 2. 模块职责边界（V1 约定）

### 2.1 本模块负责

- 提供统一的数据结构 `RecoMeta`（返回给调用方）与构建方式（builder）。
- 在推荐 pipeline 中收集各阶段统计，避免“散落日志/散落字段”：
  - 候选生成规模（raw）
  - Hard Filter 后规模
  - Dedup 后规模
  - Freqcap 后规模
  - 回退层级与触发原因（由引擎提供）
  - served_k 与 empty_reason
  - `conf_U` 与 `missing_fields`
  - （可选）risk flag 命中统计

### 2.2 不在本模块实现

- 不负责真正的上报实现（埋点 SDK / 日志落库 / 指标上报）。
- 不负责决定回退策略与过滤规则，只负责“把发生了什么”记录成统一载荷。

---

## 3. `RecoMeta` 字段定义（V1）

> 以 `modules/observability/spec.md` 为准，本 plan 补充“口径/生成时机/默认值”。

### 3.1 必须字段（每次推荐都要产出）

- `scene: "feed" | "push" | "widget"`
- `candidate_pool_size_raw: int`
- `candidate_pool_size_after_hard_filter: int`
- `candidate_pool_size_after_dedup: int`
- `candidate_pool_size_after_freqcap: int`
- `fallback_level_final: int`
- `served_k: int`
- `empty_reason: str | None`
  - `served_k=0` 时必填
  - `served_k>0` 时可为 `None`（或输出 `"unknown"`，但建议为 None 更干净）
- `conf_U: float`
- `missing_fields: { need: boolean; context: boolean; emotion: boolean }`

### 3.2 可选字段（建议支持，便于排障/调参）

- `risk_filtered_count_by_flag: { flag: count }`
- 其他调参辅助字段（V1 可先不返回给客户端，仅在内部日志/事件中使用）：
  - `config_snapshot`（权重、alpha/beta、mmr_lambda、cooldown 等）
  - `fallback_trigger_reason`（例如 pool_empty/hard_filter_all/freqcap_all）

---

## 4. 口径与生成时机（V1 必须写死）

### 4.1 数量统计口径（强约束）

- 计数必须满足：
  - 全部为非负整数
  - 单调不增：
    - `raw >= after_hard_filter >= after_dedup >= after_freqcap >= served_k`
- 每个阶段计数的来源：
  - `candidate_pool_size_raw`：候选生成阶段拿到的候选数（从 `content-repository` 返回的候选列表长度）
  - `candidate_pool_size_after_hard_filter`：Hard Filter 过滤后的候选数
  - `candidate_pool_size_after_dedup`：去重（基于历史集合）后的候选数
  - `candidate_pool_size_after_freqcap`：频控/冷却后的候选数（包含作者/模板维度若执行）
  - `served_k`：最终输出 items 的长度（≤ k）

### 4.2 empty_reason（served_k=0 时必填）

枚举建议（对齐 spec）：

- `hard_filter_all`
- `freqcap_all`
- `pool_empty`
- `unknown`

判定逻辑（V1 推荐写死，保证阶段一致）：

- 若 `candidate_pool_size_raw == 0` → `pool_empty`
- 否则若 `candidate_pool_size_after_hard_filter == 0` → `hard_filter_all`
- 否则若 `candidate_pool_size_after_freqcap == 0` → `freqcap_all`
- 否则 → `unknown`

> 说明：dedup 导致清空通常也会表现为 `after_freqcap==0`（若 dedup 发生在 freqcap 前），V1 先统一归入 `freqcap_all`，并建议在可选字段中输出更细分的 `empty_stage`（例如 `dedup`），后续迭代细化。

### 4.3 `conf_U` 与 `missing_fields` 口径

- `conf_U`：直接取 `user_profile.profile_confidence`
- `missing_fields`：
  - `need`: `user_profile.need` 为空对象 `{}` 或不存在
  - `context`: `user_profile.context` 为空对象 `{}` 或不存在
  - `emotion`: `user_profile.emotion_score` 为 `null`/不存在

---

## 5. 工程落地方式（V1）

### 5.1 Builder 模式（避免散落）

推荐在 `reco-engine` 内使用 `RecoMetaBuilder`：

- 初始化：`builder = RecoMetaBuilder(scene, user_profile, k, now)`
- 各阶段更新：
  - `builder.set_candidate_pool_size_raw(n)`
  - `builder.set_after_hard_filter(n, risk_filtered_count_by_flag=...)`
  - `builder.set_after_dedup(n)`
  - `builder.set_after_freqcap(n, freqcap_filtered_counts=...)`
  - `builder.set_fallback_level_final(level, reason=...)`
  - `builder.set_served_k(len(items))`
- 最终：`meta = builder.build()`（内部负责 empty_reason 判定与默认值填充）

### 5.2 API/Celery 的返回策略（边界清晰）

- `integration-api-worker` 对外返回：
  - `items`
  - `meta`（RecoMeta）
- 是否对客户端透出所有 meta 字段：
  - V1 建议：对客户端返回最小必要字段；但服务端事件中保留完整 meta（含可选字段）
  - 具体裁剪由 API 层决定，Observability 模块只负责提供完整结构

---

## 6. 测试计划（V1）

### 6.1 单元测试（pure）

- `empty_reason` 判定：
  - raw=0 → pool_empty
  - raw>0 且 after_hard_filter=0 → hard_filter_all
  - after_freqcap=0 → freqcap_all
- 单调性断言（若输入不满足单调性，builder 应做防御式 clamp 或记录告警，V1 可选择“以最后写入为准”并在测试中覆盖）

### 6.2 最小集成验证（与 reco-engine 串联）

- 构造一次推荐调用：
  - items 长度与 `served_k` 一致
  - `candidate_pool_size_*` 与实际阶段产物一致
  - `served_k=0` 时 `empty_reason` 与清空阶段一致

---

## 7. 风险与后续演进

### 7.1 已知风险

- V1 可能缺少细分 empty_stage（例如 dedup_all 与 freqcap_all 的区分），导致排障粒度不足。

### 7.2 演进方向

- 增加 `empty_stage: "candidate" | "hard_filter" | "dedup" | "freqcap" | "unknown"`，保持与 `empty_reason` 并存。
- 增加 `freqcap_filtered_counts`、`risk_filtered_count_by_flag` 的统一结构与上报策略，便于报表按维度聚合。

