罗德岛通讯与技术部

关系 RAG

RELATION RAG REVIEW

「阿米娅和某某是同族」这句话注入 prompt 对角色扮演没有价值 ——

状态:🟡 规划 · 最后核对 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 理由:

  1. 键空间不同 —— 图谱按 operator_idamiya/kaltsit)索引,剧情人物是无 id 的中文名
  2. 图谱的动态边按用户隔离(博士 ↔ 干员),剧情边是共享正典 —— 混在一起会破坏已有的隔离约定
  3. 剧情 NPC 一旦进了图谱节点,get_faction() / get_related() 这些现有调用者的语义会变

四、权威分层建议(⚠️ 这步必须在建库之前定)

项目已有的 正典 / 本局事实 / 待考 / 提案 分法正好该用在这儿:

数据 能否进 prompt 冲突时谁赢
正典 图谱静态边(含 note)· 剧情抽取边(带 line_id 证据) 正典赢
本局事实 图谱动态边(信赖/聊天频率)· L3 记忆 · 关系事件 让位给正典
待考 剧情抽取里 basis=inferred 的边 · unresolved 条目 ❌ 只做检索候选,标注不确定

⚠️ 不这样分的具体后果:一次剧情抽取的幻觉(模型编了一条"X 是 Y 的师父") 会被当成设定注入,而它看起来和其他设定一模一样 —— 用户不会发现, 干员会一直错下去。这正是这个项目最怕的失败形状。

extract_story_relations.py 已经在这条路上做了正确的两件事

  1. 只信行号(模型回 line_no,脚本自己映射 line_id)→ 证据可确定性回溯
  2. 每条边必须有 evidence,没有就丢弃并计数("一条编的边会污染角色认知, 比少一条边严重得多")

它的质检真的抓到东西了 —— 实测那次运行:

presence 检查降级 2 条:
  巴尼特↔小提摩西  双方在证据行附近从未同场 → direct 降为 reported
  塔妮↔罗克        同上
unresolved 12 条:多为「???身份未在原文揭晓」—— 正确的留白,不是失败

这套质检可以复用到图谱侧 —— 而图谱的 87 条 >0.6从来没被这样验过

✅ 分层已落地(2026-09-12,f054b557

app/systems/relation/authority.py —— 把上面那张表变成可被测试盯住的代码。 设计与取舍都写在模块 docstring 里,其中两条值得在这儿重复:

  1. 低权威「折扣」而不是「过滤」 —— 抄自 lorebook.py 记的那个 bug (导入内容会把逐条核对过的正典挤出 max_entries)。硬分层会让 「只有本局事实、没有正典」的干员拿到空结果,而那和「她没有关系」是两回事。
  2. ⚠️ 这个设计点被自己的测试抓了一次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_longAUTH_*,不新造同义词
3 用 87 条 re-建一个只含"真关系"的子集 2026-09-12 已实现scripts/export_relation_subset.pydata/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 —— 不抛异常。数据问题要能被看见,但不该让整轮导出失败(少一条关系 ≠ 导出不出来)。

七、待确认(不是我能定的)

  1. 剧情抽取要不要覆盖全部章节? 现在只跑了 TO-ST-1(1 章 / 200 行 / 10 条边)。 200+ 章跑下来是 2000+ 条边,成本和时间要你定。
  2. .char 锚段那 ~200 条还做不做? 设计说"低权重补充,防漏", 但现在有了剧情边这条更好的线,它的边际价值变小了。
  3. 关系 RAG 的目标是什么? 设计说"专门回答'她最在意谁'"。 而单聊里干员对博士的关系是动态的(信赖),对其他干员的关系才是静态的 —— 注入的时机和价值要再想清楚,否则可能是"注入了但用不上"。