AI 草稿保存的幂等与版本校验
可靠保存需要同时核对三件事:这次操作是否已经成功、草稿依据是否还有效、写入目标是否发生变化。 用户点了确认,只说明可以执行这次保存;网络中断或页面刷新之后,程序仍要知道已经做过什么。
这篇取材于 2026-10-27 的本地知识助手及其独立卡片工具。前者把候选草稿另存或追加到本地文档,后者把整理后的条目追加到远程看板。两条路径的写入保证不同,适合放在一起比较。
1 先确定用户确认的对象
本地知识助手的生成阶段只产生候选稿。采纳时,用户选择“另存为新文档”或“追加到已有文档”,并填写标题或选择目标。
保存接口从服务端任务记录读取草稿正文,客户端提交的是采纳方式、目标与版本,没有一个可以任意替换正文的参数。当前产品把一份草稿的采纳视为一次操作,同一草稿至多成功采纳一次。
这项产品规则很重要。如果以后支持修改草稿、保存到多个位置或多次采纳,就要增加草稿版本和新的操作身份,重新定义“同一次保存”是什么。
2 用操作标识和请求指纹识别重试
前端为保存提供稳定的 operationId。在这个案例里,任务标识同时用作操作标识,页面刷新后仍能找到同一次采纳。
服务端还会把任务、保存方式、标题、目标和目标版本计算成请求指纹。处理规则如下:
| 请求情况 | 处理结果 |
|---|---|
| 相同操作标识,相同请求指纹 | 返回原保存回执 |
| 相同操作标识,请求内容变了 | 返回冲突,阻止把重试变成新操作 |
| 换了操作标识,但同一草稿已按相同请求保存 | 仍返回原回执 |
| 同一草稿已保存,又换了方式或目标 | 提示已采纳,要求查看已保存文档 |
这比禁用保存按钮覆盖的范围更大。按钮只能减少当前页面的重复点击;稳定操作标识和数据库记录还能处理响应丢失、刷新后重试等情况。
当前请求指纹没有包含草稿正文摘要,因为正文来自这份已完成、不可在采纳请求中改写的候选稿。如果允许用户编辑候选稿,正文版本或摘要也需要进入指纹。
3 来源版本和目标版本分别检查
假设模型读取了资料 A 的第 3 版,生成草稿后,A 被改为第 4 版。即使用户只是另存一篇新文档,也要提示依据已经变化,重新生成或核对后再决定。
另一种情况是来源没有变化,但用户要追加的文档 B 已经被其他操作修改。此时草稿本身可能仍然有效,失效的是用户选定的写入基线。
| 版本变化 | 为什么阻止本次新写入 |
|---|---|
| 被引用的来源已更新 | 候选稿可能基于旧事实 |
| 追加目标已更新 | 不能按旧版本覆盖当前内容 |
当前实现会在采纳时检查被引用来源的版本;追加时还要求目标属于同一个知识库,并匹配 baseVersion。来源过期或目标冲突都会拒绝写入。
不能把冲突处理成“取最新版本号,再自动重试原操作”。新的目标内容可能改变了用户当时的选择,应该先让用户重新查看。
4 把检查与写入放在同一个事务里
如果先写文档,再记录“这个操作已经成功”,中间进程退出,下一次请求就可能再次追加。因此以下步骤要处于同一个数据库事务中:
text
开始写事务
→ 查找相同操作或相同草稿的已有回执
→ 请求一致:返回原结果
→ 请求冲突:拒绝
→ 检查候选稿状态
→ 检查被引用来源的版本
→ 检查追加目标及其版本
→ 写入正文与历史版本
→ 记录采纳回执和保存事件
提交事务
中途失败 → 回滚本次事务案例使用 Node.js 的同步 SQLite API,事务内部没有模型调用、网络等待或 await。DatabaseSync 的执行方式可见 Node.js SQLite 文档。
BEGIN IMMEDIATE 在事务开始时就尝试取得写事务;SQLite 同一时刻只有一个写事务,其他连接竞争时可能返回忙碌错误。它有助于把检查与后续更新放在一致的写入范围内,但仍需要业务版本检查和错误处理。SQLite 事务说明
本地应用还用数据目录锁避免多个服务进程同时接管任务。进程锁管服务所有权,数据库事务管一次保存,它们承担不同的职责。这套设计没有跨服务事务或远程“恰好一次”保证。
为什么先返回已有回执
已经成功保存的操作再次到达时,程序会先验证请求指纹并返回原回执,然后才考虑新写入需要的来源检查。
这样,即使来源在第一次保存后又更新了,重试仍能确认“此前已保存到哪篇文档、哪个版本”,而不会把已完成的操作错误描述成失败。返回旧回执不会再写一次正文。
5 网络中断时保留未知状态
写请求发出以后连接中断,客户端无法仅凭连接错误判断事务是否提交。直接显示“保存失败,可以重试”,容易让用户生成新的操作标识并重复写入。
案例的客户端会提示“操作结果尚未确认”,让用户刷新任务或资料后核对。服务端任务记录包含采纳回执,重试同一请求也会返回原结果。
保存回执至少应该能定位目标文档、版本、保存方式和时间。展示“已保存”应以这类可核对结果为依据,不能使用模型在正文里的完成声明。
进程重启时,未完成的生成任务会标为中断。已保存文档和采纳回执则保存在数据库里。重新生成一份草稿和确认上一次保存结果,是两种不同操作。
6 卡片工具展示了另一种写入边界
同一项目中的卡片 CLI 采用“解析文本 → 读取看板 → 生成追加计划 → 可选写入”的固定流程。默认不写看板,真正追加需要显式选择写入模式。
计划只向已有列的末尾加卡片,并检查整个看板中的标题或描述是否已经出现。这个比较基于规范化后的文本,不是语义相似度判断,也不是数据库幂等回执。
写入请求携带读取时的看板版本,遇到 409 就停止,交给用户重新核对;它还请求服务端保存历史版本。客户端能证明发出了哪些约束,实际冲突检测和历史保存仍依赖服务端履行契约。
预览不会写看板,但仍可能有其他操作
预览过程可以读取远程看板、调用已配置的模型,并保存本地计划存档。dry-run 的承诺应明确到具体副作用,不能笼统理解成“完全离线、没有任何写入”。
还有一个容易混淆的细节:一次写入命令内,打印的预览和提交内容来自同一份计划;先运行预览、稍后再运行写入命令,则会重新解析文本并重新读取看板。模型结果和看板状态都可能变化,后一次没有冻结前一次的预览。
如果产品要求“严格执行用户刚刚审批的那份计划”,需要增加计划标识、内容摘要、目标版本和有效期,并提供按该计划提交的入口。这是进一步设计,当前 CLI 没有实现。
怎样验收保存语义
用同一请求连续保存两次,检查文档数量、版本和正文只变化一次;保持操作标识、修改目标或标题,检查是否被拒绝。
再分别修改来源和追加目标,确认冲突后保留候选稿、目标正文及已有版本,没有留下半条采纳记录。对于连接中断,验收重点是能否查到真实结果,以及重试是否复用原操作身份。
文本去重、版本冲突和操作幂等分别解决不同问题。把它们的结果展示清楚,用户才能判断应该核对、重新生成,还是重试同一次保存。
延伸阅读
- 本地知识 Agent 的受限检索与来源核对:保存之前怎样形成有来源的候选稿。
- 周报 Agent 的规则降级与草稿交付:生成完成、落盘完成与通知完成怎样区分。
- 事务、幂等与补偿:更一般的业务写入场景。
- AI 产品设计边界:怎样让用户理解并控制采纳动作。