Skip to content

好像哪里都是,那就干脆做个通用的。

分支、回退、对比、记忆来源、资产版本、操作审计,看起来是不同功能。 但它们都在问同一类问题: 这个状态从哪里来,和之前有什么差别,能不能回到过去。

问题不是单点出现的

在 TavernHeadless 里,很多地方都会自然长出版本控制的需求。

会话有楼层。楼层之间有父子关系。重新生成会产生新的候选结果。分支会从某个楼层开始。使用者需要比较两个楼层为什么不同。

提示词运行也一样。一次回复生成时,系统绑定了哪些资产,命中了哪些世界书,注入了哪些记忆,使用了哪些变量,这些都需要保存下来。否则事后只能重新猜。

记忆系统也一样。一条记忆不是孤立文本。它最好能说明来自哪一楼、哪条消息、是否更新了旧记忆、为什么会在当前回合被使用。

资产也是这样。角色卡已经有版本。预设、世界书和正则配置如果只有当前内容,就很难回答“这一轮当时用的是哪一版”。

再往外看,用户操作和 LLM 操作也需要记录。谁改了预设,谁切换了世界书,哪个 LLM run 产生了哪个楼层,这些都属于审计问题。

这些问题表面不同,底层很接近。

所以不要到处补小功能

如果每个模块都单独做一套历史记录,项目会变得很散。

会话有一套回退逻辑。

资产有一套版本逻辑。

记忆有一套来源逻辑。

操作日志又有一套审计逻辑。

短期看,这样实现很快。长期看,使用者会看到很多相似但不一致的概念。开发者也会在不同模块里重复处理对比、来源、回滚和记录。

所以更合适的做法是:承认它们属于同一类问题,然后用一套通用的版本控制思路来收束。

这里说的版本控制,不是要把 TavernHeadless 变成 Git,也不是要引入一整套复杂命令。

它只是给系统一组共同语言。

这组共同语言是什么

最核心的是四个概念。

Commit

Commit 是一次已经固定下来的历史事实。

在会话里,已提交的 Floor 就接近 Commit。它有父楼层,有分支,有当时的提示词快照和结果快照。

在资产里,角色卡版本、预设版本、世界书版本、正则版本也可以视为 Commit。它们保存的是某个时间点的资产内容。

Commit 的重点是:历史事实不能靠重新计算来猜。已经提交的东西,应该从保存下来的快照读取。

Reference

Reference 表示一个事实和另一个事实之间的关系。

例如:

  • 这个 Floor 的父楼层是谁。
  • 这条分支从哪个 Floor 分出来。
  • 这条记忆来自哪条消息。
  • 这个版本替代了哪个版本。
  • 这个操作产生了哪个新快照。

有了 Reference,系统才能回答“从哪里来”。

Diff

Diff 表示两个事实之间的差异。

它可以用于比较两个楼层的提示词,也可以用于比较两个 Session State,也可以用于比较两个资产版本。

Diff 不是事实源。事实源仍然是快照。Diff 只是帮助使用者理解差别。

这点很重要。不能因为有了 diff,就丢掉快照。

Operation Log

Operation Log 记录谁在什么时候做了什么。

它和 Commit 不一样。

Commit 记录状态事实。Operation Log 记录操作事实。

例如,一个用户修改了预设,这次操作会产生一条日志,也可能产生一个新的预设版本。日志回答“谁改的、从哪里改的、改了什么”。版本回答“改完后的内容是什么”。

LLM 运行也类似。日志不需要重复保存完整 prompt 和完整输出。它应该指向已有的 Floor、prompt snapshot、tool execution record 和 result snapshot。

它解决什么问题

这套思路主要解决四件事。

第一,审计。

系统可以说明一件事情是谁做的、什么时候做的、通过哪个入口做的。

第二,追踪。

系统可以说明一个结果从哪里来。楼层有来源,记忆有来源,资产版本也有来源。

第三,对比。

系统可以比较两个历史事实。比如两次生成的提示词差在哪里,两个资产版本差在哪里,两个分支的状态差在哪里。

第四,回到过去。

使用者可以从历史 Floor 分叉继续,也可以把会话绑定回某个历史资产版本。回退不应该靠删除历史来实现,而应该靠创建新的引用关系或新的分支来实现。

它和现有结构的关系

这不是从零开始。

TavernHeadless 已经有不少基础。

  • floors.parentFloorId 已经形成楼层链。
  • sessionBranches.sourceFloorId 已经记录分支来源。
  • promptSnapshotspromptRuntimeExplainSnapshots 已经保存提示词历史事实。
  • floorResultSnapshots 已经保存回合结果事实。
  • characterVersions 已经保存角色卡版本。
  • memoryItems.sourceFloorIdsourceMessageId 已经能表达记忆来源。
  • toolExecutionRecords 已经记录工具执行。

所以通用 VC 系统不是替换这些表。

更合理的是保留这些业务表,在服务层统一规则:哪些东西是快照,哪些东西是引用,哪些东西可以计算 diff,哪些东西应该进入操作日志。

资产为什么也要进来

如果只把 Floor 当作版本控制对象,系统仍然不完整。

一次回复不只由历史消息决定,也由当时绑定的资产决定。

如果预设、世界书和正则配置后来被改了,而历史快照只记了一个数字版本或一个资产 ID,使用者就很难还原当时的真实输入。

所以资产也需要版本。

默认情况下,会话仍然可以绑定资产 ID,使用当前内容。这保持现有行为。

当使用者需要更强追踪时,可以开启深度绑定。会话绑定具体资产版本。这样全局资产后续修改时,旧会话仍然能使用当时那一版。

这不是为了让资产系统变复杂,而是为了让历史事实完整。

操作日志为什么不能代替快照

操作日志很重要,但它不能代替快照。

如果只保存“用户把字段 A 从旧值改成新值”,系统仍然需要知道改完后的完整对象是什么。

如果只保存“LLM 运行成功”,系统仍然需要知道当时的提示词、工具执行、输出结果和状态写入是什么。

所以 Operation Log 应该做连接层。

它把用户、请求、操作、目标对象和产生的快照串起来。真正的内容仍然保存在各自的事实表里。

这样既避免重复存储,也避免日志变成新的大杂烩。

需要注意的边界

这套系统也有边界。

第一,不需要第一天就做完整 Git。

先做好资产版本、会话 checkout、操作日志,就已经能解决很多问题。merge、cherry-pick、复杂冲突处理可以后做。

第二,不要把所有东西塞进一张通用 commit 表。

Floor、资产版本、操作日志各有自己的业务含义。统一的是模型和规则,不一定是物理表。

第三,日志里不能随便保存全文。

Prompt、用户消息、工具参数和模型配置都可能包含敏感内容。操作日志应默认保存摘要、hash、路径和必要预览,而不是保存所有原文。

第四,前端和后端必须使用同一份绑定事实。

如果后端按会话绑定资产生成,而前端显示的是另一个全局选择状态,使用者就会误判当前会话实际用了什么。

为什么它是基础设施

版本控制系统看起来像一个功能。

但在 TavernHeadless 里,它更像基础设施。

因为可审计、可治理、可回退、可比较、可解释,都依赖同一组底层能力。

没有它,很多高级能力会各自长出一套临时记录。

有了它,Prompt Runtime、可追踪记忆、Session State、资产版本、Agentic 运行记录,都可以使用同一套原则。

这就是为什么它应该被做成通用系统。

不是因为要增加复杂度,而是因为这些复杂度已经到处存在。把它们收束起来,反而能让系统更容易理解和维护。