Agent 的数据库变更风险:UPDATE 没有 WHERE 条件——Agent 怎么防止这种灾难

你有没有在凌晨被一个电话叫醒过?

AI technology illustration

“生产库的 orders 表,4700 万行订单,状态全部变成已归档了。”

“怎么变的?”

“AI Agent 在执行数据归档,跑了一条 UPDATE orders SET status = 'archived';——没有 WHERE 条件。”

这不是惊悚片剧本。把 Agent 接到数据库上的团队越来越多,大家最担心的往往是“Agent 懂不懂业务规则”。但业务规则错了是慢动作——它会一点点把数据搞坏,你有时间发现、有时间补救。一条没有 WHERE 的 UPDATE 或 DELETE 是快动作:瞬间、全表、不可逆。MySQL 跑完这条语句用不了 5 秒,它甚至不问你意见。

好消息是:这种灾难完全可以靠工程手段防住。坏消息是:绝大多数团队正在用的防护方式,跟没穿一样。先从根上讲清楚——Agent 为什么会写出这么蠢的 SQL。

模型不是写错了代码,它是在“续写”

LLM 生成 SQL 的过程,本质上是在逐 token 猜概率。

它看着你的表结构、用户指令、历史对话,每一步都从词汇表里挑“概率最高的下一个词”。UPDATE orders SET status = 'archived'UPDATE orders SET status = 'archived' WHERE order_id = 123,在它的概率分布里可能只差零点几个百分点。这个差距,比你想象的小得多。

更阴险的是用户指令本身。用户说“帮我把所有已完成的订单归档”,他脑子里“所有”指的是刚刚筛选出来的那 18 条;模型脑子里“所有”等于全部行。模型没有执行环境,它感知不到“18 条”和“4700 万条”的区别。

我调试 text-to-SQL 时踩过一个典型坑:用户问“删除昨天创建的临时记录”,模型生成 DELETE FROM temp_logs;——它把“临时记录”理解成 temp_logs 表的所有行,“昨天”这个时间条件被彻底丢了。如果当时接的是生产库,这篇文章的开头就是我的事故复盘。

没有 WHERE 只是最蠢的一种死法

如果你以为只要检查“有没有 WHERE”就够了,看这张表:

错误类型 示例 为什么危险
完全没有 WHERE UPDATE users SET is_active = 0 最直接,全表遭殃
WHERE 恒为真 DELETE FROM logs WHERE 1 = 1 有 WHERE,字符串检查直接通过
条件范围理解错 用户说“刚才筛出的那批”,模型更新了全表 SQL 合法,危害等于没有 WHERE
条件方向写反 UPDATE t SET price = price * 0.9 WHERE user_type != 'vip' 想给非 VIP 打折,结果全表打折

前两种是技术错误,规则可以拦住。后两种是理解错误,SQL 完全合法、日志里有 WHERE、影响行数也“合理”——因为它们本来就想动整张表。规则拦不住,只能靠流程兜底。

防住它的五层防线

  1. 权限最小化。给 Agent 的数据库账号默认只开 SELECT。需要写操作时,单独申请一个只包含目标表和目标字段的账号。这一层能拦住 80% 的事故,成本是零,先做。
  2. 写操作分级。所有写操作默认走人工审批。不带主键条件的 UPDATE/DELETE、任何 DDL,直接推到审批队列,Agent 没有资格自动执行。
  3. 执行前检查。用 SQL 解析器(不是字符串匹配)检查语句结构:UPDATE/DELETE 必须有 WHERE 子句,且 WHERE 不能是常量表达式(比如 1=1)。MySQL 有个内置的安全模式可以直接开(官方文档):
SET SQL_SAFE_UPDATES = 1;

开启后,不带 WHERE 或 LIMIT 的 UPDATE/DELETE 会被直接拒绝;即使带了 WHERE,如果条件没有用到索引键,同样会被拒绝。它的作用是兜底,不做语义判断。

  1. 影响行数预览。在事务里先执行等价的 SELECT COUNT(*),数量异常就回滚:
