罗德岛通讯与技术部

关系 RAG 设计

RELATION RAG

└─→ 关系 RAG (新增) → ┌─ "锚定关系" top-3:graph.py 内存查询,按边强度降序

状态:🟡 规划 · 最后核对 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 总结
  • 不公开发布原始爬取数据,只发布蒸馏结果