世界观注入调研
WORLDVIEW INJECTION
状态:🟢 已实施(2026-09-11)· 导入 96 条,实测注入生效 来源:ZOOT!(PC 端 AI 陪伴应用)的
static_knowledge+JourneyKnowledgeProvider起因:「AI 的知识库有时候不足」——这条正好是对方做得比较扎实的一块。
0.5 实施结果(2026-09-11)
三条决策(用户定):落点 A(写进 lorebook)/ 照搬正文 / 按来源标权威。
| 产出 | 内容 |
|---|---|
app/prompts/lorebook_terra.md |
393 KB / 96 条,正文合计 11.7 万字(生成物,勿手改) |
scripts/import_worldset.py |
导入脚本(可重跑,幂等) |
app/systems/lorebook.py |
新增 TERRA_PATH 接线 + 权威/来源 字段 + 非正典正文标记 |
tests/test_lorebook_authority.py |
19 条守卫 |
覆盖:组织 28 · 国家 19 · 种族 14(你缺的萨卡兹九王庭等)· 生物 12 · 设定 9(泰拉/经济/货币/战力分级/历史/远程武器)· 城市 3 · 地点 3 · 工业 3 · 化物 3 · 文明 1 · 侵蚀 1。
去重:跳过 34 条撞 world_lore.json 且那边有描述的(主要是种族 ——
实测你的描述质量更好:丰蹄你写分布史与农业迁徙,它写外观与胸围)。
国家和组织不跳过:那边是实体索引({name, sub_factions}),
根本没有 description 字段,同名不等于重复内容。
实测触发(真实调用 get_active_entries):
| 用户消息 | 命中 | 字数 |
|---|---|---|
| 「罗德岛现在有多少人?」 | [组织] 罗德岛 |
3378 |
| 「卡兹戴尔是什么样的地方」 | [国家] 卡兹戴尔 |
4377 |
| 「海嗣是什么」 | [组织] 深海教会 + [生物] 海嗣 |
389 + 4117 |
| 「今天天气不错」 | (无命中) | — |
单轮最坏注入约 1.3 万字 ≈ 6700 token(max_entries=3),有冷却兜底。
0.6 ⚠️ 实施中挖出的三个真 bug
① 启用 字段坏了两层,从来没生效过
LorebookEntry.enabled 和 get_active_entries 里的 if not entry.enabled: continue
一直都在,但:
- 字段白名单正则里有
启用,但if/elif链里没有对应分支 → 值被静默忽略 - 补了分支后仍不生效 —— 构造
LorebookEntry(...)时 漏了enabled=enabled传参 → 落回 dataclass 默认True
同一个开关坏了两层,全程无报错。已修 + 两条守卫 (白名单里每个字段都必须有处理分支;行为验证中英文 false 形式)。
② - 权威: / - 来源: 被当成正文
解析器白名单里没有这两个名字 → 落进 content_lines,
于是全部 93 条的正文都以 权威: 待考 / 来源: zoot-... 开头被注入 prompt。
已加为正式字段。
③ 我自己的测试有幸存者偏差(最值得记的一条)
第一版 test_no_enabled_entry_without_keywords 只断言
"解析出来的条目都有关键词" —— 而被丢弃的条目根本不在解析结果里,
断言不到。于是漏掉了 3 条(战力分级×2 / 历史):
- 它们没有关键词 → 格式化时不写
- 关键词:行 - 解析器末尾
if not keywords: continue把它们整条丢掉 - 文件里能搜到,条目列表里没有 —— 看起来导入了,其实一次都不会注入
改成对总账:len(解析结果) == len(文件里的 ## 段落数)。
可推广的教训:测「没有坏数据」时不能只检查幸存者; 要对产出与输入对总账 —— 否则「丢弃」本身是不可见的。
0. 一句话结论
它解决"知识库不足"的办法不是补更多知识,而是给"编造的东西"一个带标签的位置。
六层权威里有两层(策划设定 / 未证实传闻 / 待确认提案)专门为"知识库没有、 但剧情需要"的内容准备,并且 prompt 里带一条硬约束:
【泰拉寻旅知识依据】低权威内容不得覆盖高权威事实。
于是 AI 该编的时候照样编,但编造物被降级标注,而不是伪装成官方设定。
③ A4(c) 已实施:权威参与排序(2026-09-11)
导入时我给待考条目设的权重是 45~70,而正典(lorebook_common.md 本舰场景)
只有 25~35 —— 按原逻辑(裸 weight 排序)导入内容会把正典挤出 max_entries。
这和「低权威不得覆盖高权威」是矛盾的:一条没核对过的整理, 不该比你自己核过的本舰设定更优先。
修法:排序改用 _auth_weight = weight × AUTHORITY_DISCOUNT(默认 0.4)。
待考 70 → 28,落在正典区间(25~35)的中下段,不再压过任何一条正典。
为什么是折扣不是硬过滤:硬过滤会让用户手动打开的条目也永远抽不到 ——
那是「加了开关却接不上」的老毛病(启用 那次就是)。折扣只让它排在正典之后,
仍可被选中:保住正典优先,又不让导入内容变成死数据。
并列时的显式裁决:光靠折扣不够 —— 折扣后总会有相等的情况,
而那样胜负就取决于候选的插入顺序(正典恰好先 extend)。
那是隐式依赖,顺序一改行为就变且没有测试会红。
所以排序键加了 _canon_first:(有效权重, 是否正典, 命中数)。
实测效果:
| 问题 | 改动前 | 改动后 |
|---|---|---|
| 「罗德岛的甲板」 | [待考] 组织·罗德岛 22.0 抢在前 |
[正典] 甲板 30.0 第一,罗德岛 22.0 次之 |
| 「海嗣是什么」 | 只有 组织·深海教会 |
深海教会 + 生物·海嗣 都进来 |
守卫:tests/test_lorebook_authority.py::TestAuthorityAffectsRanking(6 条),
其中一条专门锁「任何待考的有效权重都不得压过最高的正典」。
1. 对方的结构(已核实)
1.1 世界观是独立命名空间
static_knowledge 表的 operator_id 字段里,世界观记录的取值是字面量 "worldset":
operator_id='worldset', category='worldset/races' 48 条
category='worldset/organizations' 28
category='worldset/nations' 19
category='worldset/creatures' 12
category='worldset/settings' 9
category='worldset/originium' 4
category='worldset/artifacts' 3
category='worldset/cities' 3
category='worldset/locations' 3
category='worldset/industry' 3
category='worldset/corrosion' 1
category='worldset/civilization' 1
共 12 类 134 条(另有 34,297 条角色/剧情知识走别的 category)。
1.2 每条自带关键词表(入库时抽好,不是运行时猜)
raw_data 是 JSON,三个字段:
{
"key": ["阿斯兰", "狮"], // ← 触发关键词
"comment": "[种族]:阿斯兰", // ← 分类标签
"content": "# 阿斯兰\n\n外观:..." // ← 正文
}
1.3 ⭐ 六层权威(核心)
JourneyKnowledgeProvider.render() 的分组与硬约束:
官方事实 / 当前世界线事实 / 已观察状态 / 策划设定 / 未证实传闻 / 待确认提案
【泰拉寻旅知识依据】低权威内容不得覆盖高权威事实。
排序键 (authority, score);条目字段 authority / unverified / source / content。
| 层 | 作用 |
|---|---|
| 官方事实 | canon,不可动摇 |
| 当前世界线事实 | 本局已确定的,覆盖 canon 的本地变体 |
| 已观察状态 | 角色实际看到的,限定认知范围 |
| 策划设定 | 用户/作者写的 —— 知识库没有时补位 |
| 未证实传闻 | 明确标为未证实线索 —— AI 编的有合法出处且不污染 canon |
| 待确认提案 | 同上,且更低 |
1.4 knowledge_overlays:内置只读 + 用户改动分层
knowledge_overlays(
stable_id TEXT PRIMARY KEY, -- 指向内置记录
base_id, operator_id, category, sub_id,
content, raw_data, level,
operation TEXT CHECK(operation IN ('upsert','delete')),
updated_at)
读时「读内置 → 叠加 overlay」。升级内置资产不会冲掉用户改动。
1.5 两条注入通道(值得注意的对比)
| 通道 | 取 static_knowledge 的方式 |
|---|---|
memory_retriever(动态墙那条) |
ORDER BY level ASC LIMIT ? —— 按重要性取前 N 条,无相关性过滤 |
JourneyKnowledgeProvider(长剧情那条) |
hybrid(lexical + vector),有自己的 _tokens 分词,带 canon_policy: strict_canon |
同一个人做了两条质量差很远的通道。 后者是认真做的——这也说明「按 level 取前 N 条」 是权宜之计,不是设计。
2. 与我们项目的对照(已核实落点)
app/systems/lorebook.py::LorebookEntry 现有字段:
uid, title, content
keywords: list[str] # 主关键词+扩展词
weight: int = 50 # 优先级 0-100
level: int = 0 # 0=通用 1=职业 2=个人
restricted_to: list[str] # 限定角色(空=全部)
constant: bool = False # 常驻
scene: str = "" # 绑定在场系统的场景键
sticky: int = 0 # 触发后持续 N 轮
cooldown: int = 0 # 触发后 N 轮内不再触发
enabled: bool = True
source: str = "system" # system / user_import / user_custom
| ZOOT! | 我们 | 判断 |
|---|---|---|
key 关键词表 |
keywords |
✅ 已有 |
level |
level |
✅ 有(含义不同) |
| — | weight |
✅ 我们更强 |
| — | restricted_to |
✅ 我们更强 |
| — | constant/sticky/cooldown/scene |
✅ 我们更强(对方完全没有调度维) |
source(三值) |
source(同三值) |
✅ 完全一致 |
authority + unverified |
无 | ❌ 缺这一维 |
| 内置只读 + overlay 分层 | 无 | ❌ 缺(我们有 source 区分来源,但没有"按 stable_id 覆盖同一条") |
render() 的硬约束句 |
无 | ❌ 缺 |
结论:我们的 lorebook 在触发/调度维度比它强,缺的是可信度这一维。
3. 建议怎么做(不是照搬)
3.1 只取一维:给条目加 authority,并在注入时显式标注
不引入它的六层命名(那是它长剧情模式的概念),而是按我们的语汇定三到四档:
| 我们的档位 | 含义 | 对应对方的 |
|---|---|---|
| 正典 | 官方设定 / 已确认的世界事实 | 官方事实 |
| 本局事实 | 这档剧情里已经发生并被确认的 | 当前世界线事实 |
| 待考 | 用户导入的、来源不明的、剧情需要但设定没写的 | 未证实传闻 |
| 提案 | AI 提出、待确认的 | 待确认提案 |
关键动作是注入时的标注(这是对方的精髓,也是最便宜的一步):
【世界观依据】标注为「待考」的内容是未经确认的线索,不得作为事实引用,
更不得覆盖「正典」设定。若需要确立新事实,请以「提案」形式提出。
对照我们已有的机制:CLAUDE.md 里那条「传闻会明确标记为未证实线索,避免覆盖确定设定」
已经写了这个意图,缺的只是把它落到数据字段 + prompt 句上。
3.2 覆盖层(可选,优先级低)
对方的 knowledge_overlays 解决的是「升级内置资产不冲掉用户改动」。
我们现在 source 已经区分 system / user_import / user_custom,
但没有「用 stable_id 覆盖内置同一条」的能力——用户想改一条内置设定,
只能新建一条,于是两条并存、可能矛盾。
⚠️ 但这项现在不必做:一测前不会有大量用户改内置资产。记在 backlog 即可。
3.3 ⚠️ 不要抄的:ORDER BY level ASC LIMIT ? 那条通道
对方动态墙那条通道是「按重要性取前 N 条,无相关性过滤」—— 这是它自己后来在长剧情里用 hybrid 取代掉的权宜之计。 我们 lorebook 的关键词触发比它这条好,别退回去。
4. 如需实施,落点(已核实)
| 动作 | 落点 |
|---|---|
条目加 authority |
lorebook.py::LorebookEntry + parse_markdown_lorebook / parse_st_json |
| 注入时标注 | get_active_entries() 的出口(唯一出口,已核实) |
| 硬约束句 | 注入模板(和 anti_drift / identity_guard 同级的位置) |
| 守卫测试 | 新增 tests/test_lorebook_authority.py |
⚠️ authority 应有缺省值(老条目没有这个字段 → 按「正典」或按 source 推导),
否则存量条目会全被判成最低档,反而让 AI 不敢引用真正可信的设定。
这个缺省口径必须先定,再写代码。