11 KiB
Scoring(软打分与惩罚项)|Plan
对应规范:
spec_kit/Personalized Reco/modules/scoring/spec.md规则来源(必须严格对齐):
设计说明文档/個性化推薦算法規則.md(Soft Scoring 公式、V1.2 缺失字段口径、场景默认权重)设计说明文档/句子文案打分規則.md(risk_flags 语义与默认值口径)模块边界参考:
spec_kit/Personalized Reco/overview.md(由reco-engine串起候选→过滤→打分→重排)
1. 目标与交付物
1.1 目标
- 实现三种场景统一的软打分函数
final_score(U, Cᵢ),并输出可观测的breakdown以便调参与回归测试。 - 严格实现 V1.2 缺失字段的保守策略(
S_need/S_context/S_emotion的默认值)。 - 实现 Push 默认启用的不确定性惩罚
P_uncertainty。 - 实现 Widget 情绪区间(0.4~0.8)的软降权(不硬过滤,但能明显压低分数)。
1.2 交付物
modules/scoring/plan.md:本技术计划(本文件)。- 代码实现(tasks 阶段落地)建议位置:
server/app/features/personalized_reco/scoring/- 包含:
- 纯函数
score_content(...) -> ScoreResult ScoreConfig(场景默认参数 + 可覆盖)ScoreBreakdown(稳定字段集合,用于可观测)
- 纯函数
- 单元测试(tasks 阶段落地):
- 缺失字段一致性
- Push 不确定性惩罚生效
- Widget 情绪软降权生效(区间外明显更低)
2. 模块职责边界(V1 约定)
2.1 本模块负责
- 计算并返回:
S_need/S_context/S_stage/S_emotionS_coreS_personalP_uncertainty(按开关控制,Push 默认启用)P_widget_emotion_out_of_range(Widget 专用软降权项,归入P_risk或单独字段均可;V1 建议单独字段,便于打点)
- 生成
breakdown,用于可观测与调参。
2.2 不在本模块实现(但接口预留/可注入)
为避免与 rerank-freqcap / reco-engine 的职责重叠,V1 约定以下项由外部模块产出并作为输入注入(若不提供,默认按 0 处理):
S_fresh:新鲜度/时间衰减相关(可由引擎或重排阶段计算)P_fatigue:疲劳惩罚(基于历史触达/浏览/频控)P_repeat:重复惩罚(同句/同作者/同模板等)P_risk:软风险惩罚(例如soft_health_sensitive等)
说明:硬过滤(Hard Filter)由引擎编排阶段执行,本模块只消费
pass(是否通过硬过滤)并在总分中乘上 (\mathbb{I}[pass])。
3. 输入/输出与数据结构(V1)
3.1 输入(对齐 spec)
scene:feed | push | widgetuser_profile(U,允许字段缺失/跳过)content_profile(Cᵢ)now:预留,用于 freshness/时间衰减(V1 可不实现具体公式)config:w_need/w_emotion/w_stage/w_contextalpha:个性化加成系数beta:不确定性惩罚系数enable_uncertainty_penalty:是否启用P_uncertainty(Push 默认 true)widget_emotion_soft_range:Widget 情绪软区间(默认[0.4, 0.8])widget_emotion_penalty_gamma:Widget 情绪软降权强度(V1 取“适中”默认 0.25)
pass:boolean(来自 Hard Filter 结果;默认 true)external_terms(可选,来自其他模块注入):S_fresh、P_fatigue、P_repeat、P_risk- 若未提供则按 0 处理
3.2 输出
final_score: floatbreakdown(建议始终返回,便于可观测与断言):S_need/S_context/S_stage/S_emotionS_core/S_personal/S_freshP_fatigue/P_repeat/P_risk/P_uncertaintyP_widget_emotion_out_of_range(可选但建议保留)missing_fields: string[](need/context/emotion)pass: booleanscene: feed|push|widget
4. 核心公式与实现细则(必须对齐)
4.1 总分结构(线性加权 + 惩罚)
[ final_score(U,C_i)=\mathbb{I}[pass]\times\Big(S_{core}+S_{personal}+S_{fresh}-P_{fatigue}-P_{repeat}-P_{risk}-P_{uncertainty}\Big) ]
[ S_{core}=w_{need}S_{need}+w_{emotion}S_{emotion}+w_{stage}S_{stage}+w_{context}S_{context} ]
V1 约定:
S_fresh/P_fatigue/P_repeat/P_risk允许外部注入;若缺失则当作 0,以确保函数可用且输出结构稳定。
4.2 分解项:S_need / S_context(V1.2 缺失字段兜底)
S_need:- 若
U.need缺失(为空对象{}或不存在)→S_need = 0.5 - 否则 →
S_need = Cᵢ.need_suitability[U.need_key]
- 若
S_context:- 若
U.context缺失(为空对象{}或不存在)→S_context = 0.5 - 否则 →
S_context = Cᵢ.context_suitability[U.context_key]
- 若
工程约定:
U.need/U.context在客户端为稀疏 one-hot(最多一个 key=1)。实现时需提供一个“取唯一 key”的工具函数:若出现多个 key=1,按第一个(字典序或插入序)取值并记录告警(V1 可先 debug 日志)。
4.3 分解项:S_emotion(general=0.8;否则 1-|u-c|)
- 若
U.emotion_score缺失 →S_emotion = 0.8 - 否则:
- 若
Cᵢ.emotion_score为 general(None)→S_emotion = 0.8 - 否则 → (S_{emotion}=1-|U.emotion_score - C_i.emotion_score|)
- 若
实现约束:
- 将
S_emotionclamp 到[0,1],避免异常值导致负分或溢出。
4.4 分解项:S_stage(对齐算法规则)
规则(按 设计说明文档/個性化推薦算法規則.md):
- 若
Cᵢ.stage == "general"→S_stage = 1 - 若
Cᵢ.stage命中用户阶段(例如用户expecting=1且内容stage="expecting")→S_stage = 1 - 若用户阶段为
unknown且内容阶段为非 unknown →S_stage = 0.7 - 其余 →
S_stage = 0
4.5 个性化加成 S_personal(含降个性化约束)
[ S_{personal}=\alpha \cdot C_i.personalization_power \cdot \max(S_{need}, S_{context}) ]
降个性化约束(对齐 spec 与回退梯度):
- 若
fallback_level>=1或字段缺失明显/低置信度:- L1:限制
personalization_power ≤ 0.5 - L2/L3:限制
personalization_power = 0
- L1:限制
工程实现:本模块只做“限制后的 effective_personalization_power”,由调用方传入
fallback_level(或直接传入已限制后的content_profile.personalization_power)。V1 建议:在content-repository/引擎侧先做候选池约束,本模块再做一次保护性 clamp(防御式编程)。
4.6 不确定性惩罚 P_uncertainty(Push 默认启用)
[ P_{uncertainty}=\beta \cdot (1-conf_U)\cdot(1-conf_{C_i})\cdot C_i.personalization_power ]
其中:
conf_U = user_profile.profile_confidenceconf_{C_i} = content_profile.review_confidence(缺省 0.7)
开关策略(V1):
scene=push:默认启用scene=feed/widget:默认关闭(可通过 config 打开)
4.7 Widget 情绪区间软降权(适中默认实现)
目标:当 scene=widget 且 Cᵢ.emotion_score 可计算(非 general)时,若超出 [0.4, 0.8] 不硬过滤,但应产生明显降权。
V1 选择一个“适中、可调参、可解释”的惩罚函数:
- 设区间为
[lo, hi](默认0.4, 0.8) - 距离:
- 若
e < lo,d = lo - e - 若
e > hi,d = e - hi - 否则
d = 0
- 若
- 惩罚:
- (P_{widget} = \gamma \cdot \frac{d}{(hi-lo)})
- 默认
γ = 0.25(适中强度) - clamp 到
[0, γ]
落地方式:
- 将
P_widget_emotion_out_of_range单独输出到 breakdown; - 在总分中计入:
P_risk_effective = external.P_risk + P_widget_emotion_out_of_range
解释:当
emotion_score达到区间边界外最大距离约为 0.4(例如 0 或 1)时,惩罚接近γ,足以在 Widget 场景把“过低/过高情绪”的句子压到更靠后,但不会一刀切。
5. 场景默认参数(V1 建议)
对齐 spec.md 与算法规则:
- Feed:
w_need=0.35, w_emotion=0.20, w_stage=0.15, w_context=0.30 - Push:
w_need=0.45, w_emotion=0.35, w_stage=0.15, w_context=0.05,并默认enable_uncertainty_penalty=true - Widget:
w_need=0.25, w_emotion=0.25, w_stage=0.30, w_context=0.20,并启用widget_emotion_soft_range=[0.4,0.8]
推荐默认:
alpha=0.15(可调,V1 用于让个性化加成“次要但可见”)beta=0.30(可调,V1 用于在低置信度时明显压低高个性化内容)widget_emotion_penalty_gamma=0.25(适中软降权强度)
注:
alpha/beta/gamma为工程默认建议值,后续应通过回归测试与线上指标调参;本模块必须允许 config 覆盖。
6. V1:S_fresh / P_fatigue / P_repeat 的处理方案(能落地且可演进)
6.1 V1 解决方式
- 在
ScoreBreakdown中保留S_fresh/P_fatigue/P_repeat字段; - 本模块计算时:
- 若上游未提供对应值,则默认按 0;
- 若提供,则原样计入总分(本模块不解释其来源/计算方式)。
6.2 接口建议(为后续模块对接预留)
external_terms中携带:S_fresh:例如时间衰减、跨日新鲜度(未来可由rerank-freqcap或reco-engine计算)P_fatigue/P_repeat:由rerank-freqcap基于历史集合与冷却窗口计算
好处:V1 先保证“可排序 + 可解释 + 可插拔”;后续接入重排/频控时无需改动打分主干,只需注入外部项。
7. 测试计划(V1)
7.1 单元测试(纯函数)
- 缺失字段一致性:
U.need缺失 →S_need=0.5U.context缺失 →S_context=0.5U.emotion_score缺失 →S_emotion=0.8
- 不确定性惩罚(Push 默认启用):
conf_U/conf_C低且personalization_power高 →final_score明显降低- 关闭开关后
P_uncertainty=0
- Widget 软降权:
emotion_score=0.6(区间内)→P_widget=0emotion_score=0.0/1.0(区间外)→P_widget接近gamma,final_score明显更低
pass=false:final_score必须为 0(或按实现约定为 0),且 breakdown 中保留分解项(便于排查)
7.2 断言建议
- 断言
breakdown字段集合稳定(不会因缺省而缺字段)。 - 断言所有分项均为有限数(非 NaN/Infinity),并在合理范围内(可对
S_*clamp 到[0,1])。
8. 风险与后续演进
8.1 已知风险
U.need/U.context若出现多个 key=1,会导致取值歧义;V1 需明确选择策略并记录告警,避免 silent bug。- Widget 软降权强度(
gamma)对结果影响较大,需要配合回归测试与线上指标调参。
8.2 后续演进方向(V1.1+)
- 将
P_risk细化为可配置的多项惩罚(例如 health sensitive、过度个性化翻车风险等),并在 breakdown 中拆分输出。 - 引入
S_fresh的时间衰减公式,并与rerank-freqcap的跨日多样性联动。