新功能:个性化推荐算法

This commit is contained in:
吕新雨
2026-02-02 16:47:37 +08:00
parent 936094211b
commit 6dc4e2b943
119 changed files with 7427 additions and 357 deletions

View File

@@ -0,0 +1,195 @@
# 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 项 “已实施/已完成” 并补充变更记录(日期 + 简述)