issue #127: 画像常驻注入接入自然遗忘曲线(ADR-0009) #132

Merged
Yushu merged 2 commits from feat/127-profile-forgetting-curve into main 2026-08-14 05:14:05 +00:00
Member

Closes #127

实现内容

把 ADR-0009 已 ADOPTED 的"自然遗忘曲线"概念(此前只用于事件记忆的三因子召回)扩展到画像常驻注入判断上:是否留在冻结快照里,不再是无条件全量渲染。

一、importance 字段全链路穿参

  • ProfileFactModel 新增可空 importance 列(无迁移文件,符合可空列约定)
  • _EXTRACT_MEMORIES_TOOLfacts[] schema 新增 importance 属性(未加入 required,与 events/knowledge 一致),_parse_candidate 复用既有 _resolve_importance
  • ProfileFact 补上 updated_atProfileFactModel 早有此列,_to_fact 之前没带出来)与 importanceProfileCandidate 新增 importance(不带默认值,强制每个构造点显式决定)
  • 四个写入口显式定义 importance:
    • delta.py:真实 LLM 值(复用 _resolve_importance,未指定时兜底 5)
    • runtime_loop.py::_handle_update_profileupdate_profile 工具):缺省 10(无 LLM 打标可蹭,"可变尾部=权威覆盖"语义下取高值而非中值)
    • reflection.py:缺省 5(中性)
    • cross_platform_pull.py:缺省 5(中性)

二、render_profile_snapshot 接入遗忘曲线降权

  • 新增 now/half_life_seconds/threshold 入参,判定函数 _is_retained
    weight = recency_decay(age_seconds, half_life_seconds) + importance/10加法非乘法
  • 三处调用点全部接上:runtime_loop.py_full_frozen_snapshot_run_event_driven_turnentry_drive_tick.py 的近期摘要
  • RuntimeLoopConfig 新增 profile_retention_half_life_seconds/profile_retention_threshold 两个字段(沿用 max_relationship_lines_per_turn 的既有先例:两处调用点各自持有配置读取)
  • config.py 新增对应两个配置项:arise_profile_retention_half_life_seconds 默认 30 天,arise_profile_retention_threshold 默认 0.0(数学上关闭态)
  • gather_profile_facts 喂给压缩 prompt 的 current_profile 不降权——判断逻辑完全放在 render_profile_snapshot 内部,未触碰 gather_profile_facts

三、两处 AC 要求显式裁定的语义空白

  • age 锚点 = updated_at(而非新增 created_at):ADR-0009 2026-07-10 更新节原文已定性"画像无单条时间戳概念,只有画像级'最后更新时间'",_upsert_fact 的重复断言刷新语义(反复被提及的事实不该老化)与这个锚点选择天然吻合
  • 存量 importance=None:跳过降权判断、恒渲染,不代入任何具体数值参与计算——不能照抄 EventRecord.mood=None→判0 的先例,那是加法中性项判 0 无害,画像场景判 0 会直接把全部存量条目淘汰掉

未做的事(按 AC 红线)

未加硬条目数上限、字符预算、淘汰/物理删除机制——ADOPTED 决策原样维持,未推翻。

设计决策的对抗校验

实现前用独立三路对抗校验推翻了两处初始倾向:

  1. 公式最初倾向乘法(ADR 原文"recency×importance降权"字面读出乘号)→ 校验发现同一段落里"×"与"+"对同一概念换用只是行文习惯而非公式记号,且乘法在 age 足够大时无下界,任何重要性最终都会跌出阈值,与"长期身份信息不该衰减"的设计意图矛盾。改为加法。
  2. 存量缺省最初倾向代入 10(比照"不做历史回填"的精神)→ 校验用具体数字证明:在(当时仍是乘法的)公式下,代入 10 依然会随 age 增长跌破 threshold,没有真正兑现"最保守"的自述目标。改为跳过判断、恒渲染。

测试与验证

  • 变异测试:6 处新增守卫(跳过判断、加法公式、age 锚点、upsert 原地覆盖 importance、facts[].importance 解析、config 到 RuntimeLoop 两处调用点的接线)全部可被单独抓住
  • 全量 1778 passing,ruff 干净,ty 仅有与本次改动无关的既有噪音(storage.py:2038/2324,不在本次 diff 涉及的行号范围内)
  • docs/adr/0009-memory-engineering.md 已补更新节(docs 分支本地提交,按约定不推送)

