Skip to content

为什么多了一种变量?

酒馆的变量系统有四种作用域:全局、聊天、楼层、消息页。 TavernHeadless 在中间多加了一种:分支。 这篇文章说明为什么会多这一层,以及它和分支树结构的关系。

酒馆为什么不需要分支变量

在酒馆的模型里,变量作用域大致可以分成四层:

text
global → chat → floor → page

全局变量跨聊天生效。聊天变量属于当前聊天文件。楼层变量属于某个回合。消息页变量属于某次生成或某个版本。

这个模型本身是成立的。因为在酒馆里,聊天分支并不是在同一个聊天文件里维护一棵完整的分支树。更常见的做法是:从原来的聊天中复制出一个新的聊天文件,再记录它和原聊天之间的关联。

这样做很直接,也很有效。

新分支既然已经是一个新的聊天文件,那么它天然就有自己的一份聊天级变量。对这个新文件来说,chat 作用域已经可以表达“这条分支自己的长期状态”。

因此,酒馆不需要单独设计一个 branch 变量作用域。分支隔离由文件复制完成。状态隔离也跟着文件复制完成。

TavernHeadless 的分支不是新聊天文件

TavernHeadless 选择了另一条路。

它没有把每条分支都当成一个新的聊天文件,而是把分支作为同一个 Session 里面的结构来维护。一个 Session 里面可以有主线,也可以从某个 Floor 开始长出新的分支。

这意味着,分支不再只是“另一个聊天文件”。它是 Session 内部的一条故事线。

这带来一个直接问题:如果仍然只有 chat 变量,那么同一个 Session 里的所有分支都会共享同一份聊天级状态。

例如:

text
main 分支:角色 A 没有受伤
branch-1:角色 A 在战斗中受伤
branch-2:角色 A 避开了战斗

如果“角色 A 是否受伤”只能写到 chat,那么这三条线会互相污染。

写到 floor 又太短。楼层变量只适合表达一个回合内的结果,不适合表达这条分支后续都要继承的状态。

写到 page 更短。页级变量主要服务于一次生成尝试,重试后可能就不再采用。

写到 global 更不合适。它会影响所有会话。

所以 TavernHeadless 需要一个处在 floorchat 之间的作用域。

这就是 branch

分支变量解决的是什么

分支变量解决的不是“多一种变量名”的问题。它解决的是同一个 Session 内多条故事线的状态隔离问题。

TavernHeadless 的变量读取顺序是:

text
page → floor → branch → chat → global

这个顺序可以理解为从近到远:

  • page 是一次生成尝试。
  • floor 是一个回合。
  • branch 是一条故事线。
  • chat 是整个会话。
  • global 是跨会话的全局状态。

分支变量放在 floorchat 中间,正好表达“这条故事线长期有效,但不影响其他故事线”的状态。

例如:

  • 当前分支里某个 NPC 已经死亡。
  • 当前分支里某个地点已经被烧毁。
  • 当前分支里玩家选择了阵营 A,而另一条分支选择了阵营 B。
  • 当前分支进入了坏结局路线,但主线仍然停留在普通路线。

这些状态都不应该写成全局,也不应该写成整个会话共享。它们属于某条分支。

真正的分支树需要自己的状态层

如果只是复制聊天文件,分支状态可以跟着文件走。这个做法的好处是简单:每个文件都是一份完整状态,不需要在读取时考虑分支继承。

但如果要在一个 Session 里维护真正的分支树,问题就变了。

系统需要知道:

  • 这条分支从哪个 Floor 分出来。
  • 它继承了分支点之前的哪些状态。
  • 它之后自己改了哪些状态。
  • 它和另一条分支的差异在哪里。
  • 删除或导出这条分支时,哪些变量也应该跟着处理。

这些问题只靠 chat 变量很难回答。因为 chat 太大,它代表整个会话,不代表某条具体故事线。

有了 branch 变量之后,分支树不再只是楼层之间的连接关系。它也有了自己的状态层。

这对回放、比较、导出、备份和审计都有意义。使用者可以更清楚地知道:一条分支为什么走到了当前状态,它和主线到底差在哪里。

和酒馆兼容不是同一件事

这里需要区分两件事。

第一件事是兼容酒馆资产。TavernHeadless 仍然需要理解酒馆的变量习惯,尤其是 localglobal 这类宏视图。

第二件事是 TavernHeadless 自己的底层真相模型。底层模型需要服务于真正的分支树,所以它必须有 branch 作用域。

也就是说,兼容层可以把酒馆里的 local 变量映射到适合的运行时视图,但底层资源仍然是:

text
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、状态治理和分支管理能不能真正用好这一层。