临时对话为什么重要
主叙事会话是 AI RP 的正史。它记录的是真正发生过的剧情。 但实际使用中,有大量对话不应该成为正史:草稿、试写、辅助推理、配置起草。 临时对话就是为这些对话准备的容器。这篇文章说明它为什么必须独立存在。
这个想法从哪里来
临时对话这个设计的起点,是 RP Agentic 与 NodeGraph 路线里的一个具体担忧。
NodeGraph 把一次回合的内部过程拆成节点图:Director、Narrator、Memory、Verifier,每个节点有自己的职责、输入输出和连线。这套机制对系统是清晰的,但对用户不一定。一个想调整自己 Agent 行为的用户,面对的是一大堆节点和参数:哪个节点管叙事节奏,哪个参数控制记忆提取,改了这里会不会影响那里——想改,但不知道从哪里下手。
这时候最自然的解法,是让用户直接和 AI 对话:“我想让叙事节奏慢一点,记忆提取更保守一些,帮我改。”由 AI 去理解节点图、给出修改建议,用户只需要确认结果。
但这场“帮我改配置”的对话放在哪里?它不是剧情,不应该进入主叙事;它可能要多轮往返,需要完整的对话能力;它的产出需要一条受控的路径回到正式配置,而不是直接生效。系统里没有一个现成的位置能同时满足这三点。
顺着这个担忧再看,同类场景其实到处都是:
- 开局设定。玩家在一次游玩开始前面对一整套设定表单——世界观、人物关系、初始状态——想让 AI 先给一个初版,再在这个基础上改。
- 候选回复。长篇 RP 进行中,想让 AI 先给几个不同语气的候选,挑一个再继续剧情。
- 辅助推理。让 AI 帮忙梳理剧情线索、整理设定资料,过程不需要留在正史里。
这些场景的共同点是:都需要一段完整的多轮对话,但对话本身不应该成为正史,只有被选中的产出才应该回到正式位置。
没有临时对话时会怎样
以候选回复这个最简单的场景为例。如果没有临时对话,这件事只能这样做:
- 把“请给我三个候选回复”直接发进主会话,让候选内容进入正史。
- 或者新开一个普通会话,手工复制上下文过去,用完再手工删除。
第一种做法污染了主叙事。候选内容会进入历史楼层,参与后续的提示词拼接,影响记忆提取,还会出现在时间线里。第二种做法成本高,而且那个“用完即弃”的会话和正式会话在系统里没有任何区别,清理只能靠自觉。
开局设定和 Agent 配置修改的处境只会更差:它们甚至还没有一个可以“发进去”的会话,只能在主会话里夹带着做,或者完全依赖客户端自己拼一套临时机制。
问题的根源是:系统里只有一种会话,而使用者实际需要两种。
正史和草稿是两类对话
主叙事会话有一组很强的语义:
- 楼层一旦提交就不可更改。
- 提交会触发变量提升、记忆写入、会话状态物化和项目事件。
- 历史内容会持续参与后续的提示词拼接。
这组语义对正史是正确的。正史就应该被认真对待,每一个回合都值得被记录、被审计、被追溯。
但草稿类对话恰好相反:
- 它的价值在于过程中的产出,而不在于对话本身。
- 它不应该影响任何真相层——变量、记忆、会话状态都不应该因为一次试写而改变。
- 它用完就应该可以被丢弃,不留下治理负担。
把这两类对话塞进同一种容器,结果只能是其中一类的语义被牺牲。要么草稿被迫继承正史的全部副作用,要么为了草稿在正史语义上开洞。两种都不可接受。
临时对话的设计选择
TavernHeadless 的做法是:复用对话模型,隔离对话语义。
临时对话仍然使用 session / floor / message_page / message 这套结构,只是 sessions.kind = temporary。这意味着它天然拥有完整的多轮对话能力——楼层、消息页、工具调用、流式输出,都和主会话一致,不需要再造一套对话内核。
但它的语义边界完全不同:
- 默认非正史。普通
sessions列表和详情接口不会返回它。 - 默认快照派生。从来源的 Session 或 Project 复制配置后独立运行,来源后续的变化不会影响它。
- 默认不写真相层。不写记忆、不写 Session State live、不写变量 durable truth、不进入 Project committed-only 事件流。
- 默认结果只 inline 返回。生成结果只作为本次请求的响应返回,不自动写入任何正式位置。
一句话概括:临时对话可以做主会话能做的几乎所有事,但默认什么都不留下。
“显式导出”是这个设计的关键
草稿的最终目的通常还是要回到正史。候选回复挑中了一个,总要把它放回正式页面。
这里有一个容易走错的岔路:让临时对话的结果自动写回。自动写回看起来方便,但它会让“草稿不影响正史”这条边界变成一句空话——只要有自动路径存在,污染就只是时间问题。
所以 TavernHeadless 选择了相反的方向:结果回到正史必须经过一次显式导出,而且导出的落点是 page_staged_write,一个候选写入账本。
这个落点有明确的边界:
- 它不激活页面。
- 它不提交楼层。
- 它不触发变量提升和记忆写入。
- 它只是把内容放到一个可审查的位置,等待正式流程处理。
换句话说,临时对话和主叙事之间只有一扇门,这扇门必须由调用方亲手推开,推开之后到达的也只是候选区,不是正史本身。
生命周期为什么要做全
临时对话有五个状态:active、finalized、discarded、cancelled、expired。
有人可能会问:一个用完即弃的容器,为什么需要这么完整的生命周期?
原因是“用完即弃”本身就需要语义支撑。一个没有生命周期的临时容器,系统无法回答这些问题:
- 这个对话是正常结束的,还是被放弃的,还是被中断的?
- 它什么时候可以被清理?
- 它过期之后还能不能继续写入?
finalized、discarded、cancelled 区分了三种不同的结束原因。expired 配合 TTL 保留策略,让“到期自动失效”成为系统行为而不是使用约定。所有终态都拒绝写入,保证一个已经结束的草稿不会被悄悄续写。
生命周期做全,不是为了让临时对话变重,而是为了让“临时”这个词有确切的含义。
它还是 Agent 的执行介质
临时对话的价值不止于人类用户的草稿场景。
Agent 执行任务时,同样需要一个多轮对话容器:它要调用 LLM、要调用工具、要保留中间过程供审计。这个容器的需求和临时对话完全重合——非正史、可丢弃、过程可追溯、结果显式导出。
这正是临时对话被放在 Agent Runtime 路线之前实现的原因。先把这个容器做扎实,后续的 Agent 就有了现成的执行介质,不需要再为 Agent 单独发明一套“非正史对话”的机制。
回到开头那个 NodeGraph 场景,这条路线就闭合了:用户在临时对话里描述意图,负责配置修改的 Agent 在同一类容器里理解节点图、运行工具、生成修改方案,最后把方案显式导出为候选,由用户确认后才生效。面对一堆节点不知从哪里下手的问题,答案不是把节点图做得更简单,而是给“让 AI 帮你改”这件事一个安全的发生位置。
人类的草稿和 Agent 的执行过程,本质上是同一类东西:它们都是为了产出结果而存在的过程性对话,过程本身不应该成为正史。
这个设计负担了什么
临时对话增加了系统的概念数量。使用者需要理解正史会话和临时对话的区别,需要知道结果不会自动回到正式页面,需要主动调用导出和终止接口。
API 层面也多了一组资源:创建、追加、生成、读取、三种终止、导出,共十个端点。
这些是真实的成本。
但换来的回报是清楚的:
- 主叙事的纯净性有了结构保证,而不是依赖使用者的自觉。
- 开局设定起草、Agent 配置修改、候选回复、辅助推理这类过程性对话有了名正言顺的位置。
- 结果回到正史的路径是显式的、可审计的。
- 后续的 Agent Runtime 有了现成的执行介质。
对于一个把可治理和可审计当作前提的系统来说,这笔交换是值得的。