AI 助手回答追问时,仿佛前面的对话从未发生;研究 Agent 刚做完的三次搜索,十分钟后又从头来了一遍;客服 Agent 告诉用户退款已批准,计费系统里却仍是原来的扣款状态。团队往往把这三类问题统称为“模型忘了”,但真正与模型有关的,也许只有其中一种。
Tailscale 最近公开了一份值得细读的事故复盘:在六个月内,他们一共遇到 19 次 SQLite 数据库损坏。根因最终被定位为写事务与 WAL 检查点之间一个极罕见的竞态条件。事务可能已经返回提交成功,部分数据却在后续流程中消失,而且没有任何写入报错。这个问题在 SQLite 中存在了大约 16 年,直到生产遥测、事务重放、新的 VFS 跟踪工具和 SQLite 开发者共同介入,才被准确捕获。
这不代表 SQLite 普遍不安全,也不代表今天的 AI 应用正在遭遇同一个漏洞。SQLite 的官方说明给出了很窄的触发条件:数据库处于 WAL 模式、同一个文件有多个连接、写入与检查点并发,而且时序必须恰好重合。官方认为普通用法下的发生概率极低;问题已在 SQLite 3.51.3 及后续版本修复,部分旧分支也有回补。
但这场事故对 Build Lab 有一个可以立即复用的启示:AI 产品所谓的“记忆”,通常分散在模型输入历史、Agent 检查点、审批记录、工具副作用和面向用户的业务数据中。模型给出一段连贯回答,只能说明它收到了某种连贯上下文,不能证明动作已经正确提交,也不能证明重试没有执行两次,更不能证明恢复后的系统保留了用户最后一次决定。
下面是一份面向单个真实任务形状的三账本恢复评分卡。Y Build 没有复现 Tailscale 的事故,也没有实际运行本文提出的测试。所有观察值和结果字段都刻意留空。小团队今天最该改变的一项检查是:在把问题归因于模型记忆之前,先用一次中断测试对齐对话、执行和业务三类状态。
Tailscale 事故真正证明了什么
Tailscale 的控制平面由多个分片组成。每个分片由单个 Go 进程访问一份 SQLite 数据库,并每隔几分钟生成完整快照。这套设计从 2023 年初开始稳定运行,直到一个数据管道在备份中发现损坏。之后六个月里,相似故障反复出现 19 次:有时只隔几小时,有时数周都没有异常。
影响真实存在,但边界也很明确。Tailscale 说明,受影响数据库保存的是控制平面元数据,不包含用户的私有加密密钥或网络流量。事故早期的恢复可能丢失少量刚加入的设备或最近的配置修改;修复或恢复期间,相关分片必须停机,新上线的设备无法获得最新协调信息。不过,已经建立的点对点连接通常仍能继续工作。
这段调查最值得借鉴的地方,是表面证据几乎一直在误导团队:没有近期底层改动可疑,没有稳定流量特征,也无法在实验室复现。于是 Tailscale 在生产环境增加被动诊断,与 SQLite 团队签订支持合同,对备份持续运行完整性检查,并额外建立一条确定性的事务日志。真正的突破来自事务重放:某笔事务明明已经写入并提交,后续事务却看不到它产生的数据。
SQLite 的 WAL-reset 官方说明给出了精确过程:一个连接先完成检查点,第二个检查点随后启动;就在这个时间窗口,另一个连接重置 WAL 并写入新内容。第二个检查点仍保留错误的“已复制范围”,导致之后的检查点跳过一部分已提交事务,主数据库最终可能引用根本没有写入的页面。
修复完成后,Tailscale 没有把“升级后暂时没报警”当作结论。他们直接为危险的重合条件增加监测。部署修复两个月后,这个条件真的再次出现,但数据库保持完好;发布复盘前又连续运行了四个月没有同类事故。这才是正向证据:危险条件出现了,保护机制启动了,关键不变量仍然成立。
“Agent 记忆”太宽泛,无法直接排障
今天的 Agent 框架让持久化变得很方便,也把几个含义完全不同的问题压缩进“记忆”这个词。OpenAI Agents SDK 的会话文档说明,系统会在每次运行前取回历史,并在运行后保存新消息;同时,它还提供文件型 SQLite 会话、Redis、SQLAlchemy、托管对话状态和可恢复的人类审批。它们呈现的是同一种连续对话体验,背后却是不同的存储与一致性选择。
LangGraph 也会在执行步骤之间保存状态检查点,按线程组织,以支持恢复、人类介入和历史回溯。官方把 SQLite checkpointer 定位为实验和本地工作流方案,把 Postgres 方案定位为生产使用。这些是有用的选型指导,却不能自动证明某个具体产品的业务不变量一定被保存。
做发布测试时,至少要把状态拆成三本账:
- **对话账本:**用户消息、模型输出、工具请求、摘要,以及下一次模型调用实际拿到的上下文。
- **执行账本:**运行 ID、图检查点、待审批项、工具尝试 ID、重试次数、租约和终态。
- **业务账本:**用户真正关心的事实来源,例如退款状态、预订库存、已发布内容、账户权限或交付文件。
还需要第四条辅助链路——证据日志——把三本账连接起来。它记录版本、哈希、时间戳、数据库完整性结果、恢复点、幂等键和对账结论。不要让 Agent 把这些证据改写成一段自然语言,然后又把那段摘要当成新的事实来源。
三本账可以在同一个物理数据库里,也可以分布在多个服务中;逻辑分离仍然不可省略。对话可能完整恢复,退款却没有发生;工作流检查点可能写着 completed,用户页面仍停留在旧状态;业务变更也可能已经成功,但重试记录丢失,导致 Agent 再做一次相同动作。
四种看起来像“模型不行”的状态故障
用户说的都是“它忘了”,底层机制却可能完全不同。
上下文遗漏。 历史记录仍在,但截断、压缩、过滤或 session ID 错误,让关键内容没有进入模型输入。OpenAI SDK 也提醒,同一次调用最好只选一种连续状态方案;如果把客户端会话和服务端 response 状态混在一起,可能重复注入上下文。这首先是输入组装问题。
检查点回退。 对话没有丢,执行却从工具结果或人类审批之前的检查点恢复。Agent 会重复工作、再次索要批准,甚至说出与界面相反的状态。这是编排状态问题。
已提交副作用丢失或错位。 执行日志说工具调用成功,业务记录却没有落盘;或者恢复使用了动作之前的旧备份;又或者缓存读取掩盖了新值。这是事实来源问题。
重试造成重复副作用。 外部动作已经成功,进程却在本地终态写入前崩溃。恢复后,如果没有稳定幂等键,Agent 会再次调用同一接口。两次退款、两份预订、两条消息或两次发布,都不是“幻觉”,而是恢复设计失败。
SQLite 官方的数据库损坏说明强调,它对损坏有很强抵抗力,但并非绝对免疫;文档还列出安全备份方式,并警告在活动日志存在时只复制主数据库文件,可能得到无效备份。这里不是要把每次故障都归咎于 SQLite,而是要求团队先把存储完整性、编排恢复与模型行为保留为三个独立假设,直到证据能把它们连接起来。
用一个带“不可逆边缘”的合成场景
建立一个虚构客服产品 Northstar Desk,让 Agent 处理一笔合成订单:
- 读取用户要求取消订单的消息;
- 提议测试退款 48 美元,并等待人类审批;
- 使用幂等键
ns-order-1842-refund-v1调用模拟计费接口; - 把工单、业务账本和用户时间线更新为
refunded。
整个测试不能连接真实支付渠道。模拟接口要保存每次请求;同一个幂等键重复调用时,必须返回同一结果。准备两个人类决定:准确批准 48 美元退款,并拒绝另一笔 12 美元调整。所有事件使用单调递增序号和固定测试时钟。
这个场景有一个看起来不可逆的边缘——退款——却不会造成真实财务影响。它也迫使产品对齐三种真相:对话要记得用户提出了什么;工作流要记得哪次审批对应哪个金额;业务账本要明确退款到底发生一次、零次还是两次。
用你真正准备上线的持久化方式来跑。如果本地原型用 SQLite、生产用 Postgres,两套都要分别测试、分别标注。若框架把对话历史和审批状态存放在不同服务里,就要保留两边的恢复坐标。目标不是选出“最好数据库”,而是看清产品真正掌握了哪些恢复保证。
注入故障前,先把基线固定下来
首次运行前,记录一份基线清单:
| 字段 | 必须保存的证据 |
|---|---|
| 运行环境 | 应用 commit、Agent 框架版本、模型快照或别名、工具 schema 版本 |
| 持久化 | 数据库引擎/库版本、日志模式、连接数、检查点策略、存储路径 |
| 身份 | 对话 ID、执行 ID、订单 ID、审批 ID、幂等键 |
| 恢复 | 备份时间、WAL/日志处理方式、事件日志位置、恢复命令或服务流程 |
| 不变量 | 一个审批只对应一个金额;最多一次退款;终态界面与计费记录一致 |
| 诊断 | 数据库完整性命令、trace 位置、工具调用回执、对账查询 |
如果使用 SQLite,记录运行时实际加载的库版本,不能只看包管理器清单。语言运行时、操作系统或打包后的 native dependency 可能提供不同版本。若数据库使用 WAL 模式,还要写清是否可能有多个连接同时访问,以及由谁控制检查点。
当前 SQLite 3.51.3 发布说明明确列出 WAL-reset 修复。更完整的 WAL 文档说明,修复包含 3.51.3 及以后版本,也包含 3.44.6、3.50.7 等特定回补版本。不要把这简化成“所有人只升级到某一个版本”:先确认你的发行版或供应商支持的已修补版本,再完成应用兼容性回归。
最后跑一遍可审计的正常流程。理想轨迹应当只有一条用户请求、一次审批请求、一个审批决定、一次计费尝试、一个稳定计费结果、一次业务状态转换和一条终态回复。如果正常路径都无法在不阅读 Agent 回答的情况下完成对账,故障注入只会制造更多歧义。
在六个状态边界分别中断
每个案例都从干净的合成环境开始单独运行。以下是建议测试,不是已经得到的结果。
| 案例 | 中断位置 | 恢复后要回答的问题 |
|---|---|---|
| A | 用户请求已保存、模型调用前 | 同一对话能否恢复,且不会重复写入请求? |
| B | 已发起审批、人类决定前 | 审批是否仍处于待处理,并严格绑定 48 美元? |
| C | 审批已保存、工具调用前 | 恢复后是否只执行一次,也不会要求更宽泛的权限? |
| D | 模拟计费成功、执行检查点提交前 | 幂等键能否阻止第二次退款? |
| E | 执行检查点已提交、业务界面刷新前 | 对账能否只修复读模型,而不重复计费? |
| F | 完成后执行备份与恢复 | 三本账能否恢复到彼此一致的终态? |
使用受支持的进程终止方式或可控异常,不要破坏生产数据库,也不要尝试复现 SQLite 那个时序极窄的竞态条件。案例 F 测的是你自己的正式备份与恢复路径。如果使用 SQLite,要把主数据库与活动 WAL/日志文件一起保留,或者使用官方支持的备份 API;SQLite 文档列出的安全选项包括 VACUUM INTO、backup API 和 sqlite3_rsync,但都要遵守各自适用条件。
先确保测试工具能产生确定性证据,再把每个案例重复三次。每轮只改变一个维度:进程重启、主机重启、网络超时,最后是存储恢复。一次进程重启通过,不代表从旧快照恢复也会通过;单连接通过,也不能覆盖多 worker 部署。
用三本账和五道门来评分
不要把结果平均成一个百分比。再完整的对话记录,也不能抵消重复退款。
| 门禁 | 通过条件 | 必须暂停的条件 |
|---|---|---|
| 对话连续性 | 重建上下文中,请求、决定和工具结果各出现一次且正确 | 上下文缺失、过期、重复或串会话 |
| 执行连续性 | 从待处理到终态只有一条合法转换;重试复用稳定 ID | 终态回退、分叉或无证据推进 |
| 业务正确性 | 事实来源中的副作用恰好一次,参数与审批一致 | 零次、重复或参数错误 |
| 完整性与恢复 | 完整性检查通过;恢复坐标与文件齐全 | WAL/日志缺失、检查失败或备份边界未知 |
| 对账修复 | 自动查询能发现并修复读模型,而且不重放副作用 | 只有 Agent 文字声称成功,或修复动作再次触发外部操作 |
SQLite 的 PRAGMA integrity_check会检查底层格式和一致性,包括缺失页面、重复页面使用、畸形记录、索引项缺失或多余等。它不能证明“正确退款确实发生”,也不会自动检查外键错误,后者需要单独的 foreign-key check。数据库完整性只是其中一道门,不是最终判决。
同样,trace viewer 能证明工具调用被记录,不一定证明目标系统完成提交。要保留服务方回执,或者直接查询模拟事实来源。每一个终态都应该能从 Agent 之外的证据推导出来。
把恢复证据做成机器可读评分卡
每个中断案例使用一份独立记录;真正运行之前,观察值必须留空。
experiment_id: northstar-state-recovery-v1
status: planned
run_at: null
versions:
app_commit: null
agent_framework: null
model_identifier: null
database_library: null
database_journal_mode: null
identities:
conversation_id: ns-conv-1842
execution_id: null
business_record_id: ns-order-1842
approval_id: null
idempotency_key: ns-order-1842-refund-v1
interruption:
case: D
injected_after: mock_billing_success
injected_before: execution_checkpoint_commit
restore:
backup_timestamp: null
event_log_offset: null
command_or_procedure: null
artifacts_present: []
observed:
conversation_events: null
execution_transitions: null
billing_attempts: null
billing_effects: null
integrity_check: null
reconciliation_result: null
gates:
conversation_continuity: unknown
execution_continuity: unknown
business_correctness: unknown
integrity_and_restore: unknown
reconciliation: unknown
decision: hold
owner: null
expires_at: null
unknown 不能算 pass。评分卡要保存在被测数据库之外;如果它将支撑正式发布决定,最终文件应增加哈希或签名。每条结论都要链接到原始事件、查询或完整性输出。Agent 可以为审稿人总结证据包,但不能用总结替代证据包。
需要正向证据,而不只是“最近没出事”
Tailscale 在修复后的关键动作,是为那个具体竞态条件增加告警,并等待它真正出现。此前系统曾经安静过六周,所以“暂时没有事故”本来就是弱证据。后来告警触发、数据库没有损坏,才说明防护确实在真实条件下生效。
小团队可以按同样逻辑,为每项恢复控制设计可观测的触发事件:
- 重试带着相同幂等键抵达模拟计费接口,并获得原始结果;
- 恢复后的工作流遇到已经使用过的审批,拒绝扩大权限;
- 从备份恢复后发现计费记录领先于工单,只修复工单读模型;
- 完整性监控检查真实备份文件,而不是只查在线数据库;
- 会话重建记录实际传给模型的历史范围与哈希。
同时为不可能组合建立告警,例如:workflow=completed 但 billing_effect=absent、billing_effect_count>1、approval_amount != effect_amount,或对话已宣布完成但业务记录仍是 pending。正向证据让“绿灯”不再只是没有报警,而是证明保护机制遇到了真实条件并守住不变量。
明确这次类比不能延伸到哪里
Tailscale 的负载并不普通:他们手动控制检查点,而且执行得很频繁。SQLite 明确认为 WAL-reset 在常见用法下不太可能发生,建议升级,但没有把它描述为需要恐慌的紧急事件。因此,不能借这篇复盘吓退所有使用 SQLite 的团队。
也不能声称一次干净的 integrity_check 就证明了业务语义正确;不能假设所有 Agent 框架的 SQLite 适配器都用了 WAL、多连接或受影响版本;更不能断言换成 Postgres、Redis、托管对话状态或事件溯源系统,恢复问题就会自动消失。不同架构只是移动了故障边界,仍需要对齐最终副作用。
这份评分卡适合持久化多轮历史、可从中断恢复、有人类审批,或会调用外部工具的 AI 产品。尤其适用于本地优先 Agent、桌面 Copilot、研究 Agent、客服工作流,以及从单进程转向多 worker 的 AI builder 产品。
如果产品只是一次性生成内容,输出会立刻经过人工审核,也没有任何外部动作,这套测试的价值会较低。它不是安全审计、灾难恢复认证、数据库基准评测,也不能证明某个框架存在缺陷。真实支付、医疗记录、破坏性操作和受监管数据,需要额外的行业控制。
小团队可在 48 小时内完成的状态恢复检查
第 0–4 小时:画出状态地图。 选择一个既有审批、又有模拟外部副作用的工作流。分别命名对话账本、执行账本和业务账本,记录版本、ID、恢复坐标和终态不变量。
第 4–12 小时:让正常路径可对账。 增加稳定幂等键、明确的状态转换日志、业务记录查询和数据库完整性命令。确认团队不阅读 Agent 的自然语言回答,也能还原一次干净运行。
第 12–28 小时:注入六次中断。 每个案例都从全新合成状态开始,保留原始 trace,并逐门评分。一旦出现重复副作用、参数漂移或恢复边界未知,立即暂停发布。
第 28–38 小时:执行恢复与对账。 真正跑一次官方恢复流程,确认日志文件或服务恢复点完整。只依据事实来源修复读模型,不能重新执行外部动作。
第 38–44 小时:证明控制真的启动。 安全触发一次重复请求、一次旧读模型和一次恢复后的审批,分别保存幂等、防重复对账和审批绑定发挥作用的证据。
第 44–48 小时:作出决定。 只发布实际测试过的持久化拓扑与版本组合。额外 worker、其他数据库、托管状态和移动端离线模式,都要标成未知,不能继承通过结论。为评分卡指定负责人和过期时间。
发布决定看的是状态,不是语言流畅度
AI 产品最容易走进的排障误区,是先修改文字层:重写 prompt、增加记忆摘要、扩大上下文窗口,再要求模型“保持一致”。这些办法也许能改善上下文遗漏,却无法找回丢失事务,无法修复不匹配的备份,无法把旧审批绑定到正确金额,也无法阻止外部副作用执行两次。
Tailscale 的经历说明,可靠系统需要的不只是一个听起来合理的根因和一段安静的监控曲线。他们建立独立事务历史、执行重放、对竞态条件加监测、谨慎升级、识别一次误导性的完整性报警,最后等待修复后的危险条件真正出现且没有造成损坏。
AI 产品团队不需要达到 Tailscale 的规模,也能复用这个方法:选一个贴近真实任务的合成场景,对齐三本账,保存机器可读评分卡,并拒绝把 unknown 写成通过。Agent 看起来失忆时,先问一个更有效的问题:哪一类状态存活了,哪一个副作用真正提交了,又有什么独立证据把二者连接起来?