剧情数据管线
STORY PIPELINE
状态:🟡 待审阅 · 建立 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.json → storyTextAudio |
1,170,284 | 780,189 |
| 语音 | charword_table.json → charWords(18,237 条) |
1,240,137 | 826,758 |
| 道具描述 | item_table.json → items(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_files99/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 只有 name 和 Subtitle。全量抓完后发现字符命中率异常,
按 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.04 且 dialogue_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 输出)。
验收标准(必须逐条回答)
- 抽出的边,证据行号指向的原文真的支持它吗?
basis分布合理吗?(预期多数direct,少数reported)- 有没有在场降级?降得对吗?
- 抽出的说话人,是 ASTR 规范名吗?(对比我们 385 个干员的显示名)
line_id形态对不对?--verify-dir能过吗?
3. 方案 (a):补语音 + 道具
| 步 | 动作 | 说明 |
|---|---|---|
| a1 | 抓 charword_table.json(13.1MB) + item_table.json(2.25MB) |
实测 30.8s + 10.7s |
| a2 | 写转换器:charWords / items → lines 结构 |
新代码 |
| 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.json:302 节点(全是干员)/ 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.py 的 tier1 已经能用上(干员部分),无需改 |
| 溯源展示 | 前端展示关系时带 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 验收标准
- 正典不被挤出:构造"用户消息命中大量新条目"的场景,断言 common 的条目仍在 top-3
- 向量检索召回优于关键词:用一组人工标注的查询对比 top-3 命中率
- 退化保护:embedding 不可用时退回关键词,行为不比接入前差
- 可回退:一个开关能整体关掉(复用现有
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. 我目前不确定的地方(诚实列出)
- ASTR 说话人名 vs 我们 385 个干员的匹配率 —— 完全没测过, 这是最大的未知,(c) 就是为了测它。若不匹配率高,要先建别名表 (参考首轮:nideriji 侧有 8 个歧义名)。
- 抽取在长尾 stage 上的质量 —— 只在 200 行的 TO-ST-1 上验过。 最长 stage 有 15,409 字符,分块后跨块关系会不会丢?
- 归并的真实效果 —— 17,000 条能并到多少、错并率多少,毫无依据。
other类边占比 —— 实测 run4 是 1/10,全量下会不会飙升?- 活动剧情值不值得跑 —— 从没抽过。
- 向量检索的召回提升幅度 —— 没有标注集,无法预估。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 已覆盖产物目录。