☰
RAG+Text2SQL:让业务人员用自然语言直接查数据库的完整方案
2026/10/20 15:22:01 网站建设 项目流程

把数据库交到大模型手里,让业务同事直接问一句“上个月华东区的退货率跟华南区比怎么样”,系统就能自己查出结果——这件事听起来很性感,但它恰恰是 RAG 落地时最容易翻车的一环。数据是结构化的、答案却是自然语言下的临时追问,这中间隔着的不是简单的“让模型写 SQL”,而是一整套需要认真设计的链路:schema 怎么召回、上下文怎么塞、SQL 怎么生成、结果怎么回述。这篇文章就围绕 RAG 落地里的 Text2SQL,从原理讲到我在真实项目里一步步搭出来的方案,把那些文档里不会写的坑也一起摊开。

1. 先想清楚一个问题:Text2SQL 到底解决的是谁的什么问题

很多团队一听说“让非技术用户对话结构化数据”,第一反应是“这不就是给数据库加个聊天框吗”。真去做了才发现,难点根本不在聊天框,而在后面那一整套不知道该放哪儿的逻辑。

1.1 业务用户真正想要的东西,不是 SQL

业务侧的人不关心你用的是 JOIN 还是子查询,也不在乎数据在哪个库、表里叫 order_info 还是 t_order_detail。他们脑子里想的是业务问题:这个月新客的复购率怎么样、哪个渠道的客单价跌了、某款商品在哪些地区的库存周转最慢。这些问题一旦落到 SQL,就牵扯到表结构、字段命名、时间口径、去重规则。非技术用户不会写 SQL,也不该让他们写 SQL,他们需要的是一个能“听懂业务说法”的中间层。

这个中间层要解决两件事:一是把自然语言翻译成可执行的查询,二是把查询结果翻译回人能看懂的表述。只有第一点做到的叫“Text-to-Query”,两点都做到的才是真正能上线的“对话式数据分析”。

1.2 为什么 RAG 是这条路上的关键

有人会问:大模型本身就会写 SQL,直接问它不行吗?不行。原因很朴素:模型没看过你的数据库,它不知道你有哪些表、每张表有哪些字段,更不知道 order_status=3 到底代表“已退款”还是“已发货”。如果不把数据库结构信息喂给模型,它写出来的 SQL 只能靠猜,大概率是“看起来像那么回事、一执行就报错”。

RAG 在这里起的作用,是把“知识”从模型参数里搬到了“检索”这一步。模型不必提前记住你的 schema,而是每次提问时,系统从数据库元数据里检索出相关的表和字段,拼到提示词里,让模型基于“看得见的真实结构”来生成 SQL。这样既解决了 schema 的更新问题,也解决了上下文窗口有限的问题,还让每一步都有迹可循。

1.3 适合的落地场景和可能的过度设计

这类系统最适合的场景有几个共性:数据口径相对稳定、库表数量多但单次查询只涉及少量表、用户愿意接受“自然语言出结果,人工核对口径”。比如运营看板、内部经营分析、客服问询、财务核对这类内部工具,都是典型的落地场景。

反过来,如果你的库只有两三张表、查询模式固定、用户也不太需要自由追问,那老老实实做几个筛选器下拉框就够了,不需要硬上 Text2SQL。我见过不少团队为了“智能化”而智能化,最后做了一个性能一般、准确率还不如固定报表的系统。工具是为人服务的,不是用来表演的。

2. 原理拆解:从自然语言到 SQL 的整条链路

Text2SQL 虽然听起来只是一个“模型生成 SQL”的过程,真正落地时至少要拆成五段:问题理解、schema 召回、SQL 生成、SQL 执行、结果解释。每一段都可能有独立的问题,需要单独设计。

2.1 为什么不能直接让大模型裸写 SQL

直接把问题丢给模型,让模型写 SQL,偶尔能蒙对,但生产环境不能靠“偶尔”。裸写的问题在于:

  • 模型不知道表名和字段名的真实写法,容易自己编造列名,比如把 user_id 写成 customer_id。
  • 模型不知道业务口径,比如“在途订单”可能对应 status IN (2,3),而不是简单等于某个值。
  • 模型不知道库里的枚举值和字段含义,例如 gender=1 是男还是女,不同项目的定义可能完全相反。
  • 一旦表很多,模型会把不相关的表也 Join 进来,产生笛卡尔积或者性能极差的查询。

所以必须在提示词里给足上下文,而且要给的恰恰好,不是把所有表都塞进去,而是把与当前问题相关的表、字段、注释、枚举值先筛出来,再交给模型。这一步就是 RAG 里最核心的“检索增强”环节。

2.2 Schema 信息的召回与注入

所谓 schema 召回,本质上是把“问题文本”和“数据库元数据字段描述”做语义匹配。我的做法是把每个字段都拼成一条带业务含义的文本描述,比如:

