[控场AI]
· 8 分钟阅读· 4,310 字

Agent写的SQL跨方言总出错?转译还是重问模型

Agent写的SQL跨方言总出错?转译还是重问模型

先确定性转译、仅在不一致时重问模型,并用执行夹具裁决SQL方言适配。

文章讨论AI代理生成的SQL从Postgres迁移到MySQL时的方言适配问题,指出真正危险的不是语法报错,而是两个引擎都能执行却返回不同结果的静默错误。作者对比两种方案:确定性转译(sqlglot)免费、可diff,但在窗口帧、NULL排序、排序规则等场景会悄悄出错;模型重问能处理复杂语义转换,却非确定、可能丢掉WHERE子句,必须配合执行验证。核心决策规则是:先转译,只有当目标引擎执行结果与源不一致时才升级为重问。验证依赖fixture:捕获源结果的列名、行数与校验和,在一次性目标引擎上执行比对,只把不一致项升级重问,并限制重试次数。文章强调行数相等不充分,fixture需覆盖NULL、时区、Unicode,否则会在真实月末加载中失败。

当AI代理生成的SQL从Postgres迁移到MySQL时,最危险的错误不是语法报错,而是那些在两个引擎上都能执行、却返回不同结果的查询。这类静默错误不会触发异常,却会让下游报表悄悄出错。本文基于实际项目经验,探讨如何用确定性的SQL转译与模型重问相结合,构建可靠的方言适配流程。

方言漂移:安静却昂贵的错误

方言问题看起来像语法错误,但真正昂贵的部分并非如此。那些解析失败的语句反而好处理——解析器会直接拒绝,循环重试时带上错误信息即可。真正麻烦的是那些在两种引擎上都能解析、但语义不同的写法:ILIKE 与大小写不敏感排序规则的冲突、LIMIT 和 OFFSET 的顺序、整数除法、以及MySQL中没有直接对应物的 jsonb 箭头操作符。

一个在双方都能执行却返回41行而非44行的查询,能通过所有只检查异常的验证流程。这种不对称性正是转译与模型重问之间抉择的根源:两种方法的失败点不同,决策规则必须基于哪种失败你能低成本检测。

AST(抽象语法树) 是SQL解析器将语句文本转换为的树状结构表示,节点对应SELECT、WHERE、函数调用等语法成分,剥离了空格、大小写等表层差异。sqlglot 的转译流程正是先读取源方言生成AST,再将AST按目标方言的规则重新序列化。之所以能保证确定性,是因为AST到输出的映射是纯函数式的——相同AST永远得到相同文本。但这也意味着转译器只能表达两种方言的AST之间存在的对应关系;当目标方言的AST没有等价节点时(例如MySQL缺少jsonb箭头操作符的对应物),转译器只能选择一个近似的替代表示,语义偏差就在这一步被静默引入。

方案A:确定性转译后执行

第一种立场认为方言转换本质上是解析问题,应该保持为解析问题。sqlglot 读取Postgres的AST并输出MySQL,同样的输入永远产生同样的输出,这意味着整个转换步骤可以在代码审查中通过diff来审阅。它的速度也足够快,可以在每次草稿生成时运行,修复成本几乎为零。


**窗口帧(window frame)** 是`OVER`子句中定义聚合窗口边界的部分,如`ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`。当省略帧子句时,SQL标准为有`ORDER BY`的窗口函数规定了默认帧,但不同引擎对默认帧的理解和实现存在差异——尤其是在存在并列排序值时,默认帧可能包含也可能排除并列行。转译器在输出时往往不会显式补全默认帧,于是同一条窗口查询在两个引擎上可能聚合不同数量的行。这类问题不会引发语法错误,只会让聚合结果偏离,正好落入“能执行但结果不同”的静默错误类别。
# dialect_probe.py — 示意代码;请根据你的schema调整标识符
import sqlglot

SOURCE, TARGET = "postgres", "mysql"

def transpile(sql: str) -> str:
    """确定性:相同输入,相同输出,可在代码审查中diff。"""
    return sqlglot.transpile(sql, read=SOURCE, write=TARGET, pretty=True)[0]

if __name__ == "__main__":
    src = """
        SELECT user_id, date_trunc('month', created_at) AS m, count(*)
        FROM orders WHERE note ILIKE '%refund%' GROUP BY 1, 2
    """
    print(transpile(src))

其弱点同样明显:转译器只保留它能表达出的语义,其余部分只能猜测。窗口帧默认值、字符串拼接、ORDER BY 中的NULL排序,都是转换后语句合法、可执行、却悄悄出错的地方。因此方案A只有在配合目标引擎执行验证时才站得住脚。

方案B:在提示中带上目标方言重问模型

第二种立场认为模型本身已经掌握两种方言,只需要告诉它哪个更重要。免费模型访问让额外调用的成本低到可以放进修复循环。我在MonkeyCode的免费模型访问上完成重问步骤,其免费服务器选项则提供一个一次性的目标引擎来执行验证。(披露:本文是MonkeyCode产品推广的一部分。免费额度及其限制会随时间变化,请先查看当前账户条款再规划每次提交都运行的循环。)