两轴 review

Standards + Spec 双维度审查 + 独立对抗校验,共同确认了同一处问题:新增测试类 TestFactImportanceFlowsThroughToStorage 的插入锚点选在 TestSingleChatWithPendingDelta 第一个方法之后而非类块末尾,导致该类另外两个既有方法被误吞进新类(语义无关,纯组织性问题,不影响执行结果)。已在本 PR 内以单独 fix commit 修正,归还给原类,重跑全量测试+ruff 确认无回归。

🤖 Generated with Claude Code

Closes #127 ## 实现内容 把 ADR-0009 已 ADOPTED 的"自然遗忘曲线"概念(此前只用于事件记忆的三因子召回)扩展到画像常驻注入判断上:是否留在冻结快照里,不再是无条件全量渲染。 ### 一、importance 字段全链路穿参 - `ProfileFactModel` 新增可空 `importance` 列(无迁移文件,符合可空列约定) - `_EXTRACT_MEMORIES_TOOL` 的 `facts[]` schema 新增 `importance` 属性(未加入 `required`,与 events/knowledge 一致),`_parse_candidate` 复用既有 `_resolve_importance` - `ProfileFact` 补上 `updated_at`(`ProfileFactModel` 早有此列,`_to_fact` 之前没带出来)与 `importance`;`ProfileCandidate` 新增 `importance`(不带默认值,强制每个构造点显式决定) - 四个写入口显式定义 importance: - `delta.py`:真实 LLM 值(复用 `_resolve_importance`,未指定时兜底 5) - `runtime_loop.py::_handle_update_profile`(`update_profile` 工具):缺省 **10**(无 LLM 打标可蹭,"可变尾部=权威覆盖"语义下取高值而非中值) - `reflection.py`:缺省 5(中性) - `cross_platform_pull.py`:缺省 5(中性) ### 二、`render_profile_snapshot` 接入遗忘曲线降权 - 新增 `now`/`half_life_seconds`/`threshold` 入参,判定函数 `_is_retained`: `weight = recency_decay(age_seconds, half_life_seconds) + importance/10`(**加法非乘法**) - 三处调用点全部接上:`runtime_loop.py` 的 `_full_frozen_snapshot` 与 `_run_event_driven_turn`,`entry_drive_tick.py` 的近期摘要 - `RuntimeLoopConfig` 新增 `profile_retention_half_life_seconds`/`profile_retention_threshold` 两个字段(沿用 `max_relationship_lines_per_turn` 的既有先例:两处调用点各自持有配置读取) - `config.py` 新增对应两个配置项:`arise_profile_retention_half_life_seconds` 默认 30 天,`arise_profile_retention_threshold` 默认 **0.0(数学上关闭态)** - `gather_profile_facts` 喂给压缩 prompt 的 `current_profile` **不降权**——判断逻辑完全放在 `render_profile_snapshot` 内部,未触碰 `gather_profile_facts` ### 三、两处 AC 要求显式裁定的语义空白 - **age 锚点 = `updated_at`**(而非新增 `created_at`):ADR-0009 2026-07-10 更新节原文已定性"画像无单条时间戳概念,只有画像级'最后更新时间'",`_upsert_fact` 的重复断言刷新语义(反复被提及的事实不该老化)与这个锚点选择天然吻合 - **存量 `importance=None`**:跳过降权判断、恒渲染,不代入任何具体数值参与计算——不能照抄 `EventRecord.mood=None→判0` 的先例,那是加法中性项判 0 无害,画像场景判 0 会直接把全部存量条目淘汰掉 ### 未做的事(按 AC 红线) 未加硬条目数上限、字符预算、淘汰/物理删除机制——ADOPTED 决策原样维持,未推翻。 ## 设计决策的对抗校验 实现前用独立三路对抗校验推翻了两处初始倾向: 1. **公式最初倾向乘法**(ADR 原文"recency×importance降权"字面读出乘号)→ 校验发现同一段落里"×"与"+"对同一概念换用只是行文习惯而非公式记号,且乘法在 age 足够大时无下界,任何重要性最终都会跌出阈值,与"长期身份信息不该衰减"的设计意图矛盾。改为加法。 2. **存量缺省最初倾向代入 10**(比照"不做历史回填"的精神)→ 校验用具体数字证明:在(当时仍是乘法的)公式下,代入 10 依然会随 age 增长跌破 threshold,没有真正兑现"最保守"的自述目标。改为跳过判断、恒渲染。 ## 测试与验证 - 变异测试:6 处新增守卫(跳过判断、加法公式、age 锚点、upsert 原地覆盖 importance、`facts[].importance` 解析、config 到 `RuntimeLoop` 两处调用点的接线)全部可被单独抓住 - 全量 1778 passing,ruff 干净,ty 仅有与本次改动无关的既有噪音(`storage.py:2038/2324`,不在本次 diff 涉及的行号范围内) - `docs/adr/0009-memory-engineering.md` 已补更新节(docs 分支本地提交,按约定不推送) ## 两轴 review Standards + Spec 双维度审查 + 独立对抗校验,共同确认了同一处问题:新增测试类 `TestFactImportanceFlowsThroughToStorage` 的插入锚点选在 `TestSingleChatWithPendingDelta` 第一个方法之后而非类块末尾,导致该类另外两个既有方法被误吞进新类(语义无关,纯组织性问题,不影响执行结果)。已在本 PR 内以单独 fix commit 修正,归还给原类,重跑全量测试+ruff 确认无回归。 🤖 Generated with [Claude Code](https://claude.com/claude-code)
一、importance字段全链路穿参:
- ProfileFactModel新增可空importance列;_EXTRACT_MEMORIES_TOOL的facts[]加
  importance属性,_parse_candidate复用既有_resolve_importance
