Files
mindfulness/spec_kit/Personalized Reco/modules/reco-engine/tasks.md
2026-02-02 16:47:37 +08:00

196 lines
10 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.
# Reco Engine推荐引擎编排Tasks
> 对应计划:`spec_kit/Personalized Reco/modules/reco-engine/plan.md`
>
> 执行规则:
>
> - 本任务清单**可执行、可验证**;每项完成后在“状态”处标记为 `已完成` 并补充必要的证据(测试用例/日志/截图/输出)。
> - **禁止破坏性数据库操作**(如需必须先征得同意并回复“允许操作数据库”)。
> - 本模块默认约定:
> - `locale` 主要来自客户端 API 入参;若未传,默认 `en`
> - Feed`feed_allow_partial=true` 且 `feed_fill_with_fallback=true`(允许不足,但会尝试回退补齐)
> - Hard Filter**仅实现硬规则集合**(不实现 `UserProfileV1_2_Extended.hard_rules` 扩展)
> - `explanations`:默认开启(但建议输出“轻量 explanations”避免载荷过大
---
## 0. 准备与对齐(不改代码)
- [x] **确认依赖模块接口未变更**(状态:已完成)
- **检查点**
- `ContentRepository.fetch_candidates(...)` 入参含 `locale/fallback_level/exclude_content_ids`
- `scoring.score_content(...)` 可用且 Push 默认启用 `P_uncertainty`
- `rerank_freqcap.rerank_and_freqcap(...)` 可用且会在缺失 `recent_*` 时跳过该维度过滤
- `observability.RecoMetaBuilder` 的字段口径与 `empty_reason` 规则不变
- **证据**
- `server/app/features/personalized_reco/content_repository/interface.py``fetch_candidates(..., locale, fallback_level, exclude_content_ids)`
- `server/app/features/personalized_reco/scoring/score.py``score_content(...)`
- `server/app/features/personalized_reco/rerank_freqcap/rerank.py``rerank_and_freqcap(..., recent_author_ids=None, recent_template_ids=None)`
- `server/app/features/personalized_reco/observability/builder.py``RecoMetaBuilder.build()``compute_empty_reason`
---
## 1. 代码骨架与类型(新增 reco_engine 模块)
- [x] **创建目录与初始化文件**(状态:已完成)
- **目标路径**`server/app/features/personalized_reco/reco_engine/`
- **文件**
- `__init__.py`
- `types.py`
- `defaults.py`
- `utils.py`
- `hard_filter.py`
- `orchestrator.py`
- **验收**:可被 `from app.features.personalized_reco.reco_engine import ...` 导入
- **证据**
- 已新增:`server/app/features/personalized_reco/reco_engine/__init__.py`
- 导出入口:`from app.features.personalized_reco.reco_engine import recommend`
- [x] **定义核心类型**(状态:已完成)
- **`types.py` 建议包含**
- `RecoConstraints`(可选过滤:`exclude_content_ids/exclude_author_ids/exclude_template_ids/max_candidates_limit/recent_author_ids/recent_template_ids`
- `RecoEngineConfig`Feed 补齐策略、候选倍率等)
- `RecommendedItem``content_id/text/final_score/fallback_level_final/explanations`
- `RecoEngineResult``items/meta`
- `HardFilterResult``kept_items/risk_filtered_count_by_flag/removed_count/optional_hits`
- **验收**:类型可在单测中直接构造与序列化(若用 pydantic
- **证据**:已实现于 `server/app/features/personalized_reco/reco_engine/types.py`
- [x] **默认配置落地**(状态:已完成)
- **`defaults.py` 建议**
- `get_default_engine_config(scene)` 或统一 `RecoEngineConfig()`
- 候选倍率Feed `10`Push/Widget `30`(可配置)
- Feed 策略默认:`allow_partial=true``fill_with_fallback=true`
- **验收**:不传 config 时引擎可稳定运行
- **证据**:已实现于 `server/app/features/personalized_reco/reco_engine/defaults.py`
- [x] **工具函数ID 与 locale 的防御式处理**(状态:已完成)
- **`utils.py` 建议**
- `normalize_int_id_list(mixed_ids) -> list[int]`:解析 `str|int`,无效值忽略
- `merge_exclude_ids(already, touched, extra) -> list[int]`
- `normalize_or_default_locale(locale) -> "en"|"tc"`:缺失默认 `en`;非法时抛出/返回错误由 orchestrator 捕获
- **验收**:输入包含 `"1" / 1 / "abc" / None` 不报错
- **证据**:已实现于 `server/app/features/personalized_reco/reco_engine/utils.py`
---
## 2. Hard Filter硬过滤实现
- [x] **实现硬规则集合**(状态:已完成)
- **文件**`hard_filter.py`
- **必须实现规则**
- 全场景:`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``C.need_suitability[parenting_pressure]=1``C.personalization_power=1` → 过滤
- **输出统计**
- `risk_filtered_count_by_flag: dict[str,int]`(按命中的 risk_flag 计数;跨维度规则可用固定 key 如 `rule:unknown_stage_parenting_pressure_power1`
- **验收**
- 传入 3 条候选,命中规则的被剔除
- `risk_filtered_count_by_flag` 的数值与剔除条数一致
- **证据**:已实现于 `server/app/features/personalized_reco/reco_engine/hard_filter.py`
---
## 3. Orchestrator编排器实现
- [x] **实现 `recommend(...)` 主入口**(状态:已完成)
- **文件**`orchestrator.py`
- **函数形态建议**
- `async def recommend(*, repo: ContentRepository, scene, user_profile, already_recommended_ids, touched_or_viewed_ids, k, now, locale=None, constraints=None, config=None) -> RecoEngineResult`
- **关键编排步骤**(每次 fallback level 都要跑一遍):
- Candidate`repo.fetch_candidates(...)`
- Hard Filter`hard_filter(...)`
- Soft Scoring`score_content(...)`
- Rerank/Freqcap`rerank_and_freqcap(...)`
- Serve截断到 `k` 并构造 `RecommendedItem`
- Meta`RecoMetaBuilder` 逐阶段填充并 `build()`
- **验收**
- 任意 `k`(含 0不报错
- 输出结构稳定:`items``meta` 永远存在
- **证据**:已实现于 `server/app/features/personalized_reco/reco_engine/orchestrator.py`
- [x] **Fallback Ladder 回退循环**(状态:已完成)
- **行为**
- 依次尝试 `fallback_level in [0,1,2,3]`
- 每层更新 `meta_builder.set_fallback_level_final(level, reason=...)`
- 每层记录 `fallback_trace` 并写入 `meta.config_snapshot`
- **Feed 策略**(默认):
- `served_k < k` 时继续回退补齐,直到 `k` 或 L3
- 若最终仍不足,允许返回不足,但 `served_k`/`fallback_level_final` 必须正确
- **验收**
- 构造一个“强过滤 + 频控后为空”的场景能触发逐级回退
- **证据**`meta.config_snapshot.fallback_trace` 会记录每层的 raw/after_hard/after_dedup/after_freqcap/served_total
- [x] **explanations 默认开启但保持轻量**(状态:已完成)
- **建议默认包含**
- `fallback_level_used`
- `hard_filter_hits`(命中的 risk_flags/规则 id
- `score_summary`(可选:只保留少量关键字段,如 `S_core/S_personal/P_uncertainty/P_risk`,不输出全量 breakdown
- **验收**
- 返回载荷可控Feed 30 条不会过大)
- **证据**explanations 仅包含 `fallback_level_used/hard_filter_hits/score_summary`
- [x] **异常兜底与 meta 记录**(状态:已完成)
- **要求**
- 捕获 `normalize_locale` 抛错、repo 查询异常、单条内容打分异常等
- 返回 `items=[]`,并在 `meta.config_snapshot` 写入 `{"error": "...", "stage": "..."}`(避免 500
- **验收**
- 传入不支持的 locale`jp`)时不会导致接口崩溃
- **证据**`normalize_locale` 失败时返回空 items`meta.config_snapshot.stage="normalize_locale"`
---
## 4. 与现有模块的对齐与集成
- [x] **对齐 `rerank_freqcap` 的作者/模板冷却输入含义**(状态:已完成)
- **含义说明**
- `recent_author_ids/recent_template_ids` 表示“冷却窗口内已触达的作者/模板集合”
- 本模块不负责计算窗口裁剪;调用方需按 `cooldown_*_days` 裁剪后再传
- **默认策略**
- 若调用方不提供,则传 `None`,由 `rerank_freqcap` 记录缺失并跳过该维度过滤(句子级去重仍有效)
- **验收**
- 不提供 `recent_*` 时不报错,且 meta 中 `freqcap_filtered_counts` 仍有 sentence 维度计数
- **证据**:引擎透传 `recent_author_ids/recent_template_ids`(默认为 None`rerank_freqcap` 自身会记录缺失维度
- [x] **对齐 `RecoMetaBuilder` 阶段字段写入点**(状态:已完成)
- **必须写入**
- raw / after_hard_filter / after_dedup / after_freqcap / served_k / fallback_level_final
- `risk_filtered_count_by_flag``freqcap_filtered_counts`
- **验收**
- `empty_reason` 可区分 `pool_empty / hard_filter_all / freqcap_all`
- **证据**:单测覆盖 `pool_empty / hard_filter_all / freqcap_all`
---
## 5. 单元测试(必做)
- [x] **新增 `server/tests/test_reco_engine.py`**(状态:已完成)
- **测试用例建议**
- `k=0` 返回空 itemsmeta.served_k=0
- raw=0 → empty_reason=`pool_empty`
- raw>0 且 after_hard=0 → empty_reason=`hard_filter_all`
- raw>0 且 after_freqcap=0 且 after_hard>0 → empty_reason=`freqcap_all`
- 去重生效:输出不包含 already/touched ids
- `block_health_medical` 必挡
- Push 缺失画像字段时仍稳定repository 会至少 L1引擎 meta 与 fallback_trace 正确)
- **验收**`pytest` 全绿(只跑相关 tests 也可)
- **证据**
- 新增文件:`server/tests/test_reco_engine.py`
- 在本机 venv 下执行:`server/.venv/bin/python -m pytest -q``19 passed`
---
## 6. 文档与总览标记(仅在全部任务完成后做)
- [x] **更新本子模块执行状态**(状态:已完成)
- **文件**`spec_kit/Personalized Reco/modules/reco-engine/tasks.md`
- **要求**:本文件所有任务项标记为 `已完成`,并补齐证据
- **证据**:本文件已全部打勾并补充证据
- [ ] **更新大需求总览 `overview.md`**(状态:未开始)
- **文件**`spec_kit/Personalized Reco/overview.md`
- **要求**:当 `reco-engine` 全部任务完成后,将第 6 项 “已实施/已完成” 并补充变更记录(日期 + 简述)