open-tabletop-gm 静态分析:模块设计与工作原理

对象:Bobby-Gray/open-tabletop-gm · 方法:纯静态分析

1. 总体架构

open-tabletop-gm 是一个「LLM 无关」的 TTRPG GM 框架:核心是一个提示词技能包(Markdown 驱动)加一套纯 Python 工具链。它的关键设计是把「叙述与判断」留给 LLM,把「一切机械计算」交给 Python,从而让小模型也能跑长线战役(README.md:7-25)。

┌────────────────────────────────────────────────────────────────┐
│  宿主 LLM 运行时(OpenCode / LM Studio / 任意 OpenAI 兼容端点)      │
│  README.md:7, 60-118;docs/LLM-GUIDE.md                         │
└──────────────────────────────┬─────────────────────────────────┘
                               │ 读指令 / 调 bash 工具 / 读写文件
┌──────────────────────────────▼─────────────────────────────────┐
│  SKILL 层(提示词,纯 Markdown,LLM 无关)                          │
│  SKILL.md         GM 人格与技艺(会话加载时读入,非启动常驻)          │
│  SKILL-branches.md 分支路由器(每个命令→步骤序列,常驻 ~2300 token)  │
│  SKILL-commands.md 命令签名参考;SKILL-scripts.md 脚本语法(已废弃)    │
└──────────────────────────────┬─────────────────────────────────┘
                               │ 命令 → 调用脚本
┌──────────────────────────────▼─────────────────────────────────┐
│  Python 工具链(确定性,零 LLM 依赖)                              │
│  scripts/    dice·combat·tracker·calendar·campaign_search·       │
│              paths·gm_graph·graph_extract_deterministic·         │
│              import_campaign·name_registry·migrate_system_version│
│  systems/<sys>/  系统模块:system.md + 可选脚本 + 数据               │
│  display/    Flask 电影化展示伴随程序(可选)                        │
└──────────────────────────────┬─────────────────────────────────┘
                               │ 读写
