Files
mindfulness/spec_kit/Personalized Reco/modules/observability/plan.md
2026-02-02 16:47:37 +08:00

191 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 Scoringscoring
- Rerank/Freqcaprerank-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_reasonserved_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` 的统一结构与上报策略,便于报表按维度聚合。