为什么多了一种变量?
酒馆的变量系统有四种作用域:全局、聊天、楼层、消息页。 TavernHeadless 在中间多加了一种:分支。 这篇文章说明为什么会多这一层,以及它和分支树结构的关系。
酒馆为什么不需要分支变量
在酒馆的模型里,变量作用域大致可以分成四层:
global → chat → floor → page全局变量跨聊天生效。聊天变量属于当前聊天文件。楼层变量属于某个回合。消息页变量属于某次生成或某个版本。
这个模型本身是成立的。因为在酒馆里,聊天分支并不是在同一个聊天文件里维护一棵完整的分支树。更常见的做法是:从原来的聊天中复制出一个新的聊天文件,再记录它和原聊天之间的关联。
这样做很直接,也很有效。
新分支既然已经是一个新的聊天文件,那么它天然就有自己的一份聊天级变量。对这个新文件来说,chat 作用域已经可以表达“这条分支自己的长期状态”。
因此,酒馆不需要单独设计一个 branch 变量作用域。分支隔离由文件复制完成。状态隔离也跟着文件复制完成。
TavernHeadless 的分支不是新聊天文件
TavernHeadless 选择了另一条路。
它没有把每条分支都当成一个新的聊天文件,而是把分支作为同一个 Session 里面的结构来维护。一个 Session 里面可以有主线,也可以从某个 Floor 开始长出新的分支。
这意味着,分支不再只是“另一个聊天文件”。它是 Session 内部的一条故事线。
这带来一个直接问题:如果仍然只有 chat 变量,那么同一个 Session 里的所有分支都会共享同一份聊天级状态。
例如:
main 分支:角色 A 没有受伤
branch-1:角色 A 在战斗中受伤
branch-2:角色 A 避开了战斗如果“角色 A 是否受伤”只能写到 chat,那么这三条线会互相污染。
写到 floor 又太短。楼层变量只适合表达一个回合内的结果,不适合表达这条分支后续都要继承的状态。
写到 page 更短。页级变量主要服务于一次生成尝试,重试后可能就不再采用。
写到 global 更不合适。它会影响所有会话。
所以 TavernHeadless 需要一个处在 floor 和 chat 之间的作用域。
这就是 branch。
分支变量解决的是什么
分支变量解决的不是“多一种变量名”的问题。它解决的是同一个 Session 内多条故事线的状态隔离问题。
TavernHeadless 的变量读取顺序是:
page → floor → branch → chat → global这个顺序可以理解为从近到远:
page是一次生成尝试。floor是一个回合。branch是一条故事线。chat是整个会话。global是跨会话的全局状态。
分支变量放在 floor 和 chat 中间,正好表达“这条故事线长期有效,但不影响其他故事线”的状态。
例如:
- 当前分支里某个 NPC 已经死亡。
- 当前分支里某个地点已经被烧毁。
- 当前分支里玩家选择了阵营 A,而另一条分支选择了阵营 B。
- 当前分支进入了坏结局路线,但主线仍然停留在普通路线。
这些状态都不应该写成全局,也不应该写成整个会话共享。它们属于某条分支。
真正的分支树需要自己的状态层
如果只是复制聊天文件,分支状态可以跟着文件走。这个做法的好处是简单:每个文件都是一份完整状态,不需要在读取时考虑分支继承。
但如果要在一个 Session 里维护真正的分支树,问题就变了。
系统需要知道:
- 这条分支从哪个 Floor 分出来。
- 它继承了分支点之前的哪些状态。
- 它之后自己改了哪些状态。
- 它和另一条分支的差异在哪里。
- 删除或导出这条分支时,哪些变量也应该跟着处理。
这些问题只靠 chat 变量很难回答。因为 chat 太大,它代表整个会话,不代表某条具体故事线。
有了 branch 变量之后,分支树不再只是楼层之间的连接关系。它也有了自己的状态层。
这对回放、比较、导出、备份和审计都有意义。使用者可以更清楚地知道:一条分支为什么走到了当前状态,它和主线到底差在哪里。
和酒馆兼容不是同一件事
这里需要区分两件事。
第一件事是兼容酒馆资产。TavernHeadless 仍然需要理解酒馆的变量习惯,尤其是 local 和 global 这类宏视图。
第二件事是 TavernHeadless 自己的底层真相模型。底层模型需要服务于真正的分支树,所以它必须有 branch 作用域。
也就是说,兼容层可以把酒馆里的 local 变量映射到适合的运行时视图,但底层资源仍然是:
global / chat / branch / floor / page这样做的目的不是把酒馆模型复杂化,而是在兼容酒馆的同时,给引擎自己的分支结构留下准确的位置。
这个设计带来的负担
多一层变量当然会增加复杂度。
系统需要维护 branch 宿主。变量接口需要接受 session_id + branch_id 这样的定位方式。删除分支时,需要清理对应的 branch 变量。备份和恢复时,也要处理分支变量的重建。
变量解析也更复杂。以前只需要考虑 page → floor → chat → global,现在中间多了一层 branch。
分支继承也会变得麻烦。从某个 Floor 分出新分支时,新分支应该看到哪些已有变量?这些变量是复制出来,还是通过快照和继承关系计算出来?如果之后主线继续变化,新分支要不要跟着变?这些都需要明确规则。
所以,这不是一个没有代价的设计。
它让数据模型、API、导入导出、备份恢复和提示词编排都多了一些工作。
它也许会在未来起作用
现在很难说,这个选择已经完全证明自己值得。
如果 TavernHeadless 只想做一个轻量的酒馆后端,那么复制聊天文件式的分支也许就够了。那样实现更简单,状态也更容易理解。
但 TavernHeadless 想做的是一套长期演进的 AI RP 引擎。它后面还会继续面对这些问题:
- 同一个会话里比较不同分支。
- 从某条分支继续生成,而不污染主线。
- 在 Agentic 流程中读取当前故事线的状态。
- 在 NodeGraph 中把分支状态作为明确输入。
- 审计某条分支的状态变化过程。
- 把分支作为可以导出、恢复、迁移的结构。
如果这些能力真的要做,分支就不能只是一组楼层的标签。它需要成为一等结构。既然分支是一等结构,它就需要自己的状态空间。
这就是分支变量存在的原因。
它不是为了比酒馆多一层而多一层。它是因为 TavernHeadless 选择了真正的分支树,而真正的分支树需要有自己的变量层。
这个设计带来的复杂度已经存在。它最终值不值得,要看后续的 Agentic、NodeGraph、状态治理和分支管理能不能真正用好这一层。