好像哪里都是,那就干脆做个通用的。
分支、回退、对比、记忆来源、资产版本、操作审计,看起来是不同功能。 但它们都在问同一类问题: 这个状态从哪里来,和之前有什么差别,能不能回到过去。
问题不是单点出现的
在 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已经记录分支来源。promptSnapshots和promptRuntimeExplainSnapshots已经保存提示词历史事实。floorResultSnapshots已经保存回合结果事实。characterVersions已经保存角色卡版本。memoryItems.sourceFloorId和sourceMessageId已经能表达记忆来源。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 运行记录,都可以使用同一套原则。
这就是为什么它应该被做成通用系统。
不是因为要增加复杂度,而是因为这些复杂度已经到处存在。把它们收束起来,反而能让系统更容易理解和维护。