把任意 TRPG 规则书与故事模组(PDF)转换成可检索、可问答的 RAG 知识库,为 GM 主持 / 规则问答 / 剧情推进 / LLM 集成提供支持。
- 规则书:陈述性知识(规则条目),支持多规则书独立索引、混合检索、规则问答。
- 模组:叙事性 + 指令性内容,按「场景树 + 多级披露」结构化,支持分阶段防剧透检索与 GM 准备标注。
两者共用底层检索引擎,提供统一接口(Python API / CLI / HTTP / MCP / Web)。
- 解析:书签优先;缺失时检测器链自动探测(L1 书签 → L2 编号模式 → L3 字号分档 → L4 兜底),并过滤噪声/目录/重复页
- 分块:按规则条目切分,建立父子文档关系(父=章节,子=小节),支撑上下文补全
- 混合检索:BM25 关键词 + FAISS 向量双路召回,RRF 融合,Reranker 精排;按查询类型(rule/judge/quote/narrative)动态调权;可选术语索引强信号
- 多规则书:每本独立向量库,自由切换/联合,检索带缓存
- 评测:Recall@k / MRR / 分类别统计,QueryCase 驱动(
benchmarks/test_set.json)
- 解析:检测器链(L1 书签 → L2 目录页 → L3 排版字号 → L4 LLM 语义兜底),不依赖特定标题格式,适配各种模组书写风格;同页多场景按 (page, y) 切分
- 结构化:场景树(scene/location/sequence)+ 多级披露(disclosure_levels);自动区分剧情阶段(线性推进,如 导入/第X场/地点N/结局)与背景阶段(资料性,如 角色/背景/NPC/附录)
- 分阶段防剧透:检索层硬约束——调用方传
visible_filter(玩家当前阶段 + 已满足条件),未解锁 chunk 在 BM25/FAISS 检索前被物理剔除;背景块始终可见;GM 不传 filter 则全量(主持人全知) - GM 标注:准备阶段逐个 chunk 决定是否暴露给玩家(expose_to_player → 视角)+ 设置多级披露解锁条件(conditions);支持 LLM 辅助标注;标注独立持久化,构建时应用
- 脚本工具:分块查阅(
inspect_module.py)、交互标注(annotate_module.py)、RAG 测试(test_module_rag.py,覆盖解析/防剧透/多级披露/GM全量/真实检索,benchmarks/module_test_*.json)
- 统一接口:Python API / CLI / HTTP REST / MCP Server / Web 前端(规则书与模组共用一套接口层)
- 隔离存储:规则书/模组索引(
faiss_index/rulebooksvsmodules)、许可声明(LICENSES/rulebooksvsmodules)各自独立
- 代码:本项目的 RAG 引擎(
core/、service/、interfaces/、app/等)为独立创作,采用 MIT 许可,见 LICENSE。 - 示例规则书:
data/rulebooks/下的规则书均为粉丝同人作品,采用 CC BY-NC-SA 4.0 许可,禁止商业用途:- 《明日方舟 TRPG 规则书 v0.36》(作者:Daniel 扭蛋),使用详见 LICENSES/rulebooks/ARKNIGHTS.md
- 《行于泰拉-明日方舟TRPG核心规则书 v0.3》(主创:泰拉旅社编辑组),使用详见 LICENSES/rulebooks/XINGYUTAILA.md
- 示例模组:
data/modules/下的模组为粉丝同人作品,许可见 LICENSES/modules/:- 《多索雷斯狂欢节pro》(作者:信仰の殉道),采用 CC BY-NC-ND 4.0(署名-非商业性使用-禁止演绎),禁止商业用途、禁止修改发布,详见 LICENSES/modules/DUOSUOLESI.md
- 《黑水溪》(Blackwater Creek,作者:Scott Dorward,译者:埃兰迪斯),依据原作者声明允许转载 / 制作 / 修改发布,禁止商业用途,详见 LICENSES/modules/HEISHUIXI.md
- 其他规则书/模组:您自行准备的内容(如 COC7、忍神等)版权归原作者所有,请勿上传到公开仓库。仅使用有明确开放许可的内容作为示例数据。
本项目为个人学习项目(非商业用途)。AI 辅助编码、模型调用与免责条款详见 AI-DECLARATION.md。
TRPG-RAG/
├── config.py # 配置(分块/嵌入/检索/Reranker/术语 + ContentType)
├── requirements.txt # 依赖
├── LICENSE # 代码开源许可证
├── AI-DECLARATION.md # AI 辅助编码声明
├── LICENSES/ # 示例数据许可声明
│ ├── rulebooks/ # 规则书许可(ARKNIGHTS/XINGYUTAILA)
│ └── modules/ # 模组许可(DUOSUOLESI/HEISHUIXI)
├── core/ # 核心逻辑
│ ├── pdf_loader.py # 规则书解析(书签/自动探测)
│ ├── chunker.py # 规则书分块(父子文档)
│ ├── indexer.py # FAISS + BM25 索引
│ ├── retriever.py # 混合检索 + RRF + 可见性过滤
│ ├── reranker.py # Reranker 精排
│ ├── terms.py # 术语索引
│ ├── evaluate.py # 评测工具
│ ├── module_model.py # 模组数据模型(场景树/多级披露/可见性过滤)
│ ├── module_parser.py # 模组解析(检测器链:书签/目录/排版/LLM)
│ ├── module_chunker.py# 模组分块入库
│ └── module_annotator.py # 模组 GM 标注接口
├── service/ # 服务层
│ ├── index_manager.py # 规则书索引管理
│ ├── retrieve_service.py # 规则书检索服务
│ └── module_service.py # 模组统一业务层(构建/检索/标注)
├── interfaces/ # 接口(CLI/HTTP/MCP)
├── app/ # Streamlit Web 前端demo
├── benchmarks/ # 测试集(test_set + module_test_*)
├── script/ # 测试/查阅/标注脚本
├── data/ # 素材根目录(规则书/模组)
│ ├── rulebooks/ # 规则书 PDF(示例为 CC 许可的明日方舟TRPG/行于泰拉)
│ ├── modules/ # 模组(示例为多索雷斯狂欢节pro/黑水溪)
│ └── _annotations/ # GM 标注结果持久化
└── faiss_index/ # 索引根目录(生成的向量库)
├── rulebooks/ # 规则书索引
└── modules/ # 模组索引(多索雷斯/黑水溪)
# 1. 克隆仓库
git clone https://github.com/ghostdoglzd/TRPG-RAG.git
cd TRPG-RAG
# 2. 创建虚拟环境
uv venv .venv --python 3.11
uv pip install -r requirements.txt
# 3. 配置 API key(复制 .env.example 为 .env 并填入)
cp .env.example .env.env 需要以下 key(参考 .env.example):
EMBEDDING_API_KEY:用于 embedding(必需)LLM_API_KEY:用于 LLM 生成回答 / GM 裁决 / LLM 辅助标注(必需)EMBEDDING_BASE_URL/EMBEDDING_MODEL:可选,自定义 embedding 服务地址与模型LLM_BASE_URL/LLM_MODEL:可选,自定义 LLM 服务地址与模型RERANKER_*:可选,启用 Reranker 精排时配置
注:本项目开发、测试使用的API如下
模型类型 所用API embedding bge-m3(硅基流动API) LLM deepseek reranker bge-reranker-v2-m3(硅基流动API)
若无 LLM_API_KEY,规则书检索(BM25/向量)仍可工作,但 LLM 生成回答、GM 裁决、模组 LLM 标注等依赖 LLM 的功能不可用。
# 1. 构建规则书索引(示例使用随仓库提供的明日方舟TRPG)
python -m interfaces.cli build data/rulebooks/明日方舟TRPG规则书v0.36.pdf --book 明日方舟TRPG
# 2. 检索
python -m interfaces.cli retrieve "如何创建干员" --book 明日方舟TRPG
# 3.(可选)启动 Web 前端或 HTTP/MCP 服务,见下文python -m interfaces.cli <命令> [参数]
list # 列出所有已建索引的规则书
build <pdf路径> --book <书名> # 构建规则书索引(同步)
delete --book <书名> # 删除某规则书索引
info --book <书名> # 查看某规则书索引信息
retrieve "<查询>" --book <书名> [--topk 5] # 混合检索
tasks # 查看后台构建任务# 构建索引
from service.index_manager import IndexManager
from config import DEFAULT_CONFIG
mgr = IndexManager(DEFAULT_CONFIG)
mgr.build_index_sync("data/rulebooks/明日方舟TRPG规则书v0.36.pdf", "明日方舟TRPG")
# 检索
from service.retrieve_service import RetrieveService
svc = RetrieveService(DEFAULT_CONFIG)
results = svc.retrieve_dicts("如何创建干员", book_name="明日方舟TRPG", top_k=5)
# 每条结果含 text / page / chapter / section / score# Windows
run_app.bat
# 或(跨平台)
.venv/Scripts/python.exe -m streamlit run app/Home.py
# 或
streamlit run app/Home.py然后,浏览器访问 http://localhost:8501
Web 页面包含:
- 📚 总览:规则书 + 模组概览
- 📤 上传构建:规则书上传构建
- 🔍 检索问答:规则书检索问答
- 🎭 模组 RAG:模组分阶段防剧透检索 + GM 标注 + 场景结构
- 📊 评测:规则库检索评测
python -m interfaces.api # 127.0.0.1:8000| 方法 | 路径 | 说明 | 请求参数 | 响应 |
|---|---|---|---|---|
| GET | /indexes |
列出所有已建索引 | - | {"books": [...]} |
| GET | /indexes/{book_name} |
查询单本规则书索引信息 | 路径参数 book_name |
索引信息对象 |
| POST | /indexes/build |
构建索引(异步) | Body: {"file_path": "...", "book_name": "..."} |
{"task_id": "...", "status": "pending"} |
| GET | /indexes/tasks/{task_id} |
查询构建任务进度 | 路径参数 task_id |
{"status": "...", "progress": ...} |
| DELETE | /indexes/{book_name} |
删除索引 | 路径参数 book_name |
{"deleted": "..."} |
| GET | /retrieve |
混合检索 | Query: query(必填)、book、top_k(默认5)、mode(默认 rule) |
{"query": "...", "book": "...", "results": [...]} |
| GET | /health |
健康检查 | - | {"status": "ok", "books": [...]} |
mode取值:rule/judge/quote/narrative。
# 规则书检索
curl "http://127.0.0.1:8000/retrieve?query=如何创建干员&book=明日方舟TRPG&top_k=5"
# 构建规则书索引(异步,返回 task_id 后轮询任务进度)
curl -X POST http://127.0.0.1:8000/indexes/build \
-H "Content-Type: application/json" \
-d '{"file_path": "data/rulebooks/明日方舟TRPG规则书v0.36.pdf", "book_name": "明日方舟TRPG"}'
curl "http://127.0.0.1:8000/indexes/tasks/<task_id>"python -m interfaces.mcp_server # stdio 传输启动后会在后台预热:预加载所有已建索引的检索器 + 预热 embedding(在线 API, 首次调用约 6~7s)。预热不阻塞握手,首次检索无需再承担冷启动成本。 embedding 调用带超时(
config.embed.timeout,默认 15s),网络慢时不会无限阻塞。
| 工具名 | 说明 | 参数 |
|---|---|---|
rule_list_indexes |
列出所有已建索引的规则书 | - |
rule_build_index |
上传规则书构建索引(返回 task_id) | file_path、book_name |
rule_build_status |
查询索引构建任务进度 | task_id |
rule_book_info |
查询某本规则书的信息 | book_name |
rule_delete_index |
删除索引 | book_name |
rule_retrieve |
混合检索(GM 主持 / 规则问答核心工具) | query、book(可选)、top_k(默认5)、mode(默认 rule) |
rule_quote |
精确查规则原文(含出处) | query、book(可选)、top_k(默认3) |
在客户端配置中注册本 MCP 服务:
{
"mcpServers": {
"trpg-rag": {
"command": "d:/TRPG-RAG/.venv/Scripts/python.exe",
"args": ["-m", "interfaces.mcp_server"],
"cwd": "d:/TRPG-RAG"
}
}
}接入后,LLM 客户端即可调用 rule_*(规则书检索)与 module_*(模组分阶段防剧透检索 / GM 标注)工具。
模组与规则书的信息本质不同(叙事性 + 指令性 + 顺序性),因此模组 RAG 有独立的数据模型、解析链与使用方式。本段结构对齐「规则书使用」,覆盖 CLI / Python API / Web / HTTP / MCP / 评测。
- 剧情阶段(plot_stages):线性推进(如 导入→第一场→...→结局),用于分阶段防剧透。解析器自动识别含顺序信号(第X场/地点N/导入/结局)的标题。
- 背景阶段(background_stages):资料性章节(角色/背景/设定/NPC/附录),游戏开始前注入,始终可见、不参与阶段过滤。
- 分阶段防剧透:模组信息是时间线性的,玩家不能提前看到后续剧情。通过检索层硬约束实现——调用方传入
visible_filter(玩家当前阶段 + 已满足条件),未解锁 chunk 在 BM25/FAISS 检索前就被物理剔除;GM 不传 filter 则全量(主持人全知)。本项目不维护运行时游戏状态,只按给定 filter 过滤。 - GM 标注:准备阶段决定每个 chunk 是否可暴露给玩家 + 设置解锁条件。
# 构建模组索引
python -c "
from config import DEFAULT_CONFIG
from service.module_service import ModuleService
svc = ModuleService(DEFAULT_CONFIG)
svc.build_index('data/modules/黑水溪/[COC模组翻译]黑水溪-Blackwater Creek.pdf', '黑水溪')
"
# 分块查阅(检查分块是否符合预期)
python script/inspect_module.py list # 场景树概览
python script/inspect_module.py scene S-01 # 查看某场景
python script/inspect_module.py grep 关键词 # 全文搜索
# GM 标注(准备阶段决定每个 chunk 是否可暴露给玩家)
python script/annotate_module.py list # 列出待标注块
python script/annotate_module.py # 交互式逐块审阅决策
# 模组 RAG 测试(A 解析 / B 防剧透 / C 多级披露 / D GM全量 / E 真实检索)
python script/test_module_rag.py # 离线测试
python script/test_module_rag.py build # 建索引 + 完整测试测试数据在 benchmarks/module_test_*.json(纯查询数据集),脚本顶部配置 PDF_PATH/BOOK_NAME/TEST_SET 切换模组。
from service.module_service import ModuleService
from config import DEFAULT_CONFIG
svc = ModuleService(DEFAULT_CONFIG)
# 构建索引(可选 annotation_path 应用 GM 标注)
svc.build_index("data/modules/黑水溪/[COC模组翻译]黑水溪-Blackwater Creek.pdf",
"黑水溪", annotation_path="data/_annotations/黑水溪.json")
# 查看场景树与剧情/背景阶段
structure = svc.get_structure("黑水溪")
# -> {"plot_stages": [...], "background_stages": [...], "scenes": [...]}
# GM 全量检索(主持人全知,含未来剧情)
res = svc.retrieve_gm("贾维农场有什么线索", "黑水溪", top_k=5)
# 玩家分阶段检索(玩家当前到「地点2」,未解锁内容被物理过滤)
res = svc.retrieve("贾维农场有什么线索", "黑水溪", top_k=5,
stage="地点2", stage_order=structure["plot_stages"])准备阶段,GM(或 LLM 扮演的 GM)可审阅每个信息块,决定其曝光范围与解锁条件:
# 列出待标注 chunk(含视角/条件/文本)
chunks = svc.list_chunks("黑水溪")
# 决策:某块不可暴露给玩家(设为主持人专用)
svc.decide_chunk("黑水溪", chunks["chunks"][0]["id"], expose_to_player=False)
# 决策:某块需满足「侦查成功」条件才可披露
svc.decide_chunk("黑水溪", chunks["chunks"][1]["id"],
conditions=[{"desc": "侦查成功", "type": "check", "skill": "侦查"}])
# 构建索引:标注文件默认自动查找 data/_annotations/{book_name}.json,
# 无需显式传 annotation_path(未找到时返回 annotation.warn 告警,防剧透未启用)
svc.build_index(pdf_path, book_name)
# -> result.annotation = {loaded, applied, total, warn, annotation_path}
# 方式B(推荐):已构建索引后,标注 → 就地更新(不重新向量化,即时生效)
# 原理:向量只由 chunk 的 text 生成,与 meta 无关;过滤只读 meta。
# 因此标注(audience/conditions)改动无需重建/重 embedding,直接 patch 即可。
svc.apply_annotations("黑水溪")
# -> {"status": "ok", "applied": n, "total": m, ...}
# 标注文件默认取 data/_annotations/{book_name}.json(decide_chunk 落盘位置);
# 也可显式指定:svc.apply_annotations("黑水溪", annotation_path="...")
# 把某 chunk 的自动抽取条件采纳为正式生效条件(3.1:让条件披露机制跑通)
svc.adopt_auto_conditions("黑水溪", "SCENES-01_C01_L1")
# 分阶段检索:stage_order 自动注入,无需调用方传——
# 传 stage 时自动读取 plot_stages,「截至当前阶段」语义自动生效
res = svc.retrieve("贾维农场有什么线索", "黑水溪", top_k=5, stage="地点2")
# -> res.stage_order 为实际使用的剧情阶段顺序「🎭 模组 RAG」页面提供:
- 分阶段检索:选择检索视角(GM 全量 / 玩家分阶段)→ 玩家视角下再选「玩家当前到达的剧情阶段」,未解锁阶段内容会被物理过滤
- GM 标注:审阅每个信息块(含完整内容)并决定是否暴露给玩家、设置解锁条件
- 场景结构:场景树 / 剧情阶段 / 背景阶段可视化
| 方法 | 路径 | 说明 | 请求参数 | 响应 |
|---|---|---|---|---|
| GET | /modules/indexes |
列出所有已建模组索引 | - | {"modules": [...]} |
| GET | /modules/indexes/{book_name} |
查询单模组索引信息 | 路径参数 book_name |
索引信息对象 |
| GET | /modules/indexes/{book_name}/structure |
获取模组场景树与剧情阶段 | 路径参数 book_name |
{"plot_stages": [...], "scenes": [...]} |
| POST | /modules/indexes/build |
构建模组索引(同步,可带标注) | Body: {"file_path", "book_name", "annotation_path"?} |
{"status": "done"/"failed", "result": {...}} |
| DELETE | /modules/indexes/{book_name} |
删除模组索引 | 路径参数 book_name |
{"deleted": "..."} |
| GET | /modules/retrieve |
模组分阶段检索(防剧透) | Query: query、book、stage(玩家当前阶段)、scene_id、audience、met_conditions、top_k |
{"player_limited": bool, "results": [...]} |
| GET | /modules/retrieve_gm |
GM 全量检索(主持人全知) | Query: query、book、top_k |
{"player_limited": false, "results": [...]} |
| GET | /modules/annotate/chunks |
列出待标注 chunk(暴露视角/条件) | Query: book、scene_id? |
{"chunks": [...]} |
| POST | /modules/annotate/decide |
GM 决策 chunk 曝光范围/条件 | Body: {"book_name", "chunk_id", "expose_to_player", "audience"?, "conditions"?} |
决策结果 |
| POST | /modules/annotate/apply |
把标注就地更新到已构建索引(不重新向量化) | Body: {"book_name", "annotation_path"?} |
{"status": "ok", "applied": n, "total": m} |
| POST | /modules/annotate/adopt |
把某 chunk 的自动条件采纳为正式生效条件 | Query: book、chunk_id |
{"chunk_id", "adopted": true, "conditions": [...]} |
# 模组 GM 全量检索(主持人全知)
curl "http://127.0.0.1:8000/modules/retrieve_gm?query=贾维农场有什么线索&book=黑水溪&top_k=5"
# 模组分阶段检索(玩家到「地点2」,未解锁内容被过滤)
curl "http://127.0.0.1:8000/modules/retrieve?query=贾维农场有什么线索&book=黑水溪&stage=地点2&stage_order=引入调查员,地点1,地点2,地点3,地点4,地点5,地点6,地点7,地点8,地点9,结局"
# 列出模组待标注 chunk
curl "http://127.0.0.1:8000/modules/annotate/chunks?book=黑水溪"
# GM 决策某 chunk 是否可暴露给玩家
curl -X POST http://127.0.0.1:8000/modules/annotate/decide \
-H "Content-Type: application/json" \
-d '{"book_name": "黑水溪", "chunk_id": "SCENES-04_C02", "expose_to_player": false}'模组检索的核心:传
stage表示「玩家当前只推进到该阶段」,未解锁阶段的块被物理过滤(防剧透);不传stage则为 GM 全量(主持人全知)。
同一 MCP 服务(python -m interfaces.mcp_server)暴露模组工具:
| 工具名 | 说明 | 参数 |
|---|---|---|
module_list_indexes |
列出所有已建模组索引 | - |
module_build_index |
构建模组索引(同步,可带标注) | file_path、book_name、annotation_path? |
module_delete_index |
删除模组索引 | book_name |
module_structure |
获取模组场景树与剧情阶段 | book_name |
module_retrieve |
分阶段检索(防剧透) | query、book、stage?、scene_id?、audience?、met_conditions?、top_k |
module_retrieve_gm |
GM 全量检索(主持人全知) | query、book、top_k |
module_list_chunks |
列出待标注 chunk(暴露视角/条件) | book、scene_id? |
module_decide_chunk |
GM 决策 chunk 曝光范围/条件 | book_name、chunk_id、expose_to_player、audience?、conditions_json? |
模组 RAG 测试程序验证「解析 / 防剧透 / 多级披露 / GM 全量 / 真实检索」,输出 PASS/FAIL 报告:
python script/test_module_rag.py # 离线测试(A-D)
python script/test_module_rag.py build # 建索引 + 完整测试(含 E 真实检索)测试数据在 benchmarks/module_test_*.json,脚本顶部配置 PDF_PATH/BOOK_NAME/PLOT_STAGES/TEST_SET 切换模组。
评测工具基于 core/evaluate(提供 Recall@k / MRR / 分类别统计)。script/evaluate.py 读取 benchmarks/test_set.json 测试数据,评测指定规则书的检索效果。
# 评测已建索引的规则书
python script/evaluate.py
# 若向量库不存在,先构建再评测
python script/evaluate.py build适配其他规则书:修改
script/evaluate.py顶部的PDF_PATH/BOOK_NAME/TEST_SET常量,并编辑benchmarks/test_set.json中的查询和期望命中值。
实测效果(《明日方舟TRPG》,benchmarks/test_set.json 84 条查询,Recall@5):
| 规则书 | 测试集 | Recall@5 | MRR |
|---|---|---|---|
| 明日方舟TRPG | 84 条(6 个类别) | 100% | 0.979 |
分类别命中率:创建 10/10、能力 10/10、判定 6/6、战斗 41/41、源石 8/8、装备 9/9(均 100%)。
测试数据由随仓库提供的《明日方舟 TRPG》(CC BY-NC-SA 4.0 许可)构建并评测,可直接复现。
PyMuPDF · LangChain · FAISS · rank-bm25 · 硅基流动(bge-m3) · DeepSeek · FastAPI · MCP SDK · Streamlit · pydantic
Q1:没有 LLM_API_KEY,能用吗? 可以。规则书的 BM25/向量检索、索引构建、评测不依赖 LLM。只有「LLM 生成回答」「GM 裁决」「模组 LLM 辅助标注」需要 LLM。
Q2:如何切换要测试/构建的模组?
- 测试:修改
script/test_module_rag.py顶部PDF_PATH/BOOK_NAME/PLOT_STAGES/TEST_SET,或改用benchmarks/module_test_*.json数据集 - 构建:
svc.build_index(pdf_path, book_name)或 HTTP/modules/indexes/build
Q3:模组检索时如何做到不剧透?
检索时传入 stage(玩家当前到达的剧情阶段)即可。VisibilityFilter 会在 BM25/FAISS 检索前把未解锁阶段的 chunk 物理剔除,不会进入 LLM 上下文。GM 不传 stage 则全量可见(主持人全知)。
Q4:模组的「背景」和「剧情阶段」如何区分? 解析器自动识别:含顺序信号(第X场/地点N/导入/结局)的为剧情阶段(线性、防剧透);资料性章节(角色/背景/设定/NPC/附录)为背景阶段,游戏开始前注入、始终可见。
Q5:分块结果不符合预期怎么办?
用 script/inspect_module.py 查阅分块详情(场景树/单个场景/全文搜索/带条件块),定位问题后可调整 module_parser.py 的分块逻辑。