BEGIN;
SELECT COUNT(*) FROM orders WHERE status = 'archived' AND created_at < '2025-01-01';
-- 返回行数异常?直接 ROLLBACK,不执行 UPDATE
UPDATE orders SET status = 'archived' WHERE status = 'archived' AND created_at < '2025-01-01';
COMMIT;

这一层为什么最关键?因为它不依赖模型写对。模型可能写错 WHERE,但它骗不过“40 万行订单一夜之间全变了”这个信号。

  1. 审计与告警。所有 Agent 执行的写操作,记录原始 SQL、影响行数、执行账号、触发会话。影响行数超过阈值且操作者是 Agent 时立刻报警。MySQL 审计日志、云数据库的 SQL 审计都能做到。

检查“有没有 WHERE”救不了你

我最早给 Agent 加防护时,想法很简单:检查 SQL 字符串里有没有 WHERE,没有就不让跑。于是写了个最朴素的检查:if 'where' not in sql.lower(): raise

直到有一天,Agent 生成的 SQL 长这样:

UPDATE orders SET status = 'archived' WHERE total_amount > 0;

字符串里确实有 WHERE,检查通过了。但这条 SQL 把所有金额大于 0 的订单——也就是几乎所有订单——全归档了。用户的本意只是归档一个订单号。

那次之后我想明白了:检查“有没有 WHERE”防不住“WHERE 写错了”。你真正需要的是结果导向的防线,不看模型写了什么,只看它造成了什么。

更狠的一招:让 Agent 只填参数,不写 SQL

如果你追求更高的安全水位,换一种思路:不让模型直接生成 SQL,而是让它调用封装好的函数。

def update_rows(table, set_values, where):
    # 按条件更新记录。where 为空或缺失时直接拒绝。
    if not where:
        raise ValueError('update_rows() 必须提供 where 条件')
    # 内部强制带上 updated_at = NOW()
    # 返回受影响行数,供上层做阈值判断

Agent 不写 SQL,只产生调用参数:

update_rows(
    table = 'orders',
    set_values = {'status': 'archived'},
    where = {'order_id': 123}
)

where 缺失?函数直接抛异常。where 恒真?结构化参数里根本无法表达“恒真”。模型确实可能把 order_id 填成 124,但它无法把“批量修改”伪装成“单行修改”——危害被限制在一行,而不是全表。

这一步的本质,是把灾难的概率从“模型忘了 WHERE”降到“模型填错一个值”。两者相差几个数量级。

权威机构怎么说

OWASP 发布的 Top 10 for LLM Applications 2025 里,专门有一条 Excessive Agency(过度自主权),说的就是这类风险:LLM 系统被赋予了超出职责范围的权限——比如写生产库——而缺乏人类介入机制。OWASP 的建议概括成一句话:把 LLM 的权限限制到实现功能所需的最小范围,所有有实际影响的操作必须经过人类确认。

翻译成人话:模型本身没有危险,危险的是你给了它一把能作用于整个生产库的瑞士军刀,然后让它自己决定怎么用。主流 SQL Agent 框架的文档也都在提醒同一件事:连接数据库,用只读账号。这句话很容易被跳过,直到出事故。

我的判断

未来一两年,AI 数据库 Agent 会越来越多。但我见过太多团队,把“模型能力强”当成“模型安全”的代偿——觉得大模型这么聪明,不会犯低级错误。

真相是:从工程角度,你必须假设 Agent 一定会犯错。概率生成模型的每次输出都是一次抽样,错误概率再低也永远大于零。你无法消除这个概率,只能靠权限、事务、审计这些古老但有效的数据库工程手段,让错误造成的危害趋近于零。

我的建议很简单:把 Agent 当实习生。不给生产库密码,写操作要审批,改动超过阈值要复核,所有行为留日志。如果你不放心把生产库交给一个刚来的实习生,你就不应该放心交给 Agent。

最后提醒一句:如果你看到这里才意识到,自己的 Agent 账号在生产库上有写权限——先停下来,把它收回来。这篇文章讲到的每条防线,前提都是你已经不想再赌模型下一次“碰巧写对”。

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

(0)
上一篇 1小时前
下一篇 1小时前

相关推荐