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文本缓存,否则循环每次都会重新推导同样的转换。
五步构建验证夹具
- 用只读角色在源引擎上捕获一次源结果,存储列名、行数,以及一个有序数值列上的校验和。
- 转译语句,并在运行任何东西之前记录解析是否成功。
- 在一次性目标引擎上执行输出——我用免费服务器选项加载仅schema转储加小型fixture,运行探测,然后销毁实例。
- 只把不一致项升级到重问函数,并将循环限制在两次尝试内,防止坏提示过夜烧掉预算。
- 把每个需要手工修复的查询提升到fixture集中,这样下次代理提出类似形状时回归已被覆盖。
校验和(checksum) 在此处指对结果集中某一有序数值列按固定顺序计算出的聚合指纹(如SUM或哈希),用于在不逐行比对的前提下检测结果差异。它比行数更敏感——聚合漂移、舍入差异、行顺序错位都可能改变校验和而保持行数不变。选择哪个列做校验和很关键:应选数值稳定、不受时区或本地化影响的列,并确保ORDER BY确定无歧义。校验和的局限在于它只覆盖被纳入计算的那一列,其他列的偏差不会被发现,因此它是对行数检查的补充而非替代。
局限与谁应该跳过
行数相等是必要但不充分的,校验和也只覆盖你选择覆盖的列。忽略NULL、重复键、时区边界和Unicode的fixture,会让一个在真实月末加载中失败的改写在测试中通过。温度高于零会让重问步骤不可复现,所以修复路径应保持零温度,并把接受的重写当作代码而非提示状态。如果你没有fixture数据、没有一次性环境,或者政策禁止将schema文本发送到托管模型,请跳过此工作流——在这些情况下,确定性转译器加人工审查是诚实的选项。

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

Workbuddy零基础入门:桌面AI智能体如何落地工作流
Workbuddy桌面AI智能体零基础教程解读:从安装、积分领取到文件管理与自动化工作流搭建,对比CodeX的适用场景与成本差异,帮助职场人完成从AI提问者到AI管理者的思维转变。

OpenAI智能体新栈:计算机操作、决策API与超速工作流
OpenAI Dev Day发布智能体新栈:Computer Use操作速度超越普通人,Decisions API基于Luna实现极低延迟分类,Agents API开放同源计算机操作能力,GPT-6.1 Sol成本仅为Astra七分之一。本文梳理核心更新、实测数据与开发者落地要点。

梁文峰开源推理框架DeepInfo,硬刚美国禁令
DeepSeek在开源模型后再开源推理框架DeepInfo,包含GPU/CPU交叉流水线、小模型预判大模型复核、华为昇腾与英伟达芯片全面适配等技术,直面美国联邦禁令。本文解析其对AI产业落地的意义与开源策略。