表: order_info 字段: status 类型: int 说明: 订单状态,1-待支付,2-已支付,3-已发货,4-已完成,5-已取消

把这些描述向量化,存进向量库。用户提问时,把问题也向量化,做 Top-K 检索,找出最相关的表和字段,再把这些 schema 片段作为上下文交给大模型。这里有个小经验:不要把整张表的所有字段都召回,很多字段与当前问题无关,召回太多反而让模型“选择困难”,生成 SQL 的准确率会下降。

召回之后,还需要做两件事:一是把字段之间的关联关系(主外键)也带上,否则模型面对多表查询时不知道该怎么 Join;二是把常用的查询条件、口径描述提前维护成“业务规则”,比如“近30天”统一换算成日期字段的过滤条件。这些规则是沉淀出来的业务资产,价值不比模型本身低。

2.3 SQL 生成、执行与结果返回

模型拿到 schema 和业务规则后,生成的不是最终 SQL,而是一个“候选 SQL”。这个过程我强烈建议做两遍确认:

  • 语法校验:至少做一次解析,防止模型输出残缺 SQL。可以直接交给数据库执行前先用 EXPLAIN 跑一遍,很多语法错误在这一步就暴露了。
  • 安全校验:检查 SQL 不能有 INSERT、UPDATE、DELETE、DROP 等非查询语句,尽量只允许 SELECT。即使内部工具,也应该白名单化。

执行之后还有一道关卡:结果返回。模型生成 SQL,数据库返回的是表格,但用户问的是“哪个渠道客单价跌了”,你要让模型基于查询结果生成自然语言摘要。这一步可以单独调一个“结果解释”的 prompt,避免直接把一堆数字丢给用户。

2.4 一条完整链路示例

我拿一个常见场景举例:

用户提问:“上个月华东区各渠道的客单价环比变化是多少?”

系统处理过程大致是:

  1. 召回:检索到region、channel、amount、month等字段,关联到订单表和渠道表。
  2. 注入:把这两张表的 schema、字段注释、以及“上个月”“环比”的业务口径定义拼入 prompt。
  3. 生成:模型输出类似SELECT channel, SUM(amount)/COUNT(DISTINCT order_id) ...的 SQL。
  4. 执行:语法校验通过后,在只读账号下执行。
  5. 解释:模型根据返回结果,生成一段文字:“华东区各渠道客单价环比大多下降,其中线下渠道降幅最大,为12%。”

这一步一步看下来,Text2SQL 的难点就清晰了:不是单点能力,而是整条链路的配合。

3. 落地前的关键决策:技术选型与架构取舍

很多人一上来就写代码,结果做到一半发现方向错了。我先说几个必须在动手前想明白的决策点。

3.1 两种 RAG 形态怎么选:一次性注入 vs 渐进式检索

第一种是“把所有 schema 都塞进 prompt”。适合小项目、表数量少(比如少于20张)的场景。优点是简单,缺点是上下文很快被吃满,而且字段一多模型容易晕。第二种是用向量检索做“渐进式召回”,先把表名这一层筛出来,再根据候选表去召回字段。适合表数量多、业务复杂的生产环境。我实际做下来,表超过30张或者单表字段超过40个之后,一次性注入的准确率会肉眼可见地下降,建议直接上渐进式召回。

另外,schema 信息的向量化质量很关键。直接用字段名做向量,“order_time”和“下单时间”是匹配不上的。要在 embedding 之前,把字段注释、枚举值说明、业务别名全部拼接成一段完整描述。你有多少业务沉淀,就决定了召回上限。

3.2 Prompt 模板设计里藏着 80% 的准确率

Text2SQL 的 prompt 不能写得太随意。我见过最简单的模板:“你是数据库专家,根据 schema 写 SQL”,效果非常差。要写清楚以下几点:

  • 角色设定:说明你是“企业数据分析助手”,只回答与数据有关的问题。
  • 数据库结构:列出召回后的表、字段、注释、类型。
  • 业务口径:把常用的过滤条件、指标定义写清楚,比如“客单价=成交金额/成交用户数”。
  • 输出格式:要求模型输出 JSON 包住的 SQL,比如{"sql": "..."},方便程序解析。
  • 限制条件:只允许查询语句,不执行修改操作;如果问题不明确,先输出澄清问题而不是直接猜。

这里有个小技巧:把示例也写进 prompt,特别是“相同问题不同说法”的 few-shot 示例。模型会模仿示例的表达方式,生成结果会稳定很多。

3.3 权限与安全:这条线不能省

