LLM 生成 SQL 最大的失败原因是盲生成不知道表结构,通过执行反馈循环让模型自检并重试,才是生产可用的方案。
你给 LLM 接上数据库,问它"上个月我们创造了多少收入",它会返回一条漂亮的 SQL 查询语句。你执行它——报错:column "total_amount" does not exist。真实列名其实是 amount_cents。模型猜了,猜错了。
这就是 text-to-SQL demo 看起来神奇、而生产环境中 text-to-SQL 却让人觉得不靠谱的唯一最大原因。模型根据自然语言问题生成 SQL,是在"闭眼"操作——它根本看不到这条查询是否真的能跑起来、是否返回了数据、还是返回了一堆乱码。2025 年的研究和生产系统中悄悄成为标准的那种修复方式,说起来简单得让人意外:让模型自己跑一下查询,看看发生了什么,然后重试。这个循环叫做执行反馈(execution feedback),它是区分"花拳绣腿"和"能交付的工具"的那道分水岭。
本文你会学到:执行反馈循环在代码里实际长什么样、为什么"自我修正"只在有真实结果作为依据时才有效、以及你需要哪些护栏机制来防止一个自我重试的 AI 把你的数据库或者延迟预算给熔穿。
模型一次性生成 SQL 有三种可能出错的方式,而其中只有一种是语法问题:
那些"大声"报错的其实反而是好消息。数据库拒绝一条查询时,给你的是一个非常精确的、机器可读的问题描述。MAC-SQL、CHESS 和 ReFoRCE 等系统的洞察在于:这条错误信息作为修正信号,比让模型抽象地"再检查一下你的工作"要有用得多。研究在这方面是一致的:使用执行结果进行自我修正能可靠地提升准确率,而不依赖外部反馈的自我修正通常根本没有帮助——甚至可能让查询越改越差,因为模型会对自己原本正确的答案反复质疑。
所以制胜的模式不是更聪明的 prompt,而是——一个反馈循环。
下面是整个思路的伪代码:生成、执行、如果执行失败就把错误反馈回去并重新生成——直到达到上限。
def text_to_sql(question, schema, max_attempts=3):
error = None
sql = None
for attempt in range(max_attempts):
sql = llm_generate_sql(
question=question,
schema=schema,
previous_sql=sql, # None on first pass
previous_error=error, # None on first pass
)
ok, result_or_error = safe_execute(sql)
if ok:
return sql, result_or_error
error = result_or_error # feed this back next iteration
raise RuntimeError(f"Failed after {max_attempts} attempts: {error}")
魔法完全藏在第二次 pass 时放进 prompt 的内容里。模型看到的不再只是原始问题,而是它自己失败的尝试加上精确的数据库报错:
The previous query failed. Fix it.
Question: How much revenue did we make last month?
Your previous SQL:
SELECT SUM(total_amount) FROM orders
WHERE created_at >= date_trunc('month', now() - interval '1 month');
Database error:
column "total_amount" does not exist
HINT: Perhaps you meant to reference the column "orders.amount_cents".
Rewrite the query using only columns that exist in the schema below.
Postgres 甚至在 HINT 里直接给出了正确的列名。模型拿到这个 hint 后,下一次 pass 几乎必定能把查询改对:
SELECT SUM(amount_cents) / 100.0 AS revenue_dollars
FROM orders
WHERE created_at >= date_trunc('month', now() - interval '1 month')
AND created_at < date_trunc('month', now());
这就是整套机制。一个重试循环,把一个脆弱的猜测者变成了一个能收敛到可用查询的东西。
语法错误和 schema 错误是会自己声张的。语义错误才是危险的——查询完美运行,然后返回一个自信满满的错误数字。你可以把可疑结果当作一种值得反馈的错误形式,来捕获其中有价值的子集。
最有价值的信号是空结果集。如果用户问"上个月哪些客户流失了"而查询返回零行,这几乎从来不是正确答案——通常意味着一个坏的 JOIN 或者过于严格的过滤条件。把这也反馈回去:
ok, rows = safe_execute(sql)
if ok and len(rows) == 0:
error = ("Query executed but returned 0 rows. "
"This is likely a bad JOIN or an overly strict WHERE clause. "
"Re-examine the filters and join conditions.")
ok = False # trigger another repair pass
你还可以叠加上一些廉价的合理性检查作为额外的反馈信号:
这些都不能证明答案错了,但作为反馈 prompt,它们能在人类看到结果之前就推动模型重新审视。
一旦你允许模型执行 SQL——并且反复执行它——你就构建出了一个需要系安全带的东西。以下四条是不可妥协的。
把每条生成的查询都通过一个数据库角色只有 SELECT 权限的连接来执行。不要依赖 LLM 自己去避免 DROP;在数据库层强制执行,那里是 prompt 渗透不到的地方。
-- One-time setup: a role the AI connects as
CREATE ROLE ai_readonly LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE app TO ai_readonly;
GRANT USAGE ON SCHEMA public TO ai_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ai_readonly;
-- No INSERT, UPDATE, DELETE, DROP — ever.
在执行之前,拒绝任何包含 INSERT、UPDATE、DELETE、DROP、ALTER、TRUNCATE 或 GRANT 的查询。系好安全带再系背带——只读角色是真正的防线,但静态检查能以更低成本、更早地捕获问题。
重试循环可能生成一个意外的交叉连接,扫描数十亿行。给它加个上限:
SET statement_timeout = '5s';
-- and wrap the model's query:
SELECT * FROM ( /* model SQL */ ) sub LIMIT 1000;
这是人们最容易忘记的一点,而且一旦忘记会遭到双重反噬。首先是延迟:每次重试大约增加 1.5–3 秒的模型时间加执行时间,所以一个没有上限的循环可能让用户盯着加载动画发呆 15 秒。其次是过度修正——模型有时会在后续轮次把一个正确的查询改差,反复自我怀疑后给出一个残缺的答案。三次尝试是一个合理的默认值。如果到了那时还没成功,就大声失败并记录下完整对话记录。
**反馈了错误的东西。**数据库错误字符串是黄金——原封不动地透传过去,包括 hints。那些把错误信息总结或截断的团队("查询没跑通")等于把让这个循环生效的精确信号给扔掉了。
**无限重试。**没有上限意味着无上限的延迟和成本。务必设置 max_attempts。
**只信任自我批评。**不执行就问模型"你确定这条 SQL 正确吗?"只是做戏。改进来自于真实的执行结果,而不是自我反思。
**什么都不记录。**一个你看不到的循环是无法调试或改进的。记录每一次尝试:问题、每条生成的查询、每个错误,以及最终结果。这些对话记录也是你能得到的最好的少样本示例训练数据。
**过早跳过人工确认。**在上线后的前几周,在运行之前把生成的 SQL 展示给用户。这能建立信任,并暴露出任何自动化检查都抓不到的语义错误。
单次 text-to-SQL 失败的根源在于模型在"闭眼"写查询。执行反馈循环——生成、运行、把错误反馈回去、重试——把模型锚定在现实之中,是你能做的单次最高杠杆的升级。把空结果集和失败的合理性检查也当作值得反馈的错误来处理,而不只是语法失败。然后给整个系统套上护栏:只读角色、关键词黑名单、语句超时、行数限制,以及硬性重试次数上限。这样做了之后,text-to-SQL 就不再是一个 demo,而是变成了基础设施。
好消息是,你不必从头手写这一切。像 Draxlr 这样的工具已经处理好了 AI 驱动的 SQL 生成、安全的只读执行、以及把结果转化为可分享的仪表盘——所以你既能得到反馈循环,也能得到护栏机制,而无需自己动手 wiring 这一切。
你们现在在技术栈里是怎么处理 AI 生成的 SQL 的——全自动执行、人工在环,还是介于两者之间?你见过的最离谱的查询是什么?在评论区丢出来;我收集这些。
Sources: ReFoRCE: A Text-to-SQL Agent with Self-Refinement, Consensus Enforcement, and Column Exploration, RetrySQL: text-to-SQL training with retry data for self-correcting query generation, Bridging Natural Language and Databases: Best Practices for LLM-Generated SQL, LLM Guardrails: Best Practices for Deploying LLM Apps Securely (Datadog).