为什么多了一层 Message?
酒馆的对话模型是三层:Session → Floor → Page。 TavernHeadless 在这个基础上加了一层 Message,变成 Session → Floor → Page → Message[]。 这篇文章说明这一层的来由、边界,以及它解决了什么问题。
酒馆的三层模型
酒馆的对话结构是三个层级:
Session → Floor → PageSession 是一次完整的对话。Floor 是对话时间线上的一个回合,通常由一次用户输入触发。Page 是这个 Floor 下的一个版本。如果用户重新生成了一次,同一个 Floor 下面就会有多个 Page。
在这个模型里,Page 同时承担版本容器和内容容器的职责。对于轻量 RP 场景来说,这通常是够用的。用户输入和 AI 输出可以作为一个整体处理,翻页就是切换这个回合的不同版本。
这个模型什么时候会不够用
Page 作为最小内容单位有一个隐含的假设:一个 Page 里面的内容不需要被单独处理。整页一起显示,整页一起参与提示词拼接,整页一起被删除或隐藏。
这个假设在下面几种情况下会失效。
第一种情况:需要对单条消息做精细控制
在提示词拼接时,并非一个回合里产生的所有内容都应该被送入 LLM。可能会有下面这些需求:
- 某条系统消息只在某一轮有效,之后不应该继续占用上下文。
- 某条 AI 回复的内容有问题,需要隐藏它,但不能把同一 Page 里的用户消息也一起隐藏。
- 工具调用的中间结果需要保存在对话记录里,但不应该参与后续的提示词拼接。
如果 Page 是最小内容单位,这些需求就无从实现。要么整页保留,要么整页丢弃。
第二种情况:工具调用和 MCP
当系统支持工具调用之后,一个回合里可能产生多条消息:
- 用户输入
- AI 决定调用工具
- 工具返回结果
- AI 根据工具结果继续生成
- 最终 AI 回复
这些内容都属于同一个回合的一次运行,但它们有不同的角色、不同的来源、不同的参与提示词拼接的策略。把它们全部塞进一个 Page 里而不做区分,后续的编排逻辑会很难处理。
第三种情况:Agentic 中间产物
在 Agentic 场景下,一个回合的内部远不止用户输入和 AI 回复。Director 会输出叙事意图,Memory Agent 会输出记忆操作建议,Verifier 会输出检查结果。
这些中间产物需要被记录下来,否则无法审计。但它们不是最终正文,不应该直接展示给用户。它们需要有自己的消息角色、自己的可见性标记。
Message 层到底改变了什么
TavernHeadless 在 Page 下面加了一层 Message,变成:
Session → Floor → Page → Message[]Message 是对话内容的最小独立单位。每条 Message 有自己的 ID、role、content、可见性标记和来源信息。一个 Page 下面可以有多条 Message,但这些 Message 必须属于同一个 Floor 的同一次候选结果。
这也让 Page 的含义变得更准确。Page 不只是单条消息的版本容器,而是一个回合的一次候选运行结果。一次运行里面可以记录用户输入、模型输出、工具结果、内部 Agent 产物和最终回复。
Page 没有替代 Floor
这里需要明确一个边界:Page 下面可以有多条 Message,并不表示 Page 可以执行 Floor 的职责。
Floor 负责对话时间线上的位置。新的用户输入、新的外部回合、分支从哪里开始,都由 Floor 表达。
Page 负责同一个 Floor 下的版本。重新生成、切换版本、比较不同结果,都由 Page 表达。
Message 负责一版内部的内容。工具调用、MCP 返回、Director 输出、Verifier 检查、最终回复,都以 Message 记录。
因此可以把边界写成两句话:
- Page 里可以有多步执行。
- Page 里不能有多轮对话。
如果用户发起新的独立输入,就应该创建新的 Floor,而不是继续往旧 Page 里追加 Message。这样一来,Page 可以承载一个回合内部的 Agentic 过程,但不会替代 Floor 的回合语义。
Message 层解决的核心问题
这一层解决的不是“多存一点数据”的问题。它解决的是四个更根本的问题。
独立寻址
每条 Message 有独立的 ID。可以单独查询、单独更新、单独隐藏、单独删除。不需要因为要隐藏一条 AI 回复,就把同一页的用户消息也带走。
角色分离
Message 的 role 可以是 user、assistant、system、narrator,以及后续可能出现的新角色。不同角色的消息在提示词拼接时可以被不同对待。
工具调用的结果可以用 tool 或类似的 role 标注,编排器可以识别它们并决定是否送入上下文。Director 的输出可以用内部 role 标注,只对 Narrator 可见,对用户不可见。
精确的提示词组装
当每一条消息都是独立实体时,提示词组装就从一个粗粒度的“选哪些 Page”的问题,变成一个细粒度的“选哪些 Message,以什么顺序排列”的问题。
编排器可以决定:
- 哪些历史消息参与本次拼接。
- 哪些系统消息在当前轮仍然有效。
- 工具调用结果是否保留在上下文中。
- 记忆注入的内容放在什么位置。
可审计性
这是 Know What & Know How 原则在数据模型层面的落实。
如果一个回合的输出有问题,使用者可以精确地看到这一轮里每一条 Message 的内容、角色和来源,而不是只能看到一个 Page 的聚合结果。
这对排查问题至关重要。
和酒馆模型的关系
TavernHeadless 的四层模型不是对酒馆模型的否定。三层模型在轻量场景下仍然成立。
四层模型是对三层模型的扩展:Floor 仍然表示回合,Page 仍然表示版本,只是 Page 内部的内容不再是一整块文本,而是拆成了 Message 列表。当系统只需要显示对话内容时,可以把一个 Page 下面需要展示的 Message 按顺序拼接起来,结果和三层模型下的 Page 内容一致。
这也解释了为什么这个设计可以自然支持 Agentic。Agentic 过程并没有把一个回合拆成多轮对话,它只是在同一个回合内部产生了更多步骤。Message 层给这些步骤留下了记录位置,Page 仍然保持版本语义,Floor 仍然保持回合语义。
但当系统需要更精细的控制时——工具调用、Agentic 中间产物、选择性隐藏、精确提示词拼接——Message 层就提供了三层模型无法提供的能力。
这个设计负担了什么
增加一层意味着增加复杂度。
查询一个回合的对话内容时,需要先确定当前采用的 Page,再查询这个 Page 下面的 Message。创建消息时需要指定 page_id。API 接口从“操作 Page”变成了“操作 Page 和操作 Message”两个层次。
系统还需要维护 Page 和 Floor 的边界:新的用户回合必须创建新的 Floor,不能把多个外部回合塞进同一个 Page。
这些是额外负担。
但这个负担的回报是明确的:
- 工具调用和 MCP 集成的实现逻辑不再需要绕过数据模型。
- Agentic 中间产物的存储和审计有了明确的位置。
- 提示词编排可以做到逐消息级别的精度。
- Page 可以承载一次回合运行的内部过程,同时不破坏 Floor 的时间线语义。
- 使用者排查问题时可以看到每一轮里的每一条消息。
对于 TavernHeadless 要支持的场景来说,这些回报大于负担。