Agent 的操作回滚:Agent 改错了数据库记录,一键回滚到操作前的状态

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

AI technology illustration

他问我:数据库不是天生支持回滚吗?为什么轮到 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 专用数据库账号,这套组合足以应付大多数内部工具和演示场景。

但在开放生产环境里,把宝押在回滚上是错误的。不是回滚技术不行,而是回滚的前提——“你能界定操作边界”——在真实世界里几乎不成立。你真正需要的是四道防线:

  1. 最小权限:Agent 的数据库账号只给 SELECT/INSERT/UPDATE,不给 DELETE/DROP;或者干脆只给测试库的写权限。
  2. 事前确认:所有写操作执行前,把将要影响的行数和值展示给人。
  3. 可逆设计:软删除、版本化、审计日志,让每一步操作都有后路。
  4. 审计日志:无论 Agent 干了什么,你都能精确复述“它干了什么”。这是任何回滚的前提——你连它干了什么都不知道,谈何回滚。

最后说句实话:如果你打算让 Agent 直接操作生产数据库,而且上面四条一条都没准备,那你需要的不是更好的回滚方案,而是重新想想——到底该不该让它碰数据库。

原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/1625.html

赞 (0)
上一篇 4天前
下一篇 4天前

相关推荐