罗德岛通讯与技术部

剧情数据管线

STORY PIPELINE

实际按 40k token 打包只需 144 次 → ¥32;再加归并 ¥27,真实是 ¥59。

状态:🟡 待审阅 · 建立 2026-09-11 · 第 6 步已提升为独立章节 前置:docs/剧情关系抽取-首轮.md(首轮结果与两个数据源的分析) 原则:每一步都可验证、可回退;不通过验证不进下一步。

本文所有数字都是实测的,测法写在数字旁边。


0. 执行摘要

阶段 耗时 消费
(c) 端到端小验证(抓取+转换部分) 已完成 ¥0
(a) 补语音 + 道具(抓取部分) 已完成(66 秒) ¥0
(b1) 抓全量 2,009 stage 已完成(11.0 分钟) ¥0
(b2) 抽取全部 约 1.0 小时 ¥32(闲时 ¥16)
(b3) 归并去重 0.1 小时 ¥27(混合策略可降到 ¥10 内)
(6) 接入检索 约 3–5 天开发 ¥0(本地 embedding,实测免费)
合计 ≈ ¥60–70

三个必须先说清楚的点

① 我此前报的 "¥12" 是错的。 错在假设每个 stage 一次 LLM 调用(2,009 次)。 实际按 40k token 打包只需 144 次 → ¥32;再加归并 ¥27,真实是 ¥59

② 第 6 步(接入检索)不是"收尾",而是决定效果的一步,且它不花钱、花时间。 数据进库 ≠ 对话变好。现有检索是关键词子串匹配 + 每轮最多 3 条, 条目从 96 涨到上千之后会退化(见 §6.2)。这一块的工作量 (约 3–5 天)远大于抽取本身(1.2 小时)。

③ 本地 embedding 实测免费。 46.0 条/秒(唯一文本,已扣 17.5s 模型加载), 全语料 2,000 条 0.7 分钟,含语音 20,657 条也只要 7.5 分钟


1. 已查证的事实(含测法)

1.1 数据源

来源 覆盖 实测
nideriji 3 个活动 35 stage / 30.8 万字
ASTR → 050644zf/ArknightsStoryJson 全游戏 2,009 stage / 846.5 万字
ArknightsAssets/ArknightsGamedata 无剧情 只有 excel/config/i18n/

数据入口(可直接取,无需 key):

https://r2.m31ns.top/zh_CN/...          # 主源,Cloudflare R2
https://raw.githubusercontent.com/...   # 兜底,⚠️ 走 LFS,raw 直链会 ConnectionReset

测法:读 astr.pages.dev 前端 bundle 找到 bS = {m31ns, github}; 两仓库 excel/ 逐字节比对(activity_table.json 都是 19.15 MB)。

1.2 数据块体积(全部实测)

来源 字符 ~token
对话 storyinfo.json / wordcount.json 8,465,351 5,643,567
干员档案 charinfo.jsonstoryTextAudio 1,170,284 780,189
语音 charword_table.jsoncharWords(18,237 条) 1,240,137 826,758
道具描述 item_table.jsonitems(1,556 条) 321,773 214,515
合计 11,197,545 7,465,030

⚠️ 干员档案已经在 charinfo.json 里(117 万字符), 不需要从 handbook_info_table.json 再拿一遍 —— 那是同一份数据。

1.3 抓取吞吐(实测)

模式 速率 备注
8 并发 3.27 个/秒(120/120 成功,中位 2.15s) ✅ 推荐
16 并发 0.48 个/秒,5 个超时 R2 限速,调高反而慢 7 倍

外推:2,009 stage ≈ 10.2 分钟,平均 64 KB/个,约 130 MB。

1.4 LLM 抽取成本(据实测基准外推)

基准:TO-ST-1,5,439 字符,实测 in=5809 out=1646(关思考)。

  • prompt 常量 ≈ 2,183 tk;输出/正文字符 ≈ 0.303
  • 平均 2,809 tk/stage → 每次装 14 个144 次调用
