Text-to-SQL 的工程挑战:Agent 怎么把自然语言转成准确的 SQL 查询

我第一次搭 Text-to-SQL 系统时,被一个看似简单的问题卡住了:"查一下最近 30 天每天的退款率趋势"。

AI technology illustration

打开数据库,我在退款相关的字段里绕了半天:orders 表有个 refund_amount,一张独立的 refunds 表记录每笔退款明细,order_items 表里又有个 item_status 字段标着 'refunded'。到底哪个才算'退款'?退款率按笔数算还是按金额算?这事让我想明白了一件事:Text-to-SQL 的难点从来不是把自然语言翻译成 SQL 语法,而是把用户有歧义的话,对齐到数据库里唯一的那几个字段

直出 SQL 方案为什么会在真实业务里翻车

最朴素的落地方式很好理解:用户问题 + 建表语句 + 几条示例,丢给大模型,让它直接生成 SQL。在小库上效果好极了,一上真实业务就露馅。原因有三个。

  • 字段名对齐基本靠猜。用户说'销售额',库里叫 turnover_amount,更离谱的是叫 fld_2041 的。字段注释?老系统里大量'状态1'级别的注释,等于没写。
  • Schema 大得塞不进 prompt。真实业务库几百张表不夸张,每张表几十个字段,全量建表语句轻松超过 8 万 token。只塞一部分?那得先知道该塞哪几张表——这本身就是一个 Text-to-SQL 问题。
  • 没有反馈闭环。SQL 生成后,你只能验证语法和字段名是否存在,没有任何机制告诉模型'JOIN 应该用 LEFT 而不是 INNER'。

即便在偏学术的 Spider benchmark 上,顶尖模型的准确率也远没到'放心用'的程度;真实业务库里 schema 更脏、问题更随意,表现只会更差。所以工程界的共识逐渐收敛:与其让模型一步到位,不如让它跑一个'感知-行动-反馈'的循环。这就是 Agent 方案。但在讲 Agent 之前,得先把'难'这件事拆开。

把一句话变成 SQL,中间隔着三层鸿沟

我后来想通的是:LLM 早就精通 SQL 语法了,它真正缺的是数据库语义——你的表怎么设计的、字段怎么命名的、业务规则藏在哪。具体来说,自然语言和 SQL 之间隔着三层鸿沟。

第一层:命名方式不同。同一个概念,口头说法和数据库字段名经常对不上。'客户'可能对应 customer 表,也可能对应 user 表。这一层靠字符串相似度加字段注释映射能缓解,但注释缺失就无从谈起。

第二层:聚合逻辑缺失。'退款率'不是一个字段,是两个数相除;'同比'需要找到去年同期;'增长最快'得先算增长率再排序。用户的话天然带着计算指令,数据库里没有现成的列。这一层是要推导的,不是查出来的。

第三层:业务规则根本不在库里。'有效订单'是什么?支付成功、未被取消、且不是测试单。这条规则可能散落在十几个微服务的代码分支里,schema 里没有任何字段能直接表达。不把它写进 prompt,模型永远猜不到。

这三层鸿沟决定了:直接生成 SQL 的准确率,本质上取决于你的数据库有多'干净'。

Agent 的解法:不求一步到位,只求错了能改

Agent 方案的核心思想一句话:不给模型一步到位的压力,给它一个可以反复试错的闭环。典型的 Text-to-SQL Agent 工作流长这样:

  1. Schema Linking——把用户问题里的实体和意图,锚定到候选表和候选字段。工程上一般用'关键词匹配 + embedding 相似度 + LLM 判定'的混合策略,单纯靠哪一招都可能漏。
  2. Schema 裁剪——只把最相关的 top-N 张表和字段放进 prompt,解决'8 万 token 塞不下'的问题。裁剪有一个原则:拿不准就多塞一张表,裁错了后面全错。
  3. 示例检索——从历史查询里挑语义最接近的几条示例。这一步很容易被忽略,但影响极大。DAIL-SQL 这篇论文的核心结论就是:示例要挑得准,而不是求多;语义相似的示例能让模型模仿它见过的写法,而不是每次从零开始猜。
  4. 生成与执行——模型在裁剪后的 schema + 示例 + 上下文中生成 SQL,然后在只读副本上真实执行。
  5. 反馈修正——执行报错?把错误信息原样塞回给模型,让它改。这个循环最多跑两三轮。

