I反对生成式 SQL 的理由

模型不应该
编写 SQL。

Text-to-SQL 是将 Agent 接入数据库的直观方式,演示效果确实令人印象深刻。但问题在生产环境中才会显现:同一个问题编译出不同的 SQL,一个隐蔽的 join 错误让收入偏移 15%,而模型的输出也成了攻击面。还有另一条路。

II先给予肯定

Text-to-SQL 名副其实——在其适用范围内。

适用场景

  • 基于临时数据的原型验证
  • 在人工监督下探索陌生 schema
  • 有人会事后核验的一次性问题
  • 演示场合——效果确实出色

失效场景

  • 将被直接用于决策的数字
  • 上下文中存在任何不可信文本
  • 连接使用生产环境凭据
  • 需要可复现或可审计的结果
  • 无人监督运行的 Agent

问题不在于这种模式本身,而在于其爆炸半径。本页所有风险都源于同一个设计决策:模型的输出被当作代码执行。

III两条路径

同一问题,两种架构。

跟随同一个问题走过两条路径——左侧是自由格式 SQL 字符串,右侧是类型化执行计划。编号标记对应下方的风险台账。

问题

哪个地区的总收入最高?

路径 A

模型生成 SQL

  1. 模型即兴生成字符串 010304

    模型所读取的任何内容——用户消息、检索到的行——都可能影响这个字符串。

    第 1 次运行SELECT region, SUM(total) FROM orders GROUP BY region;第 2 次运行 · 同一问题SELECT o.region, SUM(i.amount) FROM orders o LEFT JOIN order_items i ON i.order_id = o.id GROUP BY 1;
  2. 由连接直接执行 05

    以连接的完整权限执行。可读取、可关联——除非有人记得设置相应标志,否则也可写入。

结果

run 1 → east · 2130.50

run 2 → east · 2450.08+15% — 关联扇出 02

两次运行,两个数字。看起来都合理,却无从判断哪个——如果有的话——是正确的。

路径 B

模型提交类型化计划

  1. 类型化意图

    不是字符串,而是带有 schema 的值。它只能声明契约所定义的操作。

    { "kind": "query", "version": "1", "source": "revenue", "op": "sum", "group_by": "region" }
  2. 契约校验

    哈希锁定。未知操作返回 unsupported_operation 及最近匹配项——绝不猜测。

  3. 策略校验

    代码层面的允许列表。请求只能缩小操作范围,不能扩大。

  4. 确定性执行

    float64、单线程、固定运行时。相同计划,相同字节,相同结果。

结果

east · 2130.50

plan_hash f87610d8afeb…

decision_path "exact_spec"

唯一结果,附带可供复现的哈希值——下周、下季度,两种语言均适用。

IV风险台账

生成式 SQL 的五类失效

  1. 01

    P2SQLarXiv 2308.01990

    注入攻击转移至输出通道

    输入过滤检查的是进入模型的内容。P2SQL 攻击发生在输出端:格式合法、意图恶意的 SQL,由隐藏在用户消息或检索行中的指令拼装而成。任何输入过滤器都看不到它——攻击本身就是输出。

  2. 02

    −15%那个说谎的仪表盘

    错误关联静默失效

    扇出关联导致行被重复计数,收入数字偏差 15%。没有异常,没有警告——错误的 SQL 不会崩溃,只会照常上报。结果永远是一个数字,而一个看似合理的数字不会透露任何错误信号。

  3. 03

    1 → n一个问题,n 条查询

    同一问题,不同 SQL

    问两次,模型可能以两种不同方式编译这个问题——有时得出两个不同答案。没有可供审查、缓存或复现的规范查询。昨天的数字无法再现,哪怕只是为了核验。

  4. 04

    91.2 → 21.3正确率 · 基准测试 → 企业 schema

    精度断崖

    在整洁的基准 schema 上,前沿模型生成正确 SQL 的概率为 91.2%。在真实企业 schema 上:21.3%。同一研究中,约 40% 的 text-to-SQL Agent 运行彻底失败或返回错误结果。基准测试是干净的,你的 schema 不是。

  5. 05

    1 个生产数据库被 agent 删除——Replit 事件

    写入路径从未消失

    Replit 的编程 agent 无视明确禁令,删除了一个生产数据库。教训在此:只读指令不过是一句请求。连接若能写入,写入路径便始终存在,终有一次错误补全会找到它。只读必须是工具本身的属性,而非 prompt 里的一行字。

不要过滤输出。
不要生成它。

无 SQL 字符串,无注入 · 无猜测,无漂移

V证书

五个维度,并排对比

非 LLM 转 SQL 生成器引擎 README

差异证书

编写界面生成的 SQL自由格式的 SQL 字符串类型化计划类型化、版本化的意图
执行界面生成的 SQL连接所允许的一切类型化计划4,574 项只读能力
写入路径生成的 SQL存在,除非被阻断类型化计划从构造上不存在
治理生成的 SQLprompt 层面,尽力而为类型化计划代码层面的白名单,模型无法扩展
可重现性生成的 SQL类型化计划每条结果附带重放哈希

右列已锁定至合约sha256:79f1c5a6…924be9a1

细则经得起推敲:引擎唯一的逃生舱——getUnsafeRuntime——在所有面向模型的界面中均不存在,模型无从触及。README 亦明确声明:这不是 LLM 转 SQL 生成器。

VI常见问题

生产环境中的追问

如何在 LLM 生成的 SQL 中屏蔽 DELETE 和 DROP?

不要过滤 SQL——停止生成它。黑名单检查的是字符串,而模型在生成新字符串方面永无止境。SQAI 从根本上消除这一类问题:模型针对 4,574 项只读能力提交类型化计划,任何计划都无写入能力可调用。

什么是 P2SQL 注入?

Prompt 转 SQL 注入:恶意 SQL 出现在模型的输出中,由模型所读取内容中隐藏的指令拼装而成——可能来自用户消息、文档或检索到的数据行。输入过滤对此无能为力,因为攻击发生在输出通道。相关记录见 arXiv 2308.01990。

文本转 SQL 是否有适用场景?

有——在非生产数据上做原型验证或有监督的探索时,它快速且切实有用。但一旦结果将被付诸行动、上下文包含不可信文本,或答案需要可重现,它便不再是正确选择。

如何为 AI agent 提供对数据库的只读访问?

让只读成为结构性约束,而非配置项。连接上的只读标志是一个随时可被更改的设置。SQAI 的执行界面仅包含读取能力——写入路径从构造上不存在,即便是引擎的逃生舱,任何面向模型的工具也无从触及。

类型化计划与生成的 SQL 有何不同?

SQL 字符串可以表达语法所允许的任何内容。类型化计划只能表达合约所定义的内容。每个计划都经过哈希锁定合约的验证、白名单策略的检查,并在确定性引擎上执行——每条结果附带重放哈希。

走另一条路。

无需账户,无需密钥,本地数据不离本地。