增量同步
增量同步是 Luker 对 SillyTavern 数据传输架构的根本性改进,用增量 patch 替代全量覆盖,大幅降低带宽消耗并消除并发写入冲突。
问题背景
SillyTavern 的数据保存采用全量传输模式:每次修改(哪怕只是编辑一条消息的一个字),都会将整个聊天记录序列化后发送到后端覆盖写入。这带来了几个严重问题:
- 带宽浪费 — 一个包含数百条消息的聊天记录可能有数百 MB(尤其是有些插件会在 chat metadata 里存储大量数据),每次操作都要传输全量数据
- 写入冲突 — 多个标签页或设备同时操作时,后写入的会覆盖先写入的,导致数据丢失
- 性能瓶颈 — 大型聊天记录的序列化和传输本身就是性能负担
- 保存延迟 — 全量写入的 I/O 开销使得保存操作无法做到实时
增量端点
Luker 新增了三个增量端点,覆盖聊天数据的不同修改场景:
追加消息(append)
当用户发送新消息或 AI 生成新回复时,只需将新消息追加到聊天文件末尾,而非重写整个文件。这是最常见的操作路径,也是性能收益最大的场景。
后端收到请求后,直接将新消息追加到文件末尾,时间复杂度为 O(1),与聊天记录的总长度无关。后端还会对最后一条已存储消息进行去重检查,防止因网络重试导致的重复追加。
修补消息(patch)
当用户编辑已有消息(如修改内容、切换 swipe、更新消息元数据)时,按消息索引修补指定行,只更新变更的消息。
支持在一次请求中批量修补多条消息,并具有幂等性——自动检测已应用的操作,跳过重复提交。
更新元数据(patch-metadata)
当聊天的元数据(如标题、标签、聊天设置等)发生变化时,使用深度合并(deep merge)更新,而非替换整个元数据对象。
深度合并意味着只有请求中包含的字段会被更新,未提及的字段保持不变。这对于包含大量扩展元数据的聊天尤为重要。
Integrity Hash 并发冲突检测
每次写入操作完成后,后端会生成一个新的 UUID,写入聊天状态文件,同时在响应中返回。前端缓存该值,并在后续写入请求中携带:
- 匹配 — 正常执行写入,返回新的 integrity UUID
- 不匹配 — 返回
409 Conflict,表示文件在上次操作后已被其他来源修改
这从根本上防止了并发写入冲突——无论是多标签页、多设备还是多用户场景。
冲突处理
收到 409 Conflict 时,前端需要重新获取最新数据并基于最新状态重试。这确保了数据一致性,但也意味着在极端并发场景下用户可能需要等待重试完成。
设计参考
元数据更新端点的设计参考了 RFC 6902(JSON Patch)的理念,通过结构化操作实现增量更新。消息修补端点则针对行式存储做了简化——使用行索引定位,更适合聊天记录的线性结构。
设置数据的增量 Patch 保存
除了聊天数据,Luker 的设置数据(用户偏好、扩展配置、预设参数等)同样支持增量 patch 保存。设置变更通过深度合并应用到服务端的设置文件中,同样带有 integrity hash 冲突检测,避免并发修改导致设置丢失。
这意味着修改一个采样参数不再需要传输整个设置对象——只需发送变更的字段即可。
延迟触发机制
为了避免频繁的小修改产生过多的网络请求,增量同步实现了延迟触发(debounce)机制:
- 短时间内的多次修改会被合并为一次 patch 请求发送
- 用户连续编辑消息时,只有在停顿后才会触发实际的网络请求
- 设置变更同样适用延迟合并,避免滑动条拖动等操作产生大量请求
延迟触发在减少网络开销的同时,不影响数据安全——因为后端实时存储确保了数据到达服务端后立即持久化。
与后端实时存储的协作
增量同步与后端实时存储紧密配合,形成完整的数据安全链路:
- 前端检测到数据变更
- 延迟触发合并短时间内的多次修改
- 发送增量 patch 请求(携带 integrity UUID)
- 前端通过序列化聊天写入流程将本地写任务串行发送
- 验证 integrity → 应用 patch → 持久化到磁盘 → 更新状态文件
- 返回新的 integrity UUID 供下次请求使用
这个流程确保了数据变更从前端到磁盘的完整链路,既高效又安全。
性能收益
对于一个包含 500 条消息的聊天记录,编辑一条消息时:SillyTavern 需要传输约 2-5 MB 的全量数据,而 Luker 只需传输约 1-2 KB 的 patch 数据。在移动网络或高延迟环境下,差异尤为明显。