反馈修正一步是 Agent 方案提升最明显的地方,原因很简单:数据库是最严格的编译器。字段名写错、表名不存在、JOIN 条件类型不匹配,一条明确的报错信息甩回去,模型基本都能改对。语法级错误在 Agent 方案里被大量消化掉了。

另外,好的 Agent 还应该懂得'反问'。遇到明显歧义时(比如退款率到底按金额还是笔数算),与其赌一边,不如主动向用户澄清。这在学术 benchmark 里会被判错,因为 benchmark 只有一个标准答案;但在真实产品里,这恰恰是最好的用户体验。

最隐蔽的坑:SQL 能跑通,不代表结果是对的

但请停在这里想一个问题:执行反馈只对你'看得见的错误'有效。如果 SQL 语法正确、字段名存在、执行成功返回了几行数字呢?没有任何报错需要反馈。模型怎么知道这几行数字是对的还是错的?它不知道。

一个真实场景:需求是'最近 30 天每天的退款率'。模型写了 WHERE refund_date >= CURDATE() - INTERVAL 30 DAY,语法对、字段对、执行成功。但你的业务里退款率应该按支付时间归类,而不是退款发起时间。Agent 看到了执行结果,但它拿什么判断'结果不对'?它没有对照答案。

另一个高频错误:应该 LEFT JOIN 保住没退款的订单,结果写成了 INNER JOIN,没退款的订单被全部过滤掉。返回了结果,行数少了,但不报错。Agent 不会去数该有多少行。

模型在修正循环里做的事情,本质上是'根据有限反馈猜答案'。反馈里没有正确答案,它永远无法验证自己有没有猜对。

我把这个现象叫作'无监督修正'——能修它看得见的错,看不见的错永远留在那。

三个绕不过去的工程现实

除了上面的语义盲区,真实落地还有三个坎。

坎一:字段注释不等于字段语义。'状态'注释下,1 代表已支付还是已退款?不在 schema 里。很多团队花大力气治理 schema——补注释、规范命名——效果比任何 prompt 技巧都大。这不是讽刺,这是最现实的经验。

坎二:修正循环会收益递减。实测到第 3 轮以后,模型开始出现'把对的改成错的'——拿着模糊的执行结果过度推理。工程上一般限制最多 2-3 轮修正,再多反而有害。

坎三:成本是线性的,而且很贵。每轮循环都要把 schema、示例、历史对话重新传给模型。跑一个完整的 3 轮循环,token 消耗是直出方案的 5-10 倍,延迟也跟着涨。对实时性敏感的产品,这是一个真实的权衡。

用一张表总结两种方案的差距:

维度 一次性生成 Agent 方案
字段名对齐 靠模型猜 Schema Linking 显式映射
Schema 过大 塞不下 / 噪声大 裁剪后聚焦
语法错误修正 执行反馈高效修正
语义错误发现 无(执行成功不等于正确)
Token 成本 3-10 倍
适用场景 简单 schema / 原型 复杂 schema / 生产环境

最后说点真话

Text-to-SQL 的瓶颈从来不在'生成'——模型生成 SQL 的能力早就够了。瓶颈在文本到数据库之间的那层语义对齐。Agent 把'猜一次'变成'猜很多次再验证',这是一个工程上的巨大进步,但它没有改变'猜'的本质。

什么时候值得上 Agent 方案?字段命名规整、注释齐全、表关系清晰的库,它能让你像跟一个懂业务的 DBA 对话一样查数。什么时候别急着上?字段叫 fld_2041、status 没有枚举含义、业务规则埋在代码里的库——先做数据治理,把手动注释补上、关系理清楚,这比任何模型都管用。另外,想看一个方案的成色,别只信演示。拿你自己的表、你自己的脏数据、你自己的业务问题,跑一个能接受的准确率再决定。毕竟,工程的世界里,没有魔法。

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

(0)
上一篇 2天前
下一篇 2天前

相关推荐