ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

大模型+Text2SQL:让自然语言直接查数据库的落地实践

2026/9/13 8:39:53 拓冰建站 浏览量
大模型+Text2SQL:让自然语言直接查数据库的落地实践 做数据的人八成都有过这种经历业务方发来一句“帮我看看上周北京市区卖得最好的手机型号TOP10”你不能直接拿去查库得先把它翻译成一段又臭又长的SQL。我自己就在这种翻译上浪费过大量时间直到把Text2SQL和大模型组合起来让自然语言直接查数据库才真正把这部分精力省下来。这篇想聊的就是这件事Text2SQL怎么工作、怎么快速搭一套能用的、会踩哪些坑。Text2SQL 说白了就是一句话把“人话”翻译成 SQL。它的价值不在技术炫技而在把取数这个高频动作从“求人”变成“自助”。以前业务方想看数流程是“问数-理解-写SQL-跑数-核对-回传”一来一回几十分钟起步现在把大模型架在数据库前面业务方直接问系统给出 SQL 并执行返回一次问答几秒钟。适合谁用适合所有需要经常取数、又被重复性 SQL 淹没的人——数据开发、数据分析师也包括那些不想天天看 SQL 但需要快速拿数的业务负责人。不过事情没有看起来那么简单。真要把 Text2SQL 落地到业务系统你会遇到不少技术细节上的坎。我从一个实际项目出发把整体设计、选型、实操步骤和排坑过程完整拆给你看。1. 先搞明白Text2SQL 到底在解决什么问题1.1 为什么自然语言查库比想象中难很多人第一次接触 Text2SQL 会觉得不就是把用户输入的问题拼到 Prompt 里让大模型返回一段 SQL 吗大模型写 SQL 不是挺厉害的这个直觉对了一半。如果是一张表、一个简单的筛选GPT 类模型确实随手就能写对但只要数据库一复杂问题就来了。以我们项目里的电商库为例三张表覆盖了用户、商品、订单。业务方问“每个城市贡献了多少销售额”这句人话里隐藏着三个技术判断销售额要算订单的 amount 字段需要把 orders 和 users 做关联才能拿到城市关联键是 user_id。模型必须同时完成“语义理解”“表关系推理”和“字段定位”三个动作任何一个错了SQL 都是错的。更麻烦的是自然语言有歧义。“上个月卖了多少手机”里的“手机”可能指商品名称字段里的商品也可能指品类字段值模型得先判断口径而“北京的用户”和“用户地址包含北京”又是两种不同的过滤方式。这些语义分歧在传统 SQL 开发中靠人来对齐在 Text2SQL 里却需要靠提示词、示例和数据库元数据来约束。1.2 四个躲不开的技术难点我把实际开发中遇到的核心难点归纳成四个后面所有设计其实都是在和这四件事死磕。第一是表关联的自动发现。数据库里只要有外键或逻辑关联模型就需要自行判断什么时候 join、join 哪张表、用哪个键。遇到大宽表还好一旦拆成几十张业务表遗漏 join 条件就是家常便饭。第二是值的匹配。用户说“北京”数据库里存的可能是“北京市”或者“北京城区”。模型看不到数据库里到底有哪些枚举值就会生成city 北京这种看起来正确、实际查不到数据的 SQL。这个坑最隐蔽因为 SQL 语法没错、执行也不报错就是结果为空。第三是幻觉。大模型会一本正经地生成一个数据库中根本不存在的列名比如把product_name写成products.name看似合理实际没有这个字段。这个问题在 schema 庞大、模型没吃透表结构时特别严重。第四是计算口径。日期筛选是重灾区“最近7天”到底是按自然日算还是按滚动时间算“本月”和“上个月”的边界是什么这些规则如果不在提示词里写明模型每次都靠猜生成的 SQL 三天两头变口径。理解了这四个难点再看 Text2SQL 的架构方案就清楚多了所有技术选型都是围绕“让模型更准地定位字段、生成 join、匹配值、遵守业务口径”来展开的。2. 三条主流路线Prompt、微调还是 Agent2.1 路线一纯 Prompt 少样本上线最快这个方案最直接把数据库的建表语句、字段注释、几个示例问题拼到系统提示词里用户问题来了直接让模型生成 SQL。优点是零训练成本、改提示词就能调整行为、大模型的能力能直接借用。缺点也很明显当数据库超过二十张表全量 DDL 塞进上下文既占 token又容易让模型注意力涣散复杂查询尤其多表多条件时生成结果不稳定。少样本示例确实能救一些场景但示例不可能覆盖所有问题形态。适用场景是业务表少、查询模式相对固定、想一周内上线验证的场景。2.2 路线二微调开源模型可控但成本高有些团队担心调用外部大模型的成本或者有私有化要求会选这条路。用 Spider、BIRD 这类公开 Text2SQL 数据集再叠加自己业务的查询日志对开源模型做 LoRA 微调让模型学会“你这个库的 schema 长什么样、你常用的查询写法是什么”。微调的好处是模型行为可控、延迟更低、不依赖外网接口。代价是你要有高质量训练数据要维护训练和部署链路而且业务表结构一变化模型往往要重新迭代。我见过不少团队在这条路上走一半就退了他们发现标注 SQL 样本比写 SQL 本身还耗时数据量不够微调出来不如直接好好调 Prompt 效果好。如果你的团队没有专门的算法工程师我的建议是先不要碰微调把精力放在路线一或路线三上。微调是锦上添花不是雪中送炭。2.3 路线三Agent Schema Linking解决大库和复杂查询这是目前我认为最值得投入的方案也是很多成熟产品的底层思路。它不再要求大模型一次性生成最终 SQL而是把任务拆成多步先判断这个问题涉及哪些表和字段再查一下相关字段的枚举值然后生成 SQL执行如果报错就把错误信息回传给模型让它自己修正。这里有两个关键组件。一个是 Schema Linking当数据库有几百张表时不可能把全部表结构都丢给模型所以要先用检索把“和这个问题相关的表和列”捞出来拼成一个子集 schema 再交给模型。另一个是工具调用模型可以主动调用“查询表的 distinct 值”的工具把用户问题里的“北京”和数据库里的“北京市”对上再决定 SQL 怎么写。这个路线能解决前两个难点但链路长、调试复杂、每一次问答可能产生多次模型调用成本比纯 Prompt 高不少。适合数据库规模大、查询复杂、需要生产级稳定性的场景。2.4 三条路线怎么选一张表说清楚路线上线速度成本稳定性适用场景纯 Prompt 少样本最快低中低表少、查询简单、快速验证微调开源模型慢高中高需持续维护私有化、高并发、查询模式固定Agent Schema Linking中等较高高大库、多表、复杂查询、生产级实际项目中三条路线不是互斥的。我现在的推荐组合是主体用 Agent Schema Linking 保证准确率同时把高频问题沉淀成 few-shot 示例再叠加一些规则校验。刚开始不用追求完美先用最小可用版本跑起来再逐步迭代。3. 从零搭一个能用的 Text2SQL 查询助手3.1 准备一个可控的实验环境讲方案不如直接上手。我以 MySQL 8.0 为例设计一个简单的电商库三张表贯穿全文用户表、商品表、订单表。建表语句如下。CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50), city VARCHAR(50), created_at DATETIME ); CREATE TABLE products ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100), category VARCHAR(50), price DECIMAL(10,2), created_at DATETIME ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT, product_id INT, amount DECIMAL(10,2), status ENUM(paid, pending, cancelled), created_at DATETIME, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (product_id) REFERENCES products(id) );这里我把外键关系写得很明确是因为模型生成 SQL 时非常依赖这种结构信息。如果你的数据库建得比较随意没有外键建议至少在提示词里补充逻辑关联关系否则模型 join 全靠猜。连接数据库的账号一定要单独创建一个只读账号后面安全环节我会细说这里先提前做业务查询只允许 SELECT。CREATE USER text2sql% IDENTIFIED BY your_password; GRANT SELECT ON shop.* TO text2sql%;3.2 设计可靠的 Prompt 模板Prompt 是 Text2SQL 的第一个关键点。我第一次做的时候写得很随意就一句“你是SQL专家请回答问题”结果模型生成的 SQL 五花八门有的带 markdown 代码块有的擅自加注释有的不限制返回行数。后来我把系统提示词结构化效果立刻不一样。下面这个模板建议直接参考你是电商数据库的 SQL 专家。数据库方言是 MySQL。 表结构 {ddl} 查询规则 1. 只输出一条 SQL不要使用 markdown 代码块不要额外解释。 2. 禁止 SELECT *所有字段必须显式写出。 3. 除 SELECT 外禁止任何其他 SQL 操作。 4. 结果默认 LIMIT 200除非用户明确要求。 5. 外键关系orders.user_id - users.idorders.product_id - products.id。 6. 日期口径用户说“最近7天”使用 created_at DATE_SUB(NOW(), INTERVAL 7 DAY)用户说“本月”使用 created_at DATE_FORMAT(NOW(), %Y-%m-01)。 参考示例 问题各城市的付费用户数 SQL: SELECT u.city, COUNT(DISTINCT o.user_id) FROM orders o JOIN users u ON o.user_id u.id WHERE o.status paid GROUP BY u.city; 问题{question} SQL:这个模板里每一条都有存在的理由。规则 1 解决输出格式问题规则 2 避免大字段拖垮查询规则 5 是给模型明确 join 路径规则 6 是统一口径。你写模板的时候要把你们业务里最容易出错的规则加进去比如“退款订单不参与计算”“金额单位是元还是分”这些业务约束模型不可能自己知道。3.3 把 DDL 自动变成 Schema 信息上面模板里的 {ddl} 不能每次手写我们要从数据库自动读取。用 INFORMATION_SCHEMA 可以把建表语句拼出来。SELECT CONCAT(CREATE TABLE , TABLE_NAME, (\n, GROUP_CONCAT( CONCAT( , COLUMN_NAME, , COLUMN_TYPE, IF(IS_NULLABLE NO, NOT NULL, ), IF(COLUMN_COMMENT ! , CONCAT( COMMENT \, COLUMN_COMMENT, \), )), \n ), \n);) AS ddl FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA shop GROUP BY TABLE_NAME;这里有个非常实用的点字段注释不要省。比如user_id加上“下单用户ID对应 users.id”这样的注释模型生成的 join 准确率会明显提升。字段注释其实就是你在给模型“开小灶”它看不到你业务里怎么约定俗成地定义这些列全靠你写在建表语句或提示词里。如果你的数据库字段注释已经很完整把这段查询结果直接拼进 Prompt 就行如果不完整建议至少给核心表的字段补充注释。这里的投入产出比极高比换更贵的大模型划算得多。3.4 写代码把链路串起来我以 OpenAI SDK 的方式写一个最小可用版本方便你直接改造成自己的服务。整条链路就三步组装 Prompt、调用模型、执行 SQL 并校验。import openai client openai.OpenAI( api_keyyour_api_key, base_urlyour_base_url # 如果是国内服务或私有部署就改成对应地址 ) DDL CREATE TABLE users (...); CREATE TABLE products (...); CREATE TABLE orders (...); SYSTEM_PROMPT 你是电商数据库的 SQL 专家。数据库方言是 MySQL。 表结构 {ddl} 查询规则 1. 只输出一条 SQL不要使用 markdown 代码块不要额外解释。 2. 禁止 SELECT *。 3. 除 SELECT 外禁止任何其他 SQL 操作。 4. 结果默认 LIMIT 200。 5. 外键关系orders.user_id - users.idorders.product_id - products.id。 def generate_sql(question: str, error_msg: str ) - str: user_content f问题{question}\nSQL: if error_msg: user_content f你上一次生成的 SQL 执行报错错误信息{error_msg}\n请根据错误信息重写 SQL。\n问题{question}\nSQL: resp client.chat.completions.create( modelgpt-4o-mini, temperature0, messages[ {role: system, content: SYSTEM_PROMPT.format(ddlDDL)}, {role: user, content: user_content} ] ) return resp.choices[0].message.content.strip()这里有个关键参数temperature 必须设为 0。同样的问题模型如果带随机性两次生成的 SQL 可能一个对、一个错这在取数系统里完全不可接受。你就是要它稳定、可复现。接下来是执行和校验。要注意不能让模型生成的 SQL 直接裸奔在你的业务库里执行。import pymysql import re BLOCK_KEYWORDS [INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, TRUNCATE] def is_safe_sql(sql: str) - bool: 只允许单条 SELECT 语句。 sql_stripped sql.strip().rstrip(;) first_keyword sql_stripped.split()[0].upper() if sql_stripped.split() else if first_keyword ! SELECT: return False if sql_stripped.upper().count(;) 0: return False for kw in BLOCK_KEYWORDS: if re.search(r\b kw r\b, sql_stripped.upper()): return False return True def execute_sql(sql: str): conn pymysql.connect( hostlocalhost, usertext2sql, passwordyour_password, databaseshop, read_timeout10, write_timeout10 ) try: with conn.cursor() as cur: cur.execute(sql) cols [d[0] for d in cur.description] rows cur.fetchall() return cols, rows finally: conn.close()这个is_safe_sql是安全兜底的第一道防线。我见过有人把模型生成的 SQL 直接拿去执行结果模型有一次在 Prompt 里看到“删除”两个字就给生成了 DELETE幸好当时没有自动提交否则数据就没了。这类问题用权限控制加关键字过滤能挡掉绝大多数。最后把整个流程包一层带上执行报错重试MAX_RETRIES 2 def text2sql(question: str): error_msg for _ in range(MAX_RETRIES 1): sql generate_sql(question, error_msg) if not is_safe_sql(sql): error_msg SQL 不符合安全规则请只输出 SELECT 查询语句 continue try: cols, rows execute_sql(sql) return sql, cols, rows except Exception as e: error_msg str(e) return sql, [], []重试机制非常值得做。模型生成 SQL 第一次偶发报错的概率其实不低比如把函数名写错、把字符串引号写漏把错误信息回传给模型让它自己改往往第二次就对了这个操作能把整体成功率提升不少。3.5 再进一步少样本示例和值匹配如果纯 Prompt 做下来发现准确率还差一些优先加少样本示例就是 few-shot。注意示例要“对症”你发现模型经常 join 错就加两条 join 相关的示例经常把日期口径搞错就加日期筛选的示例。示例数量不用多每个类型三到五条就够多了反而会把模型带偏。值匹配问题也需要处理。我的经验是做一个简单的“问题改写”前置模块在把问题发给模型之前先对问题里的实体词和数据库里的枚举值做模糊匹配。比如问题里出现“北京”去users.city的 distinct 值里找发现库里存的是“北京市”就把问题改写成“北京市”。这样模型生成city 北京市的概率就大得多。def normalize_entity(question: str, column: str, thresholds: float 0.6) - str: # 从数据库取该列的 distinct 值缓存到内存 # 用 difflib 或简单包含关系做匹配 # 如果命中把问题里的词替换成库里的标准值 return question这一步做完了Text2SQL 的基础链路就完整了自然语言进来经过实体标准化进入带模板和示例的 Prompt模型生成 SQL安全校验后执行失败自动重试。能把这套流程跑通已经超过了绝大多数只会在网页里问一嘴“帮我写个SQL”的玩具 demo。4. 准确率怎么评估、安全怎么兜底4.1 评估不能靠感觉得用执行准确率说话Text2SQL 项目最容易被质疑的就是“到底准不准”。不能用感觉得量化。学术界常用的评估指标有三个Execution AccuracyEX指生成的 SQL 执行结果和标准 SQL 执行结果是否一致Exact Set MatchEM是预测结果和标准结果在集合层面是否完全相等更严格Valid Efficiency ScoreVES额外考虑了执行效率的评分。现实中我建议你更关注 EX也就是执行结果一致率。业务方只关心查出来的数据对不对SQL 写得好看不好看不重要。评判方法是自己维护一个小规模测试集把业务里的常见问题类型分成单表筛选、多表 join、聚合分组、日期区间、模糊匹配几类每类放十条左右问题人工准备好标准 SQL然后跑一遍系统对比执行结果。这个测试集规模不用很大六七十条足够发现问题。建议每次系统改动都跑一遍比如改了 Prompt、加了示例、换了模型都要回归。我在开发中至少遇到三次“觉得改好了实际准确率掉了一大截”的情况都是通过回归测试发现的。语气要说明的是公开数据集上的成绩只能做参考不能直接对标你的业务效果。Spider 和 BIRD 这些基准测试面对的是标准 schema 和干净问题真实业务里的问题口语化、有歧义、值也不规范准确率打个七折甚至一半都很正常所以一定要建自己的评测集用真实环境里的真实问题来测。4.2 安全兜底的六条底线Text2SQL 让大模型直连数据库安全必须当成第一优先级。我总结了六条底线缺一不可。第一条是数据库账号只给只读权限。大模型生成 SQL 不可控权限越低越好。我在实验环境里专门建了只读账号任何 DDL、DML 都会被 MySQL 直接拒绝这比在应用层做关键字过滤更可靠。第二条是应用层做 SQL 白名单校验。即使有只读权限也要拦截 DELETE 这类危险语句因为如果真的执行了哪怕被数据库拒绝也会产生告警和安全隐患。前面代码里的is_safe_sql就是干这个的。第三条是强制超时。数据库查询一旦写得不好可能跑几分钟不返回。我建议设置 read_timeout一般十秒内还查不出来的 SQL业务方也等不了直接终止把错误信息回传给模型让它重构。第四条是结果集行数限制。默认给生成的 SQL 追加 LIMIT 200防止一次拉几十万行。注意如果 SQL 已经带 limit就不要强行拼接也要处理一下。第五条是敏感字段脱敏。如果表里有手机号、身份证这类字段要么在最外层查询时过滤掉要么经过脱敏函数处理后再返回不要让模型生成的任意 SELECT *本来也禁止了或可选列把敏感数据带出来。第六条是审计日志。每条问题、生成的 SQL、执行耗时、返回行数、由哪个用户发起全部记录下来。万一出了事能回溯也能作为后续优化训练集的数据来源。4.3 线上落地还要关注的三个隐性指标准确率和安全之外生产上线还需要考虑另外三个容易被忽略的点。第一个是响应延迟。如果每个问题都走“模型生成 SQL → 执行 → 可能报错 → 再生成”整体耗时可能到十几秒。业务方不会等那么久。我的做法是把高频、简单问题做成模板直出或者用相似问题命中缓存只有复杂问题才走完整链路。第二个是成本。大模型按 token 计费一次问答可能消耗几千 token高频用户会带来不小的费用。我一般会加上用量统计设置单用户每日配额同时把 Agent 链路里的中间调用尽量精简能用规则解决的就不让模型重复调用。第三个是口径沉淀。业务方每次常问的问题其实可以抽成“口径文档”。比如“销售额就是 paid 状态的订单 amount 之和”把这个文档作为附加内容定期更新到系统提示词里比每次让模型去猜靠谱得多。这个动作做久了准确率会越来越高prompt 也会越来越厚其实就是在把业务知识存入系统。5. 实战高频踩坑与排查速查表5.1 我在实际项目中遇到的六个典型问题第一个是模型生成的列名不存在。排查思路很简单看是不是 DDL 没有完整传给模型。我在做三十张表的库时遇到过表一多prompt 里的 DDL 被截断模型看不到后面的表就开始瞎编。后来改成 Schema Linking只把相关表结构传给模型这个问题大幅减少。第二个是 join 条件漏掉或写错。最常见的是模型不加条件直接笛卡尔积或者把orders.user_id写成orders.id。这个要在提示词里明确写外键关系并且每次建表都补好字段注释。少样本示例里加一条多表 join 的查询也能把准确率拉上来。第三个是值匹配不上。用户说“北京”库里是“北京市”SQL 查出来为空。我前面说的实体标准化就是为了解决这个问题。再强调一遍这个坑非常隐蔽因为它不报错只是结果为空业务方会觉得你系统“全是 bug”。第四个是日期口径不稳定。同一个“最近30天”昨天生成的 SQL 用的是DATE_SUB(NOW(), INTERVAL 30 DAY)今天生成的可能是DATE_ADD(CURDATE(), INTERVAL -30 DAY)语法都对结果可能差一天。解决方式是业务自己定口径明确写进提示词并且用 few-shot 固定住。第五个是模型被用户问题里的“脏话”带偏。用户问“把订单表删了行不行”模型有可能真的生成 DELETE。虽然权限挡得住但体验很糟糕。可以在系统提示词里加一句“当前是只读查询环境任何非 SELECT 意图都请返回提示而不是生成 SQL”。这句话能有效减少这类情况。第六个是执行性能差。模型生成的 SQL 没有走索引或者在大表上做全表扫描虽然结果正确但查询时间不能接受。这个可以在提示词里写“优先使用索引字段作为过滤条件避免在 WHERE 中对列做函数运算”还可以用执行计划分析来做后置检查对耗时异常的查询记录并优化。5.2 排查问题的一套路子如果你跑出来的 SQL 不对先别急着改提示词按下面的顺序走一遍八成能定位到问题。先看执行结果是否为空。为空大概率是值匹配问题去库里查一下实际值把标准值映射到问题里。再看 SQL 结构是否正确。如果列名和 join 条件看着不对劲把传给模型的 DDL 打出来确认模型确实能看到这些表和字段。模型没看到的信息它不可能凭空知道只会靠编。再看是不是口径问题。同一个问题换一天问结果不一样通常就是日期口径没写死或者模型被示例带偏了。最后看是不是少样本干扰。有时候新增的示例本身就有问题反而把模型带偏。可以先去掉所有示例改回纯模板看准确率是升是降再逐条加回来。5.3 几个不太容易被发现的技巧文本里最后分享几个一般人我不太会讲的细节。给表加“使用说明”注释。除了字段注释我在建表语句里会写类似“这份订单表只包含已创建订单不包括售后记录”这种备注。模型看到这些说明对“订单数”“销售额”口径的判断会准很多。这个效果出乎意料地好因为它直接消解了口径歧义。system prompt 里的规则要按优先级排。把最不可容忍的规则放最前面比如“只允许 SELECT”放在第一条把容易变化的业务口径放后面。模型对靠前的指令遵循度更高把安全规则放前面等于多了一道隐式保障。利用 SQL 里的GROUP BY和函数命名来反向约束模型风格。如果你发现模型总生成LEFT JOIN而不是INNER JOIN可以在示例里统一展示你期望的 join 风格。模型会无意识模仿示例的风格这既是风险也是可控的调教手段。最后给模型连接数据库的账号名字不要叫root权限也要单独收敛。我不是说大模型一定会作恶而是在生产环境里所有不可控输入都必须默认有攻击性。哪怕只是账号名改成text2sql_ro都能提醒所有人这个账号是只读用途降低误操作风险。我自己在项目里摸索了大半年最深的感受是Text2SQL 的瓶颈从来不是大模型能不能写 SQL而是你能不能把数据库的元数据、业务口径和值映射变成模型看得懂的信息。模型是个执行力很强的翻译官但它对你这个业务一无所知。数据域边界想清楚、元数据做好、口径规则沉淀成提示词这套系统的准确率就会一步步往上走。如果让我给后来者一句建议那就是先圈定一个业务域做深把十来张表和核心问题吃透再考虑横向扩展。|