issue #126: 关系网tension/affinity双轴驱动+文档对齐 #131

Merged
Yushu merged 2 commits from feat/126-relationship-tension-affinity into main 2026-08-14 02:11:45 +00:00
Member

Closes #126

实现

一、affinity(改动面比原估计小——信号已在库里)

  • 不需要新 LLM schema 字段:speaker_polarity(issue #94 已产出的逐发言人互动极性)就是 affinity 需要的信号,与慢层 per-user valence 共用同一份原始信号。
  • 新增 relationship.py::record_affinity_interaction(仿 record_trust_disclosure 的"读现值→算新值→写回"三段式,纯函数换成 apply_affinity_delta 的有符号 delta 制),接入 delta.py::_compress_one_chat——与慢层 valence 更新相邻但独立的一段循环,不依赖 valence 是否接线(speaker_polarity 是否为空是唯一门槛)。
  • A 型陷阱钉住:ADR-0008 否决的是"排序键直接读 slow_valence(user)"这个合并方案本身,不是禁止两者共用同一个原始输入信号——affinity 仍是独立存储(RelationshipAxesModel.affinity 列)、独立累积(无衰减)。
  • 新增 config:arise_affinity_polarity_scale(默认 0.05,刻意远小于 valence 的 0.3——affinity 没有衰减机制,是长期累积量)。arise_affinity_bound 已在 issue #36 落地,不重复新增。
  • 非空转测试:test_the_sort_key_actually_reorders_with_real_nonzero_affinity 覆盖整条从 LLM 抽取到落库到渲染的管线,用真实非零、彼此不同的 affinity 值确认排序真的换序——不是"渲染函数本身能排序"这条早已过关的断言。

二、tension(本组唯一真需要新 LLM schema 字段的)

  • 新增独立冲突信号字段 speaker_conflict(逐发言人,[0,1]),与既有 interaction_polarity/speaker_polarity 明确区分语义——不复用极性信号驱动 tension。
  • 新增 relationship.py::record_tension_conflict,写回时把 tension_updated_at 戳成调用时刻的 now——decayed_axes 的"为 None 则跳过衰减"短路意味着不写这个字段,自然衰减永远走不到。
  • 新增 config:arise_tension_conflict_gain(默认 0.3)。arise_tension_cap/arise_tension_half_life_seconds 已在 issue #36 落地,不重复新增。
  • 无需 DB 迁移,RelationshipAxesModel 已有 tension/tension_updated_at 两列。

三、文档核对:回读 CONTEXT.md/design.md/ADR-0008"决策"节,确认"4 轴封顶"字面均为轴值上限语境,与 issue #114"独立封顶"(注入行数上限)是两件不同的事,无需改动。订正 ADR-0008"issue #36 实现反馈"节一处失实描述:record_trust_disclosure 的两个生产调用点实际都在 delta.py::_compress_one_chat,不在 runtime_loop.py

两轴 review 发现与修正(2 项,全部确认)

  1. [MEDIUM spec] record_tension_conflict 最初实现无条件把 tension_updated_at 戳成 now,即便 delta 为 0(speaker_conflict 给了 conflict=0,模型明确判断"这批没有冲突迹象")。复核用具体数值复现:tension=0.8 在真实冲突后 3 小时被判定一次 conflict=0,若无条件推锚点,6 小时后(一个半衰期)只衰减到 ≈0.566 而不是正确的 0.4——一次"确认无冲突"的评估反而让紧张状态多保留了约 40% 的相对幅度,与"自然衰减防记仇"的立意相反。已修正为 delta == 0.0 时锚点原样保留,新增回归测试用同一组数值钉住。
  2. [LOW spec] ADR-0008 更新节引用的测试名与实际不符(疑似笔误拼接了两个测试名),已订正为真实标识符。

测试

  • Part 1 新增 TestRecordAffinityInteraction(4 个用例)+ TestAffinityIsBumpedFromSpeakerPolarity(5 个用例,含非空转排序端到端测试)。
  • Part 2 新增 TestRecordTensionConflict(6 个用例,含两轴review修正的零delta锚点测试)+ TestTensionIsBumpedFromSpeakerConflict(5 个用例,含"极性不推动tension"负向测试 + 变异测试发现的"缺 conflict 键需整条跳过"回归测试)。
  • 变异测试验证 7 处新增守卫全部可被单独抓住(affinity 接线、tension 接线、tension_updated_at 写入、speaker_conflict 缺键 fail-closed、零delta锚点保留)。
  • 全量测试 1765 passed;ruff 全绿;ty check 对本片改动文件全绿。

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

Closes #126 ## 实现 **一、affinity(改动面比原估计小——信号已在库里)** - 不需要新 LLM schema 字段:`speaker_polarity`(issue #94 已产出的逐发言人互动极性)就是 affinity 需要的信号,与慢层 per-user valence 共用同一份原始信号。 - 新增 `relationship.py::record_affinity_interaction`(仿 `record_trust_disclosure` 的"读现值→算新值→写回"三段式,纯函数换成 `apply_affinity_delta` 的有符号 delta 制),接入 `delta.py::_compress_one_chat`——与慢层 valence 更新相邻但独立的一段循环,不依赖 `valence` 是否接线(`speaker_polarity` 是否为空是唯一门槛)。 - A 型陷阱钉住:ADR-0008 否决的是"排序键直接读 slow_valence(user)"这个**合并方案**本身,不是禁止两者共用同一个原始输入信号——affinity 仍是独立存储(`RelationshipAxesModel.affinity` 列)、独立累积(无衰减)。 - 新增 config:`arise_affinity_polarity_scale`(默认 0.05,刻意远小于 valence 的 0.3——affinity 没有衰减机制,是长期累积量)。`arise_affinity_bound` 已在 issue #36 落地,不重复新增。 - 非空转测试:`test_the_sort_key_actually_reorders_with_real_nonzero_affinity` 覆盖**整条**从 LLM 抽取到落库到渲染的管线,用真实非零、彼此不同的 affinity 值确认排序真的换序——不是"渲染函数本身能排序"这条早已过关的断言。 **二、tension(本组唯一真需要新 LLM schema 字段的)** - 新增独立冲突信号字段 `speaker_conflict`(逐发言人,`[0,1]`),与既有 `interaction_polarity`/`speaker_polarity` 明确区分语义——不复用极性信号驱动 tension。 - 新增 `relationship.py::record_tension_conflict`,写回时把 `tension_updated_at` 戳成调用时刻的 `now`——`decayed_axes` 的"为 `None` 则跳过衰减"短路意味着不写这个字段,自然衰减永远走不到。 - 新增 config:`arise_tension_conflict_gain`(默认 0.3)。`arise_tension_cap`/`arise_tension_half_life_seconds` 已在 issue #36 落地,不重复新增。 - 无需 DB 迁移,`RelationshipAxesModel` 已有 `tension`/`tension_updated_at` 两列。 **三、文档核对**:回读 CONTEXT.md/design.md/ADR-0008"决策"节,确认"4 轴封顶"字面均为轴值上限语境,与 issue #114"独立封顶"(注入行数上限)是两件不同的事,无需改动。订正 ADR-0008"issue #36 实现反馈"节一处失实描述:`record_trust_disclosure` 的两个生产调用点实际都在 `delta.py::_compress_one_chat`,不在 `runtime_loop.py`。 ## 两轴 review 发现与修正(2 项,全部确认) 1. **[MEDIUM spec]** `record_tension_conflict` 最初实现无条件把 `tension_updated_at` 戳成 `now`,即便 `delta` 为 0(`speaker_conflict` 给了 `conflict=0`,模型明确判断"这批没有冲突迹象")。复核用具体数值复现:tension=0.8 在真实冲突后 3 小时被判定一次 `conflict=0`,若无条件推锚点,6 小时后(一个半衰期)只衰减到 ≈0.566 而不是正确的 0.4——一次"确认无冲突"的评估反而让紧张状态多保留了约 40% 的相对幅度,与"自然衰减防记仇"的立意相反。已修正为 `delta == 0.0` 时锚点原样保留,新增回归测试用同一组数值钉住。 2. **[LOW spec]** ADR-0008 更新节引用的测试名与实际不符(疑似笔误拼接了两个测试名),已订正为真实标识符。 ## 测试 - Part 1 新增 `TestRecordAffinityInteraction`(4 个用例)+ `TestAffinityIsBumpedFromSpeakerPolarity`(5 个用例,含非空转排序端到端测试)。 - Part 2 新增 `TestRecordTensionConflict`(6 个用例,含两轴review修正的零delta锚点测试)+ `TestTensionIsBumpedFromSpeakerConflict`(5 个用例,含"极性不推动tension"负向测试 + 变异测试发现的"缺 conflict 键需整条跳过"回归测试)。 - 变异测试验证 7 处新增守卫全部可被单独抓住(affinity 接线、tension 接线、tension_updated_at 写入、speaker_conflict 缺键 fail-closed、零delta锚点保留)。 - 全量测试 1765 passed;ruff 全绿;`ty check` 对本片改动文件全绿。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
一、affinity(改动面比原估计小——信号已在库里):
- 不需要新LLM schema字段,复用speaker_polarity(issue #94已产出)
- 新增record_affinity_interaction驱动函数(仿record_trust_disclosure形状,
  纯函数换成apply_affinity_delta的有符号delta制),接入delta.py::
  _compress_one_chat,与慢层valence更新相邻但独立(不依赖valence是否接线)
- A型陷阱钉住:ADR-0008否决的是"排序键直接读slow_valence(user)"这个合并
  方案本身,不是禁止共用原始信号——affinity仍独立存储、独立累积
- 新增config:arise_affinity_polarity_scale(arise_affinity_bound已存在,
  不重复新增)
- 非空转测试:test_the_sort_key_actually_reorders_with_real_nonzero_affinity
  覆盖整条LLM抽取→落库→渲染管线,真实非零值确认排序真的换序

二、tension(本组唯一真需要新LLM schema字段):
- 新增独立的speaker_conflict字段,与interaction_polarity/speaker_polarity
  语义明确区分,不复用极性信号驱动tension
- 新增record_tension_conflict驱动函数,写回时一并戳tension_updated_at为
  调用时刻的now——否则decayed_axes的"为None则跳过衰减"短路会让tension
  自然衰减永远走不到
- 新增config:arise_tension_conflict_gain(cap/half_life已存在)

三、文档核对:确认CONTEXT.md/design.md/ADR-0008"4轴封顶"字面均为轴值上限
语境,与issue #114"独立封顶"(注入行数上限)是两件不同的事,无需改动。

变异测试验证6处新增守卫全部可被单独抓住,其中1处(speaker_conflict缺
conflict键的fail-closed行为)发现真实覆盖缺口,已补regression test。
ADR-0008更新节已补(docs分支,本地提交不推送,含订正issue #36实现反馈节
一处trust归属描述失实)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
两轴review发现1项确认(MEDIUM,spec轴),已修正:

record_tension_conflict最初实现无条件把tension_updated_at戳成now,即便
delta为0(speaker_conflict给了conflict=0——模型明确判断"这批没有冲突迹
象",与没提这个人不同,两者经_parse_speaker_conflict会产出不同的解析
结果但delta都可能算出0)。复核用具体数值复现:tension=0.8在真实冲突后
3小时被判定一次conflict=0,若无条件推锚点,6小时后(一个半衰期)只衰
减到0.8*0.5^0.5≈0.566,而不是正确的0.4——一次"确认无冲突"的评估反而
让紧张状态多保留了约40%的相对幅度,与"自然衰减防记仇"的立意相反。

修正:delta==0.0时锚点原样保留(apply_tension_conflict对该情形本身也
是数值no-op,语义一致)。新增test_a_zero_delta_does_not_move_the_anchor
用同一组数值钉住。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Yushu merged commit 646c28ec63 into main 2026-08-14 02:11:45 +00:00
Yushu deleted branch feat/126-relationship-tension-affinity 2026-08-14 02:11:46 +00:00
Sign in to join this conversation.
No description provided.