从零搭建Text-to-SQL最小闭环:用大模型将自然语言变成SQL查询

从零搭建Text-to-SQL最小闭环:用大模型将自然语言变成SQL查询 这两年大模型炒得火热可落到实际工作里真正能每天省时间的我觉得 Text-to-SQL 绝对算一个。你想想这种场景领导说“查一下上个月华东区销量前三的产品”你打开数据库客户端眯着眼看表结构、猜字段含义、拼 JOIIN运气好十分钟运气不好半小时最后还得被催。现在大模型能直接帮你把话变成 SQL你只需要确认和微调。这篇我会带你从零搭一个最小闭环建一个 SQLite 测试库、准备一张销售数据表、写一个调用大模型的 Python 脚本、再把模型生成的 SQL 拿到数据库里执行全程不超过几十行代码。我不讲花哨的架构也不堆论文里的概念。目标只有一个让你在本地把这条链路完整跑通看到“我问一句话SQL 自己出来结果也能查出来”的完整效果。适合刚接触 Text-to-SQL 的开发者、想提升取数效率的数据分析师也适合想搞清楚大模型落地边界的产品和技术同学。跑通之后你自然会知道下一步该往哪个方向优化。1. 为什么要做 Text-to-SQL先看这个需求到底解决什么1.1 一个每天都绕不开的“取数”难题数据部门或者后端开发每天最烦的事情不是写业务代码而是“被取数”。业务方不懂 SQL他们只能用自然语言描述需求你得先把需求转换成字段、表、关联关系和过滤条件再翻译成一条长长的 SELECT 语句。这个过程听着简单实际坑很多同一个指标在不同表里口径不一样日期字段一会儿是字符串一会儿是时间戳分组粒度稍微理解偏了结果就完全不同。Text-to-SQL 要解决的就是这个“翻译”成本。把“用户口语化的需求”转成“数据库能执行的结构化查询”本质上是人和数据之间的一层新接口。过去这层接口只有会 SQL 的人才能使用现在通过大模型可以让更多人直接把问题抛给数据系统。虽然现在的效果还达不到“100% 放给业务方随便问”的程度但在个人效率工具、内部 BI 对话机器人、自动化取数脚本这些场景里已经能实打实省下大量时间。1.2 大模型在 Text-to-SQL 里的角色不是“万能”而是“翻译”很多人一听到“让大模型写 SQL”就兴奋觉得以后不用学 SQL 了。我先泼盆冷水大模型在 Text-to-SQL 里扮演的核心角色是“语义到语法的翻译器”它本身并不理解你数据库里的数据质量、业务逻辑和隐藏口径。它知道的只是你喂给它的表结构、字段名、注释和示例数据。举个例子你的表里有个字段叫create_time但业务上它存的其实是“下单时间”而不是“创建时间”。模型看到字段名会猜测如果你的表结构注释没写清楚它可能写出create_time 2024-01-01看起来没毛病实际上口径完全跑偏。所以用好大模型的前提是你得把数据库的“元信息”尽可能完整地喂给它——表名、字段名、字段类型、注释、枚举值甚至代表性的几行数据。它在这个基础上做翻译才能翻得靠谱。这也是为什么我不建议一上来就去追求“模型越强越好”。在最小闭环里先搞清楚喂什么给模型、怎么约束它的输出格式远比换一个更大的模型重要。1.3 什么是最小闭环先跑通再谈优化所谓最小闭环就是“自然语言问题 - 大模型生成 SQL - 数据库执行 - 返回结果”这条链路的最短版本。它不需要 Web 界面不需要向量数据库不需要多轮对话管理甚至不需要一个能联网的在线服务。只需要一个 Python 脚本在本机就能完成。为什么强调“最小”因为 Text-to-SQL 的坑不是看出来的是跑出来的。模型生成的 SQL 第一次大概率有语法问题或者表名写错或者把日期当字符串比较。如果你一开始就搭一套复杂的系统出了问题根本不知道是提示词的问题、模型的问题还是后处理逻辑的问题。最小闭环的价值在于把整条链路的变量控制到最少让你能快速定位每一环的表现然后再一个一个环节去优化。2. 最小闭环整体设计动手前先把边界画清楚2.1 闭环的四个环节建库、取请求、生成SQL、执行回传整个最小闭环可以拆成四个清晰的部分理解这四个部分后面的代码怎么写就顺理成章了。第一是“建库”。你得有一个真实的数据库和表结构哪怕数据只有几行。没有真实可执行的环境模型生成的 SQL 就没有验证的地方。第二是“构造请求”。把你手里的数据库表结构信息、用户的问题、输出格式要求组合成一段结构化的 Prompt发给大模型。这个环节看着简单实际是整条链路里最影响效果的。第三是“生成 SQL”。大模型返回的结果不一定干净可能裹着 Markdown 代码块、可能带着解释文字、甚至可能生成两句 SQL。你需要做一次输出清洗把只属于 SQL 的部分抽出来。第四是“执行 SQL”。把清洗后的 SQL 拿到数据库里执行拿到查询结果。这里还需要考虑安全约束避免模型生成一条 DELETE 把表清了。这四个环节里第一和第四是纯工程比较稳定第二和第三是效果的关键也是后面所有调优的主战场。2.2 技术选型解析模型、数据库、编程环境怎么挑选型这块我做了一张对照表方便你根据自己的情况快速判断链路环节可选方案我的建议数据库SQLite / MySQL / PostgreSQL最小闭环用 SQLite零安装、单文件、Python 自带驱动大模型本地开源模型Qwen、DeepSeek 等/ 云端 API手头哪个方便用哪个关键是接口格式要统一调用方式OpenAI 兼容 HTTP 接口 / 各种 SDK最小闭环直接用 requests 调用 HTTP减少依赖编程语言Python首选生态完善sqlite3 直接内置运行环境Jupyter / 普通 .py 脚本我建议用普通 .py 脚本更接近真实工程先说数据库。我为什么推荐 SQLite因为“最小”两个字。MySQL 要装服务、配账号、设权限SQL Server 更重光下载安装就能劝退一半人。SQLite 是一个文件Python 自带的sqlite3库就能操作零配置跑起来特别适合验证 Text-to-SQL 这条链路。等你把链路跑通、确认效果了再换成 MySQL 或者 PostgreSQL只需要改一下连接方式其他逻辑基本复用。再说模型。这里有两类选择一类是本地部署的开源模型比如 Qwen2.5-7B-Instruct、DeepSeek-R1-Distill 系列好处是不用联网、数据不出本地、没有调用费用坏处是硬件要求高7B 模型至少也要 8GB 以上显存跑起来才流畅另一类是云端 API按量付费、效果通常更好但需要考虑数据合规问题。最小闭环阶段我建议你手头有什么用什么有 API 密钥就用云端有显卡就本地部署都没有就用一个最简单的在线 Demo 或者免费额度先跑通流程。Python 环境需要注意一下版本建议 Python 3.9 以上。整个脚本只需要用到requests和标准库的sqlite3、json、re没有复杂的依赖。2.3 为什么 SQLite 是最小闭环最合适的起点很多同学一上来就纠结“我以后要上 MySQL是不是直接用 MySQL 练比较好”。我的回答是最小闭环阶段SQLite 就是最合适的。原因有三个。第一SQLite 的 SQL 语法兼容性很好。你用它练习生成的 SQL大部分都能平滑迁移到 MySQL 或 PostgreSQL 上真正有差异的只是 LIMIT 写法、日期函数这些细节不影响理解核心链路。第二SQLite 让“执行 SQL”这个环节变得极其轻。你不需要处理连接池、账号权限、网络超时这些问题一句sqlite3.connect(test.db)就搞定。出了问题也好排查数据库就一个文件删了重建也不心疼。第三SQLite 支持设置只读模式。在最小闭环里这是个很实用的安全特性我后面会专门讲到。你可以在连接层面禁止任何写操作这样即使模型生成了一条 DELETE数据库也会直接报错不会真的把数据清掉。3. 核心细节拆解提示词与模型输出才是成败关键3.1 一套好用的系统提示词长什么样提示词是 Text-to-SQL 里性价比最高的优化点。模型还是那个模型提示词写好了生成 SQL 的准确率能明显提升提示词写得笼统再强的模型也会“自由发挥”。我实测下来一套好用的系统提示词至少包含这几部分你是一名资深 SQL 工程师。请根据给定的表结构和用户问题生成一条 SQL 查询语句。 要求 1. 只能输出 SQL 语句本身不要输出任何解释、说明或 Markdown 标记。 2. 使用 SQLite 语法。 3. 如果用户问题与表结构无关或者无法用给定表结构回答只输出一句话无法回答。 4. 只允许使用 SELECT 查询禁止生成 INSERT、UPDATE、DELETE、DROP、ALTER 等语句。 5. 如果用户问题里包含时间、日期先判断当前日期再使用合理的日期函数。 6. 字段名、表名必须严格来自给定的表结构禁止编造。这六条每一条都是在实际踩坑后加上的。第 1 条解决“模型一边写 SQL 一边解释”的问题第 3 条让模型在遇到无法回答的问题时不要硬编第 4 条从源头约束安全性第 5 条防止日期计算翻车第 6 条是防止“幻觉字段”的关键。这里我多说一句模型生成 SQL 后往往会加一句“这条 SQL 统计了每个用户的订单总额”之类的废话。如果后端直接拿整个输出当 SQL 去执行数据库就报语法错误。所以“只能输出 SQL”这句话一定要写在最前面而且要用强调的语气。3.2 表结构信息怎么喂给模型效果最好把表结构喂给模型最直接的方式就是贴建表语句。理由很简单大模型在训练时见过海量 CREATE TABLE 语句对这种格式非常熟悉理解成本最低。你不需要自己重新描述字段含义只要把真实的建表 DDL 原样贴进去。举个例子如果你的表结构是CREATE TABLE orders ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, product_name TEXT NOT NULL, amount REAL NOT NULL, order_time TEXT NOT NULL );你就直接把它放进 Prompt。模型看到order_time是 TEXT 类型就会在写日期比较时主动加引号看到amount是 REAL就会用数值计算而不是字符串拼接这套理解是模型从训练数据里学出来的。如果你的表结构有注释强烈建议一并带上。比如字段后面加一句“order_time 格式为 YYYY-MM-DD HH:MM:SS存储下单时间”模型生成的 SQL 会明显更准。如果你用的是 MySQL可以用SHOW CREATE TABLE拿到建表语句SQLite 里可以用.schema 表名或者查sqlite_master表拿到。还有一种做法是把“代表性数据”也喂给模型比如每个字段给几个示例值帮助模型理解枚举值和格式。这在字段含义不清晰的表上特别有效。不过最小闭环阶段先做“DDL 注释”就够了示例数据可以后面再加。3.3 让模型少翻车加入 few-shot 示例如果说表结构是模型翻译的“词典”那 few-shot 示例就是“示范句”。模型看到你已经给出了“问题 - SQL”的对应关系它就会模仿你的格式和风格返回结果比只给规则要稳定得多。我通常会在 Prompt 里加两个示例示例1 用户问题查询订单金额大于 100 的订单数量 对应SQLSELECT COUNT(*) FROM orders WHERE amount 100; 示例2 用户问题统计每个产品的订单总金额按金额降序排列 对应SQLSELECT product_name, SUM(amount) AS total_amount FROM orders GROUP BY product_name ORDER BY total_amount DESC;注意示例里的表名和字段名最好和你实际的表结构保持一致否则模型可能“学”到不存在的字段。示例不要贪多两到三个就够覆盖不同的 SQL 能力点比单纯堆数量更重要。这里有个小技巧示例的选择要有针对性。如果用户问题偏“聚合统计”你就放 GROUP BY 和 ORDER BY 的例子如果偏“多表关联”就放 JOIN 的例子。说直白点你想让模型生成什么样的 SQL就给它看什么样的榜样。3.4 安全边界谁也别想删库Text-to-SQL 最大的风险不是生成结果不准而是模型在自由发挥时生成了一条危险语句。虽然大模型经过对齐训练一般不会主动写 DELETE但你别指望它百分百可靠。在最小闭环里安全防线要做两层。第一层是在 Prompt 层面约束明确告诉模型“只允许 SELECT”把 INSERT、UPDATE、DELETE、DROP、ALTER 都拉进黑名单。第二层是在执行层面约束连接 SQLite 时强制只读模式。这一步代码极其简单conn sqlite3.connect(file:test.db?modero, uriTrue)这个连接方式打开数据库文件时就是只读的任何写操作都会直接抛异常。这样一来即使模型真的生成了一条危险 SQL执行到写操作时也会被数据库拦下来不会造成实际破坏。我强烈建议你在跑通最小闭环之后也把这个习惯带到所有 Text-to-SQL 项目里。另外提一句很多教程会提到“防止 SQL 注入”在 Text-to-SQL 场景里它指的是防止恶意用户用自然语言诱导模型生成攻击性 SQL。比如用户问“删除所有订单”模型如果真的生成 DELETE 就很危险。所以“只读模式 只允许 SELECT”这两条组合在一起能挡住绝大多数问题。4. 实操全过程从零跑通一个 Text-to-SQL 最小闭环4.1 准备测试库一个销售数据的建表脚本我们用一个简单的销售数据场景来走通闭环。建一个sales.db里面有一张orders表存几条订单记录。你可以在 Python 里直接建库建表import sqlite3 conn sqlite3.connect(sales.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS orders ( id INTEGER PRIMARY KEY, user_name TEXT NOT NULL, region TEXT NOT NULL, product_name TEXT NOT NULL, amount REAL NOT NULL, order_time TEXT NOT NULL ); ) orders_data [ (1, 张三, 华东, 机械键盘, 399.0, 2024-05-01 10:00:00), (2, 李四, 华北, 显示器, 1299.0, 2024-05-03 14:30:00), (3, 王五, 华东, 机械键盘, 399.0, 2024-05-05 09:15:00), (4, 张三, 华东, 鼠标, 99.0, 2024-06-01 10:00:00), (5, 赵六, 华南, 显示器, 1299.0, 2024-06-02 11:20:00), (6, 李四, 华北, 笔记本支架, 159.0, 2024-06-10 15:00:00), ] cursor.executemany( INSERT INTO orders (user_name, region, product_name, amount, order_time) VALUES (?, ?, ?, ?, ?), orders_data ) conn.commit() conn.close() print(建库完成)这里我故意把 user 信息直接放在订单表里避免一开始就引入多表关联方便你先把主链路跑通。字段注释和业务含义我建议你在真实项目里通过 DDL 注释或另附一份字段说明文档给模型不过最小闭环阶段先靠字段名本身。建完库后你可以手动查一下表结构确认数据没有问题sqlite3 sales.db .schema orders SELECT * FROM orders;4.2 写大模型调用层通用 HTTP 请求封装接下来写一个函数负责把“表结构 用户问题 系统提示词”组合成请求发给大模型然后拿到返回的 SQL 文本。我用一个兼容 OpenAI 接口格式的 HTTP 请求来做演示因为这个格式现在比较通用。你自己的模型服务只要提供兼容接口都可以直接替换 base_url 和 api_key。import requests import json API_KEY 你的API密钥 BASE_URL https://你的模型服务地址/v1/chat/completions MODEL_NAME 你的模型名称 SYSTEM_PROMPT 你是一名资深 SQL 工程师。请根据给定的表结构和用户问题生成一条 SQL 查询语句。 要求 1. 只能输出 SQL 语句本身不要输出任何解释、说明或 Markdown 标记。 2. 使用 SQLite 语法。 3. 如果用户问题与表结构无关或者无法用给定表结构回答只输出一句话无法回答。 4. 只允许使用 SELECT 查询禁止生成 INSERT、UPDATE、DELETE、DROP、ALTER 等语句。 5. 如果用户问题里包含时间、日期先判断当前日期再使用合理的日期函数。 6. 字段名、表名必须严格来自给定的表结构禁止编造。 示例1 用户问题查询订单金额大于 100 的订单数量 对应SQLSELECT COUNT(*) FROM orders WHERE amount 100; 示例2 用户问题统计每个用户的订单总金额按金额降序排列 对应SQLSELECT user_name, SUM(amount) AS total_amount FROM orders GROUP BY user_name ORDER BY total_amount DESC; def get_table_schema(): 从 SQLite 中读取建表语句 conn sqlite3.connect(sales.db) cursor conn.cursor() cursor.execute(SELECT sql FROM sqlite_master WHERE typetable AND nameorders) row cursor.fetchone() conn.close() return row[0] def generate_sql(question, schema): user_content f表结构\n{schema}\n\n用户问题{question} payload { model: MODEL_NAME, messages: [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_content} ], temperature: 0.2 } resp requests.post(BASE_URL, headers{ Authorization: fBearer {API_KEY}, Content-Type: application/json }, jsonpayload, timeout30) resp.raise_for_status() data resp.json() return data[choices][0][message][content]这里有两个细节值得注意。第一个是 temperature 参数设成了 0.2。Text-to-SQL 是强逻辑任务我们希望模型输出稳定、可复现所以 temperature 要调低。如果设成 1.0 甚至更高同一个问题每次生成的 SQL 可能都不一样这对下游执行来说是很麻烦的事。第二个是一个请求里同时包含系统提示词、示例和用户问题。系统提示词定“角色和规则”示例定“格式和风格”用户问题定“本次具体需求”。三者分开装在不同消息里比全堆在一条消息里更清晰模型也更容易理解。4.3 执行 SQL 并格式化返回结果拿到模型返回的 SQL 后不能直接丢给数据库执行需要先做一次清洗。因为模型可能在 SQL 外面包了 Markdown 代码块或者加了行号或者干脆生成了两句话。我写一个简单的清洗函数import re def clean_sql(raw_sql): 清理模型返回的 SQL 文本去掉 Markdown 标记和多余内容 if raw_sql.startswith(): raw_sql re.sub(r^(?:sql)?, , raw_sql).strip() raw_sql re.sub(r$, , raw_sql).strip() return raw_sql清洗完 SQL 后再执行查询。执行这一步我用只读模式打开数据库加一层安全保护def execute_sql(sql): 只读连接执行 SQL返回结果集 conn sqlite3.connect(file:sales.db?modero, uriTrue) try: cursor conn.cursor() cursor.execute(sql) columns [desc[0] for desc in cursor.description] rows cursor.fetchall() return columns, rows except Exception as e: return None, str(e) finally: conn.close()这个函数把列名和查询行分开返回方便打印和展示。数据库连接用uriTrue加modero从运行层面保证了“这只读库干不了写操作”。如果 SQL 里出现了模型编造的表名或字段名执行时数据库会抛错错误信息会被捕获并返回。这个错误信息在后面调试时会非常有用后面我会专门讲怎么利用它。4.4 端到端跑起来看模型生成的 SQL 和查询结果把上面几个函数串起来就是一个完整的端到端流程if __name__ __main__: schema get_table_schema() question 统计华东地区每个用户的订单总额按总额降序显示前3名 print(用户问题, question) print(- * 50) raw_sql generate_sql(question, schema) sql clean_sql(raw_sql) print(生成SQL) print(sql) print(- * 50) columns, result execute_sql(sql) if columns is None: print(执行失败, result) else: print(查询结果) for col in columns: print(col, end\t) print() for row in result: print(\t.join([str(v) for v in row]))我实测跑了一次模型对“统计华东地区每个用户的订单总额按总额降序显示前3名”这个问题生成的 SQL 大概长这样SELECT user_name, SUM(amount) AS total_amount FROM orders WHERE region 华东 GROUP BY user_name ORDER BY total_amount DESC LIMIT 3;执行结果也很符合预期张三排第一王五排第二。整个流程从提问到拿到结果我只花了半分钟不到而这拆成手工 SQL至少要三分钟起步。这就是 Text-to-SQL 带来的直观效率提升。5. 常见问题与排查技巧实录5.1 模型生成的 SQL 带着 Markdown 反引号很多模型在代码生成任务里会默认把 SQL 包在三个反引号里这是训练习惯导致的不是 bug。如果你直接执行数据库会报语法错误。我一开始就踩了这个坑排查了半天才发现是反引号的问题。解决办法就是在clean_sql里做正则替换。除了去掉反引号还要注意有些模型会在 SQL 后面追加一句解释比如“这条 SQL 将会统计每个用户的订单总金额”。这种情况下我建议在系统提示词里反复强调“只能输出 SQL”同时在后处理里加一步过滤如果文本里出现非 SQL 关键词就截断到第一个分号为止。5.2 表名/字段名瞎编乱造这是 Text-to-SQL 的经典问题模型没有见过你的表结构却写出了不存在的列名比如SELECT user_id FROM user。原因通常是表结构信息没有被模型有效理解或者 DDL 里字段名含义模糊模型只能凭猜测生成。排查思路是先把表结构打印出来人工对照看模型生成的字段名是否精确匹配。如果发现它把user_name写成username说明 DDL 里的字段名没传对或者表结构信息被截断了。另一个办法是在系统提示词里加一句硬性要求“字段名必须严格来自给定表结构禁止修改或编造”然后把表结构在用户消息里完整贴一遍。如果表字段很多一次请求放不下我建议只把相关表的 DDL 传给模型不要把所有表一股脑塞进去。表越多模型越容易迷糊。5.3 时间日期条件写错模型不知道“今天”是哪天也不知道“上个月”对应哪个时间范围。你在 Prompt 里写“查询上个月的订单”如果没有给模型一个当前日期的锚点它只能猜猜不准是常事。我在最小闭环里用的办法是在构造请求之前用程序把当前日期注入进去。比如from datetime import datetime today datetime.now().strftime(%Y-%m-%d) user_content f当前日期是{today}。\n\n表结构\n{schema}\n\n用户问题{question}这样模型就有了时间基准生成日期条件时会更准确。当然如果你的数据库时间字段是字符串还需要提醒模型用字符串比较如果是时间戳就需要用datetime()函数转换。这些细节最好在表结构注释里写清楚。5.4 多表关联时关联条件写错一旦你的测试数据从单表变成多表Text-to-SQL 的难度会立刻上升一个台阶。最常见的错误是模型拿错外键去关联比如用订单表的id关联用户表的id实际应该用订单表的user_id关联用户表的id。要解决这个问题关键在于把表之间的关联关系明确告诉模型。一个有效做法是在 DDL 里把外键关系写清楚如果用的是 SQLite可以加FOREIGN KEY声明另一个做法是在表结构描述里额外加一段“关联关系说明”例如“orders.user_id 关联 users.id”。不要指望模型从字段名里猜出正确的关联语义表多的时候猜中率真的不高。5.5 一个万能调试套路把 SQL 单独拎出来人工检查最后分享一个我常用的调试套路。当模型生成的 SQL 执行报错或者结果明显不对时我会先把这条 SQL 从上下文里拎出来单独放到数据库客户端或者命令行里执行看数据库本身报什么错。这一步看着简单但能帮你快速区分问题归属现象问题归属处理方向SQL 在数据库工具里报语法错误模型生成问题优化提示词、加 few-shot 示例SQL 能执行但结果为空条件过滤问题检查业务口径、时间范围、region 值SQL 能执行但结果数量不对逻辑问题检查 JOIN 条件、GROUP BY 粒度字段名在工具里也不存在元信息问题检查 DDL 是否传全、是否有别名这个表格我现在做项目时还会用。不要一上来就怀疑模型能力不行很多问题其实是提示词没写清楚、表结构没传全、或者下游业务口径理解错了。6. 从最小闭环到落地后续还能怎么演进6.1 多轮交互与查询修正最小闭环跑通之后你自然会发现一个限制用户说了一次问题模型给了 SQL但用户不满意想换个条件重新查怎么办这就引出了多轮交互的需求。我的建议是先做“查询修正”而不是“多轮自由对话”。用户提出新的条件系统把上一次的 SQL 和用户的修改意见一起发给模型让模型基于当前 SQL 做增量修改。比如用户说“不要华东了改成华南”模型看到上一条 SQL 里的WHERE region 华东就知道应该改成华南而不是重新生成一条完全不同的 SQL。这个机制实现起来不复杂本质上就是在用户消息里多带一段“之前的 SQL”你之前生成的 SQL 是……用户提出了新的需求……请基于原 SQL 修改。实际效果比让模型每次从头生成要稳定得多。6.2 评测集别只看个案效果如果你要让 Text-to-SQL 真正用起来一定要建评测集。不要只看一两个问题模型答对了就高兴你需要准备一套测试问题列表包含不同类型的查询聚合统计、多表关联、时间过滤、子查询、排序分页等等。评测方法也很直接把问题列表一条一条跑拿模型生成的 SQL 和你的标准答案对比。可以人工看也可以用执行结果对比来辅助判断。Spider 和 BIRD 是学术领域比较知名的 Text-to-SQL 基准数据集但它们面向的是复杂的多库多表场景最小闭环阶段不一定用得上。我建议你先用自己的业务问题建一个几十条的评测集简单有效。评测集最大的价值是每次你改提示词、换模型、加示例都能有一个客观指标告诉你“是变好了还是变差了”。没有评测集的优化都是凭感觉。6.3 工程化避坑约束解码、RAG 与日志落地到真实系统时有几个工程问题值得提前考虑。第一个是约束解码。你可以不让模型自由生成 SQL 字符串而是先让它输出一个结构化的“查询计划”再由程序拼成 SQL。另外一个思路是对模型的输出做词法校验比如用sqlparse库解析 SQL检查是否包含不允许的关键字。这些方法能大幅降低生成的失败率但实现复杂度会上升。第二个是 RAG 的引入。当数据库表很多、注释很长时把全部表结构一股脑塞给模型既不经济也影响效果。这时候可以引入检索在用户问一句之后先从表结构文档里检索最相关的几张表和字段再把检索到的元信息喂给模型。这种方案在表数量超过 50 张的系统中几乎是必经之路。第三个是日志。建议把每个问题对应的模型输出、清洗后的 SQL、执行时间、执行结果都记录下来。很多用户反馈“结果不对”其实模型生成的 SQL 没问题是数据口径问题。有了日志你才能回溯判断问题出在哪一层。我见过不少团队把 Text-to-SQL 效果不佳全归因于模型最后查日志发现八成是表结构信息没传全。我在实际跑最小闭环的过程里最大的体会是Text-to-SQL 的难度并不只在模型算法更多在你怎么把业务语义清晰地传递给模型。表结构注释写得清楚few-shot 示例选得准安全边界扎得牢这条链路远比大多数教程看起来要可靠。如果你照着上面的步骤搭起来了建议你马上拿自己工作里最常查的几个问题去试。第一次跑通全流程的感觉还是挺爽的。