R4:现网 65 个场景键里 peer 主导 0 个、topic 主导 0 个,36 个 auto:chan + 26 个 chan: 全部锚在输入通道。两个独立原因: 采集侧全仓无插件在 InjectInput 填 peer/group_id/user_id/chat_id (日志中 peer 出现 0 次);排序侧 chan 与 peer 权重同为 1.0 而 NewSituation 稳定排序让 chan 恒在前,即使采集到也进不了 Label(2)。 这与 R1/R2 是不同层面:R1 修完只会得到「正确的单一维度」。 R5:createSceneLocked 新场景成立即 DELETE FROM situation_evidence (全表清),会连带清掉别的场景尚未攒够门槛的证据。
7.7 KiB
场景式记忆修复 plan
起点:2026-09-26 生产库实测(
/home/newqqagent/memory/graph.db)。 现象:65 个场景中 6 组是同一场面的双胞胎键;主力场景auto:chan:qq+part:morningstrength=270、6 个 features、0 条记忆,日志里被"命中"179 次。 分支:feature/scene-writeback(从 main 拉出,工作树干净)。
根因(三条,逐条修)
R1 建键不过归一化(病因)
internal/memory/scene_emerge.go:329
base := "auto:" + sig.Label(2) // Label 用 "+" 拼接,未归一化
而同一层的另外三条路都过了归一化:
| 位置 | 是否归一化 |
|---|---|
EnsureScene(声明建键)scene_emerge.go:555 |
✅ |
effectiveScenes(写侧挂 ref)graph.go:1397 |
✅ |
RecallByScene(读侧召回)scene.go:328 |
✅ |
createSceneLocked(涌现建键)scene_emerge.go:329 |
❌ |
normalizeSceneSegment 把 + 归一成 _,所以涌现键天生带 +、其余三方说 _。
scenes.key 虽有 UNIQUE 约束,但两个不同字符串都合法,拦不住。
实测 6 组双胞胎(REPLACE(key,'+','_') 后重名):
auto:chan:qq+part:morning ↔ auto:chan:qq_part:morning (270/0 refs)
auto:chan:mc:event+topic:mc ↔ auto:chan:mc:event_topic:mc
auto:chan:mc:system+topic:mc ↔ auto:chan:mc:system_topic:mc
auto:chan:mc-resident+topic:mc ↔ auto:chan:mc-resident_topic:mc
auto:chan:homeagent-mail-bridge+... ↔ auto:chan:homeagent-mail-bridge_...
auto:chan:system+topic:任务 ↔ auto:chan:system_topic:任务
R2 part(时段,权重 0.2)污染主键与门槛
Label(2) 取权重最高的 2 个特征。特征顺序是
chan(1.0) → peer(1.0) → tool(0.8) → topic(0.4) → part(0.2),
只有当前 2 个强特征不足时 part 才会进键——于是出现
auto:chan:qq+part:morning 这种"时段成了场景身份"的名字。
更糟的是 recordSituationEvidenceLocked 也用 Label(2) 做桶(scene_emerge.go:380),
所以 part 不只影响名字,还决定 minSceneEvidence=2 这个门槛在哪个桶里计数。
实测证据(键名与实际指纹自相矛盾):
场景键: auto:chan:qq+part:morning strength=270
实际指纹: [chan:qq part:evening] ← 19:15 命中,evening 却并入 morning
这不是 bug 触发,是加权 Jaccard 正常工作:共享 chan:qq(1.0)、
并集含 part(0.2×2),相似度 1.0/1.4 = 0.714 > joinSceneThreshold 0.5。
R3 图整理心跳漏扫 scenes
detectEntityMerge 唯一遍历入口 Recall(nil,nil,1,""),其全量路径只查
entities(graph.go:609) + relations(graph.go:631),scenes 不在其中。
所以 R1/R2 造成的双胞胎从 9-15 起无人发现,23 号空转到 strength=270。
R3 是"没被发现"的原因,R1/R2 是病因。 顺序不能反。
R4 场景身份只有「输入通道」一个可靠维度
现网 65 个场景键的维度分布(实测):
auto:chan(涌现·通道主导) 36
chan: (输入通道) 26
tool: (工具) 3
peer 主导 0
topic 主导 0
两个独立原因叠加:
- 采集侧恒空:
situationFeaturesFor从evt.Payload找peer/peer_id/group_id/user_id/chat_id,而全仓没有任何插件在 InjectInput 时填这些键(group_id只出现在output.go:145的发送侧帮助文本里)。 ⇒ 日志中peer特征出现次数为 0。 - 排序上被挤掉:
chan与peer权重同为 1.0,而NewSituation按权重 降序稳定排序,chan先 append 就永远在前 ⇒ 即使采集到 peer,Label(2)也轮不到它。
后果:「跟谁对话」这个本该最强的身份信号(权重与 chan 并列)根本进不了 场景身份。同一件事在 QQ 和 Telegram 上会落进不同场景而无法共享。
R4 与 R1/R2 是不同层面的问题:R1/R2 让键构造正确,R4 决定键能表达
什么。R1 修完后 auto:chan:qq 会取代 auto:chan:qq+part:morning,
但它依然只认通道——所以 R4 不修,修复效果只到「正确的单一维度」。
R5 situation_evidence 全表清空
createSceneLocked 新场景一成立就 DELETE FROM situation_evidence
(不只删本指纹的足迹)。多场景并发轮次下,A 场景的建立会连带清掉 B 尚未
攒够 minSceneEvidence=2 的证据 ⇒ 门槛判定被别的场景的建立随机打断。
步骤
步骤 1:修 R1 + R2(源头,不碰存量)
Label改名语义:只取权重 ≥ 阈值的主导特征,且结果过NormalizeSceneKeycreateSceneLocked的base用归一化后的 labelrecordSituationEvidenceLocked的桶键用同一个归一化 label(R2 的另一半)- 判据:先写红测试——同一指纹两次建键必须落同一行(现状会落两行)
步骤 2:修 R3(让图整理覆盖全库)
- 给
mergeLoop加独立的场景去重路径,不塞进实体那个 O(n²) 双重循环 (理由:实体 1 万行 × bigram + LLM 裁决,实测 5000 万次配对/轮; 场景表小且已有现成的situationSimilarity加权 Jaccard,语义更准) - 键归一化后相同 ⇒ 合并 refs/features/strength,不经 LLM(键相同已证明同一场面)
- 判据:构造两个
+/_孪生键,跑一次 mergeLoop 后期望合成一个
步骤 3:清理现网垃圾场景,让它重新生成
用户明确要求:直接清理,重新生成(不做保守迁移)。
sqlite3 .backup备份(禁用 cp,WAL 模式会拷出不一致快照)- 删
origin='emergent'的全部场景 + 其scene_features/scene_refs(保留origin='declared':那些是插件声明的,不是垃圾) - 同步清
situation_evidence - 重启 homed,等 ≥2 次同类交互让场景重新涌现
- 复验:新场景键不含
+、有 features 且有 refs
步骤 4:修 R4(覆盖面:让 peer 进得来)— 需跨插件,等用户确认范围
- 先查各插件 InjectInput 时手上有没有 peer 信息可用(发信侧有
meta.group_id/user_id,收信侧是否拿得到要逐个确认) - 有则补:插件填
payload["group_id"]/["user_id"] - 排序侧:
peer权重提到高于chan,或NewSituation排序时 peer 优先 (两者都要,否则采集到了也进不了身份) - 判据:构造「同一 chan、不同 peer」的两轮,期望落进不同场景
步骤 5:修 R5(证据桶别全表清)
DELETE FROM situation_evidence改为按本指纹的桶标签删- 判据:预置两个桶的证据各 1 次;建一个场景后断言另一个桶的证据还在
步骤 6:验证与收口
go test ./internal/memory/... ./internal/agent/core/...全绿go build ./...+ 全仓go test ./...- 现网观察:日志中场景命中后能查到 refs(非 0)
- 现网观察:场景键前缀分布不再 100% 锚在 chan(R4 未做则保持挂账)
git_release_check.sh无新增红项
不做的事(防反复挂账)
- 不把
scenes塞进memory_merge工具:该工具语义是"实体删除 + 关系重定向", 场景合并需要"特征并集 + refs 重定向 + strength 相加",是另一套操作,硬塞会让工具语义变危险。 - 不改
validGraphNodeKind:它只管memory_block_edges端点校验,与scenes无关。 - 不给实体那个 O(n²) 循环做优化:属独立问题(已实测:1 万实体→~224GB 瞬时分配/轮), 混进本次修复会让 diff 失焦。单独开条目。