架构设计
这份文档面向想要理解系统设计、参与开发或进行二次开发的开发者。
1. 整体思路
传统的 AI RP 工具(比如 SillyTavern)是以「角色卡」为中心的前端应用。TavernHeadless 走了一条不同的路:
- 后端优先:核心逻辑全部跑在服务端,前端只是一个可选的管理界面。
- 项目即角色卡:不再需要一张 PNG 角色卡文件。一个 TavernHeadless 项目本身就包含了角色设定、世界观、预设、正则规则等所有内容。
- 兼容但不受限:可以导入酒馆的预设和世界书直接用,但也提供了更强大的原生能力。
系统分为三个主要部分:
┌─────────────────────────────────────────────────┐
│ apps/web │
│ 管理前端(可选) │
└──────────────────────┬──────────────────────────┘
│ HTTP / WebSocket
┌──────────────────────▼──────────────────────────┐
│ apps/api │
│ Fastify 后端服务 │
│ ┌───────────────────────────────────────────┐ │
│ │ packages/core │ │
│ │ 消息管理 · 变量系统 · 提示词编排 │ │
│ │ LLM 调度 · 记忆系统 · 事件总线 │ │
│ └───────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────┐ │
│ │ packages/adapters-sillytavern │ │
│ │ 预设导入 · 正则导入 · 世界书导入 │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘2. 消息结构:会话 → 楼层 → 消息页
这是整个系统的数据骨架。我们把一次聊天拆成三层:
会话(Session)
一次完整的聊天。创建会话时会绑定预设、世界书、正则规则、模型配置等。
Workspace / Project 阶段一归属
阶段一里,每个新 Session 都会写入 workspace_id 和 project_id。这两个字段用于服务端隔离、查询和审计。
为了保持旧客户端可用,普通客户端仍然不需要知道 Workspace / Project:
POST /sessions不传project_id仍然成功。- 服务端会使用当前账号默认 Workspace,并为新 Session 创建
session_defaultProject。 - 只有高级调用方需要把 Session 放进已有 Project 时,才传
project_id。 GET /sessions/:id和列表响应默认不返回workspace_id、project_id。- Prompt Asset、角色、用户卡、LLM 配置、工具定义和 MCP 配置的旧列表接口默认只读当前账号默认 Workspace。
楼层(Floor)
一次「回合」。你发一条消息、AI 回一条消息,这就是一个楼层。
楼层的关键能力:
- 分支:从任意楼层新开一条故事线,不影响原来的内容。
- 状态机:每个楼层有状态——
draft(草稿)→generating(生成中)→committed(已提交)或failed(失败)。 - 提交后不可改:一旦提交,楼层内容就锁定了。想改?新建一个楼层。
消息页(MessagePage)
楼层内的一个「版本」。比如你点了重新生成(regen),就会在同一个楼层里新建一个消息页,旧的还在。
消息页的作用:
- 保存重试/重新生成的不同版本。
- 流式生成时先写到消息页里,生成完再标记为生效。
- 每个楼层有且只有一个「当前生效页」。
消息(Message)
最小单位。每条消息有角色(user / assistant / system / narrator)、内容、格式等。
关系图
Session
└── Floor 1 (committed)
│ ├── MessagePage v1 (inactive) ← 第一次生成的版本
│ │ ├── Message: user "你好"
│ │ └── Message: assistant "你好呀"
│ └── MessagePage v2 (active) ← 重新生成后的版本
│ ├── Message: user "你好"
│ └── Message: assistant "嗨!很高兴见到你"
└── Floor 2 (generating)
└── MessagePage v1 (active)
├── Message: user "讲个故事吧"
└── Message: assistant "从前有座山..." ← 正在流式生成3. 变量系统
变量是用来存储叙事状态的容器。比如「角色当前的心情」「某个物品是否被拿走了」「好感度数值」等等。
五个层级
| 层级 | 作用域 | 典型用途 | 生命周期 |
|---|---|---|---|
| 全局(global) | 整个项目 | 世界观设定、全局开关 | 永久 |
| 会话(chat) | 一次聊天 | 好感度、已触发事件 | 会话期间 |
| 分支(branch) | 一条分支 | 分支内持续状态、分支走向 | 分支存在期间 |
| 楼层(floor) | 一个回合 | 本回合判定结果、临时标记 | 楼层提交后冻结 |
| 页(page) | 一次生成尝试 | 生成中间状态、工具调用暂存 | 生成完成后决定去留 |
读写规则
读取时,按从小到大的顺序查找,找到就停:
page → floor → branch → chat → global比如读取变量 mood:先看当前页有没有,没有就看楼层,再看分支,再看会话,最后看全局。
写入时,默认写到 page(最小范围)。这是一种保护机制——页级变量就像沙箱,不会意外改到全局状态。
提升:如果确实需要把变量保存到更高层级,需要显式提升。比如一次回合结束后,把 page.mood 提升到 branch.mood 或 chat.mood。这个过程由编排器控制,不会默默发生。
为什么要有「页」这一层?
主要解决重新生成时的隔离问题。假设 AI 生成了一个回复并且写了 mood = happy,你觉得不好点了重试,新的生成写了 mood = sad。如果没有页级变量,两次生成会互相覆盖。有了页级变量,每次生成都在自己的沙箱里,只有你选定的那个版本才会被提升到楼层、分支或会话。
为什么要有「分支」这一层?
RP 中经常出现分支:从某个楼层开始,同一段故事可以走出完全不同的方向。假设聊到第 10 楼时,你想试试"角色 A 选择战斗"和"角色 A 选择逃跑"两种走向。如果没有分支级变量,两条分支共用同一份 chat 级状态——一条分支里"角色 A 受伤了",另一条里"角色 A 安然无恙",双方的变量会互相覆盖。
有了 branch 这一层,每条故事线都有独立的状态空间。分支 A 和分支 B 各自积累自己的变量,不会干扰对方,也不会污染主线。
4. 提示词系统
提示词系统负责把预设、世界书、变量、聊天记录等拼成一份完整的提示词,发给大模型。
双轨设计
我们提供两条路径:
路径一:酒馆兼容模式(compat)
直接导入酒馆的预设和世界书,按照酒馆的方式拼接提示词。适合已经有成熟预设的用户,导入即用。
这条路径又分两档:
compat_strict:严格复刻酒馆行为,变量展开、世界书触发、拼接顺序都尽量一致。compat_plus:在兼容基础上加入少量已声明的增强能力,比如记忆注入,但不改变兼容层的基本边界。
路径二:原生编排模式(native)
当前会走原生编排路径。API 主链会把导入的 ST preset 转换成一种内部中间结构(Native Imported Group),再通过图编译器(PromptGraphDocument → PromptIR)输出统一的中间格式。
这一模式不承诺 ST preset 的保真执行。导入 ST preset 时,默认应优先使用 compat_strict。
| 原生图节点 | 做什么 |
|---|---|
static_text | 普通文本 prompt 节点 |
marker | 锚点或插槽标记 |
chat_history | 聊天历史 |
character | 角色描述、个性、场景、system prompt、post-history |
persona | 用户 persona 描述 |
worldbook | 世界书 before / after / depth 注入 |
example_dialogue | 示例对话 |
memory | 记忆摘要 |
tool_result | 工具结果 |
variable_template | 变量模板文本 |
原先 packages/core 中的 native-pipeline 节点链仍然保留,主要作为过渡执行层和测试基础;但 apps/api 的 native 主链已经优先使用 graph compiler。
当前 v1 已落地最小 PromptGraph 文档模型与 compilePromptGraph() 闭环。创建会话时通过 prompt_mode 字段选择使用哪种编排模式。
当前 Prompt Runtime 另外提供了独立的 session 级 mode 控制面:
GET /sessions/:id/prompt-runtime/modePATCH /sessions/:id/prompt-runtime/mode
这个控制面不会引入第二份存储。底层仍然只写 sessions.prompt_mode。
统一中间格式(Prompt IR)
不管走哪条路径,最终都会先编译成一个统一的中间格式(内部叫 Prompt IR),再交给大模型。这意味着:
- 酒馆预设和原生编排共享同一个渲染器。
- 加新功能只需要加新节点,不需要改兼容层。
- 调试时可以看到中间格式的完整内容,方便排查问题。
ST 宏运行时边界
兼容模式下的宏系统不是简单的正则替换。
当前后端实现已经把 ST 宏运行时放到明确的执行边界中:
| 场景 | 只读宏 | 写宏 | 是否写库 |
|---|---|---|---|
| 导入 | 不执行主求值 | 不执行 | 否 |
| 预览 / 编辑器预览 | 执行 | 只记录 preview mutation | 否 |
| Prompt dry-run | 执行 | 只记录 preview mutation | 否 |
| respond / regenerate assemble | 执行 | 进入 staged mutation buffer | 否 |
| turn commit | 不重新展开宏文本 | 消费 assemble 冻结结果 | 是 |
当前宏运行时内部已经采用最小稳定节点结构,而不是纯字符串级替换。可以把它理解为以下四类节点:
- text node
- macro node
- if block node
- raw fragment node
这样做的目的有三点:
- 保持求值顺序稳定
- 保持
ifblock 只展开命中的分支 - 在 parse 失败、unsupported condition、cycle detect 等场景下保留原文,并输出 warning / trace
变量兼容层当前也不是简单的一张扁平 map。运行时会区分:
- ST local 兼容视图
- ST global 兼容视图
- 分阶段 overlay
因此:
.name/getvar读取 local 兼容视图$name/getglobalvar读取 global 兼容视图setvar与setglobalvar的同轮可见性彼此隔离
当前还支持以下宏运行时调试信息:
- warning 列表
- used macro 名称
- mutation preview
- staged mutation
- macro trace
这些信息会在 Prompt dry-run 和部分调试输出中返回。详见 Chat API 与 宏系统参考。
5. LLM 实例化
在复杂的角色扮演场景中,一个大模型不够用。你可能需要一个「叙述者」负责写故事,一个「记忆员」负责整理摘要,一个「导演」负责把控剧情方向。
我们的做法是:在逻辑上把大模型拆成多个「实例」——不是真的开多个模型进程,而是用不同的配置去调用同一个(或不同的)在线大模型。
实际上,"怎么调用大模型"涉及三层配置:
| 概念 | 解决什么问题 | 举例 |
|---|---|---|
| 预设 | 提示词模板和生成参数 | 温度 0.9、top_p 0.95、酒馆格式模板 |
| 实例槽位 | 谁来调用大模型、负责什么 | 叙述者写故事,记忆员整理摘要 |
| LLM Profile(凭证配置) | 调用哪个大模型、用什么密钥 | OpenAI gpt-4o、Anthropic claude-3.5-sonnet |
三者的关系:会话绑定一个预设(决定提示词怎么拼)→ 每个实例槽位绑定一个 Profile(决定调用哪个大模型)→ 实例还可以用自己的预设覆盖会话级别的预设。
下面分别展开。
每个实例包含什么
| 配置项 | 说明 |
|---|---|
| 身份 | 名字和职责描述(如「narrator」「memory」) |
| 模型配置 | 用哪个模型、温度、最大 token 数等 |
| 提示词约定 | 这个实例的系统提示词模板,以及输入输出格式 |
| 变量权限 | 能读写哪些层级的变量 |
| 预算限制 | 单次最多用多少 token、超时时间、重试次数 |
预设的实例类型
| 实例 | 职责 | 什么时候用 |
|---|---|---|
| 叙述者(Narrator) | 生成角色扮演文本,产出故事内容 | 每个回合都用 |
| 记忆员(Memory) | 整理摘要、提取关键事实 | 回合结束后 |
| 导演(Director) | 规划剧情走向、给叙述者提供结构化指令 | 可选,复杂场景开启 |
| 校验员(Verifier) | 检查角色行为是否符合设定 | 可选,严格场景开启 |
调度顺序
一次回合中,实例按以下顺序执行:
1. 导演(可选):分析当前局势,给出本回合指令
↓
2. 记忆员:检索相关记忆,准备注入上下文
↓
3. 叙述者:根据指令和记忆,生成本回合内容
↓
4. 校验员(可选):检查生成内容是否合理
↓
5. 提交楼层兼容模式下的行为
在严格兼容模式(compat_strict)下,只启用叙述者,其余实例全部关闭,行为和酒馆完全一致。切到增强兼容(compat_plus)或原生(native)模式后才会按需开启其他实例。
实例配置的几个关键行为:
- 叙述者被禁用时,请求会返回明确错误。导演、校验员、记忆员被禁用时,对应子流程直接跳过。
- 叙述者的预设可以覆盖会话级别的预设。
- 生成参数按浅层合并,同名参数以实例配置为准。
LLM Profile:凭证配置
每个实例可以独立绑定不同的 LLM Profile。Profile 是一组加密存储的大模型凭证,包含提供商、模型名称、API 密钥等信息。
每次回合启动时,系统会把当前的 Profile 配置冻结成快照。运行中的回合只使用自己的快照,不会被中途的 Profile 修改影响。
四个实例槽位
| 槽位 | 对应实例 | 说明 |
|---|---|---|
narrator | 叙述者 | 生成故事内容 |
director | 导演 | 规划剧情走向 |
verifier | 校验员 | 检查内容一致性 |
memory | 记忆员 | 整理记忆 |
此外还有通配符 *,表示"所有未单独绑定的槽位"。
绑定方式
Profile 可以按作用域(全局或会话)和槽位粒度绑定。例如:
- 全局绑定一个 Profile 到
*→ 所有实例默认使用这个 Profile - 某会话单独绑定导演到另一个 Profile → 该会话的导演使用不同模型
解析优先级
对于某个会话的某个槽位,系统按以下顺序查找 Profile:
1. 会话 + 指定槽位 → 最高优先
2. 全局 + 指定槽位
3. 会话 + 通配符 → 通配回退
4. 全局 + 通配符
5. 都没有 → 使用环境变量配置每个槽位独立解析,互不影响。这意味着你可以:
- 让叙述者用高质量大模型(如 Claude 3.5 Sonnet)
- 让导演、校验员、记忆员用便宜快速的模型(如 GPT-4o-mini)
- 按会话粒度切换叙述者模型,而不影响其他实例
环境变量回退
如果某个槽位在五级优先级中都没有找到匹配的 Profile,就使用 LLM_API_KEY、LLM_PROVIDER 等环境变量配置。这保证了不做任何 Profile 配置也能正常运行。
6. 记忆系统
记忆系统解决一个核心问题:聊天越来越长,大模型的上下文窗口装不下所有内容,怎么办?
记忆从哪来
来源一:大模型自己写的摘要
很多酒馆预设会引导大模型在回复末尾输出类似这样的内容:
<summary>角色A向角色B表白,被婉拒。角色B透露自己即将离开这座城市。</summary>系统会自动识别并提取这些摘要。支持多种标签名(<summary>、<摘要>、<memory> 等),也可以在预设里自定义标签名。
提取顺序很重要:
- 先从大模型原始输出中提取摘要标签。
- 然后再跑正则处理(该隐藏的隐藏,该替换的替换)。
- 这样既不影响用户看到的文本,也不会丢掉摘要信息。
来源二:记忆员实例主动整理
在高级模式下,记忆员实例会在每个回合结束后:
- 读取最近几个楼层的内容。
- 结合已有的摘要。
- 输出结构化的记忆操作:新增事实、更新事实、标记过时事实。
记忆员实例的输出是严格的 JSON 格式,不是自由文本。比如:
{
"turn_summary": "角色A向角色B表白被拒,角色B将离开城市",
"facts_add": [
{ "key": "角色B即将离开", "value": "计划下周离开", "scope": "chat" }
],
"facts_update": [
{ "id": "fact_001", "value": "角色A的心情变为沮丧" }
],
"facts_deprecate": [
{ "id": "fact_002", "reason": "之前推测角色B会留下,现已矛盾" }
]
}记忆怎么存
每条记忆是一个独立的记录,包含:
- 类型:事实(fact)、摘要(summary)、开放剧情线(open_loop)。摘要进一步分为短摘要(micro,覆盖一两个回合)和长摘要(macro,由多条短摘要压缩而来)。
- 层级:全局、会话或楼层,决定这条记忆在什么范围内可见。
- 来源:哪个楼层、哪条消息产生的,方便追溯。
- 评分:重要度和可信度评分,影响注入优先级。
- 生命周期:活跃(active)→ 已压缩(compacted)→ 已弃用(deprecated)。已弃用的记忆不再注入提示词。
记忆之间可以有六种关系:支持(supports)、矛盾(contradicts)、更新(updates)、派生(derived_from)、压缩(compacts)、消解(resolves)。
记忆怎么用
每次组装提示词时,编排器会:
- 按 token 预算分配记忆可用空间。
- 按重要程度和相关性选取记忆条目。
- 打包成「记忆摘要块」注入到提示词中。
- 在兼容模式下,还会按酒馆的方式将摘要放到旧楼层的位置(替代被隐藏的完整内容)。
主聊天链读取记忆时,会先按当前上下文展开 global → chat → floor 三层可见范围, 再按既有的 importance / balanced / dual-summary 规则统一排序、裁剪和注入。
安全机制
- 记忆员实例的输出需要经过校验才会写入数据库,不会直接写入。
- 摘要文本会做基本的清洗,过滤掉可能的提示词注入(比如「忽略以上所有指令」这种内容)。
- 所有记忆操作都有完整的来源追溯,可以知道每条记忆是什么时候、从哪个楼层产生的。
自动维护
系统在后台运行维护任务,不阻塞对话:
- 衰减排序:按半衰期对记忆做时间衰减,越老的记忆权重越低。
- 过期弃用:自动把过期的摘要标记为弃用。
- 宏摘要压缩:当短摘要(micro)积累到一定数量时,自动触发长摘要(macro)压缩。触发条件包括 active micro 数量、token 总量和楼层跨度阈值。
这些任务由后台作业系统调度,支持重试和超时处理。每个作用域有独立的作业状态。
7. 一次完整回合的流程
把前面所有系统串起来,一次用户发消息到 AI 回复的完整过程是这样的:
用户发送消息
│
▼
① 创建新楼层(状态:draft)
创建消息页 v1,写入用户消息
│
▼
② 导演实例(可选)
分析当前局势,输出本回合结构化指令
│
▼
③ 记忆检索
查找相关记忆条目,准备注入
│
▼
④ 提示词编排
收集:预设 + 世界书命中 + 记忆 + 历史楼层 + 变量
按 token 预算裁剪
正则前处理
拼装成最终 messages[]
│
▼
⑤ 叙述者实例生成
楼层状态 → generating
流式写入消息页
│
▼
⑥ 后处理
提取摘要标签 → 送入记忆候选池
正则后处理 → 生成用户可见文本
│
▼
⑦ 校验员实例(可选)
检查生成内容一致性
│
▼
⑧ 记忆员实例整理
整理记忆,输出新增、更新、弃用操作
校验通过后从页级提升到楼层或会话级
│
▼
⑨ 提交楼层
楼层状态 → committed
页级变量按策略提升
触发 floor.committed 事件
│
▼
返回结果给用户如果中间任何步骤失败,楼层标记为 failed,保留现场方便排查。
8. 数据库设计
使用 SQLite 数据库,通过 Drizzle ORM 操作。核心表结构详见 数据库字典。
9. API 概览
所有接口都是 REST 风格,返回 JSON 格式数据。详见 API 参考。
会话管理
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /sessions | 创建会话 |
| GET | /sessions | 列出会话 |
| GET | /sessions/:id | 获取会话详情 |
| PATCH | /sessions/:id | 更新会话配置 |
| DELETE | /sessions/:id | 删除会话 |
生成与聊天
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /sessions/:id/respond | 发送消息并获取 AI 回复(核心接口) |
| POST | /sessions/:id/respond/stream | SSE 流式返回 AI 回复片段 |
| POST | /sessions/:id/respond/dry-run | 仅组装 Prompt 并返回调试元信息(无副作用) |
| POST | /sessions/:id/regenerate | 重新生成最后一个楼层 |
| GET | /sessions/:id/timeline | 获取完整时间线(楼层 + 消息页) |
其中 dry-run 会返回本轮提示词组装的完整运行结果,包括生成意图、预填充状态、世界书命中情况等。字段说明见官方集成层 - assembly 字段。
导入(酒馆兼容)
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /import/preset | 导入酒馆预设 |
| POST | /import/worldbook | 导入酒馆世界书 |
| POST | /import/regex | 导入酒馆正则规则 |
| POST | /import/character | 导入酒馆角色卡 |
| POST | /import/chat | 导入聊天文件(自动识别 .thchat 原生 / ST .jsonl) |
导出
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /export/chat/:id | 导出会话(.thchat 无损 / .jsonl ST 兼容) |
| GET | /export/preset/:id | 导出预设(ST 原始 JSON) |
| GET | /export/worldbook/:id | 导出世界书(ST 格式 JSON) |
| GET | /export/regex/:id | 导出正则配置(ST 格式 JSON 数组) |
| GET | /export/character/:id | 导出角色卡(ST Character Card V2 JSON) |
导入和导出形成对称关系:导入解析外部格式写入数据库,导出从数据库序列化为标准文件。聊天文件额外有一套 TavernHeadless 原生格式(.thchat),能无损保留完整四层数据结构、变量、记忆,以及 superseded 楼层历史关系。
10. 事件系统
系统内部通过事件总线来解耦各模块。当某件事发生时,系统广播对应事件,其他模块可以监听并做出反应。以下是主要事件:
| 事件名 | 触发时机 | 携带数据 |
|---|---|---|
session.created | 创建会话后 | session 对象 |
floor.created | 创建楼层后 | floor 对象 |
floor.committed | 楼层提交后 | floor 对象 + 变量变更 |
floor.failed | 楼层生成失败 | floor 对象 + 错误信息 |
page.activated | 切换生效消息页 | page 对象 |
message.appended | 新消息写入 | message 对象 |
generation.started | 开始调用大模型 | 模型配置 + token 预算 |
generation.chunk | 收到流式片段 | 文本片段 |
generation.completed | 生成完成 | 完整文本 + token 统计 |
generation.failed | 生成失败 | 错误信息 |
memory.extracted | 提取到摘要 | 摘要内容 + 来源 |
memory.committed | 记忆写入数据库 | 记忆操作列表 |
worldbook.matched | 世界书条目命中 | 命中的条目列表 |
regex.applied | 正则规则执行 | 规则 ID + 替换结果 |
| tool.call_started | 工具调用开始 | 工具名 + 参数 + 调用方实例 | | tool.call_completed | 工具调用完成 | 工具名 + 返回值 + 耗时 | | tool.call_failed | 工具调用失败 | 工具名 + 错误信息 | | tool.call_denied | 工具调用被拒绝 | 工具名 + 拒绝原因 |
| mcp.connected | MCP 服务器连接成功 | serverId, serverName, toolCount | | mcp.disconnected | MCP 服务器断开连接 | serverId, serverName | | mcp.error | MCP 服务器连接出错 | serverId, serverName, error |
这些事件可以用来:
- 在前端实时显示生成进度(通过 WebSocket 转发)。
- 记录日志和调试信息。
- 触发自定义逻辑(插件系统的基础)。
11. 工具调用
工具调用让大模型在角色扮演回合中执行结构化操作——读写变量、掷骰子、查询记忆等——而不仅仅是生成自由文本。
设计目标
- 所有实例(叙述者、导演、校验员、记忆员)都可以调用工具,每个实例的权限独立配置。
- 每次工具调用都会生成一条执行记录,归属到当前楼层,方便审计和排查。
- 运行时工具目录是会话级快照,通过
/sessions/:id/tools/runtime查看当前会话真正可调用的工具;它不直接展开未来 run / node / step overlay。 - 当前公开配置的
toolMode仍只有inline,即大模型在生成过程中自主决定是否调用工具。但部分 allowlisted 工具在inline回合内部可以先返回 deferred receipt,再由后台runtime_job延后完成。
运行时工具目录中的单个工具仍可能带有 defaultDeliveryMode = async_job。这表示该工具在当前内联回合里会返回受理回执,并由后台 runtime_job 继续完成真实执行。
工具来源
| 来源 | 说明 |
|---|---|
| 内置工具 | 引擎自带的 7 个通用工具:读写变量、掷骰子、随机选择、获取时间、查询记忆、获取角色信息 |
| 资源管理工具 | 23 个资源操作工具,覆盖角色卡、世界书、正则配置、预设的读写操作。允许大模型在对话中主动读写这些资源。 |
| 预设/角色卡工具 | 从数据库加载的自定义工具定义,支持脚本执行 |
| MCP 工具 | 通过 MCP 协议连接外部工具服务器。支持标准输入输出和 HTTP 两种传输方式。通过环境变量 ENABLE_MCP=true 启用。 |
当前支持的执行模式
| 模式 | 说明 |
|---|---|
inline | 大模型在生成过程中自主决定是否调用工具。这是默认模式,也是当前唯一可用的公开配置模式。对于允许延后执行的工具,本轮也可能先返回 deferred receipt,再通过 runtime_job 继续执行。 |
standalone | 当前未实现。服务端会返回结构化配置错误。 |
both | 当前未实现。服务端会返回结构化配置错误。 |
权限控制
每个会话可以独立配置工具权限,控制哪些实例能调用哪些工具:
- 白名单和黑名单:按实例槽位设置允许或禁止调用的工具列表。
- 不可撤销工具开关:是否允许调用有外部副作用的工具(如 MCP 外部接口)。
- 调用次数上限:单回合最大调用次数。
- 生成步数上限:大模型单次生成中最多执行多少步工具调用(默认 5 步)。
副作用级别
每个工具都标记了副作用级别,系统据此决定如何处理工具的写入操作:
| 级别 | 含义 | 示例 |
|---|---|---|
none | 纯查询,不产生任何变更 | 读取变量、掷骰子 |
sandbox | 写入操作先存到页级沙箱,楼层提交时才正式生效 | 设置变量 |
irreversible | 不可撤销的外部操作,一旦执行无法回滚 | MCP 外部接口调用 |
执行审计与隔离
- 每次工具调用都会生成一条
tool_execution_record,归属到当前楼层。 - 重新生成创建新楼层,工具重新执行,不复用旧记录。
- 工具的写入副作用先停留在页级沙箱,只有楼层提交时才正式生效。
- 系统同时保留了一套兼容的
tool_call_record查询接口,供旧版客户端使用。 - MCP 工具目录在 live 列举失败时,可以回退到 cached 快照;会话级运行时目录会通过
catalog_source标记live或cached。 scripthandler 在 Beta3 默认关闭。- 只有服务端显式设置
ENABLE_UNSAFE_SCRIPT_HANDLER=true时,/tools/definitions的 script 创建、更新和重新启用才会放行。 - 默认关闭时,历史 definition-backed script tools 会继续出现在
/sessions/:id/tools/runtime中,但会被标记为unavailable,不会进入可执行目录。 /mcp/servers和/mcp/servers/:id现在会回显live_status,用来说明数据库配置是否已经进入 live runtime manager。- 当
ENABLE_MCP=true时,MCP 配置 create / update / enable / disable / delete 会直接同步McpConnectionManager,避免数据库状态和运行时状态分裂。 - 世界书
position=outlet现在会进入真实 prompt 组装:优先按同名 outlet marker/placement 注入;没有匹配 marker 时,会回退为显式 section,而不是静默丢弃。 - Regex 主链现在会透传
runOnEdit与 depth 上下文:edit-and-regenerate使用channel="edit",USER_INPUT / AI_OUTPUT / at-depth WORLD_INFO 会消费minDepth/maxDepth。
API 端点
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /tools/builtin | 列出内置工具 |
| GET | /tools/definitions | 列出自定义工具定义 |
| GET | /tools/definitions/:id | 获取单个工具定义 |
| POST | /tools/definitions | 创建自定义工具 |
| PATCH | /tools/definitions/:id | 更新工具定义 |
| PATCH | /tools/definitions/:id/toggle | 启用/禁用工具 |
| DELETE | /tools/definitions/:id | 删除工具定义 |
| GET | /tool-executions | 查询主执行审计记录 |
| GET | /tools/call-records | 查询兼容调用记录 |
| GET | /sessions/:id/tools/runtime | 获取会话级运行时工具目录 |
| GET | /sessions/:id/tool-permissions | 获取会话基础工具权限 |
| PUT | /sessions/:id/tool-permissions | 替换会话基础工具权限 |
| PATCH | /sessions/:id/tool-permissions | 合并更新会话基础工具权限 |
| GET | /mcp/servers | 列出 MCP 服务器配置 |
| GET | /mcp/servers/:id | 获取单个 MCP 服务器配置 |
| POST | /mcp/servers | 创建 MCP 服务器配置 |
| PATCH | /mcp/servers/:id | 更新 MCP 服务器配置 |
| DELETE | /mcp/servers/:id | 删除 MCP 服务器配置 |
| PATCH | /mcp/servers/:id/toggle | 启用/禁用 MCP 服务器 |
| GET | /mcp/servers/:id/status | 查看连接状态 |
| GET | /mcp/statuses | 查看所有连接状态 |
| POST | /mcp/servers/:id/connect | 连接/重连 |
| POST | /mcp/servers/:id/disconnect | 断开连接 |
| GET | /mcp/servers/:id/tools | 查看服务器工具列表 |
| POST | /mcp/servers/:id/test | 测试连接 |
连接状态接口还会返回 reconnect_required(是否需要重连)和 last_timeout_at(上次超时时间)。超时类型为"结果不确定"时表示工具调用的结果未知,需要重连后重试,而不是普通的调用失败。
同一会话同一分支上的生成排队只在当前进程内生效。