**排序规则(collation)** 决定字符串比较和排序时如何对待大小写与重音。PostgreSQL的`ILIKE`是运算符级别的显式大小写不敏感匹配,而MySQL的行为取决于列或连接的collation设置,例如`utf8mb4_general_ci`默认即大小写不敏感。因此把`ILIKE`直接改写为`LIKE`在MySQL上可能碰巧正确,也可能因collation不同而行为不一致。模型重问之所以在这类场景有优势,是因为它可以根据目标列的collation选择`LOWER()`包裹或`COLLATE`子句,而确定性转译器只能做符号层面的替换,无法感知schema中的collation配置。
# reask.py — 未执行示例;如果你的端点不同请替换客户端
import os

PROMPT = """Rewrite this {src} query for {dst}.
Keep the output column names and their order identical.
Do not change filtering semantics. Return SQL only.

{sql}
"""

def reask(client, sql: str, src="postgres", dst="mysql") -> str:
    r = client.chat.completions.create(
        model=os.environ["LLM_MODEL"],
        temperature=0,
        messages=[{"role": "user", "content": PROMPT.format(src=src, dst=dst, sql=sql)}],
    )
    return r.choices[0].message.content.strip()

重问能处理转译器只能猜测的情况,包括对排序规则敏感的比较,以及需要重写而非简单替换符号的 ON DUPLICATE KEY UPDATE 转换。它的弱点正好相反:不确定性、可能悄悄丢掉一个 WHERE 子句,而且一个看似合理的重写与正确重写,在执行前无法区分。没有执行验证的模型重问,只是多绕了几步的自信猜测。

我的决策规则

两种方案都无法独立生存,而分界线不是“简单查询对复杂查询”,而是“我能否通过fixture运行检测到失败”。无论走哪条路,fixture都是同一个产物。

步骤后的观察路由理由
转译抛出解析错误重问,然后执行语法层面缺口;模型往往知道正确写法
转译解析成功,目标引擎报错重问,然后执行AST合法,但不支持该函数或语法
执行成功,列名或顺序不同拒绝草稿会静默破坏下游消费者
执行成功,行数不同重问,然后执行语义差异;绝不能仅凭语法通过就合并
执行成功,行数相同,校验和不同手工编写,加入fixture聚合或舍入漂移,模型不会捕捉到
执行成功,行数和校验和都相同接受并缓存重写不再为已解决的配对花费调用

表下规则很直接:先转译,因为它免费且确定;只有当目标引擎的执行结果与源不一致时,才升级为重问。已经接受的重写要按规范化SQL文本缓存,否则循环每次都会重新推导同样的转换。

五步构建验证夹具

  1. 用只读角色在源引擎上捕获一次源结果,存储列名、行数,以及一个有序数值列上的校验和。
  2. 转译语句,并在运行任何东西之前记录解析是否成功。
  3. 在一次性目标引擎上执行输出——我用免费服务器选项加载仅schema转储加小型fixture,运行探测,然后销毁实例。
  4. 只把不一致项升级到重问函数,并将循环限制在两次尝试内,防止坏提示过夜烧掉预算。
  5. 把每个需要手工修复的查询提升到fixture集中,这样下次代理提出类似形状时回归已被覆盖。

校验和(checksum) 在此处指对结果集中某一有序数值列按固定顺序计算出的聚合指纹(如SUM或哈希),用于在不逐行比对的前提下检测结果差异。它比行数更敏感——聚合漂移、舍入差异、行顺序错位都可能改变校验和而保持行数不变。选择哪个列做校验和很关键:应选数值稳定、不受时区或本地化影响的列,并确保ORDER BY确定无歧义。校验和的局限在于它只覆盖被纳入计算的那一列,其他列的偏差不会被发现,因此它是对行数检查的补充而非替代。

局限与谁应该跳过

行数相等是必要但不充分的,校验和也只覆盖你选择覆盖的列。忽略NULL、重复键、时区边界和Unicode的fixture,会让一个在真实月末加载中失败的改写在测试中通过。温度高于零会让重问步骤不可复现,所以修复路径应保持零温度,并把接受的重写当作代码而非提示状态。如果你没有fixture数据、没有一次性环境,或者政策禁止将schema文本发送到托管模型,请跳过此工作流——在这些情况下,确定性转译器加人工审查是诚实的选项。

rss source: Transpile or Re-Ask: Fixing Dialect Breaks in Agent-Written SQL

让模型远离“重写是否正确”的决定,让执行来裁决。如果你想在不支付第二个引擎或模型账单的情况下尝试循环,从一个fixture查询和免费服务器选项开始,让上面的表格来决定路由。

#SQL方言转换#AI代理SQL#sqlglot转译#模型重问#SQL验证夹具#Postgres转MySQL#Agent SQL
分享:

相关推荐