# 世界书条目怎样进入本轮 RP

[交互项目地图](project-map.html#worldbook-selection) · [选择流程图](worldbook-selection.html) · [RP 例子](project-map.html#worldbook-rp) · [两个仓库的完整关系](worldbook-sources.md)

源码基线：Worldbook `3a11e333afd335aeff165e0ccc3654af8e1897d1`；Harness `80aef3fa5582d8508ecdd1afa206391940ce9502`。下面区分服务默认值与 Harness 传入值，不把它们当成已核实的线上配置。

## 先看一轮

**用户输入 → 取本轮状态与固定世界书版本 → 过滤条目 → 常驻 / 关键词 / hybrid 等途径激活 → 检查依赖、冲突与预算 → 返回入选原文 → 放进 Actor 输入 → 生成回复。**

这里的“动态加载”是每轮从已经保存的资料里选内容。选择器是 Python 规则程序；hybrid 可调用 embedding 模型检索，但没有让一个聊天 LLM 阅读整本书并决定选哪些条目的步骤。选择期间也不自动生成新条目或推进剧情状态。[选择编排](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/service.py#L122) · [确定性选择器](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L106) · [词法检索](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/lexical.py#L1)

## 1. 常驻、动态、必需有什么区别？

| 便于理解的类别 | 字段 | 怎样参选 | 预算不够时 |
| --- | --- | --- | --- |
| 必需 | `required=true` | 过资格过滤后，不需要关键词 | 已激活的必需包放不下会报错 |
| 常驻 | `constant=true` 且不是 required | 过资格过滤后，每轮直接参选 | 可以整组丢弃，还受常驻包成本上限约束 |
| 动态 | 两个字段都为 false | 关键词触发，或 hybrid 排名候选；另可被显式依赖带入 | 可以整组丢弃 |

`required`、`constant` 是两个独立布尔字段，表格按选择器的处理优先级分组；同时为 true 时先按 required 处理。**常驻表示免关键词，不表示永久保存在模型上下文，也不表示每轮一定入选。** [条目字段](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#L178) · [预算顺序](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L239)

所有类别都先过这些资格检查：启用、目标受众、状态条件、运行时支持、概率、模板替换和非空正文。`required=true` 也不能绕过它们；例如必需条目被 disabled 排除时，不会因为“必需但没入选”在这里自动报错。[资格过滤](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L127)

## 2. 动态条目具体怎么触发？

### 关键词这条路

选择器把传入的历史消息内容与当前 query 合并，再取最后 `scan_depth` 条。默认是 **4 条消息，包含当前 query**，不是 4 轮；条目自己的 `scan_depth` 可以覆盖请求默认值。`scan_depth=0` 时，第一轮关键词扫描文本为空。[扫描文本来源](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L126) · [扫描窗口](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L161) · [请求默认值](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/models.py#L119)

- 主关键词 `keys` 至少命中一个；空 keys 不会靠关键词途径激活。
- 普通字符串默认不区分大小写、做子串匹配；可启用整词边界。
- `/pattern/flags` 形式可用正则，单次匹配超时 5ms；不合法或超时的条目记录未选原因。
- `selective=true` 且有 `secondary_keys` 时，还要判断次关键词：`and_any` 至少一个命中，`and_all` 全命中，`not_any` 一个都不能命中，`not_all` 不能全部命中。主关键词仍要先命中。

这些是关键词途径的触发条件。**hybrid 候选激活与显式依赖带入，不以通过关键词或次关键词逻辑为前提。** 如果业务需要“无论怎样召回都不能绕过”的限制，应在现有资格条件中表达，不能把次关键词当成全局硬过滤。[匹配细节](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L50) · [次关键词逻辑](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L73) · [hybrid 候选激活](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L187) · [依赖展开](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L202)

### hybrid 补充召回这条路

`rules / hybrid` 是一次选择请求的策略，和条目上的 `constant / required` 是不同设置。

| 策略 | 动态候选来源 | 常驻 / 必需 |
| --- | --- | --- |
| rules | 关键词，可选递归与显式依赖 | 仍然参与 |
| hybrid | 保留规则路径，额外加入词法 + 向量的排名候选 | 仍然参与 |

hybrid 用**当前 query**做检索；Harness 的 query 是本轮用户输入加公开场景。它不会直接把全部历史拼进向量 query。词法检索是 BM25：检索标题、keys 和正文，标题权重通过重复 3 份、keys 重复 2 份实现；中文使用单字和双字切词。它和“在最近消息里匹配触发 keys”是两个算法。[词法切词与排名](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/lexical.py#L9) · [Harness query 构造](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/worldbook/runtime.ts#L39)

词法、向量排名用 RRF 合并，最终最多提供 **30 个 hybrid 候选**给选择器；这是候选数量，不是最终固定插入 30 条。当前 selector 对排名名单中的条目没有另设最低相似度门槛；它们第一轮可以以 `hybrid` 原因激活，随后还要过完整资格过滤、依赖、冲突和预算。向量不可用时，hybrid 记录 degraded 并回退词法排名。[排名合并与降级](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/service.py#L67) · [hybrid 预筛与候选数](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/service.py#L130) · [按名单激活](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L187)

因此“用户没提海港”不等于 hybrid 一定排除海港；也不能把“语义相关”理解成代码保证的准确率。

### 条目能不能带出另一个条目？

能，存在两种机制。

1. **递归触发：** 已激活条目的正文可以成为下一轮关键词扫描的补充文本。请求默认 `max_recursion_rounds=2`，实际循环包括第 0 轮和最多 2 轮扩展。原生 Entry 模型默认 `prevent_recursion=true`、`exclude_recursion=true`：前者阻止它把正文供给其他条目触发，后者阻止它自己在扩展轮被触发。但文件导入器对省略的这两个字段默认填 `false`，所以不能把原生默认值套到所有导入世界书；应查看保存后的条目参数。`exclude_recursion` 不阻止初始轮，`prevent_recursion` 不阻止自己入选。[文件导入默认值](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/importer.py#L186)。
2. **显式依赖：** A 的 `requires` 指向同一本书、同一 revision 内 B 的 entry id。A 参选时把 A+B 视为整体；B 可以没有关键词命中，但必须通过资格过滤。依赖不存在、被过滤、过深或包太大会使该包不可用；创建书时也检查缺失引用和循环依赖。

递归激活发生在预算选择之前：某个条目最后因预算被丢掉，它之前触发出的其他候选不一定跟着丢掉。显式依赖才是成组留存约束。[原生条目递归默认值](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/models.py#L31) · [创建时依赖校验](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/models.py#L56) · [递归先于预算](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L159) · [同版本依赖](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L202)

## 3. 选中的太多，留谁？

先处理 **required → 可选 constant → 动态**。每组内部依次比较：`priority` 高的优先，关键词直接命中的优先，hybrid 分数高的优先，`order` 大的优先，最后用完整条目 id 稳定排序。注意这是“谁先争取预算”；最终 Prompt 内的显示顺序是另一套排序。[候选排序](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L216) · [三组先后](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L240)

每个候选会连同 `requires` 依赖形成一个包：

1. 依赖不可用：普通候选丢弃；required 报 `required_dependency_unavailable`。
2. `conflict_key` 与已选包重复，或包内自相冲突：普通候选丢弃；required 报 `required_conflict`。
3. 加入包后超过条目数或 token 预算：普通候选丢弃；required 报 `budget_exceeded`。
4. 可选 constant 发起的包还检查 `constant_budget_ratio`。

**整条、整包取舍，不把长条目切一半凑预算。** 最终服务 token_count 包含返回 context 的条目 XML 包装和间隔，不只是正文字符数。[依赖、冲突与预算决策](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L247) · [计数内容包装](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L101)

### 35% 到底什么意思？

服务默认 `constant_budget_ratio=0.35`。Harness 此场景传 `token_budget=2400`，所以通常可选常驻包的新增成本上限为 **840 token**。这是上限，**不是预留 840 个 token 的空槽**；不用满的部分没有锁死。整体选择仍以 2400 和条目数上限为准。

精确记账方式：只有以“可选 constant”为本轮包的发起候选时，包所增加的总成本才累计到 `constant_spent`，包括顺带依赖的成本。required 包不计入这一计数；已被其他包带入的条目不重复收费。因此它不是“最终所有 constant 标记条目的字数永远不超过 35%”这种严格全局限制。[常驻上限](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L237) · [新增成本与累计](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L279)

## 4. RP 时怎么使用？

在已启用的 Worldbook 场景，Harness 准备本轮状态后、Actor 调模型前，执行一次世界书准备。同一逻辑轮内缓存选择 Promise；模型重试复用结果。没有绑定书时跳过 RPC。[Actor 前选择](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/planning/runtime.ts#L338) · [本轮缓存](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/worldbook/runtime.ts#L22)

世界书返回的每个 block 带完整原文与插入信息。Harness 包装成 `<worldbook-entry id="…" title="…">正文</worldbook-entry>`，保留原角色设定和对话，并按照下表加入额外消息：

| position | 放在哪里 |
| --- | --- |
| before_character（条目默认） | 第一段角色相关设定之前；找不到角色锚点时回退到最前 |
| after_character | 最后一段角色相关设定之后；找不到角色锚点时回退到开头连续 system 消息之后 |
| at_depth | 从待装配消息尾部按 depth 向前定位，但不越过开头连续 system 区；`depth=0` 在尾部，可能位于当前用户输入之后 |

`role` 默认 system，也支持 user / assistant。**位置 depth 数插入时已经渲染的消息块，和触发窗口 scan_depth 是两个参数；它们都不是对话轮数。** 先插 before / after 条目再计算 depth，后续相邻同角色消息还可能合并，所以不能把 depth 当成最终 provider 消息编号。服务返回 blocks 时按 position、depth 降序、order 升序和 id 排序，Harness 再按位置插入。[条目位置与角色默认](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/models.py#L34) · [输出顺序](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L228) · [Harness 注入实现](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/worldbook/runtime.ts#L67) · [相邻消息合并](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/integrations/roleplay-axon-adapter.ts#L261)

可以把默认输入理解成一叠材料：

```text
世界书原文（默认 before_character / system）
角色设定（原有 Prompt）
近期对话历史
本轮用户输入
            ↓
        Actor 生成回复
```

上面仅示意默认位置。配置 after_character / at_depth 后，世界书会插到相应位置。模板只支持已提供的 `char`、`user` 变量；脚本、iframe、运行时模板等不会在这里执行。世界书内容影响模型生成，但没有额外机制保证回复必然逐条遵守资料。[模板与运行时限制](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/selection.py#L89)

最终还要检查**完整 Actor Prompt**的模型预算：世界书 2400 只是世界书部分，角色设定、历史、其他提示也需要空间。注入条目标记 `historyCount=0`，不算真实聊天记录；不应把世界书预算不足理解成自动裁剪真实历史后总能成功。世界书装饰后的消息直接用于 Actor；并行 Planner 从它自己的原始 / 压缩历史构造消息，不直接收到同一份世界书 blocks。若世界书事实体现在已接受的回复里，它之后可能作为普通历史被看见。[完整模型预算](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/actor/roleplay-runtime.ts#L842) · [注入元数据](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/integrations/roleplay-axon-adapter.ts#L618) · [Planner 历史来源](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/planning/runtime.ts#L402)

## 5. 一个完整的小例子

**虚构示意，不是仓库里的真实世界书或模型测试结果。** 为了看清关键词，假设 `strategy=rules`、所有条目通过资格检查、扫描窗内没有海港、预算足够且无冲突。

| 书里的资料 | 配置 | 用户说“我推开北塔大门”后 |
| --- | --- | --- |
| 魔法需要付出代价 | constant=true | 不需要提到“魔法”，直接参选并在预算内入选 |
| 北塔午夜封门，由守卫把守 | keys=[北塔] | 命中北塔，动态入选 |
| 海港由商会控制 | keys=[海港] | 未命中，未入选 |

Actor 实际收到的是：**原角色设定 + 对话历史 + 当前用户输入 + 入选的两条世界书原文**。它可能写出“门后的守卫拦住你，提醒你任何开门魔法都要付出代价”之类的内容。措辞只是示意；资料是供模型参考的上下文。

切到 hybrid 后，第三条不能只因没命中海港关键词就断定不入选；还要看召回名单。下一轮如果北塔仍在默认最近 4 条消息里，即使新 query 没提北塔，关键词途径也可能继续触发；滑出窗口后，仍可能由 hybrid、递归或依赖途径带入。

## 参数速查：哪些默认值属于哪一层？

| 参数 | 世界书服务默认 | 当前 Harness 调用 |
| --- | --- | --- |
| strategy | rules | 此场景配置 / 兜底 hybrid |
| token_budget | 1500 | 此场景配置 / 兜底 2400 |
| history | 空列表 | 最近 20 条 user / assistant，每条最多 16000 字符 |
| scan_depth | 4 条，包含 query | 未显式传，采用服务默认；条目可覆盖 |
| audience | actor | 未显式传，采用服务默认 |
| constant_budget_ratio | 0.35 | 未显式传，采用服务默认 |
| max_recursion_rounds | 2 次扩展，范围 0–5 | 未显式传，采用服务默认 |
| max_entries | 50，范围 1–200 | 未显式传，采用服务默认 |
| seed | 字符串 0 | 本轮 requestId，配合 entry id 决定概率过滤 |
| template_vars | 空字典 | char / user |

字段在服务 API 或条目中可配置，**不等于当前 Harness 已给每个字段提供场景配置或 UI**。本次所读调用只显式传策略、预算、书版本、query、history、state、seed 和模板变量。[服务请求契约](https://github.com/FlowGPT/worldbook-service/blob/3a11e333afd335aeff165e0ccc3654af8e1897d1/src/worldbook/models.py#L119) · [实际请求负载](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/worldbook/runtime.ts#L49) · [Harness 配置](https://github.com/FlowGPT/roleplay-harness/blob/80aef3fa5582d8508ecdd1afa206391940ce9502/apps/emochi/server.ts#L747)

此次仅核查源码与更新图示，没有启动生产服务、改动业务配置、执行真实检索或调用付费模型。

## 图示交付验证

新增选择图为 workflow，showcase 9/9 校验、0 错误与 0 警告；四种桌面尺寸无溢出，最小 / 最大尺寸明暗截图及导出 PNG 已人工查看。精确规格和 HTML 哈希见[交付回执](project-map-receipts.json)。阅读器做了静态检查，未实际测试浏览器导航。
