关系 RAG 设计
RELATION RAG
状态:🟡 规划 · 最后核对 2026-06-28
独立的关系向量检索系统——与 L3 记忆分离,专门回答"她最在意谁"。 2026-06-28 · 审计修订版
动机
当前关系网已存在但未充分使用:
relation_graph.json:302 节点 + 2163 边,单聊完全不调用RELATIONSHIP_MATRIX:15 对手工叙事关系,只在群聊注入- 单聊(最常用路径)的系统 prompt 中没有任何关系上下文
现有 graph + MATRIX 的值没有进入对话。 建独立 RAG 的目的:把这些静态数据变成可检索、可注入、上下文感知的关系知识。
为什么独立于 L3
| 维度 | L3 记忆 | 关系 RAG |
|---|---|---|
| 数据性质 | 动态——每轮可能新增 | 静态——更新周期以天计 |
| 检索意图 | "当前话题相关" | "她在意谁" + "谁与当前话题相关" |
| 权限 | operator_name + username 隔离 | 全局只读知识库——干员间的关系是客观事实,不按用户隔离 |
| 注入策略 | 按语义相似度 top-k | 锚定 top-3(不管话题)+ 语境 top-2(按语义) |
| 索引重建 | 增量追加 | 整库替换(改 MATRIX 或 graph 后一次重建,~730 条文档 <2 秒) |
两种检索意图不可互换。 分开让每种检索做自己该做的事。
明确声明:关系 RAG 是全局只读知识库。 它不包含用户私有数据。所有用户的同一位干员看到的是同一套关系背景——和所有用户看到同一部明日方舟 wiki 是同一性质。未来如需用户自定义关系,需新建用户级 collection。
架构
查询路径(并行,不互斥):
POST /api/chat
│
├─→ L3 RAG (已有) → top-5 话题相关事实
│
└─→ 关系 RAG (新增) → ┌─ "锚定关系" top-3:graph.py 内存查询,按边强度降序
│ (不进 ChromaDB——纯排序查询不需向量检索)
└─ "语境关系" top-2:两步检索
1. Trie 从对话提取出现的干员名
2. 若命中 → embedding 检索关系文档;若无命中 → 跳过
两条检索线
锚定关系(graph.py 内存查询,零延迟):
# app/systems/relation/graph.py 新增方法
def get_top_anchor_relations(operator_id: str, k: int = 3) -> list[Edge]:
"""按边强度降序返回 top-k 关系。不进 ChromaDB。O(degree log degree)"""
edges = self.get_edges(operator_id)
edges.sort(key=lambda e: e.strength, reverse=True)
return edges[:k]
语境关系(ChromaDB 语义检索):
def get_contextual_relations(operator_id: str, query_text: str, top_k: int = 2) -> list[dict]:
# 第1步:Trie 从 query_text 提取干员名
names = trie.extract_names(query_text)
if not names:
return [] # 对话没涉及任何人,跳过语境检索
# 第2步:干员名做 target_id 过滤 + embedding 排序
return collection.query(
query_embeddings=[get_embedding(query_text)],
where={"$and": [{"operator_id": operator_id}, {"target_id": {"$in": names}}]},
n_results=top_k,
)
为什么两步:关系文档是"A 对 B 的关系描述",直接拿"今天食堂不错"做 embedding query 语义空间不重叠——余弦相似度全低,召不回有用的东西。先确定对话提到了谁,再检索和那个人的关系。
ChromaDB 结构
collection: "relation_rag"
# 每条文档 = 一条关系的一侧视角
{
"doc_id": "lappland_to_texas_v1",
"document": "德克萨斯——麻烦。旧日同僚。但每次她经过走廊时尾巴会停半拍。",
"metadata": {
"operator_id": "lappland", # 谁持有这段记忆(权限键)
"target_id": "texas", # 关系指向谁
"rel_type": "complex", # friend/colleague/family/rival/complex/...
"strength": 0.92, # 图边强度
"source": "matrix", # matrix | graph_edge | char_anchor
"source_path": "", # 来源文件路径(char_anchor 时记录 .char 路径)
}
}
权限过滤:where={"operator_id": current_operator} ——每个干员只检索自己的视角。
current_operator 来自 chat.py 的 _resolve_operator(),已经过 manifest 校验,非客户端参数。
数据源与权重
| 来源 | 数量 | 文档形式 | 权重 |
|---|---|---|---|
| RELATIONSHIP_MATRIX | 15 对 → 30 条文档(双向视角) | 直接使用现有叙事文本 | 最高——手工精修 |
| graph 边(strength > 0.6) | ~500 条 | Claude Sonnet 将边类型转为 1-2 句自然语言描述 | 高——强模型生成,人工抽检 |
| .char 锚段提取 | ~200 条 | 从"核心经历"段提取涉及的人物名 + 关系 | 低——补充,防漏。记录 source_path 方便追溯 |
token 预算管理
注入到 prompt 时需要 token 估算和超限控制:
总预算:800 tokens(含关系前缀文本)
优先级:锚定 top-3 > 语境 top-2
超限时:
1. 先保留全部锚定关系("无论如何都在意的人")
2. 逐条削减语境关系
3. 仍超限则砍锚定最后一条
空结果处理:
- 锚定 0 条:不注入关系块
- 语境 0 条:只注入锚定,不加语境前缀
L3 去重
get_relation_context 返回前,接收 L3 召回结果,按 target_id 去重——L3 已提到的人不再出现在关系注入中。
与现有系统的关系
graph.py ——保留,角色扩展
get_relation(A, B):O(1) 精确查询——用于"她和他什么关系"这类确定性查询get_faction():同阵营列表——关系 RAG 不替代- 新增
get_top_anchor_relations(op_id, k):锚定关系查询——从内存读取,零延迟 - 建库脚本从 graph 读边、用 LLM 转为自然语言文档
边界定义:精确关系查询走 graph.py(确定性、O(1)),关系背景注入走关系 RAG(语义检索、上下文感知)。
RELATIONSHIP_MATRIX ——数据源,不是注入逻辑
- 现有的 15 对高质量叙事直接入库,权重最高
- 群聊和单聊统一切换到关系 RAG 作为数据源
- MATRIX 不再直接被群聊代码调用——
get_all_relationships_for()改为走关系 RAG - 未来新增手工关系先写入 MATRIX,再重建 RAG 索引
L3 ChromaDB ——不改
- 继续做话题相关的事实召回
- 关系 RAG 不抢它的 job
- 两者结果在注入前做 target_id 去重
.char 文件 —— 新增 ## 语录 段
任务 B(语音蒸馏)输出的代表性原话存储在 .char 新段中,供 prompt 拼装直接引用。这和关系 RAG 无关,但共享数据管线。
实施计划
Phase 1:建库脚本(2-3 天)
输入:relation_graph.json + RELATIONSHIP_MATRIX + *.char 文件
输出:ChromaDB collection "relation_rag" + graph 内存可用
步骤:
1. MATRIX 对 → 拆成单向文档(30 条)→ 人工确认 → 入库
2. graph 边(strength > 0.6)→ 批量 Claude Sonnet 转自然语言 → 入库
- 3 次重试,仍失败则跳过并记录
- 成本约 ¥30-50(一次性)
3. .char 锚段 → Trie 提取人名 → 生成简单关系描述 → 入库
- 记录 source_path 方便追溯
4. embedding(BGE-small-zh-v1.5,512d,与 L3 共用模型)
输出校验(入库前自动执行):
- 格式校验:document 非空、metadata 字段完整
- target_id 校验:target_id 存在于 graph 节点中
- 长度校验:document 在 10-200 字范围内
- doc_id 校验:符合 ^[a-zA-Z0-9_\-\.]+$
- 失败文档不入库,写入 distill_errors.jsonl
断点续传:
- 每个干员的处理状态写入 distill_progress.json
- 中断后从上次位置继续
Phase 2:检索接口(1 天)
# app/systems/relation/rag.py
def get_anchor_relations(operator_id: str, top_k: int = 3) -> list[dict]:
"""graph.py 内存查询,按 strength 降序。O(degree log degree),零延迟。"""
def get_contextual_relations(operator_id: str, query_text: str, top_k: int = 2) -> list[dict]:
"""Trie 提取干员名 → 有命中则 embedding 检索,无命中返回 []。"""
def get_relation_context(operator_id: str, query_text: str,
l3_recalled_targets: set = None) -> str:
"""合并锚定 + 语境,token 预算 800,L3 去重。返回 prompt 注入文本。空则返回 ''。"""
干员名提取:从 character_table.json 生成中文名列表,构建 Trie 做精确多模式匹配(Aho-Corasick)。比正则更准确、更快。
Phase 3:注入到 prompt(1-2 天)
chat.py 阶段 F:
追加 relation_context → 放在记忆块之后、氛围块之前
需要反复测试注入位置和格式对生成质量的影响
deep_chat.py ANCHORS 层:
追加静态关系背景——不进入 ARC/ECHO 动态层
群聊 group_chat.py:
get_all_relationships_for() 改为走关系 RAG
MATRIX 降级为纯数据源
Phase 4:定期更新(脚本,按需跑)
改 MATRIX 或 graph 后 → 运行 rebuild_relation_rag.py → 全量重建(~730 条,<2 秒)
暂不做的
- 关系文档的在线增量更新(聊一次天就改关系库太重了)
- 基于对话历史自动改写关系描述(那是 L3 的事,不是关系 RAG 的事)
- 用户级关系覆盖(博士眼里的关系 ≠ 干员眼里的关系——先从全局客观关系做起)
- 剧透级别控制(所有用户看到相同的关系信息——与明日方舟 wiki 同等透明)
- 锚定关系轮次去重(常聊干员可能重复注入,先上线看实际效果再决定是否需要)
未来考虑(不进入当前实施计划)
- embedding 模型版本管理:在 collection metadata 记录版本号
- ChromaDB 并发评估:当前日均用户个位数,无需提前优化
- 监控面板:召回率、注入 token 数、空结果率
数据管线:爬虫 → LLM 蒸馏 → .char + 关系入库
目标:从公开游戏数据自动生成 .char 初稿和关系文档,人工只需审核和调整口吻。 与关系 RAG 共用数据出口——蒸馏结果同时喂给 .char 和 ChromaDB。
数据源
源 1:ArknightsGameData(GitHub 仓库,结构化 JSON)——首选
仓库:github.com/Kengxxiao/ArknightsGameData
分支:仅 zh_CN(避免日服独占干员数据污染)
| 数据 | 文件路径 | 内容 |
|---|---|---|
| 干员档案 | character_table.json |
种族、身高、出身、感染状态、战斗经验、tag |
| 语音文本 | charword_table.json |
全部语音台词(任命助理/交谈/晋升/信赖/作战……),中日文 |
| 干员档案文本 | handbook_info_data.json |
档案资料 1-4(客观资料/临床诊断/战斗经验/晋升记录) |
| 悖论模拟 | char_meta_table.json |
干员个人故事文本 |
| 剧情文本 | story_review_table.json + 剧情文件 |
关卡对话全文、活动剧情 |
优点:结构化,不需要 HTML 解析。git pull 即可更新。
覆盖:全球所有已实装干员。
源 2:PRTS Wiki 关系网(人工一次性导出)——补充
PRTS 编辑组手工整理了人物关系页面(prts.wiki/w/关系网)。不写爬虫——HTML 解析每 3-6 个月会因模板变更而失效,维护成本远高于数据价值。
做法:人工导出一次 JSON 存仓库,后续通过 PR 提交增量补充。
源 3:干员 ID ↔ 中文名映射
从 character_table.json 生成映射表,供建库脚本和运行时 Trie 使用。Aceship/ArknightsGameResource 可用于交叉验证。
数据流
┌─────────────────────────────────────────────────────────┐
│ 数据层(离线) │
│ │
│ ArknightsGameData/zh_CN ──→ git pull ──→ 本地 JSON │
│ PRTS 关系网 ──→ 人工导出 ──→ JSON 存仓库 │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ LLM 蒸馏层(离线批处理) │
│ 每个干员一个 distillery job,状态记录到进度文件, │
│ 支持断点续传。每个 task 最多重试 3 次,仍失败则跳过。 │
│ │
│ 任务 A:档案 → 锚段 │
│ ┌──────────────────────────────────────────────┐ │
│ │ 输入:character_table + handbook_info │ │
│ │ 输出:种族/身高/出身/身体/核心经历 │ │
│ │ 难度:低。结构化数据 → 格式化输出。 │ │
│ │ 模型:DeepSeek V3 / Qwen(免费端点) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 任务 B:语音 → .char ## 语录 + 我段·性格 │
│ ┌──────────────────────────────────────────────┐ │
│ │ 输入:charword_table(该干员全部语音台词) │ │
│ │ 输出:5-8 条代表性原话 + 1-2 句性格自述 │ │
│ │ 难度:中。需要挑选最具辨识度的句子。 │ │
│ │ 关键:原话优先——few-shot 对 LLM 口吻模仿 │ │
│ │ 比抽象总结有效得多。 │ │
│ │ 存储:原话写入 .char 新增的 ## 语录 段 │ │
│ │ 模型:DeepSeek V3(免费端点) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 任务 C:语音 + 档案 → 我段·关于博士 │
│ ┌──────────────────────────────────────────────┐ │
│ │ 输入:不同信赖等级的语音 + 档案资料 │ │
│ │ 输出:"关于博士"段落 │ │
│ │ 难度:中。需要整合她跨信赖等级的态度变化。 │ │
│ │ 模型:DeepSeek V3(免费端点) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 任务 D:剧情 → 关系描述 │
│ ┌──────────────────────────────────────────────┐ │
│ │ 输入:该干员出现的所有剧情文本(story 文件) │ │
│ │ 输出:对每个互动干员,"observer 视角的关系描述" │ │
│ │ 难度:高。需要从对话中推断关系,而非陈述。 │ │
│ │ 模型:Claude Sonnet(成本 ~¥30-50,一次性) │ │
│ │ 输出格式:"{她} 对 {target}:{1-2句叙事描述}" │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 任务 E:语音 + 剧情 → 干员涉及的其他人名列表 │
│ ┌──────────────────────────────────────────────┐ │
│ │ 输入:全部语音 + 全部剧情文本 │ │
│ │ 输出:她明确提到/互动的干员 ID 列表 │ │
│ │ 难度:低。Trie 多模式匹配,不需要 LLM。 │ │
│ │ 用途:确认 graph 边是否遗漏。 │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 工程保障: │
│ - 断点续传:distill_progress.json 记录每个干员状态 │
│ - 重试机制:LLM 调用失败自动重试 3 次 │
│ - 输出校验:规则校验(格式/长度/ID 有效性)优先于 LLM 自评│
│ - 多语言:charword_table 中日文优先取中文,日文做补充 │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 数据出口 │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ .char 文件 │ │ ChromaDB relation_rag │ │
│ │ │ │ │ │
│ │ 锚段 ← 任务A │ │ 关系文档 ← 任务D │ │
│ │ 语录 ← 任务B │ │ metadata: operator_id, │ │
│ │ 我·性格 ← 任务B │ │ target_id, strength, │ │
│ │ 我·关于博士 ← 任务C│ │ source, source_path │ │
│ │ 物理标签 ← 任务A │ │ │ │
│ │ │ │ graph 边补充 ← 任务E │ │
│ │ [AUTO-DRAFT] │ │ │ │
│ │ 待人工审核 │ │ │ │
│ └──────────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
质量分层
| 级别 | 标记 | 含义 | 注入行为 |
|---|---|---|---|
| S | 无标记 | 人工精修过——现有 ~195 个已完成的 .char | 正常注入,权重最高 |
| A | [AUTO] |
LLM 生成 + 人工审核通过——口吻确认无误 | 正常注入 |
| B | [AUTO-DRAFT] |
LLM 生成,未人工审核——锚段可靠,语录/我段可能模板化 | 降权注入(只注锚段和物理标签,语录/我段跳过) |
| C | — | 未生成 | 不注入关系,仅 graph 边存在 |
升级路径:B → 人工看一眼,调口吻 → A。A → 正常精修流程 → S。
模型选择
| 任务 | 模型 | 原因 |
|---|---|---|
| 任务 A(锚段提取) | DeepSeek V3 免费端点 | 结构化输出,低难度 |
| 任务 B(语录+性格) | DeepSeek V3 免费端点 | 挑选原话不需要创造力,关键是 prompt 设计 |
| 任务 C(关于博士) | DeepSeek V3 免费端点 | 整合语音即可 |
| 任务 D(关系描述) | Claude Sonnet | 需要从对话推断关系——免费模型不达标。¥30-50 一次性 |
| 任务 E(人名提取) | 不需要 LLM | Trie 精确匹配 |
实施步骤
Step 1:数据准备(半天)
# 克隆游戏数据仓库(仅 zh_CN 分支)
git clone --depth 1 --branch zh_CN https://github.com/Kengxxiao/ArknightsGameData.git data/arknights_raw/
# 从 character_table.json 生成干员名 → ID 映射 + Trie
python scripts/build_operator_trie.py → data/operator_trie.pkl
# PRTS 关系网人工导出(一次性)
# 存为 data/prts_relations.json
Step 2:LLM 蒸馏脚本(2-3 天)
scripts/distill_char.py
--task A 只跑锚段提取
--task B 只跑语录+性格
--task C 只跑关于博士
--task D 只跑关系描述
--op {id} 指定干员(缺省=全部)
--retry 3 失败重试次数
--resume 从 distill_progress.json 断点续传
关键设计:
- 每个干员的处理状态持久化到
distill_progress.json - 输出校验层:格式/长度/ID 有效性 → 失败不入库,写入
distill_errors.jsonl - 任务 D 用 Claude Sonnet,其余用 DeepSeek 免费端点
- 已有 S 级 .char 的干员自动跳过
Step 3:关系入库(Step 2 的任务 D + E 输出)
scripts/build_relation_rag.py
输入:任务 D 输出 + RELATIONSHIP_MATRIX + graph 边(经任务 D 蒸馏的)
输出:ChromaDB collection "relation_rag"
校验:target_id 存在性、doc_id 合规性、文档长度
失败文档 → distill_errors.jsonl
Step 4:人工审核队列
输出文件:docs/char-auto-review-queue.md
按干员列出:
- .char 路径
- 各 task 输出状态(成功/跳过/失败)
- 关系文档质量抽检(建议抽检 10%,约 50 条)
不爬的
- 立绘/图片:前端已有 avatar 系统,用本地 PNG 或游戏 ID 拼接 CDN URL
- 技能/游戏数据:与角色扮演无关
- 微博/同人/二设:只做官方内容。关系描述的口吻来自原话,不来自社区解读
- PRTS HTML 爬取:人工导出一次 JSON,不写爬虫维护
- 实时在线爬取:全部离线批处理
版权注意
- ArknightsGameData 为公开的社区维护数据仓库,仅包含游戏客户端内提取的文本
- PRTS wiki 内容遵循 CC BY-NC-SA 协议
- 蒸馏后的 .char 和关系文档属于转换性使用——不是原文复制,是 LLM 总结
- 不公开发布原始爬取数据,只发布蒸馏结果