1. 先弄清楚:Agent 任务里的"数据一致性"和数据库里的不是一回事
1.1 一次 OpenClaw 任务到底包含多少"副作用"
系列写到第 18 篇,这次聊聊一个特别容易被忽视、但一到生产环境就躲不开的问题:OpenClaw 任务执行过程中的事务管理,以及它和数据库事务的根本差异。
先描述一个我上个月真实踩过的场景。我们团队用 OpenClaw 跑了一个"数字员工",任务内容很简单:每天凌晨从 ERP 导出订单快照,清洗后同步到业务库,再给客户打标签。这个任务刚开始跑得很正常,第三天凌晨,负责同步的流程因为数据库连接池超时失败了一次,系统按默认策略自动做了重试。结果第二天早上,业务同事告诉我,有两千多条客户标签彻底乱了——不是少了,是重复、错位、新旧数据混在一起。
我后来仔细复盘才发现,OpenClaw 的数字员工执行一个真实业务任务,几乎从来不是"一次模型调用"那么简单。以我们跑的"ERP 订单快照同步 + 客户打标签"为例,完整链路是:任务启动后,先调用 ERP 接口拉取增量订单;接着把订单数据清洗、去重、落成中间文件放到 workspace;再连接业务库执行一批 upsert;然后按规则给客户打标签;最后把结果汇总发送到飞书群。这里每一步都是对真实世界的"副作用":拉数据没有副作用,但写中间文件、写数据库、改标签、发消息,全都是会产生持久影响的操作。
问题在于,这些副作用分散在文件系统、外部 API、数据库、IM 机器人等多个系统里,任何一个环节都可能失败。任务管理器把失败的任务重新调度起来时,它只有一个朴素的目标:把这个任务重新执行完。可它不知道的是,上一轮执行已经写入了 30% 的数据库记录、已经往外部系统发了请求。重试一旦启动,旧数据和新增数据混在一起,最终结果就会变得不可预测。
1.2 没有事务管理时的三类典型事故
我总结了一下,OpenClaw 任务如果不做事务管理,数据一致性事故基本逃不出这三类。
第一类,也是最常见的一类,是重复执行。任务失败后自动重试,上一个执行周期里已经成功写入的数据,又被当作新数据处理了一遍。如果目标是"插入客户记录",那就会出现重复客户;如果目标是"累加计数器",那数字直接翻倍。我们这次订单标签错乱,就是典型的重复执行叠加了脏数据。
第二类是半更新。一个任务要同时更新数据库里的订单状态、文件系统里的报表、某个外部系统的记录,结果写到第二个步骤时挂了,三个系统各留下不一致的状态。数据库里显示订单已同步,外部系统里根本没有这条记录,workspace 里的中间文件又停留在写入一半的状态。这种情况比重复执行更隐蔽,因为从单个系统里看每个数据都是"正常"的,只有跨系统比对才能发现问题。
第三类是脏工作区。OpenClaw 的数字员工依赖 workspace 里的文件作为中间产物,上一次失败执行留下的残缺文件,会被下一次执行当成有效输入。我见过最典型的一种:任务是"读取昨天的订单快照,生成今天的新报表",失败后中间文件只写到一半,重试时读取到的却是残缺数据,最后生成了一张看起来完整、实际上缺了 60% 数据的报表。
这三种事故,表面上看是 bug,本质上是没有事务管理的保护:任务没有明确的提交点,失败后没有回滚或补偿机制,重试时也没有幂等保障。任何跑真实业务的团队,迟早都会撞上一次,只是时间早晚的问题。
1.3 为什么不能直接把数据库那套 ACID 搬过来
很多人第一个念头是:数据库事务不是现成的吗?直接把任务里的所有操作包在一个大事务里不就行了?
可惜,ACID 那一套是针对单一存储系统设计的。数据库可以靠 undo log 把未提交的修改全部撤回来,但 OpenClaw 任务面对的是一个分布式环境:文件系统没有 undo log,外部 API 一旦调用了就无法撤销,飞书消息发出去了也不能撤回。这种场景下,传统的原子性根本做不到,一致性只能靠"业务层的最终一致"来找补。
数据库事务和 OpenClaw 任务事务的区别,我用一个表格来对比,大家感受一下:
| 维度 | 数据库事务 | OpenClaw 任务事务 |
|---|---|---|
| 作用范围 | 单一数据库/存储引擎 | 文件系统、外部 API、数据库、IM 等多个系统 |
| 回滚方式 | undo log 物理撤销 | 快照恢复、补偿动作、幂等重放 |
| 提交点 | COMMIT 一条指令 | 每个步骤的完成标记 + 人工审批门禁 |
| 隔离性 | 锁和 MVCC | 任务级隔离,依赖幂等键 |
| 一致性目标 | 强一致 | 最终一致 |
所以 OpenClaw 的事务管理,本质上是另一套思路:不追求全链路原子性,而是通过记录执行状态、支持部分回滚、提供补偿动作、保证重试幂等,最终达到数据在整个任务生命周期内的一致。理解了这一点,再看它的事务机制各个组成部分,就顺理成章了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. OpenClaw 事务机制的四个核心构成:边界、快照、审批、补偿
2.1 任务级事务边界
OpenClaw 在事务管理上做了一件很聪明的事:它把一次完整的任务执行(一次 task run)作为事务的基本单位。任务开始时,运行时会记录一个事务起始标记;任务正常结束时,写入一个 commit 状态;任务异常退出时,写入 rollback 状态。这个状态记录在 runtime metadata 里,是后面所有恢复动作的依据。
这个设计里有一个关键点:事务边界是可以调整的。默认情况下,整个任务被当作一个事务,但是如果你在生产环境里跑过,就会发现这个粒度太粗了——任务里任何一个步骤失败,整个任务都被标记为回滚,但真正已经提交到外部系统的副作用却无法回滚。
所以更合理的做法,是把边界往细了划到"单个业务原子操作"层级。一个 skill 里的每个步骤都有自己独立的提交点,这样重试时可以从失败步骤继续,而不是全量重来。用一句话概括:事务边界的粒度,应该等于你能够原子撤销的最小操作粒度。
2.2 workspace 快照与 runtime metadata
OpenClaw 有一个长期被低估的机制:runtime metadata。每次任务执行,运行时会记录每个步骤的输入参数摘要、执行结果、输出文件路径、耗时、错误信息,形成一个结构化的执行元数据。这相当于数据库里的事务日志,平时不起眼,排障时价值巨大。
配合 metadata 的是 workspace 快照机制。在任务执行前,运行时会为 workspace 里的关键文件生成快照;任务失败并决定回滚时,可以用快照把中间文件恢复到一致状态。我实际使用中,这一招在"任务半路写入了一堆残缺中间文件"的时候特别有用,至少能避免脏工作区污染下一次执行。
不过要注意,快照对性能有一定影响,所以通常只对声明了事务性的目录开启,不需要覆盖整个 workspace。我一开始图省事,把整个 workspace 目录全部做了快照,结果任务执行时间直接翻了一倍,后来改成只对中间产物目录做快照,效果一样,开销小很多。
2.3 exec-approvals 审批门禁
很多人在 OpenClaw 的目录里见过 exec-approvals.json 这个文件,但不清楚它是干什么的。它其实是事务管理里一个非常重要的控制点:持久化"哪些高风险动作需要人工审批"的配置。当一个任务执行到需要审批的步骤时,事务会挂起,不再继续,直到有人在控制台上确认或者驳回。
这其实就是事务管理里的"人工提交点"。数据库事务里,commit 之前可以检查所有条件再决定提交;在 OpenClaw 里,高风险动作(比如写生产库、删除文件、对外发送消息)前面插入一个审批门禁,本质上就是在提交前增加了一道人工校验。
对数据一致性来说,这个机制比任何代码层面的回滚都更靠前、更有效。很多数据错乱,就是在没有任何人确认的情况下,模型自动把"看起来合理但实际错误"的动作执行掉了。审批门禁相当于给了你一次"提交前深呼吸"的机会。
2.4 补偿动作与幂等键
如果说快照和审批是"事前"和"事中"的保障,补偿和幂等就是"事后"的兜底。
补偿针对的是"无法回滚的外部副作用"。比如任务调用了外部 CRM 的创建客户接口,这条客户记录已经创建成功了,任务后续失败回滚时,数据库里的日志可以删掉,但外部 CRM 那条记录删不掉。这时候就需要注册一个补偿动作:在回滚阶段调用 CRM 的撤销接口,或者标记该客户为"已作废"。这个思路,和微服务架构里的 Saga 模式同源。
幂等键则解决"重试安全"的问题。给每个业务操作分配一个全局唯一的 key,比如 order_sync_{task_run_id}_{step_id},下游系统在处理时先查这个 key 是否已经处理过。如果处理过,直接返回上一次的结果,不重复执行。只要这个 key 设计得合理,任务重试多少次都不会产生重复副作用。
我在生产环境里验证过,幂等键是投入产出比最高的一致性手段。加一个幂等键的成本极低,但能把"重复执行"这一类事故直接消灭在根上。
3. 实操落地:从配置到验证
3.1 在配置里开启事务策略
在我使用的版本里,OpenClaw 的事务策略是在 ~/.openclaw/config.yaml 里配置的,Windows 下路径是 C:\Users\你的用户名\.openclaw\config.yaml。核心配置大致是这样:
yaml复制transaction:
enabled: true
default_boundary: step # task / step 两种粒度
snapshot_dirs:
- workspace
- data/tmp
approval_file: exec-approvals.json
compensation_timeout: 300
default_boundary: step 是我非常推荐的一项。默认的 task 粒度看起来省事,但生产环境里几乎一定会遇到"整个任务回滚不干净"的问题。用 step 粒度,至少能保证每个步骤独立提交、独立恢复,不会因为一个步骤失败把前面的成果全部作废。
晚些时候,如果你在日志里看到 legacy exec approvals exist at /root/.openclaw/exec-approvals.json 这样的提示,别慌,这是 OpenClaw 在告诉你,老版本的审批配置还在那个路径,建议迁移到新的配置目录。迁移本身很简单,就是把旧的 exec-approvals.json 复制到新目录,然后检查一遍审批项是否符合当前任务列表。
3.2 在 skill 里声明事务语义
配置只负责全局策略,具体的步骤语义需要在 skill 里声明。以我们团队的 ERP 订单同步 skill 为例:
yaml复制name: erp_order_sync
version: 1.0.2
transactional: true
steps:
- id: fetch_orders
type: tool
tool: erp_client.get_orders
idempotency_key: "sync_{task_id}_fetch"
- id: write_db
type: tool
tool: db.upsert
idempotency_key: "sync_{task_id}_write"
requires_approval: true
- id: notify
type: im
channel: feishu
compensation:
action: "send_patch_message"
params:
content: "通知发送失败,请人工核查订单同步结果"
每个步骤都有独立的幂等键,写库步骤被标记为需要人工审批,发送通知步骤注册了补偿动作。这样配置之后,任务中途失败时,OpenClaw 会拿着 metadata 里的执行记录,精确判断哪些步骤已经提交、哪些需要重试、哪些需要补偿。
写 skill 的时候有一个容易踩的坑:只给"写操作"定义幂等键,忽略"读操作"。实际上,读操作如果进入了一个更长流程,也可能因为重复执行带来问题。所以我的建议是:所有有副作用的步骤,不管看起来是读还是写,都定义幂等键,代价很小,收益很确定。
3.3 本地模型与网关代理场景的注意事项
如果你像很多用户一样,本地用 Ollama 跑模型、通过 litellm proxy 把请求转发给不同模型厂商,那么事务管理还牵扯到一个之前没人提醒我的点:模型网关的稳定性。
我踩过的坑是:litellm proxy 超时会引发任务失败,而任务失败后的重试又会对网关造成重复请求。如果你的任务里某个步骤包含模型调用,而且这个模型调用后面跟着写库操作,那么一旦模型调用重复,后面的写库也跟着重复。这种情况,仅仅给写库步骤定义幂等键还不够,最好把模型调用步骤本身也声明幂等键,这样重试时可以直接复用上一次的响应结果,避免重复计算、重复调用、重复计费。
在 OpenClaw 本地部署 + Ollama 的组合下,还有一个经验:本地模型的响应时间波动很大,任务失败率也会比云端模型高。所以不建议把任务级重试次数设得太高,我一般设 3 次,超过 3 次就降级为"标记失败,等人工介入"。比起无限重试,这个策略更符合数据一致性目标,因为无限重试意味着无限次向生产系统发起重复请求。
3.4 故意制造一次失败来验证回滚
配置完成后,别急着上生产,先做一次故障注入。我的做法是写一个临时 skill,第一个步骤先写一个文件,第二个步骤主动抛异常,然后用 OpenClaw 的 CLI 跑这个任务。
python复制# 临时故障注入 skill 的逻辑
async def run(context):
# 步骤1:生成一个中间文件
await write_file("data/tmp/partial.json", {"status": "half"})
# 步骤2:模拟失败
raise RuntimeError("intentional failure for transaction test")
跑完以后检查三件事:
- metadata 里的状态是不是
rollback - workspace 里第一个步骤写的
partial.json是否被快照恢复机制清理或恢复 - exec-approvals 审批记录里是否留了对应条目
如果这三项都符合预期,再放真实任务上去。这一步看起来繁琐,但强烈建议做,因为事务配置的 bug 通常不是"不生效",而是"你以为生效了,实际上根本没走到那一步"。
4. 生产环境里我认为最有用的 6 条最佳实践
4.1 先写幂等键,再写业务逻辑
这条是我个人最想强调的。很多人在写 skill 时,第一反应是"我先实现功能,等出 bug 了再考虑重试"。但在 OpenClaw 这类 agent 平台上,重试几乎是必然发生的,不是可能发生。模型网关抖动会重试,外部接口超时会重试,手动让任务重跑也是一种重试。没有幂等键的重试,就像没有保险的炸弹。
幂等键的设计有一个通用公式:{业务动作}_{task_id}_{step_id}。task_id 保证每次任务执行都不同,step_id 保证同一个任务里步骤之间不冲突。如果你的业务动作跨多次任务执行(比如按日期同步数据),可以把日期也加进去:sync_order_2025-06-01_{task_id}。这听起来像小事,但就是这些小事决定了重试时是"安全跳过"还是"全量重放"。
4.2 事务边界宁窄勿宽
把整个任务包成一个大事务,听起来很省心,实际上等于没保护。正确的是把事务边界切到"有真实副作用的最小操作":一次 upsert 是一个事务,一次文件写入是一个事务,一次消息发送是一个事务。这样任何一个步骤失败,受影响的只是这一步,前面的成果可以保留,后面的步骤可以单独重试。
代价是多写几行配置,收益是排障时间大幅缩短。我之前在老版本 skill 里用的是 task 级边界,每次失败都是整个任务重跑,表面省事,实际每次重跑都要重新拉取 ERP 数据、重新清洗、重新写库,既慢又容易产生脏数据。改成 step 级之后,失败只从失败点继续,速度快了不止一倍。
4.3 高风险动作不要自动提交
我见过不少生产事故,都是模型觉得"应该"删除某批数据,就真的删了。OpenClaw 的 exec-approvals 机制就是为了防这种事。凡是涉及删除、更新生产数据、对外发送消息的动作,一律挂上审批门禁。
审批不是冗余,是数据一致性的最后一道人工防线。数据错乱归根结底是错误的数据变更被执行了,审批门禁的意义在于"让正确的变更快速通过,让可疑的变更停下来"。它在 OpenClaw 事务管理中的地位,相当于数据库里的 SELECT FOR UPDATE——不是让操作变慢,而是让并发和错误变更没有可乘之机。
4.4 把 runtime metadata 当作审计资产
每次任务执行产生的 metadata,不要清理得太勤。我建议至少保留 30 天。在排查数据错乱时,metadata 的价值远超普通日志:它能告诉你每一步的输入输出、用了哪个幂等键、消费了哪条审批记录。有了这些,你才能回答"这个脏数据到底是怎么进来的"。
后来我复盘订单标签事故时,正是因为 metadata 里记录了 write_db 步骤没有幂等键,才快速定位到根因。如果那天的 metadata 只保留 24 小时,凌晨的失败记录早就被清理掉了,我可能还在怀疑模型判断逻辑出了问题。
4.5 更新前先看通道兼容性
OpenClaw 有 dev 和 stable 两个更新通道,命令大概是 openclaw update --channel dev 或 --channel stable。事务机制在不同版本间有过变化,尤其是 metadata 格式和审批文件路径。
我的建议是:生产环境永远用 stable,dev 只做验证,升级前先把 metadata 目录和 exec-approvals.json 备份一份。OpenClaw 的机制设计得再好,版本升级永远是不可控变量的来源。备份成本很低,丢失 metadata 的代价却很高。
4.6 定期做一次回滚演练
事务机制跟消防设施一样,不演练等于没有。我每季度会挑一个低风险任务,故意把它跑挂,验证回滚、补偿、审批三个环节是否正常。
演练的收获通常不是"机制坏了",而是"我们又改了配置导致脚本路径变了"这类人情债问题。比如有一次演练时发现,某个 skill 里的补偿动作指向的接口 URL 已经换掉了,但补偿配置没同步更新。这种问题只有在真正跑一次回滚才能发现,平时盯着配置看是看不出来的。
5. 一次真实事故的完整排查链路:两千条客户标签错乱
5.1 现象与第一反应
回到开头那个事故。业务同事反馈两千多条客户标签错乱时,我的第一反应是查模型输出——毕竟标签是模型打的。结果查了任务日志,发现任务在凌晨 2 点 17 分的时候失败过一次,系统自动在 2 点 18 分重试成功。从日志看,第二次执行确实"正常完成",但数据已经乱了。
当时我面临两个选择:一是直接在数据库里手工修正标签,二是先搞清楚脏数据是怎么产生的。我选择了后者。因为如果只修数据不修机制,同样的错乱明天还会再来一次。
5.2 排查链路
我没有急着清数据,而是按照事务排查的标准顺序走了一遍。
第一步,看任务日志。确认失败点是 write_db 步骤,原因是数据库连接池超时,于是系统触发重试。
第二步,看 runtime metadata。重点看 write_db 步骤的幂等键记录。结果发现这个 skill 是老版本写的,根本没声明幂等键,metadata 里只记录了"执行成功",没有记录"处理了哪些订单"。
第三步,对比 workspace 快照。发现中间文件 orders_snapshot.json 在第一次执行和第二次执行中都生成了,而且第二次执行时把同名文件覆盖了。这意味着第一次执行的部分订单数据,在第二次执行时被当成了新输入。
第四步,查数据库表。客户标签表没有任何唯一约束,两次执行的 upsert 操作因为没有唯一键,变成了物理插入,产生了一大批重复记录,再加上中间文件被覆盖导致的错位,最终标签就乱了。
我把排查过程整理成一个表格,方便参照:
| 排查步骤 | 检查对象 | 发现 |
|---|---|---|
| 1 | 任务日志 | 失败点为 write_db,超时触发重试 |
| 2 | runtime metadata | write_db 步骤无幂等键记录 |
| 3 | workspace 快照 | 中间文件被第二次执行覆盖 |
| 4 | 数据库表 | 无唯一约束,upsert 变成物理插入 |
5.3 根因
这个事故有三个叠加原因:
一是 skill 没有定义幂等键,重试时无法识别"这份订单已经处理过";二是事务边界是 task 级,失败后没有对已完成步骤做标记,重试等于全量重放;三是数据库表缺少唯一约束,写库操作没有兜底机制。
如果只修其中一个,另外两个仍然可能引发类似问题。比如只加唯一约束,没问题,但重复写入还是会尝试执行,白白消耗数据库资源;只加幂等键,但中间文件已经被覆盖了,幂等键也只能跳过"处理过"的订单,无法恢复已经被覆盖的数据。所以修复必须三个动作一起做。
5.4 修复与验证
修复动作分三步。
第一步,给 write_db 步骤加上幂等键,并在数据库表里增加唯一索引,让重复写入从根上被拦掉。
第二步,把事务边界从 task 调整为 step,这样重试时可以从失败的 write_db 步骤继续,而不是重新跑 fetch_orders。
第三步,给 write_db 步骤开启 requires_approval 审批门禁,避免模型在下一次"觉得应该写库"时直接自动执行。
修复后我做了四轮验证:正常执行一轮,结果正确;手动触发重试一轮,没有产生重复数据;提前关掉数据库服务让它失败一次,确认失败后的 recovery 行为正常;放开数据库服务后重试,确认只补写了缺失的部分而不是全量重放。四轮全过才放回生产。跟踪了整整一周,没有再出现标签错乱。
6. 踩过这些坑之后,我的一点实际体会
事务管理这块,最难的不是配置,而是改变思维方式。写普通脚本时,代码执行一遍就结束了;但 OpenClaw 的数字员工是会被反复调度、自动重试、长时间运行的生命体。你得从一开始就假设:这个任务一定会失败,一定会被重试,一定会在某个凌晨两点以你想不到的方式挂掉。在这个前提下做的设计——幂等键、窄边界、审批门禁、补偿动作、metadata 审计——每一项都值回票价。
最后再分享一个具体的小技巧:给所有 skill 命名的时候,把版本号和事务边界写进 manifest 的 description 里。比如"erp_order_sync v1.0.2, step-boundary, idempotent"。这样在 OpenClaw 控制台或日志里看到任务名,就能立刻知道这个任务的重试安全性。这不算什么高深机制,但排障的时候能省掉大量反复翻配置的时间。毕竟,真正跑过生产环境的人都知道,凌晨三点被叫起来处理数据错乱时,最值钱的就是"一眼看出问题在哪"的能力。