输入 输出 标准价 闲时价
抽取(144 次) 5.96M 2.56M ¥32 ¥16
归并(17,079 条,2 趟) 5.12M 2.05M ¥27
合计 ≈ ¥59 ≈ ¥43

闲时窗口:北京时间 00:30–08:30(其余为高峰)。 来源:DeepSeek 官方定价

1.5 本地 embedding(实测,免费)

实测
热态速率 46.0 条/秒(500 条唯一文本 / 10.86s,384 维)
模型加载 17.5 秒(一次性)
全语料 2,000 条 0.7 分钟
含语音 20,657 条 7.5 分钟
增量(每 1,000 条) 0.4 分钟

⚠️ 踩过的坑:第一次测出 75615 条/秒,那是重复文本命中缓存的假象。 用唯一文本重测才是 46.0。基准测试必须用唯一输入,否则测的是缓存不是模型。

1.6 可行性验证(已做)

  • astr_convert.py 跑通真实 stage,说话人覆盖 86%
  • ✅ 锚点自检能检出三种漂移(中间插入 / 原地改写 / 末尾删除)
  • 交叉验证:两条独立管道内容一致 —— personnel_files 99/99 达标,中位数 0.995
  • 抽取脚本可直接吃转换后的数据(字段齐全,validate() 在真实 stage 上可运行)
  • ✅ 已有 57 条测试,全部反向验证过

1.7 ✅ 免费部分已完成(2026-09-11)

结果
抓取 stage 2,009 / 2,009,0 失败,用时 11.0 分钟(3.05 个/秒)
转换产出 411,192 文本行 / 822.4 万正文字符 / 139.2 MB JSON
说话人覆盖 363,364 行(88.4%)
不同说话人 6,830 个
entry_type ACTIVITY 1058 / MAINLINE 429 / NONE 390 / MINI_ACTIVITY 132
prop 分布 name 399,307 / Subtitle 5,919 / Sticker 3,768 / multiline 1,950 / animtext 248
语音+道具表 data/story_extra/:charword 13.10 MB / item 2.25 MB(66 秒)

消费 ¥0(本阶段不调 LLM)。

抓取过程中发现并修掉的两个真问题

① 漏了 3 种文本元素类型(会静默少内容)。

第一版 TEXT_PROPS 只有 nameSubtitle。全量抓完后发现字符命中率异常, 按 mine/expected 排序取最差 30 个文件,扫它们的非文本 prop 里 带长字符串的,按字符数统计才定位到:

prop.字段 次数 字符数 是什么
Sticker.text 1,665 39,084 画面贴字(旁白/内心独白)
multiline.content 20 258 多行文本块
animtext.content 10 213 动画文本(含 <p=n> 标记)

⚠️ 方法论教训:如果只看"总字符差了 4.3%"会得到一个含糊的结论, 很容易当成"正常的格式差异"放过去。按文件看比值分布才发现问题是局部的 —— 1,328 个文件精确 =1.0,但有 22 个掉到 0.7、最差 0.374, 而且最差的全部集中在 obt/main。分布形态指向"某几种元素只在特定章节用"。

wordcount.json 是纯正文字符,不含说话人名。

我一度以为它含人名(因为单个样本 2130+2588+3161 = 5743 ≈ 5691 看着像), 于是把"人名"也加进去对账,结果比值变成 111.2%(中位 1.118), 只有 15/2008 个文件精确命中,明显不对。

回到只算正文:中位 1.0000,最大 1.0000,1,182 个文件精确 = 1。 所以口径是纯正文,全量命中率 97.15%

⚠️ 两次都栽在"用一个样本推断口径"上(一次算漏、一次算多)。 口径要用全量分布验证,不能拿单例拟合。

顺带确认的一件事

Character 元素不是说话人来源 —— 老格式(如 level_main_01-07_beg) 里它是立绘资源 id(char_130_doberm_ex),而新版是 char_003_kalts_1。 说话人只在 name 元素上(attributes.name)。 这一点已加回归测试锁住。

关于 6,830 个说话人