结构化数据往往比文档更敏感,跑 Text2SQL 时权限控制要前置。我的建议是:

  • 数据库账号:单独开一个只读账号,权限最小化。
  • 行级权限:业务上不同角色能看的数据范围不一样,这个最好在 SQL 生成之后、执行之前,由程序在 WHERE 条件上强制拼接部门或角色过滤,而不是依赖模型自觉。
  • 敏感字段:某些字段(如个人手机号)应该在 schema 召回时就过滤掉,不暴露给模型,也不用在结果中返回。
  • 限流:防止有人用自然语言把整个库拖出来查,导致数据库压力过大。

权限不是“可选项”,而是“上线前置条件”。我见过内部工具因为没加行级权限,一个普通用户问出了别的部门的数据,那个场面很尴尬。

4. 从零搭一套可用的 Text2SQL 服务

下面是我在项目里实际用过的搭建路径,不涉及具体框架依赖,只讲通用做法,你用任何主流语言和组件都能复现。

4.1 准备数据与 Schema 元数据

首先把库里的表结构整理成“元数据层”。可以直接读系统表,也可以手工维护一份 JSON。我推荐手工维护一份“补充了业务描述”的元数据,因为数据库里的注释往往不够业务化。

比如有一张表sales_order,手工维护的元数据会长这样:

{ "table": "sales_order", "table_comment": "销售订单主表", "primary_key": "id", "fields": [ {"name": "id", "type": "bigint", "comment": "订单唯一ID"}, {"name": "order_no", "type": "varchar", "comment": "订单编号,业务唯一"}, {"name": "channel", "type": "varchar", "comment": "渠道,enum: online/offline/partner"}, {"name": "region", "type": "varchar", "comment": "大区,如华东、华南"}, {"name": "amount", "type": "decimal", "comment": "订单金额,单位元"}, {"name": "user_id", "type": "bigint", "comment": "下单用户ID"}, {"name": "order_status", "type": "int", "comment": "1待支付 2已支付 3已发货 4已完成 5已取消"}, {"name": "created_at", "type": "datetime", "comment": "下单时间"} ], "relations": [ {"from": "sales_order.user_id", "to": "user.id", "comment": "订单表关联用户表"} ] }

这份 JSON 的内容,决定了后面的召回质量,值得花时间好好维护。字段注释写得好,胜过大模型调参。

4.2 召回层实现

召回层不需要太复杂,核心工作是把上面的元数据“拍平”成可检索的文本块。我习惯按“表”作为基本块,每个块包含表名、表注释、字段列表、字段注释和关系说明。然后对每个块做 embedding。

当用户提问进来,先对问题做 embedding,再用余弦相似度或者向量库自带的检索能力取 Top-K 个表。K 不要太大,我一般取 3-5 个。取回来之后,还要做一次“字段级过滤”:有些表本身相关,但字段太多,我只保留与问题关键词匹配的字段,比如问题里出现了“渠道”,就把channel、channel_name这类字段保留,其他字段丢掉。

4.3 模型接入与 SQL 生成

这一步就是把召回结果拼进 prompt,调用模型。我的伪代码如下:

def generate_sql(question, schema_context, rules): prompt = f""" 你是一个数据分析助手。用户会问一个业务问题,请根据给定的表结构和业务规则,生成一条 SQL 查询语句。 表结构如下: {schema_context} 业务规则如下: {rules} 用户问题:{question} 请只输出 JSON,格式: {{"sql": "生成的SQL语句", "reason": "简要说明你的查询思路"}} """ resp = llm_chat(prompt, temperature=0) # 解析 JSON return resp["sql"]

这里有个容易踩的坑:temperature一定要调低,否则同一个问题两次生成的 SQL 可能完全不同。我直接设成 0,不让模型发挥“创造性”。

4.4 执行层与结果解释

SQL 生成后,先做静态检查。我常用的检查有:

  • 是否只包含 SELECT。
  • 是否出现;之后的第二条语句。
  • 是否包含系统表、敏感字段。
  • 是否在允许的表白名单内。

检查通过之后再执行。执行时设置超时时间,我一般设 10 秒,超过就中断,避免复杂联表查询把库拖垮。

拿到结果后,让模型做“结果解释”。这个 prompt 和生成 SQL 的 prompt 要分开:把 SQL、查询结果和用户问题一起给模型,让它生成自然语言回答。这个步骤很妙,因为模型能看到真实的数字,回答就不会空对空。

4.5 前后端交互封装

最后是交互层。一个朴素但好用的形态是:聊天界面 + 追问上下文。用户每问一句,系统把上一次生成的 SQL 和结果摘要也存起来,下一次提问时作为上下文带入。否则用户问完“上个月华东区各渠道的客单价”再问“那环比呢”,系统根本不知道“那”指的是什么。

上下文管理不需要做太重的记忆,只需要维护最近 2-3 轮的关键信息就够了。太多历史反而会让模型分心。

5. 实战踩坑记录:这些问题我在项目里都遇到过