┌──────────────────────────────▼─────────────────────────────────┐
│  持久化(纯文本 Markdown + JSON,游戏无关)                          │
│  <campaigns-dir>/<name>/  state.md·world.md·npcs.md·npcs-full.md·│
│      session-log.md·session-log-archive.md·characters/*.md·      │
│      tracker.json·calendar.json·graph.json·session_tail.json     │
│  <root>/  .name_registry.json·characters/(全局角色名册)           │
└────────────────────────────────────────────────────────────────┘

架构分两层(README.md:29-46、SYSTEM-PORTING.md:7-16):SKILL.md 永不随游戏变化(GM 技艺),systems//system.md 随游戏变化(具体规则)。二者在 /gm load 时一起读入。

2. 核心工作流

链路一:会话加载 /gm load(最重要的链路)

SKILL-branches.md:7-106 定义,七步顺序执行,是「路由架构」的典型体现:

  1. 检查 display 状态SKILL-branches.md:11-15):读 display/app.pid 判断展示程序是否在跑。
  2. 同步战役 + 重放上一会话尾巴:17-34):send.py --set-campaign 注册战役,读 session_tail.json 逐条回放。
  3. 系统版本迁移检查:36-52):跑 migrate_system_version.py --check(exit 0/1/2),旧战役提示补盖 **System Version:**
  4. 读三文件:54-63):state.mdworld.mdnpcs.md,并检查 roll_mode、读 ## Pinned Facts
  5. 拉取场景子图:65-102):gm_graph.py scene-context --place ... --present ... 用 BFS 取 2 跳子图;未初始化则强制走 graph init。
  6. 输出开场叙述:104):纯文本,不调任何命令。
  7. 进入 GM 模式:106):此后玩家输入不需 /gm 前缀。

对应 SKILL.md:373-380 给出高层步骤。这一链路的目的是用最小 tool-call 深度完成加载——README.md:428 明确 70B 以下模型在 4-5 次连续 tool-call 后会漂移。

链路二:战斗回合(每回合确定性判定)

SKILL.md:287-297 给出「每回合战斗序列」,SKILL-branches.md:156-178 定义状态机(COMBAT 开始/回合/结束):

a. send.py --player          ← 玩家动作
b. combat.py / dice.py 掷骰  ← 所有判定
c. send.py --dice            ← 带上下文回传结果
d. tracker.py effect tick    ← 扣减计时效果,打印过期告警
e. 写完整叙述
f. send.py [--stat-*]        ← 叙述 + 全部状态变更(HP/条件/资源)
g. push_stats.py --turn-current ← 推进回合指针

combat.py init 掷先攻并输出 STATE_JSON 存入 state.md → ## Active Combat(combat.py:126-137、SKILL-branches.md:160)。判定本身由 dice.py(d20+mod、优劣势、暴击/大失败标记,dice.py:64-112)与 combat.py attack(命中判定,combat.py:70-97)完成,全程不经过 LLM。

3. 模块逐一分析

3.1 SKILL.md(265 行)— GM 核心人格

职责 关键内容(文件:行号)
GM 人格 「有氛围感、描述性、yes-and 取向」的 GM(SKILL.md:1-6)
12 条 GM 标准 即兴不写剧本、倾听校准、后果可见、生动高效、NPC 难忘、控节奏、公平一致、热情、读玩家、结构化情境、世界自行运转、奖励大胆(:9-76)
Table Dials difficulty/spotlight/pacing 三档可调(:78-84)
Script-First 规则 任何机械计算先查脚本,再做(:118-127)
骰子约定 roll_mode players/auto + 每角色覆盖(:217-231)
Display 同步 每个响应的 send.py 调用顺序与 stat 标志表(:235-298)
启动命令 /gm new/gm load 的完整流程(:355-380)

工作原理:这是写给 LLM 的行为约束,本身无代码逻辑;它对「何时读哪个脚本、何时停止、何时读哪一节文件」做了极细的编排,把 GM 的隐性知识显式化成可执行的提示词。

3.2 SKILL-branches.md(293 行)— 分支路由器

常驻上下文(~2,300 token,README.md:97,428)。把每个命令映射到「先读哪个脚本文件 → 执行哪个终态动作」的线性步骤。覆盖 /gm load/gm display/gm path/gm update、ACTIVE 叙述回合、/gm combat/gm rest/gm roll/gm character new/gm save/gm end/gm list、过去事件查找。这是项目对抗「小模型工具调用漂移」的核心手段(docs/LLM-GUIDE.md:36-42)。

3.3 SKILL-commands.md(261 行)— 命令参考

命令签名表 + 详细流程:/gm new 世界生成(:46-74)、/gm save 会话保存与归档(:78-99)、/gm pin(:102-112)、/gm import(:115-159)、/gm arc(:162-200)、/gm graph(:204-261)。

3.4 SYSTEM-PORTING.md(194 行)+ systems/ — 系统移植层

职责 关键内容(文件:行号)
两层架构 SKILL.md(不变)vs system.md(随游戏变)(SYSTEM-PORTING.md:7-16)
无需改动部分 dice/combat/tracker/calendar/campaign 文件/display/12 原则/arc/import/Live State Flags(:19-34)
每系统需配置 骰子判定、属性、主资源、HP、条件、休息、失能/死亡(:38-123)
移植步骤 复制 TEMPLATE.md → 填 system.md →(可选)改 tracker 条件色 → 建战役 → 迭代(:141-165)
兼容预期 d20 最高、骰池中高、百分位中、无骰低(:169-179)

systems/TEMPLATE.md(244 行)是脚手架;systems/brp/system.md(254 行)是纯 system.md 即够用的最小移植范例(BRP 百分位系统,无系统脚本,见其 :254)。5e 是完整参考实现。核心机制:system.md 声明 ## System Versions(如 2014/2024),paths.campaign_system_version()/campaign_system() 从 state.md 头部正则解析 **System Version:**/**System Module:**(paths.py:98-151),数据文件按版本落到 systems/<sys>/data/(paths.py:154-168)。