- ProfileFact/ProfileCandidate加importance字段,ProfileFact.updated_at补上
  (ProfileFactModel早有此列,_to_fact之前没带出来)
- 四个写入口显式决定importance:delta.py真实LLM值;update_profile工具缺省10
  (可变尾部=权威覆盖,无LLM打标可蹭);reflection.py/cross_platform_pull.py
  缺省5(中性,同_resolve_importance"不确定"语义)

二、render_profile_snapshot接入遗忘曲线降权:
- 新增now/half_life_seconds/threshold入参,weight=recency_decay(age)+
  importance/10(加法不是乘法——乘法在age足够大时无下界,会让任何重要性
  的事实最终都跌出阈值,与"长期身份信息不该衰减"的设计意图相悖)
- age锚点=updated_at(距上次被重新断言多久),不新增created_at——ADR-0009
  2026-07-10更新节原文已把这件事定过性:"画像无单条时间戳概念,只有画像级
  '最后更新时间'"
- importance=None(存量事实)跳过降权判断、恒渲染,不代入具体数值参与计算
- threshold默认0.0(数学关闭态,同一次grill引入的mood_congruence=0.0同一
  先例)——代码路径/测试覆盖齐全但不改变任何现有部署的注入内容
- gather_profile_facts喂给压缩prompt的current_profile不降权——ADR原文范围
  限定在"冻结快照",判断逻辑完全放在render_profile_snapshot内部

三、设计决策均经三路对抗校验:最初倾向乘法公式+存量缺省代入10,校验发现
"×"只是ADR原文同段落里与"+"换用的行文习惯而非公式记号,且乘法/代入10组合
在数学上无法兑现"不破坏存量画像"的自述目标,改为加法+跳过判断。

变异测试验证6处新增守卫全部可被单独抓住(跳过判断、加法公式、age锚点、
upsert原地覆盖importance、facts[].importance解析)。ADR-0009更新节已补
(docs分支,本地提交不推送)。全量1778 passing,ruff干净。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TestFactImportanceFlowsThroughToStorage 插入锚点选在
TestSingleChatWithPendingDelta 的第一个方法之后而非类块末尾,导致该类原有
的另外两个方法(test_buffer_is_drained_so_a_second_cycle_finds_nothing_pending、
test_event_candidates_are_embedded_and_written)被错误并入新类——二者与
importance 无关,只是踩了缩进层级。两轴review的Standards/Spec两个维度各自
独立发现并经对抗校验确认。挪回原类,全量1778 passing,ruff干净。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Yushu merged commit 2453b6a068 into main 2026-08-14 05:14:05 +00:00
Yushu deleted branch feat/127-profile-forgetting-curve 2026-08-14 05:14:06 +00:00
Sign in to join this conversation.
No description provided.