# 世界书：本轮到底塞进了什么，谁决定，谁能看见？

这里展开此前被压缩成“原文直接注入、Planner 不直接接收”的数据流。分析基线为 Harness `80aef3fa5582d8508ecdd1afa206391940ce9502` 与 Worldbook Service `3a11e333afd335aeff165e0ccc3654af8e1897d1`。说明针对当前 `agentic-v2-worldbook` 接入；仓库配置不等于已核验的线上配置。

**“本轮直接注入”是：用户这句话到来后，程序先挑出一些已经写好的世界书条目，把这些条目的正文临时加入这次 Actor 的模型请求，Actor 随后参考它们写角色回复。**

选条目发生在写回复之前。当前这条链路不需要 Planner 先决定是否调用世界书。

![同一轮，Actor 与 Planner 收到不同材料](worldbook-input-boundary.png)

[打开交互图](worldbook-input-boundary.html)。图中的共同来源经过各自模板和裁剪，不表示两份输入完全相同。

## 1. 先用一轮具体对话理解

以下内容是虚构教学例子，不是线上书籍、真实请求日志或已运行的模型输出。为便于看到确定的关键词行为，本例使用 `rules`；仓库世界书场景配置是 `hybrid`。

用户说：**“我把铜钥匙递给北塔守卫，请他开门。”**

会话绑定《北塔世界设定》第 1 版。内容作者事先写了这些条目：

| 条目 | 保存的正文 `content` | 作者设定的触发方式 | 本例结果 |
| --- | --- | --- | --- |
| 世界基本规则 | 这个世界中，死亡不能被魔法逆转。 | `constant=true` | 通过条件、预算检查后入选 |
| 北塔门禁 | 北塔午夜后禁止开门；只有摄政官的书面命令可以解除禁令。 | `keys=["北塔"]` | 用户提到北塔，关键词命中 |
| 铜钥匙 | 铜钥匙证明持有人与旧王室有关，但不能代替摄政官的书面命令。 | `keys=["铜钥匙"]` | 用户提到铜钥匙，关键词命中 |
| 南港航线 | 南港每周三有商船开往群岛。 | `keys=["南港"]` | 假设近期扫描文本也没提到南港，本例不入选 |

假设以上条目都启用、允许 Actor 使用，概率为 100%，没有其他状态条件或依赖，前三条能装进预算。服务返回前三条。**“我要注入什么”在这里有明确答案：前三条的正文，不是整本书，也不是仅注入三个标题。**

每条默认作为一条 `system` 消息装配。下面是代码采用的包装格式，ID 与正文为示意：

```xml
<worldbook-entry id="book-id:1:tower-gate" title="北塔门禁">
北塔午夜后禁止开门；只有摄政官的书面命令可以解除禁令。
</worldbook-entry>
```

角色本轮因此有机会写出：“守卫看了一眼铜钥匙，仍然摇头：没有摄政官手令，我不能放你进去。”这只是可能的回复；**装入资料不等于模型必然引用、遵守或正确理解了资料。**

