Skip to content

为什么多了一层 Message?

酒馆的对话模型是三层:Session → Floor → Page。 TavernHeadless 在这个基础上加了一层 Message,变成 Session → Floor → Page → Message[]。 这篇文章说明这一层的来由、边界,以及它解决了什么问题。

酒馆的三层模型

酒馆的对话结构是三个层级:

text
Session → Floor → Page

Session 是一次完整的对话。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,变成:

text
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 可以是 userassistantsystemnarrator,以及后续可能出现的新角色。不同角色的消息在提示词拼接时可以被不同对待。

工具调用的结果可以用 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 要支持的场景来说,这些回报大于负担。