理论讲再多,不如真踩坑。下面这些问题都是我在实际项目中撞过的,每一个都值得提前预防。

5.1 召回不到正确的列名

最开始我直接拿字段名做向量化,“channel”这种词还能匹配上,但“客单价”这种业务词就完全找不到对应字段。我排查后发现,元数据里根本没写“客单价”的定义。后来把所有指标口径都补进了字段描述,召回率才明显上来。记住:召回的是“语义”,不是“字符串”,字段描述里没有的词,系统永远答不上来。

另外,业务里大量使用同义词,比如“下单”和“成交”、“用户”和“客户”。建议在元数据里手动维护同义词典,或者在 embedding 前做一次查询改写,把“客户”改成“user_id/customer_id”等实际字段词,召回效果会好很多。

5.2 大模型漏掉过滤条件

模型写 SQL 时经常只按用户的字面问题写,漏掉一些隐含的过滤条件。比如用户问“各渠道的销售额”,模型可能忘了排除“已取消”的订单。这不是模型笨,而是你的业务规则没有喂到位。

我的解决方法是把“全局默认过滤”直接写死在执行层:不管模型生成的 SQL 是什么,程序都会在 WHERE 条件里自动追加order_status != 5这种硬条件。这比反复调 prompt 可靠得多,因为它是强制性的,不依赖模型状态。

5.3 多表 JOIN 的语义难题

一旦涉及多张表,模型的 JOIN 条件就经常写错。比如订单表和用户表,模型可能用order.user_id = user.id,也可能会用order.user_name = user.name,后者在数据上可能不唯一,导致结果翻倍。

我踩过之后做的改进是:在元数据里明确声明“表间关系”,并且在 prompt 里直接给出一组“标准 JOIN 写法”。这样模型就不是瞎猜,而是从预设的选项里选。我在项目里维护了一张“关系模板表”,把常用 JOIN 路径全部写好,模型只需要选择,不需要发明。

5.4 执行安全与性能

上线初期,我遇到过有人问“所有用户的所有订单明细”,结果系统真生成了一条全表扫描的 SQL,跑了快两分钟,把数据库 IO 打满了。后来我加了三道保险:只读账号、超时限制、以及限制返回行数。在 SQL 外层包一层SELECT * FROM (你的SQL) t LIMIT 1000,既保住了正确性,又防止了内存爆炸。

还有一个容易被忽略的问题:并发。多个用户同时提问,数据库连接池可能会被占满。我给 Text2SQL 服务加了一个简单的信号量,控制同时执行的 SQL 数量,比如最多 5 个,其余排队。这比事后优化 SQL 更快见效。

6. 可复用的小技巧与后续扩展

到了这一步,你已经有一版能跑的 Text2SQL 服务了。但离“好用”还差几个细节,我挑五个最划算的优化方向。

6.1 五个让系统更稳的工程细节

  • 给每个 SQL 生成结果打上“置信度标签”。模型能明确找到表和字段时,置信度就高;需要猜测时,置信度就低,让用户看到提示“此结果为模型推断,请人工核对口径”。这能少挨很多骂。
  • 把常见问题做成缓存。运营天天问的那十几个问题,SQL 基本是固定的,直接走缓存,又快又省成本。我甚至会把高频问题的 SQL 固化成“预置查询”,不经过模型。
  • 建立反馈闭环。用户对回答点“赞”或“踩”,把踩过的“问题-错误SQL-正确SQL”收集起来,定期补充到 few-shot 示例里。模型不会自己进步,但你的示例库会。
  • 监控成本。每轮问答消耗多少 token、执行时间多少,都要统计。文本量一大,成本上涨很吓人,需要定期清理 prompt 里的冗余信息。
  • 日志要留全。包括用户问题、召回的 schema、生成的 SQL、执行结果、模型解释,全部落表。出了问题可以复盘,也能用来生成训练数据。

6.2 从 Text2SQL 继续延伸

Text2SQL 跑通之后,你会发现这个架构还能继续长出很多东西。比如同样的 schema 召回和 prompt 拼接逻辑,可以支持“生成图表”的需求,让模型输出一段图表配置,前端直接渲染成折线图。也可以做“异常原因分析”,用户问完“为什么销售额降了”,系统自动做多维拆分,找出主要贡献因素。

我个人在实际操作中的体会是,Text2SQL 的价值不在于“让模型写 SQL 写得多漂亮”,而在于你借此建立了一套“用自然语言管数据”的基础设施。它可能不如搜索引擎项目那么宏大,但一旦跑通,业务团队自己就能查数,数据团队能省下大量取数的时间,去做更深的分析。最后说一个很多人忽略的点:这类系统最宝贵的资产不是模型,而是你日复一日维护的那份 schema 元数据和业务规则。它越完整,系统越聪明。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询