依据：[条目正文和默认字段](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/models.py#L17)、[入选结果返回完整渲染正文](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L308)、[Harness 包装为模型消息](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/worldbook/runtime.ts#L67)。

## 2. 谁控制？分清六个控制点

| 控制点 | 谁负责 | 当前代码中的实际动作 |
| --- | --- | --- |
| 世界里有哪些资料 | 内容作者准备；管理员或接入程序提交 | 通过世界书创建、导入、修订 API 保存条目的正文和字段；代码不规定公司内必须由哪个职位操作 |
| 这个会话能使用哪本书 | 产品接入配置与会话创建后端 | 当前 Demo 从服务端配置取固定 `book_id + revision`，创建会话时写 pin；pin 可理解为会话的书籍版本绑定记录 |
| 每条资料何时有资格进来 | 内容作者的字段 + 服务的规则 | 设定启用、常驻、必需、关键词、状态条件、受众、概率、优先级、依赖与冲突等 |
| 这一轮具体挑中谁 | Worldbook Service | 根据 Harness 提交的查询、历史、状态与预算计算入选条目；没有一位聊天模型逐条阅读后拍板 |
| 入选正文放在哪里 | 条目位置配置 + Harness | 条目指定人设前、人设后、消息深度及角色；Harness 按这些字段插入 Actor 输入 |
| 看完资料如何演出 | Actor | 模型结合角色、历史、状态、用户输入和入选正文生成回复 |

**当前 Planner 不在世界书选择的这六步控制链里。** 它做后台分析与状态规划，不负责这一次 `worldbook.select` 的发起或条目选择。

注意“程序选中”的含义：`rules` 使用明确规则；`hybrid` 再引入文本相关性与向量召回，向量环节可能调用 embedding 模型，但这不等于让 Planner 或一个聊天 LLM 决策选条目。

依据：[世界书创建与导入 API](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/app.py#L190)、[修订 API](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/app.py#L281)、[Demo 创建固定 pin](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/worldbook/debug-api.ts#L104)、[服务选择入口](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/service.py#L122)。

## 3. Harness 给选择器什么信息？

每轮会先读已经保存的 Agentic 状态，再调用世界书选择。发送的材料可分为四类：

| 材料 | 实际内容 | 用途与限制 |
| --- | --- | --- |
| 书籍范围 | 会话 pin 中的书籍及固定版本 | 从这些版本里找；本轮不会自动换到最新修订 |
| 查询 `query` | 当前用户输入 + 公开场景的位置、时间、状况 | 最长 8000 字符；不是把整份状态或角色卡拼成搜索词 |
| 对话 `history` | 原请求最后最多 20 条 user/assistant 消息，每条最多 16000 字符 | 提供关键词扫描材料；不是默认把 20 条都扫描，更不是 20 轮 |
| 状态 `state` | 已保存状态若可解析成 JSON，就传解析结果 | 给 `conditions` 判断；解析失败则是空对象，另尝试从文本 Current scene 行补查询场景 |
| 运行设置 | strategy、token_budget、requestId seed、char/user 名称 | 决定策略、预算、可复现的概率筛选和占位符替换 |

选择器默认 `scan_depth=4`，是在“history 消息 + query”这个列表里取最后 4 项。query 本身占一项，因此不能解释为“最近 4 轮”。每个条目可覆盖自己的扫描深度。当前 Harness 不传这个请求字段，所以走服务默认值；它也没有把服务的每一个设置都开放成场景配置。

**关键词扫描和混合相关性召回也不是同一件事。** 关键词会按上述窗口查看历史与 query；hybrid 的文本/向量排名用当前 query。它先召回最多 30 个排名候选，再与常驻、关键词等激活结果一起做最终筛选；30 不是最终必定注入的条目数。

依据：[Harness 准备查询及请求](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/worldbook/runtime.ts#L39)、[服务请求默认值](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/models.py#L119)、[扫描窗口](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L159)、[hybrid 召回入口](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/service.py#L122)。

## 4. 常驻、动态、必需，分别意味着什么？

它们是同一套条目机制中的配置与优先级，不是三份完全独立的资料库。

| 方式 | 激活方式 | 容易误解的边界 |
| --- | --- | --- |
| 普通常驻 `constant=true` | 不要求关键词命中 | 仍要通过启用、受众、条件、概率、正文可用性等检查；仍可能被预算排除 |
| 动态条目 | 关键词命中，或 hybrid 排名进入候选；开启递归时还可能被别的条目触发 | 命中仅代表候选，之后还要检查预算、冲突、依赖等 |
| 必需条目 `required=true` | 不要求关键词命中，优先装配 | 也先经过资格检查；通过后若依赖、冲突或预算无法满足，会报错，不是无上限硬塞 |

整体步骤是：**先检查有无资格 → 再激活候选 → 按优先级处理依赖包、冲突与预算 → 返回完整正文。**

当前场景文件设置 `hybrid`、世界书预算 `2400 tokens`；服务自身的通用默认预算是 `1500`，两者不要混用。服务默认常驻预算比例为 `0.35`，所以在 2400 的配置下，普通常驻批次的新增占用受 840 tokens 上限限制。它是上限，不是预留空间；必需条目不受该常驻上限限制，但仍受总预算限制。依赖包和共享依赖会影响新增占用统计，不能简单理解成“数一下所有 constant 正文长度”。

默认最多选 50 条，但很多时候先碰到 token 预算。条目连同依赖作为包处理，装不下通常整体放弃，不把正文截一半凑数。required 包出现无法满足的情况则报错。

关键词也不只有简单字符串：可配置大小写、全词、正则、主次关键词逻辑。依赖 `requires` 能带入同版书中的其他合格条目；`conflict_key` 阻止同一冲突组同时入选。递归可让已激活正文触发其他条目。这里要区分入口：原生 Entry 默认 prevent_recursion=true、exclude_recursion=true；SillyTavern/角色卡文件导入时，normalize_entry 对省略的这两个字段默认填 false，因此导入条目可能参与递归。最终要看保存后的条目字段，且仍受最大递归轮数约束。[导入默认值](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/importer.py#L186)。

hybrid 的向量后端不可用时可以降级到词法排名，并返回降级原因；这不是所有世界书错误都能忽略。例如必需包超预算仍会报错。

依据：[场景实际设置](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/config/scenarios/agentic-v2-worldbook.json#L11)、[资格检查](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L123)、[依赖、优先级与预算](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L202)、[向量失败降级](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/service.py#L67)。

## 5. “原文”“直接”“本轮”分别是什么意思？

**原文：主要是条目的 `content`，没有先让聊天 LLM 总结成另一段话。** 服务支持替换 `{{char}}` 与 `{{user}}`，再由 Harness 做标签字符转义与包装；因此不是每个字节完全不变。标题和 ID 进入标签；关键词、条件、priority 等主要用于选择，不会把整份条目 JSON 原样展示给 Actor。服务返回的 `blocks` 是一条条结构化结果，Harness 使用它们，而不是只把一个总 `context` 字符串粗暴追加到末尾。

**直接：这次模型开始生成前，正文已经进入它的消息输入。** 不需要 Actor 先发起世界书工具调用，再等工具回传。世界书召回仍要耗时，属于当前回复前的准备工作。

**本轮：这一个用户输入对应的逻辑请求。** 同轮模型重试复用已经选好的快照；下一次用户输入会重新选择，因此两轮可能用不同条目。该临时资料不写入正式聊天历史，不因为注入过一次就成为永久记忆。

例如第一轮谈北塔，第二轮换到南港：第二轮不一定马上排除北塔，因为扫描窗口还可能含上轮对话；同时南港条目可能入选。准确规则是每轮按当前窗口重新计算，而不是每换一个话题就立即清空旧地点资料。

位置默认 `before_character`，即人设前；还支持 `after_character` 与 `at_depth`。默认消息角色为 `system`，也允许 user/assistant。`at_depth` 按渲染后的消息块计数，不是按对话轮数；depth=0 在尾部。后续提供商适配还可能合并消息，所以不要把它当最终 HTTP 请求中永久固定的数组下标。

这些位置和 role 是内容数据/接入配置控制，不是每次让 Actor 或 Planner 自行决定。

依据：[支持的正文替换](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L89)、[请求级缓存与位置装配](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/worldbook/runtime.ts#L22)、[装配进入实际 Actor history](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/integrations/roleplay-axon-adapter.ts#L504)。

## 6. Planner 没收到同一批条目，具体是什么差别？

**Actor 与 Planner 是分别发出的模型请求。每个请求只看到程序给它准备的材料，不会自动共享另一请求的上下文。** 即使使用同一家模型服务，甚至相同模型名，也不能推导出“Actor 已读过，Planner 自然知道”。

下面比较材料来源，省略实际模板、裁剪、消息合并；不是最终请求数组的精确顺序：

| 本轮输入材料 | Actor 的请求 | Planner 的请求 |
| --- | --- | --- |
| 人设 | 正文用的角色模板与定义 | 分析用的角色定义与模板 |
| 用户输入、已有对话 | 使用前台渲染与预算后的版本 | 独立组织最新问题与近期 transcript；当前世界书场景来自原请求历史 |
| 已保存状态 | 写作用投影 | 分析用投影，两边裁剪范围可能不同 |
| “北塔午夜后只有手令才能开门” | 本轮入选后，收到完整条目正文 | 没有直接收到这次 selection 的该条消息 |
| “铜钥匙不能代替手令” | 本轮入选后，收到完整条目正文 | 没有直接收到这次 selection 的该条消息 |
| 本轮 Actor 新生成的守卫回答 | Actor 自己正在生成 | 并行规划时没有收到 |

数据层的原因很具体：Actor 使用世界书装配后的 `axon.history`；Planner 的 `planningContext.history` 则单独选自原请求消息，且构造对象中没有把选出的世界书 `blocks` 或 `context` 放进去。Planner 的实际模型调用再使用这个分析上下文生成自己的 messages。

**这不等于 Planner 对北塔一无所知。** 如果相同门禁规则已经写在角色定义、旧状态、它这次看到的历史里，它可以通过那些材料知道。准确结论是：没有保证这次 Actor 新读到的世界书原文同时提供给 Planner。

延续例子：假设门禁规则只存在世界书中，之前对话与状态都没记载，Actor 可以根据它拒绝开门；Planner 却可能仅从“用户递出旧王室钥匙”建议“推动守卫放行”。这是输入不一致带来的可能性，并不是代码证明每次都一定冲突。

如果 Actor 最终把“必须有手令”说在真实回复里，宿主保存后、未来轮次再带回且被保留在 Planner 的窗口内，Planner 才可能通过对话间接知道。若 Actor 只是参考了规则但没有说出来，就不存在这条自动传递路径。**后台状态更新本身也不能自动补上未提供的整段世界书知识。**

另一个关键点：条目的 `audience=["actor","planner"]` 只是表示选择请求的 audience 可以通过这条资格检查。当前 Harness 的选择请求默认按 actor 运行，且只把结果交给 Actor。**多写一个 planner 受众，并不会自动新增 Planner 检索或转发。** 让二者共享这批条目需要明确接线；本次没有改动这个行为。

依据：[Actor 与规划上下文准备](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/planning/runtime.ts#L338)、[Planner 历史来源](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/planning/runtime.ts#L402)、[planningContext 的实际字段](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/planning/runtime.ts#L450)、[受众资格检查](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L132)。

## 7. PM 到底应该准备什么内容？

下面是基于现有机制的内容组织建议，不是仓库已经自动实施的规则。

| 要表达的内容 | 适合放哪里 | 例子 |
| --- | --- | --- |
| 全局且稳定的世界规则 | 小而必要的世界书基础条目；是否 constant/required 要结合预算和失败策略 | 魔法不能复活死者 |
| 特定地点、物品、人物背景 | 动态世界书条目，设定触发词与必要条件 | 北塔门禁、铜钥匙含义、旧王室历史 |
| 当前已经发生的变化 | 持续状态或已接受对话对应的记忆 | 用户手里的钥匙已经交给守卫、塔门刚被破坏 |
| 角色今后可能怎么做 | 规划/叙事建议，明确区分计划与事实 | 可以在后续制造验证钥匙真伪的冲突 |
| 以前实际发生的经历 | 对话历史、摘要或记忆模块 | 用户上周曾帮助这位守卫 |

例如“铜钥匙代表旧王室身份”可以是世界书背景；“铜钥匙现在在谁手里”通常属于动态状态。把后者也固定写入书籍旧版条目，会让故事推进后的状态与静态资料竞争。当前服务不会自动替 PM 判断这两句话该放在哪个模块。

不要把全世界所有规则都标 required：它会增加无法满足预算时的失败机会。也不要把“角色永远遵守规则”理解为一个 constant 开关能保证的模型行为。

## 8. 当前 Demo 页面，用户实际能控制什么？

| 动作 | 当前 Demo 是否支持 | 含义 |
| --- | --- | --- |
| 从配置列表选择角色、发消息 | 支持 | 选择 prompt，输入正文 query |
| 浏览固定世界书、搜索标题/关键词/正文、查看参数 | 支持 | 界面查阅；搜索框只是过滤展示列表，不改变本轮检索输入 |
| 勾选几条，指定本轮必须注入 | 不支持 | 没有本轮人工条目覆盖入口 |
| 在聊天页面调整 strategy 或 tokenBudget | 不支持 | BFF 从服务端 scenario 取设置 |
| 编辑、导入、改版、构建索引、切换书籍 | 当前 Demo 不支持 | 通用服务仍有管理 API；不能把 Demo 只读误解为服务不支持 |
| 查看这轮真正装入了哪些条目 | 支持“本轮世界书”快照 | 展示正文、ID/版本、理由、位置及筛选决策 |

书籍绑定由 `WORLDBOOK_DEMO_CONFIG_JSON` 控制；新会话由后端写入固定 pin。Demo 当前只配置一本固定版本的书，通用服务请求允许多本书，这两层能力不同。书更新到 revision 2 后，原来固定 revision 1 的会话不会自动切换；Demo 改固定配置后，旧 pin 也会被当前范围校验拒绝，而非悄悄改绑。

“本轮世界书”快照在首次 Actor 装配时通过 SSE 发回，可见正文和被排除原因。它不能证明模型遵守了内容，也不是完整模型输入日志。快照保存在浏览器工作区，服务端聊天历史不会补存整份快照；换浏览器或清缓存后，不能依靠历史 API 恢复它。没有快照记录与确实选中零条，是两种不同状态。

依据：[只读范围与固定版本校验](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/worldbook/demo-config.ts#L23)、[BFF 采用服务端检索设置](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/worldbook/debug-api.ts#L137)、[界面搜索只过滤显示列表](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/worldbook/web/src/main.tsx#L140)、[正文保存与 trace 分流](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/worldbook/debug-api.ts#L149)、[界面与恢复说明](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/docs/worldbook-integration.md#L1)。

## 9. 当前能力能承诺到哪里？

可以确认：会话固定书籍范围，回复前自动选择条目，按配置位置加入 Actor，本轮重试复用选择结果，并在 Demo 中展示装配快照。

不能据此承诺：所有常驻条目每次都在、注入正文必然被遵守、知识专家已对照同一批原文核验、世界书原文自动写入长期记忆、用户当前在页面点开的条目必然进入下一次回复。

如果产品需要“前台按规则演出，后台也基于同一规则规划”，还需明确一个当前没有完成的数据连接：**把允许 Planner 看到的世界书资料提供给它，并定义角色可见与后台可见的范围。** 这是从现有代码得出的接入缺口，不是本次已经实现的修改，也不能仅凭当前代码确定原作者为何选择这种边界。