远超我们 385 个干员。绝大多数是一次性 NPC —— 这与首轮 nideriji 侧的发现一致(349 个未映射名里 347 个只活在单个事件里, 129 个出现 ≤3 次)。所以建节点前必须过滤, 上游主角门槛(dialogue_ratio ≥ 0.04dialogue_count ≥ 30)正好可用。

已完成的部分:抓取(b1)+ 语音道具抓取(a1)+ 转换(c1) 未完成(需要 LLM 或新代码):抽取(c2/b2)、归并(b3)、表格转换器(a2)、接入检索(第 6 步)

2. 方案 (c):端到端小验证 —— 先做这个

为什么先做

前三轮验的都是单点没有任何一次从 ASTR 数据跑通完整抽取。 方案 c 用最小代价回答唯一还开着的问题:整条链路接起来能不能用。

做什么

动作
c1 抓并转换 3 个 stage(1 密录 + 1 主线 + 1 活动)
c2 extract_story_relations.py
c3 review_story_relations.py 出带原文的复核报告
c4 人审:逐条看证据行号是否真的支持该边

耗时与消费

10 分钟(含人审),¥0.03(3 次调用,9k 输入 + 8k 输出)。

验收标准(必须逐条回答)

  1. 抽出的边,证据行号指向的原文真的支持它吗
  2. basis 分布合理吗?(预期多数 direct,少数 reported
  3. 有没有在场降级?降得对吗?
  4. 抽出的说话人,是 ASTR 规范名吗?(对比我们 385 个干员的显示名)
  5. line_id 形态对不对?--verify-dir 能过吗?

3. 方案 (a):补语音 + 道具

动作 说明
a1 charword_table.json(13.1MB) + item_table.json(2.25MB) 实测 30.8s + 10.7s
a2 写转换器:charWords / itemslines 结构 新代码
a3 charId / itemId 归并
a4 加测试 + 反向验证

耗时约 2 分钟抓取,¥0(不调 LLM)。

⚠️ a2 是真工作量:语音没有"上下文行号",不能沿用 stage 的锚点机制, 需另定 id 规则(如 voice:<charWordId>)并单独测试。


4. 方案 (b):抓全量 + 抽取 + 归并

b1 抓全量(¥0,11 分钟)

必须 8 并发(16 并发实测慢 7 倍)。脚本需加:断点续传、失败重试、退避。

b2 抽取(¥32 / 闲时 ¥16,1.0 小时)

144 次调用,产出约 17,000 条原始关系

b3 归并去重(¥27,不可跳过

17,000 条里大量重复(同一对角色在几十个 stage 反复出现)。不归并,图谱被稀释到不可用。

⚠️ 归并比抽取还贵,因为要把全部关系同时装进上下文才能判重。 这是我初稿漏掉的一块。

b4 总账

标准价 闲时价
抓取 ¥0 ¥0
抽取 ¥32 ¥16
归并 ¥27 ¥27
合计 ≈ ¥59 ≈ ¥43
总耗时 约 1.2 小时

5. 第 6 步(独立章节):接入检索

⚠️ 这一节是决定"做完有什么效果"的地方。 前面五步只是把数据变成文件; 这一节决定它能不能影响对话。而它的工作量(3–5 天)远大于前面全部(约 1.2 小时)。

6.1 现状(实测)

现在
世界书 lorebook_common.md 10 条(正典,本舰场景)+ lorebook_terra.md 96 条(导入)= 106 条 / 40 万字节
关系图 app/systems/relation/relation_graph.json302 节点(全是干员)/ 2163 边
关系质量 204 条非「同族」边;1959 条是同族(种族属性推导)
覆盖率 228 / 302 个干员(75%)只有同族边
检索方式 关键词子串计数for kw in entry.keywords: if kw in msg_clean: hits += 1
注入上限 max_entries = 3(每轮)
排序 _auth_weight(= weight × 权威折扣)→ _canon_first → hits

6.2 为什么"加数据"会让效果变差(这是本节的核心风险)

排序公式:

def _auth_weight(entry):
    if entry.authority == AUTHORITY_CANON:
        return float(entry.weight)          # 正典不打折
    return float(entry.weight) * 0.4        # 非正典打 4 折

现有权重分布(实测):

文件 条数 权重
lorebook_common.md(正典,无 权威 字段) 10 25 / 30 / 35
lorebook_terra.md(导入,标了非正典) 96 45–70 → 打折后 18–28

正典 25–35 对 terra 折后 18–28,只是略胜 —— 平衡本来就很脆弱。

⚠️ _auth_weight 的缺省行为是:没有 权威 字段 = 视同正典、不打折。 (代码注释写明这是刻意的:"存量条目都没有这个字段,打折会让它们集体降级"。)

后果:我新导入的条目如果不显式标权威,会按全权重参与 —— 权重 45–70 直接压过 common 的 25–35,把正典挤出 max_entries。 代码里已经有一处同样的教训:

# 否则导入的「待考」条目(45~70)会把正典(25~35)挤出 max_entries

再者,关键词匹配在 106 条时勉强可用,到上千条会退化: 一个常见词(如"博士")的 hits 会命中几十条,然后靠权重硬切 3 条 —— 新数据越多,"切中正确那条"的概率越低

6.3 要做的四件事

# 事项 说明 工作量
6a 新条目一律显式标权威 导入内容一律 权威: 待考(或按来源分档),绝不回落缺省。并在导入脚本里加断言:任何写入 lorebook_*.md 的新条目必须有 权威 字段 0.5 天
6b 换检索:关键词 → 向量 本机已有 ChromaDB + get_embedding()(384 维,实测 46 条/秒)。世界观条目全局共享,所以不能塞进 L3(L3 是 operator + username 隔离的),要新建独立集合 2 天
6c 混合排序 向量相似度 × 权威权重(正典仍优先)。保留关键词作为兜底(向量不可用时退回现状,不比现在差) 1 天
6d max_entries 与预算 3 条是 106 条时代的常数,需要按 "向量候选质量" 动态决定,并受 app/core/prompt_budget.py 约束 1 天

6.4 关系图怎么接(与 lorebook 不同)

关键事实:直接合进 relation_graph.json 是安全的,因为下游会自动过滤:

# narrative/engine.py:1181
related_ids = get_related(poster_id)
tier1 = [op for op in base_pool if op in related_ids]   # ← 与干员池取交集

非干员节点(塔妮、巴尼特…)会被 base_pool 交集自然滤掉。 节点遍历也是容错的(orgs.get(node.name) 取不到就跳过,if node.faction)。

但"安全"不等于"有用" —— 被滤掉就等于没接。所以要给 B 层单独开一条消费路径:

用途 接法
对话时提角色 get_related(poster_id)非干员邻居,作为"她认识谁"注入
叙事引擎 engine.pytier1 已经能用上(干员部分),无需改
溯源展示 前端展示关系时带 evidence_line_ids,可点开原文

6.5 第 6 步的耗时与消费

数值
开发 约 3–5 天(6a–6d)
embedding 全量 0.7 分钟(2,000 条)/ 7.5 分钟(含语音 20,657 条)
embedding 增量 0.4 分钟 / 1,000 条
API 消费 ¥0
新增存储 ChromaDB 约 2,000×384 float ≈ 3 MB(+ 元数据)

6.6 验收标准

  1. 正典不被挤出:构造"用户消息命中大量新条目"的场景,断言 common 的条目仍在 top-3
  2. 向量检索召回优于关键词:用一组人工标注的查询对比 top-3 命中率
  3. 退化保护:embedding 不可用时退回关键词,行为不比接入前差
  4. 可回退:一个开关能整体关掉(复用现有 lorebook_enabled 机制)

7. 关键决策点(需要你定)

问题 我的建议
D1 抽取范围 全量抓,抽取只跑密录 + 主线(817 个),省一半钱
D2 evidence_ids 粒度 保持 stage路径#序号:哈希,另加 story_group(同 eventid+storyCode)便于按剧情段聚合
D3 语音/道具进不进关系图谱 不进图谱;语音/道具进 lorebook 与知识层
D4 归并策略 混合:先按 (subject, predicate, object) 确定性归并(免费,去掉大部分重复),剩余冲突送 LLM。可把 ¥27 压到 ¥10 内
D5 第 6 步要不要做 我的建议:要做。否则前五步的产出只躺在文件里,对话不会变好
D6 第 6b 的集合设计 新建独立 Chroma 集合 rhodes_world_lore不加 username 隔离(世界观是全局的)

8. 建议的执行顺序

第 1 步  (c) 端到端小验证          10 分钟    ¥0.03   ← 先做,验链路
   ↓  通过验收标准 5 条才继续
第 2 步  (a) 补语音 + 道具          2 分钟     ¥0
   ↓  a2 转换器 + 测试通过
第 3 步  (b1) 抓全量               11 分钟    ¥0      ← 可反复,免费
   ↓  抽样 100 个 stage 人工看质量
第 4 步  (b2) 抽取(先密录+主线)    1 小时     ¥16–32
   ↓  抽查归并结果
第 5 步  (b3) 归并                  0.1 小时   ¥10–27
   ↓  ⚠️ 到这里数据才齐,但对话还没变好
第 6 步  (6a–6d) 接入检索           3–5 天     ¥0      ← 决定效果
   ↓  四条验收标准
第 7 步  前端展示溯源(可后置)       待估       ¥0

每一步都单独跑全量回归(基线 30 failed / 1,839 passed)。 任何一步不过验收,停下来报告,不自行往下走


9. 做完之后的效果

9.1 确定能拿到的(第 1–5 步完成后)

效果 现状 → 之后
干员关系覆盖 228/302(75%)只有同族边 → 大部分补上剧情关系
关系可溯源性 现有手写边无证据链 → 每条边带 line_id,可还原到原文那几行
证据强度 无法区分 → direct / reported / inferred 分开标,"有人转述"不会伪装成"当面确认"
世界书规模 106 条 → 上千条(+441 档案 / +18,237 语音 / +1,556 道具)

9.2 只有做完第 6 步才有的

效果 说明
对话真的用上 角色能引用自己密录里的具体经历,而不是泛泛而谈
提到角色时有依据 能给出有出处的说法,可点开到原文
不再挤掉正典 权威分级 + 向量检索让新数据补充而非取代

9.3 明确不会有的

  • 不会自动让对话变好(只做到第 5 步的话)
  • ❌ 不会全自动无人工(c4、b3 后、6.6 都需人工抽检)
  • ❌ 关系数据不是权威事实reported 边是"某人这么说过", note 字段是模型的解读。原文自己都会自相矛盾 (实测:巴尼特对安洁莉娜说"我们家刚出生的小提摩西", 转头对塔妮说"你的表妹出生了")

10. 我目前不确定的地方(诚实列出)

  1. ASTR 说话人名 vs 我们 385 个干员的匹配率 —— 完全没测过, 这是最大的未知,(c) 就是为了测它。若不匹配率高,要先建别名表 (参考首轮:nideriji 侧有 8 个歧义名)。
  2. 抽取在长尾 stage 上的质量 —— 只在 200 行的 TO-ST-1 上验过。 最长 stage 有 15,409 字符,分块后跨块关系会不会丢?
  3. 归并的真实效果 —— 17,000 条能并到多少、错并率多少,毫无依据。
  4. other 类边占比 —— 实测 run4 是 1/10,全量下会不会飙升?
  5. 活动剧情值不值得跑 —— 从没抽过。
  6. 向量检索的召回提升幅度 —— 没有标注集,无法预估。6b 需要先造一个小的 人工标注查询集(约 50 条)作为基准,否则"换向量有没有用"也测不出来。

11. 附:脚本与测试

脚本 作用 测试
scripts/astr_convert.py ASTR 原生 → lines,带锚点自检 tests/test_astr_convert.py(19)
scripts/extract_story_relations.py 抽取 + 三道闸门 tests/test_story_relation_gates.py(27)
scripts/review_story_relations.py 行号还原成原文供人审
scripts/crossvalidate_sources.py 两源交叉验证 tests/test_crossvalidate_sources.py(11)

⚠️ 四个脚本都还没 commit,在工作区未跟踪状态。 .gitignore 已覆盖产物目录。