3.5 scripts/ — 通用确定性工具链

脚本 职责 关键函数(文件:行号)
dice.py XdY+Z 掷骰,优劣势,kh/kl,暴击/大失败 parse_notation:30-51、run:64-112
combat.py 先攻排序、攻击判定(命中/暴击/伤害翻倍) initiative_order:45-53、resolve_attack:70-97
tracker.py 条件/专注/计时效果/死亡豁免,持久化 tracker.json CONDITION_COLOURS:60-76、cmd_effect:189-295、cmd_saves:360-394
calendar.py 战役内历法(自定义月/日/月长),时间推进、休息 _advance_hours:136-159、cmd_rest:233-248
campaign_search.py 跨战役文件关键词 AND 检索 search_file:49-89
paths.py 路径解析 + 系统版本/模块解析(GM_CAMPAIGN_ROOT) find_campaign:48-81、campaign_system:128-151
gm_graph.py 带类型边的战役关系图(graph.json) _edge_active_at:173-189、_expand(BFS):457-483、_emit_subgraph:486-526
graph_extract_deterministic.py 无 LLM 的关系抽取(动词表模式匹配) build_alias_index:132-182、extract_proposals:296-418
import_campaign.py 从 PDF/DOCX/MD/TXT 抽取文本 extract_pdf:28-48、extract:71-88
name_registry.py 跨战役角色名去重注册表 rebuild:184-222、check:275-316
migrate_system_version.py 旧战役补 **System Version:** 字段(幂等+备份) cmd_check:102-117、cmd_migrate:120-173
npc_rename.py 跨所有战役文件安全改名(含 graph 节点/边保留) 未逐行读,见 SKILL-commands.md:36

tracker.pycalendar.py 都通过 subprocess 把状态推给 display,且display 未运行时静默失败(tracker.py:162-184、calendar.py:162-172)——这是「展示层可插拔」的体现。

3.6 systems/dnd5e/ — 5e 参考实现(规则数据组织)

文件 职责 关键内容(文件:行号)
system.md 5e 规则上下文(版本/骰子/属性/角色/XP/死亡豁免/条件/灵感) 版本:7-23、SRD 查询:145-164、大胆奖励硬触发(自然 20 必给灵感):167-170
lookup.py 查询 dnd5e_srd.json(1453 条:法术/装备/魔法物品/条件/怪物/职业特性) CATEGORY_MAP:35-53、_load:63-100、lookup:310-339、suggest(纠错提示):399-451
character.py 角色属性计算(熟练加值/生命骰/豁免/技能/XP) PROF_BONUS:25-27、HIT_DICE:38-42、SAVE_PROFS:45-58、do_calc:104-149
ability-scores.py 属性生成(4d6kh3/购点校验 27 点) POINT_BUY_COST:24、do_pointbuy_check:75-102
xp.py 完整 5e 遭遇 XP 表(难度阈值/CR→XP/数量倍率/升级) XP_THRESHOLDS:38-59、CR_XP:62-72、cmd_award:253-309
build_srd.py 从 5e-bits/5e-database + FoundryVTT 拉取并构建 SRD 数据集 来源:5-14、BITS_FILES:43-49、HTML 清洗 _strip_html:76-120
sync_srd.py/build_supplemental.py 数据集同步/非 SRD 补充(wikidot 缓存) 未逐行读,见 SYSTEM-PORTING.md:135

5e 规则数据以代码内硬编码表 + 外部 JSON 数据集两种方式组织:表格型规则(熟练加值、XP、生命骰、CR)直接写在 character.py/xp.py 字典里;开放授权内容(SRD)经 build_srd.py 打包成 dnd5e_srd.json,由 lookup.py 提供模糊匹配与格式化输出。

