前阵子有个朋友跟我抱怨:他们的客服 Agent 把客户表里 300 条记录的邮箱字段批量改错了。他翻遍后台,想找一个“一键回滚”的按钮,没有。

他问我:数据库不是天生支持回滚吗?为什么轮到 AI Agent 就不行了?
这个问题把我问住了。不是因为它难,而是因为它暴露了一个多数人没想过的盲区:数据库确实能回滚,但数据库只知道“你执行了什么”,不知道“你是不是故意的”。而 Agent 恰恰是一个会执行你完全没见过的 SQL 的家伙。
这篇文章把这件事彻底讲透:Agent 回滚到底难在哪,有哪些可用的技术方案,以及“一键回滚到操作前”什么时候是真的、什么时候是幻觉。
数据库的“后悔药”,有一个隐藏的前提
先看数据库自己怎么回滚。底层是三件东西:
- 事务:
BEGIN之后的改动先不算数,直到COMMIT才生效。中途发现不对,一句ROLLBACK全部撤销。 - 日志:PostgreSQL 的 WAL 把每次写入按顺序记下来。有了日志,就能把整个库恢复到历史上任意一个时间点(PITR)。
- 快照:定期把整个库拷贝一份,出了事直接整体覆盖,最粗暴也最省事。
这三个方案的精确度是递减的:事务能撤销“单个操作”,日志能恢复到“某个时间点”,快照只能回到“备份那一刻”。
但它们共享同一个关键前提:系统能区分“你的操作”和“别人的操作”。事务靠会话边界区分,日志靠时间戳区分。Agent 一来,这个前提就塌了。
Agent 让回滚变难的三个真相
真相一:你猜不到 Agent 要执行什么。Agent 的工作方式是“你给目标,它自己写 SQL”。你没法在它动手之前精确设计事务的边界——只有 Agent 自己知道它打算跑几条 SQL、按什么顺序跑。有人会说:那把它所有 SQL 都包进一个大事务里不就行了?下一节你会看到这个方案的边界。
真相二:Agent 不只碰数据库。真实的 Agent 会调一堆工具:写文件、发 HTTP 请求、调支付接口、发邮件。数据库事务管得住 UPDATE,管不住“邮件已经发出去了”。你回滚了数据库,收件人的邮箱里依然躺着那封错误的邮件。
真相三:Agent 不知道自己错了。这是最反直觉的一点。人写错 SQL 多半会报错,人知道“我错了”。但 Agent 生成 SQL 的过程是概率性的——它只是在词的概率分布里采样,选出“当时看起来最合理”的语句。它没有“我这次写错了”的内部感知。如果错是“逻辑对、业务错”(比如把 100 改成了 1000),它在任何一步都不会察觉。
所以错误往往在几小时甚至几天后才暴露。等你知道的时候,“操作前的状态”早就不存在了——别的事务已经在上面堆了几百层。
四种“回滚”方案,一个比一个狠
方案 A:事务包装。把 Agent 的 SQL 放进一个事务,出错立刻 ROLLBACK。更精细的做法是用 SAVEPOINT 把多步操作切成检查点,错一步只撤一步:
BEGIN;
SAVEPOINT before_step1;
-- 第 1 步:Agent 生成的 SQL
UPDATE customers SET status = 'active' WHERE id = 123;
SAVEPOINT before_step2;
-- 第 2 步:Agent 生成的 SQL
UPDATE orders SET total = 0 WHERE customer_id = 123;
-- 发现第 2 步错了,撤销第 2 步,保留第 1 步
ROLLBACK TO SAVEPOINT before_step2;
COMMIT;
这个方案精确、成本低,但边界清晰:只对“纯数据库写操作”有效。Agent 中途一旦调了外部 API,事务就管不到了。
方案 B:操作前快照。执行前把涉及的表导出,发现错误再恢复。听着简单,实操是个坑。我最早给内部工具加回滚用的就是这个方案:第一次测试特别顺利,Agent 乱改一通,我恢复快照,数据完好。直到一次真实使用——Agent 下午两点跑了错误更新,我四点才发现,恢复快照后,同事两点半改的两个客户备注消失了。快照恢复会覆盖 Agent 执行期间其他用户的全部合法操作。你修好了 Agent 的错误,但顺手制造了更多事故。
方案 C:补偿事务。撤销不了,就反向操作。这是微服务处理跨系统事务的标准模式,源头是 1987 年的经典论文 Sagas:
一个长事务可以拆成一串子事务,每个子事务都配一个补偿事务。当某个子事务失败时,依次执行前面所有已提交子事务的补偿,把整个 Saga 的效果冲抵掉。
翻译成大白话:扣错的款,退款;发错的邮件,追回;改错的库存,改回来。补偿事务不追求“回到过去”,它追求“把后果抚平”。这是跨系统场景下唯一真正可行的方案,代价是你得为 Agent 的每一种操作预先写好补偿逻辑——而 Agent 的行为空间几乎是无限的。
方案 D:事件溯源。不保存状态,保存“状态是怎么一步步变成这样的”事件流。要回滚,就重放到某个事件之前。思路来自 Martin Fowler 的 Event Sourcing,适合自建业务系统,但不适合“让 Agent 去改一个历史遗留数据库”——那个库根本没给你留事件流。
四个方案放在一起看:
| 方案 | 精确度 | 主要风险 | Agent 场景可行性 |
|---|---|---|---|
| 事务回滚 | 高,单操作级 | 管不住外部副作用 | 适合纯数据库写,需要包装层 |
| 快照恢复 | 低,备份点级 | 覆盖并发操作,制造新事故 | 不推荐用于生产 |
| 补偿事务 | 中,业务级 | 补偿本身也可能失败 | 跨系统操作的唯一出路 |
| 事件溯源 | 高,事件级 | 需要从第一天就设计好 | 只适合自建系统 |
“操作前的状态”是个陷阱
你可能已经发现了:我一直在绕开“一键回滚到操作前”这个说法。因为它是个陷阱。
Agent 的操作不是原子性的,是链式的。它跑一个任务通常要好几步:先查 A 表拿到 ID,再更新 B 表,再调 C 接口。第 5 步依赖第 2 步的输出。如果你只回滚第 2 步,第 5 步就变成了悬空的孤儿操作——它引用了不存在的前提。
再加上三个维度:
- 时间维度:错误发现得越晚,“操作前”就越模糊,中间塞满了别人的事务。
- 链式维度:多步操作之间有数据依赖,删掉中间任何一步,后续步骤全变孤儿。
- 外部维度:邮件、支付、通知这些副作用根本不在数据库里,无法被“回滚”。
所以“回滚到操作前”只在一种条件下接近可执行:Agent 的整个操作被关在隔离环境里,没有外部副作用,没有并发干扰。其余情况下,你回滚的是“你想象中的操作前”,而不是“真正的操作前”。
现实中的 Agent 系统,没有一个是靠“一键回滚”活下来的
看几个实际例子就明白了。
ChatGPT 的高级数据分析(代码解释器),把所有代码跑在沙箱里,文件是临时的,会话结束全部销毁。它的“回滚”根本不需要回滚——丢掉一个本来就可丢弃的环境。
Claude Code 这类编码 Agent,靠权限模式和 diff 预览兜底:每次改动先把 diff 拿给你看,你点头才应用。就算应用了,用户可以用 git revert 回去。注意,它先做的是“事前确认”,而不是“事后补救”。
LangGraph 这类编排框架确实有 checkpointer,能保存 Agent 每一步的内部状态,支持重放和分叉。但它恢复的是 Agent 的“脑内状态”,不是外部数据库的状态。这两者经常被混为一谈,是概念性的错误。
Text-to-SQL 类的 Agent,最常见的工程实践是人在回路:执行 UPDATE 之前,先把 SELECT 的结果摆给你看,你确认了它才写。这叫事前预防,比事后回滚便宜一个量级。
还有一条朴素但极其有效的思路:把操作本身设计成可逆的。用软删除(deleted_at 字段)代替 DELETE,用版本号代替原地覆盖。这样 Agent 再犯傻,你只需要“再执行一次反向操作”,连数据库回滚都不需要。
我的判断:防线应该在回滚之前
在封闭环境里,“一键回滚”是真实存在的——沙箱、临时表、事务包装器,加上一个权限受限的 Agent 专用数据库账号,这套组合足以应付大多数内部工具和演示场景。
但在开放生产环境里,把宝押在回滚上是错误的。不是回滚技术不行,而是回滚的前提——“你能界定操作边界”——在真实世界里几乎不成立。你真正需要的是四道防线:
- 最小权限:Agent 的数据库账号只给 SELECT/INSERT/UPDATE,不给 DELETE/DROP;或者干脆只给测试库的写权限。
- 事前确认:所有写操作执行前,把将要影响的行数和值展示给人。
- 可逆设计:软删除、版本化、审计日志,让每一步操作都有后路。
- 审计日志:无论 Agent 干了什么,你都能精确复述“它干了什么”。这是任何回滚的前提——你连它干了什么都不知道,谈何回滚。
最后说句实话:如果你打算让 Agent 直接操作生产数据库,而且上面四条一条都没准备,那你需要的不是更好的回滚方案,而是重新想想——到底该不该让它碰数据库。
原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/1625.html