关系 RAG
RELATION RAG REVIEW
状态:🟡 规划 · 最后核对 2026-09-12
对 relation-rag-design(自标 🟡 规划)做的落地前评估。 那份是设计,这份是拿真实数据核对设计假设的结果 —— 三处假设与数据不符, 而且都往"工作量更小、但要改架构"的方向偏。
一、先说结论
| 设计文档的假设 | 实测 | 影响 |
|---|---|---|
| graph 边(strength > 0.6)~500 条 | 87 条 | 工作量降 5.7 倍 |
| 静态图谱是"人物关系网" | 90.5% 是「同族」标签(1959/2163,strength 全 0.2) | 不能当关系素材用 |
| 剧情抽取 = 图谱的补充 | 两者粒度不同,剧情人物 9 个里 8 个不在图谱节点里 | 不能合并,要分层 |
净结论:Phase 1 比设计估的小得多(约 1 天,不是 2–3 天),
但真正的素材不在静态图谱里,而在 note 字段和剧情抽取产物里。
二、静态图谱的实际形状(app/systems/relation/relation_graph.json)
302 节点 / 2163 边
⚠️ 发现 1:90.5% 的边是「同族」
| type | 条数 | 占比 |
|---|---|---|
| 同族 | 1959 | 90.5% |
| 血缘/家人 | 134 | 6.2% |
| 关联 | 33 | 1.5% |
| 同事/搭档 · 上下级 · 同伴 · 挚友 · 宿敌… | 37 | 1.7% |
而 strength 分布暴露了同一件事:
strength=0.2 : 1957 条 ← 就是那批「同族」
strength=0.6 : 73 条
strength=0.7 : 38 条
strength=0.8 : 29 条
strength=0.9 : 19 条
strength=1.0 : 1 条
「同族」是分类标签(谁和谁同种族/同阵营),不是人物关系。 「阿米娅和某某是同族」这句话注入 prompt 对角色扮演没有价值 —— 它不改变她怎么说话,也不改变她怎么看待对方。
✅ 发现 2:设计文档的 >0.6 过滤恰好是对的
>0.6 的边 = 87 条,按 type:
血缘/家人 71
挚友 4
上下级 3
同事/搭档 3
挚友/信赖 2
宿敌/执念 2
旧怨/误解 1
爱慕 1
阈值 0.6 天然把「同族」全滤掉了(它们全是 0.2)。 这说明当初定 0.6 的人对数据形状是有感觉的 —— 但他估的"~500 条"是按 2163 的粗比例猜的,没真数过。实际 87 条,且几乎都是「血缘/家人」。
✅ 发现 3:note 字段才是可用的自然语言(100% 覆盖)
2163 条边全部有 note,而设计 Phase 1 第 2 步原计划是
「批量 Claude Sonnet 将边类型转为 1-2 句自然语言描述」+「成本约 ¥30-50」。
这一步不用做了 —— note 已经是人写的自然语言:
amiya -> kaltsit [上下级] 凯尔希·思衡托是阿米娅的监护人与导师
amiya -> theresa [上下级] 魔王传承,阿米娅继承特蕾西娅的黑冠
kaltsit -> theresa [挚友/信赖] 巴别塔时期并肩作战的挚友
kaltsit -> mon3tr [上下级] Mon3tr是凯尔希·思衡托的同伴,现已独立为M3
kaltsit -> priestess [旧怨/误解] 前文明遗留下的复杂关系
省掉一整步 LLM 批处理(¥30–50 + 3 次重试逻辑 + 断点续传)。
三、⚠️ 发现 4:两个数据源粒度不同,不能合并
拿剧情抽取(data/story_relations_to-st-1.json,ST-1「一封想写的信」)抽出的人名
去查图谱的 302 个节点:
| 剧情人物 | 在图谱里? |
|---|---|
| 安洁莉娜 | ✅ 是干员 |
| 巴尼特 · 塔妮 · 梅拉 · 罗克 · 小提摩西 · 皮塔 | ❌ 剧情 NPC / 未实装 |
| 工程部干员 · 好心的路人 | ❌ 泛称,不是专名 |
9 个名字里只有 1 个是图谱节点。
两个数据源的定位其实是:
| 静态图谱 | 剧情抽取 | |
|---|---|---|
| 节点 | 干员 ↔ 干员 | 剧情人物 ↔ 剧情人物(含 NPC) |
| 粒度 | 跨全部剧情的角色设定级 | 单章事件级 |
| 例子 | amiya -> kaltsit「监护人与导师」 |
巴尼特 -> 塔妮「叔侄」+ 逐行证据 |
| 权威 | 正典(设定) | 正典(原文可回溯) |
所以不能做的事
不能把剧情关系直接塞进 relation_graph.json。 理由:
- 键空间不同 —— 图谱按
operator_id(amiya/kaltsit)索引,剧情人物是无 id 的中文名 - 图谱的动态边按用户隔离(博士 ↔ 干员),剧情边是共享正典 —— 混在一起会破坏已有的隔离约定
- 剧情 NPC 一旦进了图谱节点,
get_faction()/get_related()这些现有调用者的语义会变
四、权威分层建议(⚠️ 这步必须在建库之前定)
项目已有的 正典 / 本局事实 / 待考 / 提案 分法正好该用在这儿:
| 层 | 数据 | 能否进 prompt | 冲突时谁赢 |
|---|---|---|---|
| 正典 | 图谱静态边(含 note)· 剧情抽取边(带 line_id 证据) |
✅ | 正典赢 |
| 本局事实 | 图谱动态边(信赖/聊天频率)· L3 记忆 · 关系事件 | ✅ | 让位给正典 |
| 待考 | 剧情抽取里 basis=inferred 的边 · unresolved 条目 |
❌ 只做检索候选,标注不确定 | — |
⚠️ 不这样分的具体后果:一次剧情抽取的幻觉(模型编了一条"X 是 Y 的师父") 会被当成设定注入,而它看起来和其他设定一模一样 —— 用户不会发现, 干员会一直错下去。这正是这个项目最怕的失败形状。
而 extract_story_relations.py 已经在这条路上做了正确的两件事:
- 只信行号(模型回
line_no,脚本自己映射line_id)→ 证据可确定性回溯 - 每条边必须有 evidence,没有就丢弃并计数("一条编的边会污染角色认知, 比少一条边严重得多")
它的质检真的抓到东西了 —— 实测那次运行:
presence 检查降级 2 条:
巴尼特↔小提摩西 双方在证据行附近从未同场 → direct 降为 reported
塔妮↔罗克 同上
unresolved 12 条:多为「???身份未在原文揭晓」—— 正确的留白,不是失败
这套质检可以复用到图谱侧 —— 而图谱的 87 条 >0.6 边从来没被这样验过。
✅ 分层已落地(2026-09-12,f054b557)
app/systems/relation/authority.py —— 把上面那张表变成可被测试盯住的代码。
设计与取舍都写在模块 docstring 里,其中两条值得在这儿重复:
- 低权威「折扣」而不是「过滤」 —— 抄自
lorebook.py记的那个 bug (导入内容会把逐条核对过的正典挤出max_entries)。硬分层会让 「只有本局事实、没有正典」的干员拿到空结果,而那和「她没有关系」是两回事。 - ⚠️ 这个设计点被自己的测试抓了一次:
sort_key第一版把is_canon放在第一维 —— 那就是硬分层。而模块 docstring 里我自己写着「折扣乘在强度上」。 写在文档里的理由和实际实现可以是两回事,只有测试分得出来。
五、Phase 1 重新估算
| 设计文档 | 实测后 |
|---|---|
| 矩阵对 → 30 条文档 | 30 条(不变,手工精修) |
graph 边 >0.6 ~500 条 + Sonnet 转换(¥30–50) |
87 条,且 note 已是自然语言 → 省掉 LLM 这步 |
.char 锚段 → ~200 条 |
待核(未评估;设计说"低权重补充,防漏") |
| 合计 ~730 条 | ~120 条 + 剧情边(新增,设计里没有) |
时间:设计估「Phase 1 2–3 天」→ 实际约 1 天(省掉的 LLM 批处理是最大一块)。
⚠️ 但设计里漏了一整类素材:剧情关系
设计的三类来源(矩阵 / 图谱边 / .char 锚段)都没有"剧情里发生过什么"。
而角色扮演真正的血肉恰恰是后者 —— 「巴尼特的小侄女塔妮」这种
有原文行号可回溯的关系,比「同族」有用一万倍。
这是设计文档最该补的一处。
六、建议的落地顺序
| 步 | 事 | 为什么这个顺序 |
|---|---|---|
| 1 | 补 backlog 登记 | 这条从来没进过清单(backlog.md 搜「图谱」零命中,只在 tech-debt 的「大件另议」表里一行、连状态都没有)。没进清单的东西不会出现在"接下来做什么"里 —— 它能被拖到今天才想起来,就是这个原因 |
| 2 | 定权威分层(第四节) | ✅ 2026-09-12 已实现:app/systems/relation/authority.py + 16 条测试(f054b557)。档位复用 lorebook 的 正典 与 l3_long 的 AUTH_*,不新造同义词 |
| 3 | 用 87 条 re-建一个只含"真关系"的子集 | ✅ 2026-09-12 已实现:scripts/export_relation_subset.py → data/relation_subset.json(派生数据,已 gitignore)。权威标注复用 authority.py,不另写一套 |
| 4 | 重跑剧情抽取(补 line_id) |
现有产物是旧脚本的(18:56/19:01 生成,脚本 19:05 才加行号绑定),evidence 全是 line: null。要拿到可回溯证据必须重跑(调 LLM,成本很低) |
| 5 | 建库(Phase 1) | 素材齐了再建 |
| 6 | 检索接口 + prompt 注入(Phase 2/3) | ⚠️ 最危险的一步 —— 见下 |
⚠️ 第 6 步要防的东西
新增一条注入线,第一件要防的是它和 L3 的 personalize() 收口打架。
这个项目最稳定的失败形状就是「第二份名单」(backlog 自己记着:一天命中六次)。
设计文档已经考虑了一条(get_relation_context 返回前按 target_id 与 L3 去重),
但没有提 test_doctor_address.py 那个收口守卫 ——
关系文本里必然出现干员名和"博士",注入时必须走同一个收口。
六点五、⚠️ 导出时撞出的图谱数据缺陷(本轮新增)
跑 scripts/export_relation_subset.py 时,87 条里暴露出三类数据问题。
它们之前不会被任何人看到 —— 因为那 87 条边从来没被这样逐条过一遍。
| 问题 | 实例 | 影响 |
|---|---|---|
| 节点用称号当名字 | theresa 节点的 name 是 「魔王」,而 note 写的是「继承特蕾西娅的黑冠」 |
RAG 里「特蕾西娅」检索不到这个节点;注入时会说「魔王」,而用户认知里那是特蕾西娅 |
| 节点值是 null | swire: null(key 存在、值为空) |
chen→swire(挚友, 0.7)这条边渲染不出对方名字,只能回退成 id 字面 |
| 指向不存在节点 | 6 条边的端点不在 nodes 里(其中 1 条是 >0.6 的) |
该边注入时对方无名 |
⚠️ theresa 那条尤其值得注意:它不是缺数据,是名字取错了字段
(用了称号 魔王,而 tags 里反而有「黑冠」「巴别塔」这种指向特蕾西娅的线索)。
修它要改源数据 relation_graph.json,属于设定订正,不是脚本能兜的。
导入脚本的做法:name_of() 查不到就原样返回 id 并记进 broken_refs ——
不抛异常。数据问题要能被看见,但不该让整轮导出失败(少一条关系 ≠ 导出不出来)。
七、待确认(不是我能定的)
- 剧情抽取要不要覆盖全部章节? 现在只跑了
TO-ST-1(1 章 / 200 行 / 10 条边)。 200+ 章跑下来是 2000+ 条边,成本和时间要你定。 .char锚段那 ~200 条还做不做? 设计说"低权重补充,防漏", 但现在有了剧情边这条更好的线,它的边际价值变小了。- 关系 RAG 的目标是什么? 设计说"专门回答'她最在意谁'"。 而单聊里干员对博士的关系是动态的(信赖),对其他干员的关系才是静态的 —— 注入的时机和价值要再想清楚,否则可能是"注入了但用不上"。