mood_congruence:三因子召回升级四因子情感一致性偏置 #72

Merged
Yushu merged 2 commits from feature/67-mood-congruence into main 2026-07-25 03:37:16 +00:00
Member

Closes #67

What

三因子召回(recency + importance + 向量余弦)升级为四因子,新增 mood_congruence 情感一致性偏置——机器人回忆起来的记忆跟当下聊天的心情更贴,心情好的时候更容易聊起开心的往事。复用已有情感态/事件写入基础设施,零新增 LLM 调用。

实现要点

  • 新增 MoodTagaffective_state.py:valence/energy 两轴的写入时情感态标签。不复用三轴 AffectiveState——dominance 在写入侧从不被标注;EventRecord/EventCandidate 持有单个 mood: MoodTag | None,"只知道 valence 不知道 energy"这种半填状态不可表示。
  • mood_similarityaffective_state.py:归一化欧氏距离相似度,落在 [0,1],与其余三项同量纲。选欧氏而非余弦——中性态 (0,0) 是最常见的默认值,余弦对零向量未定义/数值不稳定。
  • 写入时打标签(delta.py:Delta 压缩 cycle 落盘事件前读一次该 chat 当前快情感态,同批事件共用同一标签;只在真有事件候选时才读(同 _flush 既有节流手法)。零新增 LLM 调用。
  • 非对称护栏:当前 valence 为负时整项按 RecallWeights.mood_congruence_negative_guard_factor 打折,防"心情差→更容易召回难过记忆→心情更差"正反馈环。系数默认取打折态而非 1.0——"忘了配"落在保守侧。
  • 旧存量兼容:字段引入前落盘的记录无标签(SQL 两列 NULL / Qdrant payload 缺 key),mood_congruence 项直接判 0,不做历史回填。current_mood 缺省 None 时该项恒为 0,既有三因子排序行为完全不变。
  • 读取节流_recall_events 只在 mood_congruence 权重非零且真有候选时才读快情感态。

Review

/code-review 两轴(Standards + Spec)已跑,两轴都发现了真实问题:

  • Spec_recall_events 最初无条件读快情感态(即便权重为 0),逼得既有测试 test_a_single_send_never_reads_affective_state 只能从"整轮零读"改写成计数断言——违反 AC"既有测试断言不受意外改动"。加守卫后旧测试原文恢复,热路径也少一次 DB 查询。另补了 storage.py 真实路径的 MoodTag 往返测试(此前只覆盖内存假存储)。
  • Standardsmood_valence/mood_energy 这对裸 float 在 7 处成对旅行且允许非法半填状态——收口成 MoodTagmood_similarity 随之迁到 MoodTag 旁边(它本就只是两个情感态间的距离,不该住在 recall.py 反向 import AXIS_BOUND);护栏系数并入 RecallWeights,消掉 RuntimeLoop 构造器那一串散射改动。

Tests

全仓库 942 个测试通过(新增约 20 个:mood_similarity 纯函数、score_events 四因子/护栏/旧记录判 0、Delta 打标签 + 零新增 LLM 调用断言、持久化往返、_recall_events 接线与读取节流、端到端排序验证)。

Closes #67 ## What 三因子召回(recency + importance + 向量余弦)升级为四因子,新增 mood_congruence 情感一致性偏置——机器人回忆起来的记忆跟当下聊天的心情更贴,心情好的时候更容易聊起开心的往事。复用已有情感态/事件写入基础设施,零新增 LLM 调用。 ## 实现要点 - **新增 `MoodTag`(`affective_state.py`)**:valence/energy 两轴的写入时情感态标签。不复用三轴 `AffectiveState`——dominance 在写入侧从不被标注;`EventRecord`/`EventCandidate` 持有单个 `mood: MoodTag | None`,"只知道 valence 不知道 energy"这种半填状态不可表示。 - **`mood_similarity`(`affective_state.py`)**:归一化欧氏距离相似度,落在 [0,1],与其余三项同量纲。选欧氏而非余弦——中性态 (0,0) 是最常见的默认值,余弦对零向量未定义/数值不稳定。 - **写入时打标签(`delta.py`)**:Delta 压缩 cycle 落盘事件前读一次该 chat 当前快情感态,同批事件共用同一标签;只在真有事件候选时才读(同 `_flush` 既有节流手法)。零新增 LLM 调用。 - **非对称护栏**:当前 valence 为负时整项按 `RecallWeights.mood_congruence_negative_guard_factor` 打折,防"心情差→更容易召回难过记忆→心情更差"正反馈环。系数默认取打折态而非 1.0——"忘了配"落在保守侧。 - **旧存量兼容**:字段引入前落盘的记录无标签(SQL 两列 NULL / Qdrant payload 缺 key),mood_congruence 项直接判 0,不做历史回填。`current_mood` 缺省 `None` 时该项恒为 0,既有三因子排序行为完全不变。 - **读取节流**:`_recall_events` 只在 mood_congruence 权重非零且真有候选时才读快情感态。 ## Review `/code-review` 两轴(Standards + Spec)已跑,两轴都发现了真实问题: - **Spec**:`_recall_events` 最初无条件读快情感态(即便权重为 0),逼得既有测试 `test_a_single_send_never_reads_affective_state` 只能从"整轮零读"改写成计数断言——违反 AC"既有测试断言不受意外改动"。加守卫后旧测试原文恢复,热路径也少一次 DB 查询。另补了 `storage.py` 真实路径的 MoodTag 往返测试(此前只覆盖内存假存储)。 - **Standards**:`mood_valence`/`mood_energy` 这对裸 float 在 7 处成对旅行且允许非法半填状态——收口成 `MoodTag`;`mood_similarity` 随之迁到 `MoodTag` 旁边(它本就只是两个情感态间的距离,不该住在 `recall.py` 反向 import `AXIS_BOUND`);护栏系数并入 `RecallWeights`,消掉 `RuntimeLoop` 构造器那一串散射改动。 ## Tests 全仓库 942 个测试通过(新增约 20 个:`mood_similarity` 纯函数、`score_events` 四因子/护栏/旧记录判 0、Delta 打标签 + 零新增 LLM 调用断言、持久化往返、`_recall_events` 接线与读取节流、端到端排序验证)。
回忆起来的记忆跟当下聊天的心情更贴:EventRecord/EventCandidate 新增写入时
的 (mood_valence, mood_energy) 标签,Delta 压缩管线落盘事件前顺手读一次该
chat 当前快情感态填入(零新增 LLM 调用,只在真有事件候选时才读);
score_events 新增第四项,相似度取归一化欧氏距离(与其余三项同量纲)。

非对称护栏:当前 valence 为负时整项按新增静态 config 系数打折,防"心情差
→更容易召回难过记忆→心情更差"正反馈环。字段引入前落盘的旧存量事件缺省
标签,该项直接判 0,不做历史回填。current_mood 缺省 None 时该项恒为 0,
既有三因子排序行为与测试断言不受影响。
Spec review(最重要):_recall_events 无条件读快情感态,即便 mood_congruence
权重为 0(默认值)也读——这逼得既有测试 test_a_single_send_never_reads_
affective_state 只能从"整轮零读"改写成计数断言,违反 AC"既有测试断言不受
意外改动"。加上"权重非零且真有候选"守卫(同 delta.py/_flush 既有节流手法)
后旧测试原文恢复,热路径也少一次 DB 查询。另补 storage.py 真实路径的 MoodTag
往返测试(此前只覆盖内存假存储)。

Standards review:mood_valence/mood_energy 这对裸 float 在 7 处成对旅行,
且允许"只填一半"的非法状态——收口成 affective_state.MoodTag,域层单个
mood: MoodTag | None,只在 SQL/Qdrant 边界拆成两列。mood_similarity 随之
迁到 MoodTag 旁边(它本就只是两个情感态间的距离,不该住在 recall.py 反向
import AXIS_BOUND)。护栏系数并入 RecallWeights——与 mood_congruence 权重
同属静态 config 阈值,消掉 RuntimeLoop 构造器那一串散射改动;默认值取打折
态而非 1.0,"忘了配"落在保守侧。delta.py 的 tagged_events 改名(未打标签
分支上装的正是未打标签的事件)。
Yushu merged commit 36b20c4d5e into main 2026-07-25 03:37:16 +00:00
Yushu deleted branch feature/67-mood-congruence 2026-07-25 03:37:17 +00:00
Sign in to join this conversation.
No description provided.