# Scoring(软打分与惩罚项)|Tasks > 对应计划:`spec_kit/Personalized Reco/modules/scoring/plan.md` > > 本清单执行原则: > > - `scoring` 只做**软打分**与本模块定义的惩罚项(`P_uncertainty`、Widget 情绪软降权)。 > - Hard Filter / 频控重排 / 新鲜度等由其他模块产出,本模块通过 `pass` 与 `external_terms` 接收注入(缺省按 0)。 --- ## 0. 任务标记规则 - 用勾选框标记执行状态: - `[ ]` 未开始 - `[x]` 已完成 - 每个任务都要求可独立验收(有明确产出/可运行的检查方式)。 --- ## 1. 文档对齐(先把口径写死,避免实现漂移) - [x] 1.1 校对 `modules/scoring/spec.md` 与 `modules/scoring/plan.md` 一致性 - **检查点**: - 输入:`scene/user_profile/content_profile/now/config` 是否一致 - 输出:`final_score` 与 `breakdown` 字段集合是否一致 - 关键公式:`S_core/S_personal/P_uncertainty` 是否与 `设计说明文档/個性化推薦算法規則.md` 一致 - **验收**:两份文档无冲突描述,且关键参数命名统一(例如 `enable_uncertainty_penalty`、`widget_emotion_soft_range`)。 - [x] 1.2 在 `modules/scoring/plan.md` 中“明确写死”V1 的职责边界(若后续有调整再更新) - **变更点**(如需微调措辞): - `S_fresh/P_fatigue/P_repeat/P_risk` 为外部注入项,缺省按 0 - `pass` 来自 Hard Filter,本模块只消费并乘上 \(\mathbb{I}[pass]\) - **验收**:阅读 plan.md 时不会产生“谁负责计算哪一项”的歧义。 --- ## 2. 目录与骨架(与推荐子模块同级) - [x] 2.1 新建目录 `server/app/features/personalized_reco/scoring/` - **包含**: - `__init__.py` - `types.py`(`ScoreConfig/ScoreBreakdown/ScoreResult/ExternalTerms`) - `defaults.py`(场景默认参数) - `utils.py`(clamp、one-hot 取 key 等纯工具) - `score.py`(核心纯函数 `score_content`) - **验收**:可通过 `app.features.personalized_reco.scoring.*` 正常 import。 --- ## 3. 类型与接口(稳定契约,便于引擎编排调用) - [x] 3.1 定义 `ScoreConfig`(含场景默认值 + 可覆盖) - **字段**: - 权重:`w_need/w_emotion/w_stage/w_context` - 系数:`alpha/beta` - 开关:`enable_uncertainty_penalty` - Widget:`widget_emotion_soft_range`、`widget_emotion_penalty_gamma` - **验收**:类型完整;能从 scene 推导默认 config(或由调用方传入)。 - [x] 3.2 定义 `ExternalTerms`(注入项,V1 可选) - **字段**:`S_fresh/P_fatigue/P_repeat/P_risk` - **默认值策略**:缺省按 0 - **验收**:score 函数即使没有 external_terms 也能工作且输出字段稳定。 - [x] 3.3 定义 `ScoreBreakdown`(用于可观测/调参) - **必须包含**: - `S_need/S_context/S_stage/S_emotion` - `S_core/S_personal/S_fresh` - `P_fatigue/P_repeat/P_risk/P_uncertainty` - `missing_fields` - `pass/scene` - `P_widget_emotion_out_of_range`(建议保留) - **验收**:字段集合固定;不会因缺省而缺字段;所有值为有限数(非 NaN/Infinity)。 --- ## 4. 纯函数实现(严格对齐公式 + 防御式兜底) - [x] 4.1 实现 one-hot 取唯一 key 的工具函数(need/context) - **规则**: - `{}` 或不存在 → 视为缺失 - 只有一个 key=1 → 返回该 key - 多个 key=1 → 选择“第一个”(写死策略:字典序优先或插入序优先)并记录 debug 日志 - **验收**:单测覆盖空对象/单 key/多 key 的行为,且行为确定。 - [x] 4.2 实现 `S_need`/`S_context`(V1.2 缺失兜底) - **规则**: - need 缺失 → `S_need=0.5`;否则取 `Cᵢ.need_suitability[key]` - context 缺失 → `S_context=0.5`;否则取 `Cᵢ.context_suitability[key]` - 若内容侧缺 key/值非法 → 兜底为 0.5(防御式) - **验收**:单测覆盖用户缺失与内容缺失两侧情况,且不抛异常。 - [x] 4.3 实现 `S_emotion`(general=0.8;否则 `1-|u-c|`) - **规则**: - `U.emotion_score` 缺失 → 0.8 - `Cᵢ.emotion_score` 为 general(None)→ 0.8 - 否则 `1-abs(u-c)` 并 clamp 到 `[0,1]` - **验收**:单测覆盖缺失/general/正常值/越界值。 - [x] 4.4 实现 `S_stage`(对齐算法规则口径) - **规则**: - `content.stage=general` → 1 - 命中用户阶段 → 1 - 用户 unknown 且内容非 unknown → 0.7 - 其余 → 0 - **验收**:单测覆盖 general/命中/unknown→非unknown/其余组合。 - [x] 4.5 实现 `S_core`(线性加权) - **规则**:`S_core=w_need*S_need + w_emotion*S_emotion + w_stage*S_stage + w_context*S_context` - **验收**:单测断言与手算一致;权重可配置。 - [x] 4.6 实现 `S_personal`(对齐公式) - **规则**:`S_personal=alpha * personalization_power * max(S_need, S_context)` - **防御**:`personalization_power` clamp 到 `[0,1]` - **验收**:单测覆盖 power=0/0.5/1,且随 `alpha` 单调变化。 - [x] 4.7 实现 `P_uncertainty`(Push 默认启用) - **规则**:`beta*(1-conf_U)*(1-conf_C)*personalization_power` - **默认值**:`conf_C` 缺失→0.7;`conf_U` 缺失→按 1.0(或 0.7,需在代码注释写死;V1 建议按 1.0 避免过惩罚) - **开关**:`enable_uncertainty_penalty` 控制;Push scene 默认 true - **验收**:单测覆盖低置信度与关闭开关两类情况。 - [x] 4.8 实现 Widget 情绪区间软降权(适中默认) - **规则**: - `scene=widget` 且 `content.emotion_score` 非 general: - 若超出 `[0.4,0.8]`:`P_widget = gamma * d/(hi-lo)` 并 clamp `[0,gamma]` - 区间内:`P_widget=0` - `P_widget` 计入总分(建议并入 `P_risk`),并在 breakdown 中单独暴露 - **验收**:单测断言: - `emotion_score=0.6` → `P_widget=0` - `emotion_score=0.0/1.0` → `P_widget` 接近 `gamma` - 不硬过滤(仍返回分数,只是更低) - [x] 4.9 实现 `score_content(...) -> ScoreResult`(总分与 breakdown) - **规则**: - `final_score = I[pass] * (S_core + S_personal + S_fresh - P_fatigue - P_repeat - P_risk - P_uncertainty)` - `S_fresh/P_fatigue/P_repeat/P_risk` 从 `external_terms` 读取,缺省 0 - `pass=false` 时 `final_score=0`(breakdown 仍输出便于排查) - **验收**:单测覆盖 `pass=false` 与 external_terms 缺省两种情况。 --- ## 5. 单元测试(pytest,纯函数为主) - [x] 5.1 新建测试文件 `server/tests/test_scoring.py` - **包含用例**: - 缺失字段一致性(need/context/emotion) - Push 不确定性惩罚生效(低 conf 时分数更低) - Widget 软降权生效(区间外明显更低) - pass=false 行为(final_score=0) - **验收**:`pytest -q` 能跑通该文件。 - [x] 5.2 增加“输出稳定性”断言(breakdown 字段集合固定) - **验收**:任意输入(含缺失字段)都返回同一套 breakdown key。 --- ## 6. 最终自检清单(合入前) - [x] 6.1 文档一致性检查 - **检查点**:`spec.md` / `plan.md` / 实现接口签名三者一致(尤其是 config 字段与默认值策略)。 - **验收**:阅读任一文档都能找到对应实现位置与参数含义。 - [x] 6.2 回归检查(不影响其他模块) - **验收**:不修改现有 `content-repository` 与 `user_profile_scoring` 逻辑,仅新增 scoring 模块与测试。