# 世界书：保存设定，并在每次生成前选择需要交给模型的原文

世界书适合保存作者事先写好的地点、人物、规则和背景。例如“灰港没有电力”“铜钥匙只能开侧门”“守卫放行前要核对手令”。接入方把这些资料存进服务；用户说“我把铜钥匙递给守卫”时，服务选择本轮相关条目，把原文和放置位置交回来。接入方再把它放进回复模型的输入，模型才有机会按这条规则写出“手令呢？”这样的回应。

**可复用的交付物是独立 HTTP 服务及源码；它本身不生成 RP 回复，也不是默认挂在主 Agent 工具菜单里的一个 function tool。** 当前 Harness 已有一条专用世界书接线；客户也可以从自己的后端直接调用服务，把结果交给自己的模型或工作流。[服务入口](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/app.py#L216) · [Harness 接线](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L765)

## 能拆出来使用的能力

| 能力 | 交什么进去 | 拿到什么 | 谁继续处理 |
|---|---|---|---|
| **世界书创建与版本管理** | 一本书的名称、条目原文及触发规则 | `book_id`、不可变 `revision`；可读取旧版本 | 内容管理后台保存 ID；发布修改时提交新版本 |
| **角色卡／世界书文件导入** | CCv2/v3 JSON 或 PNG 中的世界书、SillyTavern 世界书 JSON | 归一化条目、支持范围报告；保存后得到书籍 ID | 运营检查被阻止的动态内容，再决定使用或改写 |
| **Prompt 与世界书关联** | 角色／作品 `prompt_id`、书籍 ID、固定或跟随最新的版本策略 | 已保存关联；运行时解析出的具体版本 | 宿主每轮重新解析，再使用这个版本选条目 |
| **词法／向量搜索** | 查询文本、允许查询的书籍范围 | 排序结果、分数、最多 600 字符的原文摘录 | 搜索页面或自己的召回程序；它不是最终模型上下文 |
| **本轮条目选择** | 本轮输入、历史、状态条件、书籍版本、预算 | 原文 `blocks`、聚合 `context`、每条入选／排除理由 | 模型输入组装程序按位置放入条目 |
| **指定版本索引构建** | 书籍 ID、版本、是否强制重建 | 索引状态、分块数量、Embedding 版本 | 部署／运营侧维护；规则选择不需要向量索引 |
| **原文位置装配** | 条目返回的 `position`、`role`、`depth` 等 | 按角色资料、历史或指定区域装好的模型消息 | Harness 的 Actor 接线已实现；自建宿主需实现或复用 SDK 装配函数 |

## 一次使用从准备到生效

1. **运营准备设定**：把独立规则写成条目。为“灰港基本设定”标记常驻；为“铜钥匙与城门守卫”填写关键词“铜钥匙、城门”。原文由作者／运营提供，不是服务自动生成。
2. **保存一本书**：调用原生创建接口。服务把原文、条目和版本写入 PostgreSQL，返回书籍 ID 与版本 1。以后修改是发布版本 2，不覆盖版本 1。
3. **决定谁使用它**：若走现成 Harness 接线，把书关联到角色／作品的 `prompt_id`；若走独立服务，自己的程序在每次请求中直接提供书籍 ID 与版本。
4. **用户发来一句话**：“我把铜钥匙递给守卫。”宿主取得这次允许使用的书籍版本，提交本轮输入与状态。
5. **服务选出本轮原文**：“灰港基本设定”因常驻进入候选，“铜钥匙与城门守卫”因关键词进入候选；通过条件与预算后返回两段原文。返回原因分别是 `constant`、`keyword`。
6. **回复前注入**：宿主把这两段文字放到角色资料前，再与角色、历史、本轮用户输入一起发给回复模型。模型生成的守卫回应受这份资料影响；选择成功不等于模型一定遵守，仍需业务侧验收实际回复。
7. **下一轮再算**：书籍原文继续留在数据库；本轮选中的两段原文不会因此变成永久聊天历史。下一次输入重新选择，已关联的固定版本／最新版本策略决定用哪份书。

原文选择和位置装配发生在生成前，会占用本轮准备时间。它不是等后台累计若干轮再执行的记忆任务。[逐轮请求与选择](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/worldbook/runtime.ts#L59)

## 运营准备什么数据

**推荐从原生 JSON 开始。** 下面整个 JSON 对象可以直接作为 `POST /v1/books` 的请求体。`name` 是书名；`entries` 中每项是一条设定；`content` 就是希望模型读到的原文。不要把书籍 ID、租户或 Prompt ID 写进这个对象，它们属于保存结果或关联关系。

```json
{
  "name": "灰港世界设定",
  "description": "演示：常驻世界规则与按铜钥匙触发的城门规则。",
  "entries": [
    {
      "id": "greyport-background",
      "title": "灰港基本设定",
      "content": "灰港是一座蒸汽城市，城内没有电力，夜晚依靠煤气灯照明。",
      "constant": true,
      "position": "before_character",
      "audience": ["actor"]
    },
    {
      "id": "copper-key",
      "title": "铜钥匙与城门守卫",
      "content": "铜钥匙只能开启灰港城门的侧门。守卫在放行前必须核对来访者的手令，不能仅凭钥匙放人。",
      "keys": ["铜钥匙", "城门"],
      "position": "before_character",
      "audience": ["actor"],
      "priority": 20
    }
  ]
}
```

运营首先需要填好 **条目名称、原文、何时使用**。常驻条目用 `constant: true`；动态条目用 `keys`。每项给一个稳定且不重复的 `id`，方便后续修改与定位。同一条目可以写多条触发词，命中任意主关键词就有机会进入本轮候选。

**原生 JSON 和文件导入是两个入口。** 这个示例应提交到 `/v1/books`，批量原生书则提交 `{"books":[上述对象]}` 到 `/v1/imports`。不要把原生 JSON 当作 ST 文件上传到 `/v1/imports/files`：文件入口会按 ST／角色卡规则重新归一化，字段含义和保存方式不同。[原生模型](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/models.py#L46) · [两个导入入口](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/app.py#L216)

<details>
<summary>原生数据格式、校验与字段默认值</summary>

| 字段 | 含义、默认值与边界 |
|---|---|
| `name` | 必填，1–300 字符；`description` 默认空，最多 10,000 字符 |
| `entries` | 默认空数组，一本最多 5,000 条；原生批量入口一次 1–10 本 |
| `id` | 同一版本内唯一，最多 150 字符；建议明确填写。省略时程序按顺序补 `entry-0` 等 |
| `title`／`content` | 标题默认空，最多 500 字符；原文 `content` 必填，最多 200,000 字符。空原文不进入本轮上下文 |
| `enabled` | 默认 `true`；关闭后仍可保存、查询，默认不参与本轮选择 |
| `constant`／`required` | 均默认 `false`；常驻和必需的实际规则见后文，二者并非同义 |
| `keys`／`secondary_keys` | 主／次关键词数组，分别最多 200 个，每项最多 500 字符；默认空 |
| `selective`／`logic` | 默认 `false`／`and_any`；开启且有次关键词时，在主关键词命中后额外按次关键词条件过滤 |
| `case_sensitive`／`match_whole_words` | 默认均 `false`；可选择区分大小写、匹配完整词；支持有限正则 |
| `probability` | 默认 100，范围 0–100；依赖请求 `seed` 与条目 ID 作确定性抽选 |
| `scan_depth` | 条目自己的扫描深度；默认 `null`，使用请求层的深度；范围 0–100 |
| `conditions` | 最多 20 项，按调用方 `state` 的路径检查；操作为 `eq`、`ne`、`in`、`exists` |
| `requires` | 最多 20 个同一本书同一版本内的条目 ID；发布时拒绝缺失依赖、自依赖及循环依赖 |
| `priority`／`order` | 默认 0，范围 ±1,000,000；前者影响优先选择，后者参与选择排序与最终装配顺序 |
| `conflict_key` | 同一冲突组只能选一个条目；默认 `null` |
| `audience` | 默认 `["actor"]`；请求的 audience 必须在条目允许列表内，它不会主动把内容发给该模型 |
| `kind` | `lore`、`character`、`rule` 可作为静态资料；`template`、`state_update`、`presentation` 被默认选择器排除 |
| `position`／`depth`／`role` | 默认 `before_character`／0／`system`；实际可用位置由宿主声明，见后文 |
| `exclude_recursion`／`prevent_recursion` | 原生条目默认均 `true`：不被递归内容激活，也不把本条内容用于激活别人；ST 导入有其映射默认值 |
| `runtime_requirements` | 默认空；非空表示需要尚未提供的运行环境，本轮不会直接注入 |
| `metadata`／`source` | 书籍附加信息／条目来源信息；不会替代 `content` 自动成为模型上下文 |

数据模型拒绝未知字段。把 `tenant_id`、`book_id`、`prompt_id` 随意混入创建书籍 JSON，会触发校验错误，而不是自动建立绑定。[完整契约](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/models.py#L16)

</details>

## 已有 ST／角色卡资料怎样导入

**ST 指 SillyTavern。当前代码支持它的静态世界书资料导入，不是完整运行 SillyTavern。** 文件可以是独立 world-info JSON，也可以是 CCv2／CCv3 角色卡 JSON 或 PNG 中嵌入的 `character_book`。这里提取的是世界书；角色卡的其他角色字段不会自动变成此服务管理的角色产品。[解析格式与报告](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/importer.py#L208)

| 顺序 | 入口 | 结果与操作 |
|---|---|---|
| 1. 预览 | `POST /v1/imports/preview`，multipart 字段为 `file` | 返回 `book` 和 `import_report`，不落库。先看条目数、被阻止的条目和 warning |
| 2. 保存 | 原文件走 `POST /v1/imports/files`，multipart 字段为 `files`；若已修改归一化 `book`，走原生 `/v1/books` | 一批文件全部解析成功才进入同一保存事务，返回书籍 ID 和 revision |
| 3. 建索引（需要向量时） | `POST /v1/books/{book_id}/index` | 显式构建本次精确版本的向量索引；规则／词法使用不依赖这步 |
| 4. 关联或调用 | 保存 Prompt 关联，或自己的程序直接传书籍版本 | 保存文件不自动关联某个角色，也不自动修改已有聊天 |

预览报告中的 `runtime_blocked_count` 是需要额外运行环境的条目数。EJS 模板、MVU 状态更新、CharInfo 人物生成协议、脚本／前端面板、sticky／cooldown／delay 等动态功能，不会因为导入成功就执行；带这类要求的条目会被默认上下文选择排除。普通 `{{char}}`、`{{user}}` 可以替换，但调用方必须传入对应变量。[动态能力识别](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/importer.py#L117)

<details>
<summary>文件上传请求、重复导入和发布新版本</summary>

以下命令运行在能访问内网服务的开发环境；`tenant_id=local` 是示例租户。`WB_BASE` 应设置为实际服务地址。

```sh
WB_BASE='http://127.0.0.1:8876'
curl --fail-with-body -sS "$WB_BASE/v1/imports/preview?tenant_id=local" \
  -F 'file=@world-info.json'
curl --fail-with-body -sS "$WB_BASE/v1/imports/files?tenant_id=local" \
  -H 'Idempotency-Key: greyport-file-import-v1' \
  -F 'files=@world-info.json'
```

单文件默认上限 16 MiB，批量文件和整个 HTTP body 默认各 48 MiB；一次 1–10 个文件。PNG 只读取支持的内嵌卡片元数据，不执行脚本，也不抓取其中的远程链接。

创建／导入接口支持 `Idempotency-Key`：同租户、同 key、同内容返回同一结果；同 key 换内容返回 409。原生 JSON 不传 key，每次会新建一本书。原文件按文件 SHA-256 去重；重复上传同一个原文件返回最初的 revision 1，而不是后来编辑出的最新版。

编辑已有书要调用 `POST /v1/books/{book_id}/revisions`，提交**新版本全量书籍内容**，并加 `expected_revision`。例如当前版本 1，则提交 `expected_revision: 1`，成功返回版本 2；如果别人先改过，返回 `revision_conflict`，重新读取后再发布。此接口不是单条条目 PATCH；省略的旧条目不会自动继承进新版本。[创建与去重](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/repository.py#L103) · [全量修订](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/repository.py#L340)

</details>

## 怎样独立接入：一次创建与选择的最小请求

这条路径不依赖 Agent V2、Story 或 Axon。你的后端调用世界书 HTTP API，再负责把结果装进自己的模型请求。

**第一步：创建。** 将前面的原生 JSON 保存为 `worldbook-native-example.json`，提交：

```sh
WB_BASE='http://127.0.0.1:8876'
curl --fail-with-body -sS "$WB_BASE/v1/books?tenant_id=local" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: greyport-native-v1' \
  --data-binary @worldbook-native-example.json
```

返回的结构如下。UUID 是示意，实际使用服务返回的值：

```json
{
  "books": [{
    "book_id": "00000000-0000-0000-0000-000000000001",
    "revision": 1,
    "deduplicated": false,
    "import_report": {"format": "native", "entry_count": 2}
  }],
  "replayed": false
}
```

**第二步：选择。** 把真实 `book_id` 替换进下列请求，保存为 `select.json`。书籍可固定 `revision`；为了同轮结果稳定，接入方应先解析出具体版本，再在同一轮中复用。

```json
{
  "books": [{"book_id": "00000000-0000-0000-0000-000000000001", "revision": 1}],
  "query": "我把铜钥匙递给守卫。",
  "history": [],
  "state": {},
  "audience": "actor",
  "strategy": "rules",
  "scan_depth": 4,
  "token_budget": 1500,
  "seed": "example-turn-1",
  "template_vars": {"char": "守卫", "user": "旅人"}
}
```

```sh
curl --fail-with-body -sS "$WB_BASE/v1/context/select?tenant_id=local" \
  -H 'Content-Type: application/json' \
  --data-binary @select.json
```

下面是返回中一条 `blocks` 元素的完整形状。真实响应还包含另一条常驻设定和整体诊断：

```json
{
  "id": "00000000-0000-0000-0000-000000000001:1:copper-key",
  "book_id": "00000000-0000-0000-0000-000000000001",
  "revision": 1,
  "entry_id": "copper-key",
  "title": "铜钥匙与城门守卫",
  "content": "铜钥匙只能开启灰港城门的侧门。守卫在放行前必须核对来访者的手令，不能仅凭钥匙放人。",
  "position": "before_character",
  "depth": 0,
  "role": "system",
  "order": 0,
  "reason": "keyword"
}
```

**第三步：使用。** `content` 是作者的文字，并未经过另一个 LLM 总结。自建宿主按 `position` 放置这些 `blocks`，再加角色资料、历史和本轮输入，最终调用自己的回复模型。简单单位置接法也可使用聚合 `context`，但如果书里使用不同深度或插槽，就必须按 `blocks` 的位置装配，不能把所有内容统一贴在末尾。

| 返回字段 | 接入方怎样使用 |
|---|---|
| `blocks` | 正式装配材料：书籍／版本／条目 ID、原文、角色、位置、原因 |
| `context` | 已转义并用 `<worldbook-entry>` 包装的聚合文本；适合诊断或单一位置注入 |
| `decisions` | 每条是否入选、排除原因；用来回答“为什么这条没被读到” |
| `snapshot` | 本次实际书籍版本与内容哈希；与本轮请求关联，方便复现 |
| `token_count`、`token_budget`、`tokenizer`、`token_scope` | 本服务返回文本的预算，不是整个回复模型请求的 token 数 |
| `degraded` | 混合检索发生了什么降级；空数组表示未记录降级 |
| `policy_version`、`prompt_hash`、`selection_fingerprint` | 选择策略及结果／输入条件的诊断标识 |
| `duration_ms` | 服务处理耗时；不包含后续 RP 模型生成 |

本文样例已用此固定版本的 `BookCreate`、`SelectRequest`、真实选择函数和 OpenAPI 结构验证：选出两条，分别因为关键词与常驻；示例聚合文本为 191 tokens。此验证没有连接数据库或调用模型，不作为线上部署或模型遵循效果的验证。[返回实现](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/selection.py#L337)

## 常驻、动态、必需：谁决定本轮读哪些条目

**决定者分三层：作者决定条目的规则，接入方决定本轮材料与预算，服务程序按规则选择。** 当前这步不需要一个 LLM 阅读全书再挑条目。

| 类型 | 作者怎样设置 | 实际行为 |
|---|---|---|
| **常驻** | `constant: true` | 不要求关键词命中，每轮进入候选；仍受启用、受众、条件、概率、位置、预算约束，并不保证每次一定放入 |
| **动态** | 普通条目填写 `keys`；请求使用 `rules` 或 `hybrid` | 关键词命中，或 hybrid 召回相关条目，再经过统一的过滤和预算；没有命中不代表资料被删除 |
| **必需** | `required: true` | 在通过资格过滤后优先完整装入；依赖、冲突、位置或预算无法满足时可报错。它不是绕过所有过滤的强制开关 |

执行次序：

1. 排除停用、受众不符、状态条件不符、不支持动态运行环境、概率未通过、空内容或无法放置的条目。
2. 激活必需、常驻、关键词命中和 hybrid 召回的条目；按配置允许有限递归激活。
3. 展开同版本内的显式依赖，把“条目＋依赖”作为一组，检查冲突。
4. 按优先级和预算选择：必需优先，其次可选常驻，再处理其他候选；默认常驻可选条目最多使用总预算的 35%。
5. 返回完整原文和实际选择原因。装不下的可选条目整条被排除，不自动缩写成摘要。

例如“守卫手令规则”命中了，但被标记为仅在 `state.quest.started = true` 时允许使用；本轮宿主没有交这项状态，规则仍不会进入。`state` 必须由宿主提供，服务不自行查询剧情数据库。[选择步骤](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/selection.py#L107)

<details>
<summary>扫描、混合召回、预算与递归的具体控制参数</summary>

| 参数 | 服务接口默认值 | 作用 |
|---|---|---|
| `strategy` | `rules` | `rules` 走作者规则；`hybrid` 加入同范围词法＋向量召回，再经相同条件和预算选择 |
| `scan_depth` | 4，范围 0–100 | 扫描“history 的内容＋本轮 query”末尾多少条消息；query 也算一条。4 是消息数，不是四轮对话；0 关闭关键词扫描 |
| `token_budget` | 1500，范围 0–32000 | 返回原文包装后的总预算；不是整个模型上下文窗口 |
| `constant_budget_ratio` | 0.35，范围 0–1 | 可选常驻条目的预算上限占比；必需条目不受此常驻配额限制，但受总预算限制 |
| `max_entries` | 50，范围 1–200 | 最多放入多少条，依赖也计数 |
| `max_recursion_rounds` | 2，范围 0–5 | 首次匹配后额外进行的有限递归轮数；需条目双方允许，不等于默认递归所有内容 |
| `seed` | `"0"`，最多 200 字符 | 概率抽选的稳定种子；相同输入下便于复现。Harness 使用 request ID |
| `audience` | `actor` | 只有条目的 audience 包含它，才有资格参与；不是权限认证 |
| `template_vars` | 空对象 | 内容中 `{{char}}`、`{{user}}` 的替换来源；未提供所需变量则排除该条 |
| `history` | 空，最多 100 条 | 每条必须有 role/content；每条 content 最多 16,000 字符 |
| `query` | 必填，1–8000 字符 | 本轮查询文字；不等于整个系统 Prompt |
| `books` | 必填，1–20 本 | 明确限定书籍范围；单次快照还受条目数量和原文总量上限约束 |

当前选择器先放必需组，再放常驻组，再放其他候选；组内依次看 `required`、高 `priority`、关键词命中、混合得分、高 `order`、稳定 ID。装配输出另按位置、深度及 `order` 排序，不能把“优先入选”理解为“最后放在 Prompt 哪儿”。

次关键词逻辑仅在主关键词已命中、`selective=true` 且有次关键词时参与：`and_any` 至少命中一个；`and_all` 全部命中；`not_any` 一个都不能命中；`not_all` 不能全部命中。

`hybrid` 在同一本书版本范围内结合词法和向量排序；不是 LLM reranker。向量不可用或指定版本索引没就绪时，hybrid 在 `degraded` 标注原因并退回词法候选；纯 `vector` 搜索会直接返回错误。规则选择无需 Embedding。

服务的计数默认 `cl100k_base`，只计算返回的 `context`。接入方仍须计算角色、聊天历史、工具消息和输出预留组成的完整请求。当前 Harness 在世界书装配之后、发模型之前做最终 Actor 请求预算检查；超出时会以 `agent.context_overflow` 失败，世界书 1500／2400 tokens 的小预算不能替代模型总窗口配置。[接口默认值](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/models.py#L168) · [混合降级](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/service.py#L68) · [最终请求预算](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/actor/roleplay-runtime.ts#L845)

</details>

## 原文放进模型输入的哪里

“本轮注入”就是把选中的条目原文装成这次模型请求中的消息。它不表示永久改写角色卡、给模型训练新知识，或把整本世界书塞进历史。

| 条目 `position` | 模型实际看到的位置 | 宿主需要准备什么 |
|---|---|---|
| `before_character`／`after_character` | 角色资料之前／之后 | 角色资料区域；默认可用 |
| `at_depth` | 真实聊天历史按消息深度定位的位置 | 聊天消息位置，默认可用；不把示例和世界书条目当作聊天轮次 |
| `before_examples`／`after_examples` | 对话示例区域前／后 | 明确提供结构化 examples；不能靠猜角色卡内的小标题定位 |
| `author_note_top`／`author_note_bottom` | 作者注释容器内的顶部／底部 | 宿主提供 authorNote；注入内容继承该容器的角色 |
| `outlet` | 宿主指定的命名插槽，例如 `scene_lore` | 条目填 `outlet_name`，宿主声明同名模板与标记 |

服务根据 `placement_capabilities` 先排除宿主不支持的位置。普通条目被排除并记录原因；必需条目找不到位置则报错。服务不声明位置能力时只允许前三种默认位置。当前 Harness 会明确声明本次真实具备的区域，而不是把所有 8 种位置都声称可用。[位置资格](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/placement.py#L6) · [Harness 位置装配](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/worldbook/prompt-layout.ts#L40)

## 接现成 Harness：具体开关、每轮读取与模型边界

**当前已经接好的入口是 `agentic-v2-worldbook` 专用运行配置。** 它要求 Agentic V2、Actor 的模型配置和 preset；不能同时启用 `preActorDirector`、`storyV1` 或 `sumiV1`。这意味着世界书服务能够给客户自己的 Story／生图流程复用，但此固定版本没有“给现成 Story 或 Sumi 加一个 worldbook 开关就自动接好”的组合。[启动配置限制](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L793)

宿主配置片段如下；这是添加到完整 Agentic V2 场景中的字段，不是独立运行所需的全部配置：

```json
{
  "worldbook": {
    "tenantId": "tavern",
    "strategy": "hybrid",
    "tokenBudget": 2400
  }
}
```

`tenantId` 必填，选择资料所在命名空间；`strategy` 在 Harness 默认 `hybrid`，`tokenBudget` 默认 2400。它们不同于独立服务 `SelectRequest` 的 `rules`／1500 默认值。Harness 当前这组配置不直接开放所有服务选择参数，例如 `constant_budget_ratio`、`scan_depth`、`max_entries`；如需逐项控制，要扩展适配层，或从自己的后端直接调用服务。

现成接线每轮做这些事：

| 顺序 | 程序做什么 | 使用的具体材料 |
|---|---|---|
| 1 | `worldbook.conversation.check` 校验身份 | 配置的 tenantId，加可信用户 ID、会话、Prompt、场景；不是固定世界书版本 |
| 2 | `worldbook.bindings.resolve` 读取该 Prompt 关联 | 返回最多 20 本的具体 `book_id + revision`；本轮保留此快照 |
| 3 | 读取已保存的 Agentic V2 状态 | 先尝试解析为 JSON 对象，成功才把字段交给条件选择；普通 V2 保存的栏目文本不能解析为 JSON，条件状态为 `{}`。查询文字仍可从文本的 `Current scene:` 行提取公开场景 |
| 4 | `worldbook.select` 选择一次 | query 是本轮输入＋公开场景文本，最多 8000 字符；传最近最多 20 条 user/assistant 历史，每条最多 16000 字符；服务仍默认只扫描末尾 4 条消息，条目可覆盖深度 |
| 5 | 组装 Actor 输入并检查总预算 | 把 `blocks` 按位置放入角色／历史之间，再生成本轮回复 |
| 6 | 后续新一轮再次解析与选择 | 同一逻辑轮的重试复用已解析版本与选择结果；下一轮重新计算 |

**世界书的条件不会自动读取 Director 笔记里的事实。** 例如，笔记写了“守卫已接过铜钥匙”，不等于选择器收到 `keyHolder:"守卫"` 这个字段。若要按任务进度、人物状态等条件选条目，宿主必须向选择接口提供结构化 `state`；使用当前 Harness 接线则需扩展适配层，把所需字段明确传入。JSON 状态中的 `current_scene.location/time/situation` 可补入查询；普通 V2 文本只有 `Current scene:` 行参与这条查询补充路径。[状态解析与查询生成](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/worldbook/runtime.ts#L59)

**Actor 与后台没有自动共享这批原文。** `WorldbookTurn` 只用于本轮 Actor；慢通道 planner 的输入独立组装，并不会自动收到本轮入选的世界书条目。“知识专家”这个名字也不会额外授予世界书访问能力。若客户希望后台根据同一份规则做校验，需要明确把条目传给后台，或给后台另外接一个带书籍范围的读取工具；这是二开工作。[本轮持有与 Actor 注入](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/worldbook/runtime.ts#L36) · [Actor 状态与查询准备](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/runtime.ts#L350)

## 哪些资料什么时候保存，什么时候生效

| 对象 | 谁触发保存 | 保存到哪里 | 什么时候使用／生效 |
|---|---|---|---|
| 世界书原文、规则、导入报告 | 运营或宿主调用创建／导入／修订 API | PostgreSQL 的书籍、不可变版本、条目记录 | 成功后可用具体版本执行规则选择；不是等用户聊天才保存 |
| 当前版本指针 | 发布新 revision 的同一事务 | 书籍 head | 跟随最新的关联在下次 resolve 读到新版本；固定旧版不变 |
| Prompt 与书籍关联 | 管理端／可信后端调用 PUT | `prompt_worldbook_bindings` | 下一逻辑轮 resolve 生效；本轮已取得的快照不重算 |
| 向量索引 | 明确调用 index 构建 | PostgreSQL 中的向量、索引状态和 Embedding 版本 | 对应精确 revision ready 后可参与向量检索；新原文版本不自动沿用旧索引 |
| 本轮选择结果 | 世界书程序生成 | Harness 当前 `WorldbookTurn` 内存 | 本轮 Actor 和同轮重试使用；不自动写入规范聊天历史 |
| 选择诊断 trace | Demo 的 onTrace 接收 | Demo 可按回复 ID 保存在浏览器缓存 | 用于查看本轮实际装配了哪些条目；不是持久化的世界事实 |
| 用户正文、模型回复、剧情／人物状态 | 聊天宿主和各产品自己的保存流程 | 各自聊天与状态存储 | 世界书服务不代替它们保存，也不自动把剧情变化回写世界书 |

关联保存示例（书籍 ID 替换为实际返回值）：

```json
{
  "bindings": [{
    "book_id": "00000000-0000-0000-0000-000000000001",
    "revision": null,
    "position": 0,
    "enabled": true
  }]
}
```

提交到 `PUT /v1/prompt-bindings?tenant_id=local&prompt_id=guard-role-1`。`prompt_id` 是外部角色／作品 ID，不是 PromptManager 模板 ID。`revision: null` 表示跟随最新版；正整数表示固定版本；`bindings: []` 清空。PUT 原子替换这个 Prompt 的**全部关联**，不是只追加本次列表；最多 20 本，不能重复。

每轮 `GET /v1/prompt-bindings/resolve?tenant_id=local&prompt_id=guard-role-1` 返回：

```json
{
  "tenant_id": "local",
  "prompt_id": "guard-role-1",
  "books": [{"book_id": "00000000-0000-0000-0000-000000000001", "revision": 1}]
}
```

这是**逐轮解析角色关联**，不是“建会话时永久 pin 一版书”。无启用关联返回空 books，Harness 正常聊天且跳过 select；显式关联失效则失败，不悄悄改用别的书。修改书或关联会影响已有会话的下一轮，不会重写已经产生的历史消息。[关联保存与解析](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/repository.py#L173) · [Harness 逐轮解析](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/worldbook/resolve.ts#L4)

<details>
<summary>索引、异常处理、独立部署与身份接入</summary>

**索引。** `POST /v1/books/{book_id}/index?tenant_id=local`，请求 `{"revision":1,"force":false}`。这是同步构建接口；已有相同 Embedding 版本的 ready 索引会复用。正文每 1000 字符取最多 1200 字符块，存在重叠；按批生成 Embedding。生成索引不会改变原文和条目 enabled。失败记录 failed，重试该版本；单版本同时构建冲突返回 `index_busy`。[索引实现](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/service.py#L152)

**部署。** 服务需要 Python 3.12–3.13、PostgreSQL；仓库提供依赖锁文件、Dockerfile、迁移和本地 Compose。仅规则模式 `VECTOR_BACKEND=none` 不调用 Embedding；启用 `pgvector` 后需要对应向量配置和 Embedding 服务／本地后端。原文与向量同在 PostgreSQL，向量可由原文重建。[依赖与启动配置](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/config.py#L8)

**调用身份。** 当前普通 REST 路由要求 query `tenant_id`，服务没有默认租户，也没有实现请求认证。租户是资料命名空间，不是登录凭据。不能把“能够传 tenant_id”理解为“服务会确认这个用户有权访问该租户”。公网接入需要业务后端／网关处理登录、租户授权、管理端权限等，再调用此服务；`/v1/admin/*` 还能跨租户浏览，须放在受控入口。自带 Python client 虽会发送 Bearer Header，当前服务代码不会据此校验用户。[实际租户参数](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/app.py#L35) · [无认证测试](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/tests/test_api.py#L60)

**接入 Harness。** 已有调用链是业务请求 → Harness → Axon 的 worldbook RPC → Worldbook Service。Harness 将可信 `X-User-ID` 交给 Axon，用于会话身份命名空间；它不是最终用户可自行宣称的认证凭据。Worldbook Service 本身无需 Axon；直接 HTTP 复用时由自己的后端提供同等权限和版本管理。Axon 属于外部依赖，本文对 RPC 的说明以 Harness 调用端为准，不把外部仓库实现当作本次已扫描代码。

**Demo。** Harness 的世界书 Demo 提供查看关联、聊天和注入诊断；共享导入和关联修改不对 Demo 开放。不能把 Demo 的页面包装直接当成完整对外的世界书管理产品。[Demo 接口边界](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/worldbook/debug-api.ts#L94)

| 情况 | 当前结果 | 接入方应做的事 |
|---|---|---|
| 无 Prompt 关联 | 空书籍列表，现成 Harness 跳过选择继续生成 | 可展示“没有关联资料”，无需伪造选中结果 |
| 无动态条目命中 | 对应条目 `not_triggered`；其他合格条目照常选择 | 检查 query、扫描窗口、关键词、条件与预算 |
| 普通常驻装不下 | `constant_budget_dropped` 或 `budget_dropped` | 缩短／拆分资料，调整常驻配额或总预算 |
| 必需条目无法容纳、依赖失效、冲突 | 明确错误 | 修复资料／预算；不要悄悄删掉必需规则继续 |
| 向量索引未就绪 | hybrid 标注降级；纯 vector 搜索报错 | 检查精确 revision 的索引；明确是否接受词法退化 |
| 修订并发冲突 | 409 `revision_conflict` | 重新读取 head，合并修改后再发布 |
| 同幂等 key 用了不同内容 | 409 `idempotency_conflict` | 新操作使用新 key；同操作重试保留原 key 和原内容 |
| 租户／书籍／版本不匹配 | 404 或明确绑定错误 | 检查资料归属，不能跨租户猜书籍 ID |
| 最终 Actor 输入超模型窗口 | `agent.context_overflow` | 调整完整输入预算或模型配置，不只看世界书 token_count |
| 服务调用失败 | 当前世界书解析／选择接线向上抛错 | 明确产品错误提示与重试；不要描述成现成的“自动无世界书继续” |

</details>

## 二开时能直接复用什么，还需自己补什么

**可以直接复用**：版本化存储、静态资料归一化、Prompt 关联解析、规则／混合选择、精确版本索引、诊断返回；接自己的模型无需先启用整套 Agent V2。调用 `search` 可做资料浏览，调用 `context/select` 可做生成前的资料选择。

**需在宿主实现**：把返回资料正确放进模型窗口、总 token 预算、用户／租户授权、聊天与状态保存、所用产品的接入流程。若让 Agent 自己决定何时查世界书，还需注册一个带明确 scope 与输入返回契约的工具；服务存在不等于它已经挂进每个 Agent 的工具菜单。

**当前没有自动完成**：Story／Sumi 的现成开关组合、后台专家读取同批条目、剧情状态自动修改世界书、从新 NPC 自动生成并持久化独立小卡、完整 ST 动态运行时、独立服务自带的公网用户鉴权。客户如果需要，应把它们明确列为二开范围，而不是世界书默认交付内容。

源码基线：Worldbook `9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b`；Harness `fa41d220d48327e58d994f936517b23f2f15f658`。本章使用固定源码、类型、路由与样例验证；未把 main 合并状态当作生产开启或客户交付验证。
