Pouchy World 规模上限,以及每条上限的原因
英文原版(以其为准):world-limits.md
基于 world API 1.48.0。下面每个数字都由服务端强制执行;机器可读的版本是
world OpenAPI 里的 maxItems / maxLength
(components.schemas.StoryPackageContent,整包体积上限见 x-pouchy-max-bytes)。
你发送的内容超出上限时,服务端返回 400,并写明是哪条上限
(roles exceeds 24、story package is 1043211 bytes; the limit is 900000 …),
绝不会悄悄截断。第 5 节的上限属于另一类:运行时每一拍保存、展示或报告多少,
每一条旁边都写明了它的具体行为。
1. 1.48.0 改了什么
| 上限 | 之前 | 现在 | 常量 |
|---|---|---|---|
| 故事包角色数 | 12 | 24 | STORY_MAX_ROLES |
| 世界定义角色数 | 12 | 24 | WORLD_MAX_ROLES(与上一行由测试钉为相等) |
| 场景 | 50 | 100 | STORY_MAX_SCENES |
| 节点 | 100 | 200 | STORY_MAX_NODES |
| 分支 | 100 | 200 | STORY_MAX_BRANCHES |
| 整包体积 | (未检查) | 900,000 字节 | STORY_PACKAGE_MAX_BYTES |
这是一次放宽,只新增一种拒绝:超过 900,000 字节的故事包。超过 1 MiB 的包本来就存不进去, 现在改为返回一个说明原因的 400,而不是存储报错;介于 900,000 字节与 1 MiB 之间的包, 以前能发布,现在会被拒绝。已发布的修订不受影响:内容只在创建或发布时校验,读取时从不 重新校验,所以已被钉住的修订照常运行。只有再次发布它(或发布同样大小的新修订)才会遇到新上限。
2. 阵容
| 上限 | 数值 | 原因 |
|---|---|---|
| 每个故事 / 世界的角色数 | 24 | 产品取舍,不是技术限制。12 是"刻意取小",没有任何实测结论依赖它(见下方"成本")。 |
| 每个角色绑定的 agent | 恰好 1 个,且互不相同 | 世界拒绝把同一个 agent 绑定给两个角色,所以 24 个角色的世界需要 24 个 agent。 |
| 每一拍开口的角色数 | 3(WORLD_TURN_MAX_ROLES) |
戏剧取舍,刻意不放宽:三人是一场戏,再多观众就跟不上。它同时把每一拍的模型调用限制在最多 3 次。 |
| 一个事件最多唤醒的角色数 | 5(WORLD_MAX_ROLES_PER_EVENT) |
与事件路由的扇出上限(MAX_EVENT_FANOUT)相等。如果某个事件需要唤醒的角色多于路由器能投递的数量,世界在发布时就会被拒绝,而不是运行时被截断。 |
| 每个角色的 secrets / goals | 各 20 条 | 容量控制。每一条只会进入该角色自己的提示词。 |
成本不随阵容增长。 不在说话的角色,永远不会进入其他角色的提示词:它的 description、
goals、secrets 都不会。唯一与其他角色相关的内容,是同一拍里已经说过的台词(最多 2 句),
以及效果简报里两份有上限的 id 列表。24 个角色的故事和 3 个角色的故事,说话角色拿到的
提示词逐字节相同;world-envelope-composition.test.ts 在每次构建时都以最大阵容验证这一点。
大阵容需要由这一拍来决定"谁在场"。 3 个座位按 roles 的声明顺序填满。
不加指定时,第 4 到第 24 个角色每一拍都会被跳过:它们出现在 skippedRoles 中,
code: "role_cap_reached"。请用 trigger.focusRoles(turn 接口,1.29.0 起)指定这一拍
的出场角色,或通过事件订阅来路由。如果某个角色"从来不说话",先查 skippedRoles。
3. 故事包
| 项目 | 上限 | 说明 |
|---|---|---|
| 场景 | 100 | |
| 节点 | 200 | 每个节点必须指向一个已声明的场景 |
| 分支 | 200 | 每个分支连接两个已声明的节点 |
| 每个节点的前置条件 | 10 | |
| 结局 | 60 | 早先已从 20 放宽,用于多集连续剧 |
| 既定事实 | 200 | |
| 正史设定(canon) | 100 | |
| 约束 | 50 | |
| 旗标(flags) | 50 | |
| 初始关系 | 50 | 与运行时状态能容纳的数量绑定(WORLD_STATE_MAX_RELATIONS) |
| 剧集(series) | 24 | |
| 每集结局规则 / 每条规则的条件 | 20 / 10 | |
| 每集回合预算 | 200 拍 | 证据窗口:更长的一集无法转成剧本 |
| 每条文本 | 500 个字符 | canon、事实、secrets、goals、objective、条件、描述 |
| 整包 | 900,000 字节 | 见第 4 节 |
4. 体积上限:你真正会碰到的那一条
一个已发布的修订存储为一个 Firestore 文档,而 Firestore 拒绝任何超过 1 MiB (1,048,576 字节)的文档。故事包上限是 900,000 字节,按规范化后内容的 JSON 的 UTF-8 字节数计算,剩下约 140 KiB 留给修订记录自身的字段。
为什么它会比条目上限先碰到: 条目上限是相乘的。中文、日文、韩文每个字 3 字节, 所以一个写满的角色(20 条 secrets + 20 条 goals,每条 500 字)约 60 KB,24 个这样的角色 就是 1.44 MB。真实剧本远小于此,但长文本的中文故事包,会在达到 24 个角色或 200 个节点 之前,先碰到体积上限。
碰到时怎么办(400 story package is N bytes; the limit is 900000 …):
- 先精简长文本字段:角色的
secrets/goals、canon、establishedFacts。 - 参考资料(完整剧本、设定集)不要放进故事包。故事包只放运行时做判断、投影状态所需的内容;
原始剧本通过
source: { title, version }引用,不要内嵌。 POST …/story-packages/import-candidate/validate用同一个校验器校验但不发布, 返回同样的错误信息,可以在发布前先检查。
5. 你可能遇到的其他运行时上限
| 上限 | 数值 | 行为 |
|---|---|---|
| 每一拍的状态补丁操作数 | 20 | 超过则整批拒绝(补丁是全有或全无) |
| 世界状态中的实体 / 关系 | 50 / 50 | 会让数量变成第 51 个的补丁被拒绝,整批一起拒绝。关系图很密的大阵容,会在达到 24 个角色之前先碰到 50 条关系:24 个角色有 276 种两两组合 |
| 每个角色在状态中的私人笔记 | 20 | 同上:会新增第 21 条的操作被拒绝 |
| 每一拍报告的分支选项 | 5 | 按声明顺序取前 5 个可达且未完成的分支;其余仍在故事里,前面的完成后才会出现 |
| 单个角色效果简报里的关系 id | 8 个其他角色 | 只有 id;简报会报告截掉了多少 |
| 推演(deliberation) | 2 个候选、3 个角色 | 服务端常量;请求只能要更少,不能更多 |
| 剧本导入(模型辅助) | 16,000 个字符的剧本 | 更长的剧本会被拒绝;提取出的故事包同样遵守上面所有上限 |
| 剧本草稿 | 500 拍 | 一份草稿最多覆盖 500 个已提交的拍;更长的世界线请按集分别起草(episodeRunId,1.27.0) |
| 每个项目启用的世界 / 故事包 | 1,000 / 1,000 | 达到上限时创建会被拒绝 |
6. 数字定义在哪里
src/lib/world/story-package.ts(故事上限与体积上限)、src/lib/world/definition.ts
(世界定义),以及 src/lib/server/platform/world-coordinator.ts(WORLD_TURN_MAX_ROLES)。
控制台的世界创建向导直接引用同一组常量,并显示"n / 24 个角色",因此不会与合约脱节。