酒馆 SillyTavern:把一段 system prompt 换成每轮重拼的上下文
摘要
酒馆是一个 AI 角色扮演前端:自己不带模型,接用户的 API,管理角色卡、世界书、预设,每轮把它们拼成 prompt1。拿网页版 ChatGPT 当 baseline——一段 system prompt 写人设,历史全量回放,每次生成一条——它在角色扮演里会撞上八类毛病:人设一刀切、设定塞不下、指令被历史淹没、换模型就换规矩、说错改不了、聊久了忘、多角色串台、规则写死。酒馆的回答是每轮从零重拼:世界书只扫最近 2 条消息决定哪些设定进场,作者注释这类指令按「距末尾第几条」插进历史,固定部分先占位,历史从新往旧填满剩下的预算。设计重心是规则全在拼装这一步,由用户和作者配置,模型只管接着写。
关键词
AI 角色扮演(LLM roleplay,RP)· 角色扮演前端(RP frontend)· role-playing language agents · 角色卡(character card)· 世界书(world info / lorebook)· 预设(preset)· 作者注释(Author's Note)· prompt 组装(prompt assembly)· 上下文工程(context engineering)· Chat Completion · Text Completion
术语
| 词 | 出处 | 意思 |
|---|---|---|
| 角色卡 | 领域通用 | 一张内嵌 JSON 的 PNG,装一个角色的描述、开场白、示范对话等字段 |
| 世界书 | 领域通用,酒馆叫 World Info,也叫 Lorebook | 设定拆成的条目集,每条带关键词,命中才注入 |
| 蓝灯、绿灯 | 酒馆中文社区 | 常驻的世界书条目、关键词触发的条目 |
| 预设 | 领域通用 | 采样参数加全部提示词条目和它们的顺序,打成一个 JSON |
| 槽位 | 本文起,酒馆叫 marker | Prompt Manager 里没有正文、运行时才填内容的条目,如 charDescription、chatHistory |
| 深度注入 | 本文起,深度(depth)是酒馆的叫法 | 按「距末尾第几条」把一段文字插进聊天历史;深度 0 在最后一条之后 |
| 作者注释 | 领域通用,Author's Note | 用户写给当前聊天的一段指令,出厂插在深度 4 |
| 角色笔记 | 酒馆自造,Character's Note | 卡里随卡分发的一段深度注入 |
| 末尾指令 | 本文起,酒馆叫 Post-History Instructions,旧称 jailbreak | 排在整个聊天历史之后的指令 |
| 用户人设 | 本文起,酒馆叫 Persona | 用户扮演的那个人的名字和描述 |
| swipe | 领域通用 | 同一位置的多个候选回复,左右滑动切换 |
| 文件头 | 本文起,酒馆叫 chat_metadata | 聊天 JSONL 的第一行,存这一局的状态 |
| Chat Completion、Text Completion | 领域通用 | 发消息数组的 API、发一整条字符串的 API |
引言
领域是 AI 角色扮演:不是问答,用户扮演故事里的一个角色,和模型一起把故事演下去。
- 软件形态叫「角色扮演前端」:自己不带模型,接用户的 API,管理角色卡、世界书、预设,再把它们拼成 prompt。酒馆自称「LLM Frontend for Power Users」,2023 年 2 月从 TavernAI 1.2.8 分出来2,同类还有 RisuAI、Agnai
- 研究界多叫 role-playing language agents;商业产品那边是 Character.AI 这类「AI 角色 / AI 陪伴」
baseline 是网页版 ChatGPT:一段 system prompt 写人设,历史消息全量回放,每次生成一条回复。下面八条功能线,各从它在角色扮演里撞上的一个具体毛病讲起,只写现状;每条线一篇细讲,每步贴源码模版和用酒馆自己的代码跑出来的效果。实跑用随包的角色卡 Seraphina 和世界书 Eldoria 的中译(塞拉菲娜、埃尔多利亚),用户名「旅人」,模型接 DeepSeek,其余是出厂设置。
一、角色卡:一段人设按寿命拆成几格
- 人设是一段字,换个前端、发给别人就得整段复制 → 卡是一张 PNG:JSON 写进图片的文本块,转发图片就是转发角色
- 示范对话写在 system prompt 里,聊到第 80 轮还占着位置 → 字段分槽:描述常驻;示范对话只拿剩余预算,最先被挤掉;开场白是聊天的第 0 条;末尾指令贴在最后;角色笔记固定在深度 4
- 每局开头都一样 → 备选开场白:和开场白一起成为第 0 条的候选
- 卡想在全局指令上补一句,而不是整段替换 →
{{original}}:展开成被覆盖的原文 - 「我是谁」只能和角色写在一起 → 用户人设:独立一份,可以放在深度 2 跟着对话前沿走
- 名字写死,卡没法给别人用;随机值每轮重算就每轮变 → 晚绑定宏:
{{char}}、{{user}}每轮代入;{{pick}}以聊天 ID 为种子,同一局不变
细讲:角色卡:一段人设按寿命拆成几格 —— 卡的存法、字段落进哪个槽、宏
二、世界书:设定按关键词进场
- 几万字的设定塞不进 system prompt → 世界书:每轮只扫最近 2 条消息,关键词命中才注入
- 同一个词在不同话题里指不同的东西 → 次关键词:四种逻辑消歧
- 设定互相引用,聊天里没提到的名字模型不认识 → 递归:激活条目的内容再扫一轮
- 世界书一扩张就把历史挤掉 → 预算:上下文的 25% 封顶,第一次溢出就停
- 输入一样 prompt 就一样,随机事件做不出来;互斥的变体同时命中 → 概率和互斥组
- 没有跨轮状态:地牢规则只在提到「地牢」的那两条里有效 → 定时效果:sticky、cooldown、delay,以聊天条数为时钟
- 设定放在开头影响弱 → 8 个插入位置,含按深度插进聊天
细讲:世界书:设定按关键词进场 —— 每步判定取自酒馆自己的调试日志
三、每轮现拼 prompt:放哪、放不下砍谁
- system prompt 是一整段,顺序改不了、也没法单独关一段 → 具名槽位:Prompt Manager 把 prompt 拆成可排序、可开关的条目,运行时填内容
- 该随剧情变的东西没地方放 → 每轮从零重拼:作者注释、世界书、角色笔记每轮重算;实跑后几轮世界书全部退场,省下近九百个 token
- 「保持第三人称」写在最前面,聊到第 80 轮被历史淹没 → 深度注入:一张注入表,所有模块登记文字和深度;同一深度按角色、键名排序,规则写死
- 全量回放,聊长了就溢出 → 预算:固定部分必放,历史从新往旧连续填,示范对话最后拿余量
- 用户不知道模型看不见哪些消息 → 截断线标在界面上
细讲:每轮现拼 prompt:放哪、放不下砍谁 —— 出厂槽位的真实账本、注入表、三档上下文各留下什么
四、一份内容喂各种模型
- Claude 要 system 单独放、严格交替,Gemini 的 assistant 叫 model → 服务端按供应商改写
- 自搭的端点各有怪规矩 → 几档通用后处理:merge、semi、strict、single;DeepSeek 固定走 semi
- 回复被截断,再发「继续」会重起一段 → continue:被截断的那条拿到最后,用续写提示或预填接着写
- 想照文风让模型替自己起草一句 → impersonate
- 调好的 prompt 和参数带不走 → 预设:参数、条目、顺序打成一个 JSON
- 本地模型只收一整条字符串 → Text Completion:前端按 instruct 模板套 chat template,模板序列兼作停止串
细讲:一份内容喂各种模型 —— 同一份消息数组过各家转换、续写、ChatML 拼法
五、重来、分叉、改历史
- 重新生成会覆盖上一版 → swipe:同一位置留多个候选,只有选中的进下一轮
- 模型说错的一句被一直回放 → 编辑:任意消息都能改,改的就是下一轮的输入
- 只有删和留两种选择 → 隐藏:人看得见,模型看不见
- 想试另一个走向,又不想丢掉现在这条线 → 分支:复制前缀,带上文件头另存;变量、世界书计时跟着走
- 整文件覆盖写,删错没法撤销 → 备份
细讲:重来、分叉、改历史 —— 候选的存法、隐藏、用原样的 createBranch 开分支
六、聊久了
- 开头的约定被挤出窗口 → 摘要:每 10 条带着完整上下文写一次,作为一段注入紧跟主提示;实跑出厂设置下,摘要被 300 token 的回复长度截断
- 删了消息,摘要还写着不存在的情节 → 摘要挂在消息上,随删改回退
- 摘要不删原文,token 没省下来 → 配合 /hide
- 摘要丢细节 → 向量召回:按语义把相关的旧原文挪回本轮 prompt
细讲:聊久了 —— 两次真实摘要和它落在哪
七、多角色同场
- 群里说一句,该谁回 → 调度器:点名的一定说,其余按话痨度掷骰
- 模型一次替所有人说话 → 一次只让一个角色生成,后说的看得到先说的
- 几个 NPC 都是 assistant,分不清谁说的 → 名字前缀、「只写某某的下一句」、截掉别人的台词
- 换人就换整段人设,本地缓存全失效 → Swap 和 Join 两种拼卡
细讲:多角色同场 —— 调度函数实跑、两轮真实群聊的完整请求
八、用户自己加规则
- 显示的、存下的、发给模型的是同一个字符串,状态栏被一直回放 → 正则三层分离,可按深度只作用于旧消息
- 好感度只能靠模型记 → 变量存在文件头,宏读写
- 「每轮后做点什么」没地方挂 → STscript 和 Quick Reply,世界书条目激活也能触发脚本
- 扩展要在发出前改 prompt → 串行可改的事件总线、生成拦截器
- 模型没法掷骰子、查东西再接着讲 → 工具调用:递归生成,最多 5 轮
细讲:用户自己加规则 —— 正则三层对照、事件监听原地改 prompt
局限
- 规则都按字面算:世界书按关键词字面命中,点名按
\w切词,摘要按条数触发,程序不理解内容;中文点名点不中就是这么来的。向量匹配能补同义说法,但文档自己建议要可预期就用关键词 - 每轮重拼和前缀缓存相冲:世界书、深度注入、向量召回每轮在变,前缀跟着变,缓存就失效。文档自己提醒向量召回和缓存冲突;群聊的 Join 就是为本地后端的前缀缓存做的让步,代价是角色混淆自己和别人
- 状态由用户维护:模型只写文本,好感、物品这些状态要用户自己用正则、变量、脚本抽取维护,程序不会从回复里更新结构化状态
- 分支只靠文件名互链:没有会话树,分支是一份复制出来的新文件,文档原话改名「will break links to that chat from checkpoints」
结论:可以搬走的设计
- 按「距末尾第几条」注入:指令、状态、临时设定都用深度定位,跟着对话前沿走,不随历史变长被推远;同一深度按角色、键名排序,结果可预期
- 关键词进场、按比例封顶、溢出即停:外部知识按最近几条消息的字面命中注入,条目内容还能再触发别的条目,适合做 RAG 前面那层确定性的检索
- 预算分三档:必放的先占位,历史从新往旧连续填(不留空洞),可有可无的示范拿余量
- 定时效果拿消息条数当时钟,存在会话头:sticky、cooldown、delay 只需要一个起点和一个长度;重来时聊天没前进,计时就作废,不用写回滚代码
- 显示、存储、发给模型三层分离:同一条消息三处可以是三个样子,按深度只改旧消息
- 前端只产一种格式,供应商差异收进一个纯函数模块:再给未知端点几档通用后处理,接新供应商只动服务端一处
- 分支 = 复制前缀 + 继承会话头:状态都放在会话文件头,开分支就是带着它另存一份,不需要专门的状态迁移
- 晚绑定宏,随机值以会话 ID 为种子:名字每轮代入,一张卡给谁都能用;要随机又要同一局稳定的地方,用会话 ID 和位置做种子
维护记录:读的哪个版本、素材从哪来、实验怎么跑的。正常阅读不用看。
-
SillyTavern/SillyTavern,2026-10-20 读。 ↩
-
SillyTavern 文档首页,2026-10-20 读。 ↩