3.7 templates/ — 战役数据模型(文件模板)

模板 定义的数据结构(文件:行号)
state.md 头部(创建/会话数/系统模块/系统版本):2、## Pinned Facts:10-12、## Live State Flags(抗压缩关键值):37-47、## Campaign Arc(dynamic/structured YAML):49-146、## Session Flags:152-154、## GM Style Notes:156-158
world.md 基调:4-9、世界基础(地理/魔法/神系/历法):13-40、Three Truths:47-50、威胁升级弧(5 阶段):71-82、谜团线索链:95-101、阵营:105-129、冒险节点:133-153、任务种子:157-161
npcs.md / character-sheet.md / session-log.md NPC 索引、角色卡字段、会话日志骨架

3.8 display/ — 电影化展示伴随程序(可选 Flask)

gm-display-app.py(2609 行)是核心:SSE 流式推送到浏览器,端点见其 :7-21(/chunk/stats/stream/player-input/*/srd-lookup)。场景检测按关键词切背景(README.md:304-322)、动态天空画布按 world_time 渲染(README.md:324-340)、SFX 需 numpy 否则静默降级(README.md:350)。它完全独立于 LLM,未运行时所有脚本静默失败(README.md:253)。子模块拆解:

子模块 职责 关键点(文件:行号)
send.py 发送叙述/骰子/NPC 对话/玩家动作 + stat 标志,段落分块 stat 标志表 send.py:42-55、CHUNK_LIMIT:78
push_stats.py 推侧栏统计(HP/资源/条件/先攻/任务/阵营/世界时间),按名字合并 用法 doc:1-69
check_input.py 每回合开始非阻塞排空玩家输入队列 HTTP drain + 文件回退:86-107、叙述长度/掷骰指令:47-70
wrapper.py (遗留)PTY 包裹 agent,安全注入玩家输入 安全模型(字符白名单/元字符剥离/审计):27-37
dm_help.py / audio.py / tts.py 情境化 GM 提示 / Web Audio 合成 SFX / 旁白 TTS 未逐行读
setup_tls.py / verify_tail.sh 自签证书 / 会话尾巴健康检查 未逐行读

玩家输入走「stage→ready→skip」队列(gm-display-app.py:16-19),设备首次出现需 GM 批准(README.md:294-302);会话缓冲把最近 60 块叙述落盘到 text_log.json,断线重连自动重放(README.md:374-376)。

3.9 probe/ — 模型评测工具

probe.py(421 行)对任意 OpenAI 兼容端点跑 5 个测试用例;narrative_probe.py(890 行)做叙事质量评测;model_sweep.py(302 行)批量扫模型;results/narrative/ 存几十个模型的评测 JSON。docs/LLM-GUIDE.md 汇总结论(如 24B 在 4-5 次 tool-call 后漂移、gguf 需 eval_batch_size:4096,:36-88)。

4. 数据模型与持久化

4.1 工程与安全实践

5. 与 Z.R.I.C 功能对照表

Z.R.I.C 功能 本项目状态 依据
剧本解析 ✅ 部分 /gm import + import_campaign.py 抽取 PDF/DOCX/MD/TXT 文本,但结构提取(act/chapter/beat)由 LLM 完成而非代码(import_campaign.py:71-88、SKILL-commands.md:115-159)
推演分支 ✅ 部分 dynamic 弧以 what_changes(后果而非事件)承载分支,/gm arc revise 三条修正路径(SKILL.md:192-215、SKILL-commands.md:162-200);分支路由靠 SKILL-branches.md,非代码引擎
自动判定 ✅ 强 骰子/命中/HP/条件/计时效果/死亡豁免/XP 全部确定性脚本,零 LLM(dice.py、combat.py、tracker.py、xp.py)
世界状态 ✅ 强 state.md+world.md+calendar.py## Live State Flags 抗压缩(templates/state.md:37-47)
NPC 情绪记忆 ⚠️ 部分 NPC 性格轴/关系/隐藏目标在 npcs-full.md;阵营/立场在 Live State Flags + graph set-disposition(gm_graph.py:94-101);无显式「情绪」字段,是立场而非情绪
地图 ⚠️ 弱 无地图模块;只有 display 关键词场景检测(README.md:304-322)、world.md 冒险节点、graph place 节点
触发器 ⚠️ 部分 威胁升级弧 5 阶段触发表(templates/world.md:71-82)、world_pressure## Faction Moves;无事件引擎/自动触发
时间线 ✅ 强 calendar.py 历法推进 + session-log/Continuity Archive 归档 + graph 边 since_session/until_session(gm_graph.py:173-189)
记忆系统 ✅ 强 分层记忆:Pinned Facts(稳定软正典)+ Live State Flags(抗压缩)+ Continuity Archive + GM Style Notes(玩家校准)+ 会话日志归档(SKILL.md:166-174、templates/state.md)
本地模型 ✅ 强 纯提示词+Python,OpenCode/LM Studio 适配(README.md:60-118);probe/ 评测 + docs/LLM-GUIDE.md 硬件/参数指南;确定性抽取替代上游 Haiku 依赖(gm_graph.py:8-13)
许可 ✅ 已落实 AGPL-3.0-or-later(README.md:457、LICENSE);systems/brp/NOTICE 标注 ORC 许可来源(brp/system.md:5)

6. 三个重点问题的答案

  1. SKILL.md 如何驱动 GM:SKILL.md 是「GM 技艺的行为约束提示词」,不参与启动常驻(README.md:97)。它用「Script-First 规则」(:118-127)+「每回合 Display 同步序列」(:287-297)+「压缩后重读最小源节」(:166-174)等显式约束,把 GM 经验写成 LLM 可执行的编排步骤;SKILL-branches.md 再把命令映射成最短 tool-call 序列。

  2. SYSTEM-PORTING.md 的移植机制:两层分离——SKILL.md 恒定、system.md 承载规则。移植 = 复制 TEMPLATE.md 填 system.md,最小可用集只需「骰子判定/属性/HP/资源/条件」(SYSTEM-PORTING.md:149-156)。通用脚本不依赖具体系统;系统特定数据经 paths.py 版本化落到 systems/<sys>/data/brp/system.md 证明「纯文档即可跑」的底线。

  3. LLM 无关性如何实现:三层——(a) 宿主层无绑定,README 给 OpenCode 与 LM Studio 两份配置(README.md:84-118);(b) 所有机械操作移出模型,LLM 只做叙述与判断(docs/LLM-GUIDE.md:7-20);(c) 上游 claude-dnd-skill 中依赖 Claude API/Haiku 的部分,此 fork 用确定性等价物替代或暂缓——关系图是「manual + query-only」,抽取用 graph_extract_deterministic.py 动词表模式匹配(gm_graph.py:8-13、graph_extract_deterministic.py:1-16)。

7. 设计评价

优点

  1. 机械/叙述严格分离:所有算术(骰子、HP、先攻、计时效果、XP 表)都在 Python 里确定性完成,模型不碰数字——直接降低对小模型的能力门槛(docs/LLM-GUIDE.md:7-20、README.md:424-426)。
  2. 提示词工程精良:SKILL-branches.md 把系统提示从 ~18,000 压到 ~2,300 token,并逐命令定义线性步骤以对抗小模型工具调用漂移(README.md:97,428;docs/LLM-GUIDE.md:36-42)。
  3. 持久化选型务实:战役数据全用可读可改的 Markdown + JSON,配合「最小源节重读」「Live State Flags」等抗压缩策略,明确针对长线战役的上下文压缩问题(templates/state.md:37-47、SKILL.md:166-174)。
  4. 系统可移植性设计干净:SKILL/system 两层分离 + TEMPLATE.md 脚手架 + 通用脚本不耦合具体系统,brp 模块证明最小移植成本极低(SYSTEM-PORTING.md:149-156)。
  5. 防御性降级做得好:display 未运行、numpy 缺失、PyYAML 缺失、SRD 未构建时均静默降级不中断会话(tracker.py:162-184、build_srd.py:27-32、gm-display-app.py:44-76)。

缺陷

  1. 对 LLM 上下文窗口高度依赖:核心状态(state.md/world.md/npcs.md + 全部角色卡 + system.md)每次 /gm load 全量读入(SKILL.md:378),战役变长后上下文压力线性上升,虽有用归档/图缓解但仍靠模型自己按提示词「只读最小节」,无硬约束。
  2. 触发器/事件是「人肉」而非引擎:威胁升级弧、world_pressure、Faction Moves 都是「提示词让 GM 记着做」的约定,没有代码自动触发(templates/world.md:71-82、SKILL.md:63-64);tracker.py 计时效果需手动 tick(tracker.py:243)。
  3. 图是「手工+查询」而非自动维护:graph 边靠 GM 在会话中手动 add-edge,save 时的关系变化扫描也靠 LLM 事后提出、GM 审批(SKILL-branches.md:254-268);确定性抽取器明确标 recall ~50%(graph_extract_deterministic.py:12)。
  4. 规则数据硬编码在代码里:XP/CR/生命骰/豁免熟练等 5e 表直接写死在 character.py/xp.py 字典,改版本(2014/2024)或换系统时需改代码而非数据(character.py:25-68、xp.py:38-92),与「system.md 即规则」的移植理念部分冲突。
  5. display 状态与战役状态双写易失同步:tracker.json/calendar.json/display 侧 stats.json 与 state.md 里的人工字段(HP/资源/条件)多处并存,靠 GM 逐回合同步(SKILL.md:268-297),无单一权威源,存在漂移风险。

8. 结论

open-tabletop-gm 是一个**「提示词技能包 + 确定性 Python 工具链」的 GM 框架,其真正价值不在某个模型能力,而在工程化的职责分离**:把 TTRPG GM 工作拆成「叙述判断(LLM)」「机械判定(脚本)」「世界记忆(Markdown/JSON 分层)」「展示(可选 Flask)」四层,并针对小模型/本地模型的工具调用深度做专门优化。它已是一个成熟可运行的开源项目(非文档占位),5e 全支持、BRP 作移植范例,LLM 无关性落实到位。其短板集中在「剧情/世界自动推演仍是提示词约定而非代码引擎」——推演分支、触发器、NPC 情绪等 Z.R.I.C 维度目前靠 GM 模型的自觉与 SKILL 编排维持,而非确定性状态机。对后续 Z.R.I.C 设计的直接借鉴点:**分层记忆(Pinned Facts / Live State Flags / Continuity Archive)与「把可计算部分移出模型」**这两条路线,是本地小模型跑长线战役最可复用的两个经验。

9. 关键代码位置速查

关注点 定位
GM 行为约束(SKILL 驱动核心) SKILL.md:9-76(12 标准)、:118-127(Script-First)、:217-231(骰子约定)
会话加载七步链路 SKILL-branches.md:7-106
战斗回合状态机 SKILL-branches.md:156-178、SKILL.md:287-297
会话保存/归档/关系扫掠 SKILL-branches.md:228-269、SKILL-commands.md:78-99
系统移植机制 SYSTEM-PORTING.md:38-123、paths.py:98-168
5e 规则表(硬编码) systems/dnd5e/character.py:25-68、xp.py:38-92
5e SRD 数据集构建/查询 build_srd.py:5-49、lookup.py:63-100、:310-339
确定性关系抽取 graph_extract_deterministic.py:296-418、data/graph/verb_table_seed.yaml
战役数据模型模板 templates/state.md:37-47(Live State Flags)、templates/world.md:71-82(威胁弧)
LLM 无关性与本地模型 README.md:84-118、docs/LLM-GUIDE.md:7-20、gm_graph.py:8-13