winson-text2sql
> 一种利用大模型将自然语言自动转换为结构化查询语言(SQL)的可执行技能。
> 对标 Wren AI Cloud「自然语言转 SQL:直接说人话,自动生成精准查询」的核心能力,让任何 AI Agent 加载本 SKILL 后,就能把用户的日常提问变成跑得通、答得对的 SQL。
一、核心哲学
1.1 唯一铁律
> Text2SQL 的成功标准不是"生成了一条 SQL",而是"生成了一条能跑通、且结果答得对用户问题"的 SQL。
一条语法正确但答非所问的 SQL,比一条报错的 SQL 危害更大——因为它会带着自信的数字误导决策。所以本 SKILL 的每一个环节,都为"可执行 + 答得对"这两个终点服务。
1.2 三个底层认知
| 认知 | 说明 |
|---|---|
| 三件套不可拆 | Schema 链接 + Prompt 工程(含 Few-shot)+ 执行反馈自纠正。只靠 LLM 直出 SQL,准确率会断崖式下跌。 |
| 先链接,后生成 | 大库 Schema 塞不进上下文,必须先把"这个问题需要哪些表、哪些列"筛出来,再生成。Schema 链接是第一瓶颈,也是第一收益点。 |
| 执行反馈是底线,不是优化 | 生成后必须真跑一次。报错回灌修正;跑通但结果可疑也要复核。纯靠 LLM 自评"对不对"会降性能,验证要靠执行结果对比。 |
1.3 核心公式
用户提问
→ Phase 1 意图理解与澄清(问清楚到底要什么)
→ Phase 2 Schema 链接(提取元数据 + 筛出相关表/列)
→ Phase 3 Few-shot 检索(找相似历史问句-SQL 对)
→ Phase 4 Prompt 组装 + SQL 生成
→ Phase 5 执行验证 + 自纠正(跑不通就修,最多 max_corrections 轮)
→ Phase 6 结果解读(把数字翻译成人话)
每一环都有"输出物"和"检验标准",跳过任何一环,准确率都会掉。
1.4 全局参数
执行前先约定以下参数(未指定时取默认值):
| 参数 | 默认值 | 说明 |
|---|---|---|
dialect |
mysql |
目标 SQL 方言:mysql / postgresql / clickhouse / sqlite 等 |
max_rows |
1000 |
强制 LIMIT 行数,防止全表扫描 |
max_corrections |
3 |
Phase 5 自纠正最大轮次 |
few_shot_k |
3 |
Phase 3 检索的示例条数 |
二、完整执行流程
Phase 1:意图理解与澄清
目标:把模糊的自然语言问句,变成一个"无歧义、可查询"的精确意图。
做什么:
1. 识别查询目标(要查什么实体、什么指标、什么维度、什么时间范围)。
2. 识别隐含条件("上个月""销量最高的""活跃用户"等业务口径需要定义)。
3. 若存在歧义或缺失关键信息,必须向用户澄清,不要瞎猜。
澄清判断标准(任一命中就要问):
- 时间范围缺失或不明确("最近"是几天?)
- 业务口径未定义("活跃用户"指登录过还是下过单?)
- 排序/Top-N 缺失("销量高的"取前几?)
- 指代不明("它""这个"指什么?)
输出物:结构化意图卡。
意图卡示例:
- 查询目标:订单金额合计
- 维度:按商品类目
- 时间范围:2025 年 Q1(2025-01-01 至 2025-03-31)
- 过滤:仅已支付订单
- 排序:按金额降序,取前 10 类目
检验标准:意图卡能让一个不懂 SQL 的业务人员确认"对,我就是要这个"。
Phase 2:Schema 链接
目标:从全库 Schema 中,筛出回答该问题必需的表和列,避免上下文爆炸和幻觉字段。
做什么:
1. 提取数据库元数据(表名、列名、类型、注释、主外键关系)——方法见下方"Schema 元数据提取"。
2. 用 LLM 做相关性筛选,输出"本次查询需要用到的表 + 列 + 连接键"。
3. 大库可先用向量检索/关键词召回候选表,再做 LLM 精筛(RAG 式两阶段)。
Schema 元数据提取(具体方法)
用只读账号执行下方 SQL 取元数据,再格式化为带注释的 Schema 文本喂给 LLM。
MySQL:
-- 表与表注释
SELECT TABLE_NAME, TABLE_COMMENT
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE();
-- 列、类型、列注释
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT, COLUMN_KEY
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE();
-- 主外键关系
SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE() AND REFERENCED_TABLE_NAME IS NOT NULL;
PostgreSQL:
-- 表与注释
SELECT relname AS table_name, obj_description(oid) AS table_comment
FROM pg_class WHERE relkind = 'r';
-- 列、类型、注释
SELECT c.relname AS table_name, a.attname AS column_name,
format_type(a.atttypid, a.atttypmod) AS data_type,
col_description(a.attrelid, a.attnum) AS column_comment
FROM pg_class c JOIN pg_attribute a ON a.attrelid = c.oid
WHERE c.relkind = 'r' AND a.attnum > 0 AND NOT a.attisdropped;
-- 外键关系
SELECT conrelid::regclass AS table_name, conname,
pg_get_constraintdef(oid) AS definition
FROM pg_constraint WHERE contype = 'f';
格式化后的 Schema 文本示例(喂给 LLM 的形态):
-- orders(订单表)
id BIGINT PK -- 订单ID
order_time DATETIME -- 下单时间
amount DECIMAL -- 订单金额
status VARCHAR -- 状态:paid/unpaid
-- products(商品表)
id BIGINT PK -- 商品ID
category VARCHAR -- 商品类目
-- FK: order_items.product_id -> products.id
Schema 链接 Prompt 模板(可直接复制)
你是数据库专家。给定数据库 Schema 和用户问题,请筛选出回答该问题所需的表和列。
数据库 Schema(含表注释、列注释、主外键):
{schema}
用户问题:{question}
要求:
1. 只返回必需的表和列,无关表一律不要。
2. 给出每个表的用途和 JOIN 连接键。
3. 严格只使用 Schema 中真实存在的表名和列名。
输出 JSON:
{
"tables": [
{
"table": "表名",
"columns": ["列1","列2"],
"purpose": "为什么需要这张表",
"join_key": "与其他表连接的键,无则填 null"
}
]
}
输出物:精简后的 Linked Schema(仅含相关表/列 + 连接关系)。
检验标准:Linked Schema 里的每一个表名、列名都能在原始 Schema 中找到原文,无臆造字段。
Phase 3:Few-shot 示例检索
目标:从历史"问句-SQL"对中,检索最相似的几条,注入 Prompt,通常可提升 8~12 个百分点的执行准确率。
做什么:
1. 维护一个示例库(DDL、业务文档、历史正确 SQL 三类语料)。
2. 对用户问题做向量检索,取 top-k(参数 few_shot_k,默认 3)最相似的问句-SQL 对。
3. 若无示例库,至少手写 2~3 条"黄金示例"覆盖典型查询模式。
示例库构建要点:
- 每条示例含:自然语言问句 + 对应正确 SQL + 简短说明。
- 优先收录高频查询、复杂 JOIN、易错聚合。
- 数据库内容/敏感值不进示例库,只存结构与 SQL 模式(Vanna 式本地执行,数据不外发)。
输出物:Few-shot 示例块。
示例块示例:
Q: 查询每个部门有多少员工
SQL: SELECT dept_id, COUNT(*) AS cnt FROM employees GROUP BY dept_id;
Q: 查询 2024 年每月销售额
SQL: SELECT DATE_TRUNC('month', order_time) AS m, SUM(amount) FROM orders WHERE order_time >= '2024-01-01' AND order_time < '2025-01-01' GROUP BY m ORDER BY m;
检验标准:检索到的示例与当前问题在"查询模式"上确实相似,而非仅文本相似。
Phase 4:Prompt 组装与 SQL 生成
目标:把 Linked Schema + Few-shot + 意图卡组装成结构化 Prompt,让 LLM 生成一条高质量 SQL。
主生成 Prompt 模板(可直接复制):
你是资深 {dialect} SQL 工程师。请根据以下信息生成一条 SQL。
【相关表结构】
{linked_schema}
【参考示例】
{few_shot}
【用户意图】
{intent_card}
【原始问题】
{question}
【硬性要求】
1. 只能使用【相关表结构】中列出的表和列,禁止臆造字段。
2. JOIN 必须基于明确的连接键,禁止笛卡尔积。
3. 聚合查询注意 GROUP BY 字段完整性。
4. 只生成 SELECT 语句,禁止 INSERT/UPDATE/DELETE/DROP 等写操作。
5. 必须加 LIMIT {max_rows},防止全表扫描。
6. 时间过滤用明确日期区间,不用模糊的"最近"。
7. 只输出一条 SQL,用三个反引号 sql 包裹,不要解释。
输出物:一条候选 SQL。
检验标准:SQL 中所有标识符都在 Linked Schema 内;含 LIMIT;无写操作。
Phase 5:执行验证与自纠正
目标:把 SQL 真跑一次,跑不通就修,跑通也要复核结果合理性。这是底线环节。
做什么:
1. 用只读账号执行 SQL,捕获执行结果或报错。
2. 若报错 → 进入自纠正循环(把错误信息回灌给 LLM 修正),最多 max_corrections 轮(默认 3)。
3. 若跑通 → 做结果合理性检查(空结果?数值量级异常?列数对不对?)。
4. 复杂查询可生成多候选,按执行成功 + 置信度筛选最优(MCS-SQL 式)。
自纠正 Prompt 模板(可直接复制):
你之前生成的 SQL 执行失败,请分析原因并修正。
【原 SQL】
{sql}
【执行报错】
{error_message}
【相关表结构】
{linked_schema}
【用户意图】
{intent_card}
请:
1. 先用一句话指出错误原因(字段不存在?类型不匹配?JOIN 键错?)。
2. 输出修正后的 SQL,用三个反引号 sql 包裹。
自纠正停止条件:
- 修正后执行成功 → 进入结果复核。
- 达到 max_corrections 轮仍失败 → 如实告知用户失败原因 + 建议提供更多上下文,不要伪造结果。
结果合理性检查清单:
- [ ] 结果非空?(空结果要排查过滤条件是否过严)
- [ ] 数值量级符合业务直觉?(销售额出现负数/天文数字要警惕)
- [ ] 返回列数与意图一致?
- [ ] 行数被 LIMIT 截断?需提示用户数据可能不完整。
输出物:一条执行成功、结果合理的最终 SQL + 执行结果。
检验标准:SQL 在真实库上执行成功,且结果通过合理性检查。
Phase 6:结果解读与回答
目标:把表格数字翻译成用户能懂的自然语言结论,而非甩一堆行数据。
结果解读 Prompt 模板(可直接复制):
请根据以下信息,用简洁的中文回答用户问题。
【用户问题】
{question}
【执行的 SQL】
{sql}
【查询结果(前 {n} 行)】
{result}
要求:
1. 直接给出回答用户问题的结论,突出关键数字。
2. 给出最高/最低/趋势等洞察,而非平铺数据。
3. 若结果为空,说明可能原因并给出排查建议。
4. 若结果被 LIMIT 截断,明确提示"数据可能不完整"。
5. 不要编造结果中不存在的数字。
输出物:自然语言回答 + 可选的图表/表格呈现。
检验标准:用户无需看 SQL 也能理解结论;所有数字都能在执行结果中找到出处。
三、核心模板库(可直接复制)
| 模板 | 用途 | 位置 |
|---|---|---|
| Schema 元数据提取 SQL | 取表/列/主外键 | Phase 2 |
| Schema 链接 Prompt | 筛选相关表/列 | Phase 2 |
| 主生成 Prompt | 组装上下文生成 SQL | Phase 4 |
| 自纠正 Prompt | 报错回灌修正 | Phase 5 |
| 结果解读 Prompt | 数字翻译成人话 | Phase 6 |
| 意图卡 | 结构化澄清 | Phase 1 |
多候选生成与筛选模板(高准确率场景):
请用 3 种不同思路(如子查询 vs JOIN vs 窗口函数)生成 3 条等价 SQL,分别用三个反引号 sql 包裹并标注思路。
随后我会全部执行,按执行成功 + 结果一致 + 简洁度选出最优。
四、常见陷阱与避坑指南
| 陷阱 | 表现 | 后果 | 修正 |
|---|---|---|---|
| 跳过 Schema 链接 | 把全库 Schema 灌给 LLM | 上下文爆炸、幻觉字段、成本飙升 | 必做 Phase 2 精筛 |
| 臆造字段名 | SQL 里出现 Schema 中不存在的列 | 执行报错 | Phase 4 硬性要求"只用 Linked Schema 字段"+ Phase 5 执行兜底 |
| JOIN 错键 | 笛卡尔积或一对多膨胀 | 结果数字异常偏大 | 校验连接键基数;结果合理性检查兜底 |
| 纯 LLM 自评 | 让模型自己判断"对不对" | 反而降性能 | 验证靠执行结果对比,不靠模型空想 |
| 写操作未禁 | 生成 UPDATE/DELETE | 数据被改坏 | 只读账号 + Prompt 硬性禁止 + SQL 白名单 |
| SQL 注入 | 用户输入拼进 SQL | 安全事故 | 参数化、用户输入只进意图理解,不直接拼 SQL 文本 |
| 空结果不排查 | 过滤过严返回空 | 用户以为没数据 | Phase 5 空结果必排查并提示 |
| 无 LIMIT | 全表扫描 | 库被拖垮 | 强制 LIMIT,默认 1000 |
| 时间口径模糊 | "最近"未定义 | 结果不可复现 | Phase 1 必须澄清成明确日期区间 |
| 伪造结果 | 执行失败仍编数字 | 误导决策 | 失败如实告知,禁止编造 |
五、场景适配指南
| 场景 | 重点强调的 Phase | 说明 |
|---|---|---|
| 小库单表查询 | Phase 1-4-5 | Schema 小可省链接,但仍需执行验证 |
| 大库多表分析 | Phase 2-3-5 | Schema 链接 + Few-shot 是命脉 |
| 高频固定报表 | Phase 3-6 | 沉淀示例库,命中率最高 |
| 探索性即席查询 | Phase 1-2-5 | 澄清 + 链接 + 自纠正,容忍多轮 |
| 对外/生产环境 | Phase 5 安全项 | 只读账号、LIMIT、脱敏、审计日志必做 |
| 低准确率要求快速 demo | Phase 2-4 | 可省自纠正,但必须声明未验证 |
六、安全与权限规范
- 只读账号:执行 SQL 的数据库账号只授 SELECT 权限,从根上杜绝写操作。
- SQL 白名单:执行前校验语句以 SELECT 开头,拦截 DML/DDL。
- 强制 LIMIT:所有查询注入 LIMIT,防止全表扫描拖垮库。
- 数据不外发:数据库内容不进 Prompt,只发 Schema 结构与执行结果摘要(Vanna 式本地执行)。
- 敏感字段脱敏:结果中的身份证、手机号等在解读前脱敏。
- 审计日志:记录每次查询的问题、SQL、执行人、时间,便于追溯。
七、质量检验清单
完成一次 Text2SQL 后,逐项检查:
- [ ] Phase 1:意图卡是否经用户确认(或无疑义)?
- [ ] Phase 2:元数据是否用只读账号成功提取?Linked Schema 中所有表/列都在原始 Schema 中存在?
- [ ] Phase 3:Few-shot 示例是否与当前查询模式真相似?
- [ ] Phase 4:SQL 是否只用 Linked Schema 字段?是否含 LIMIT?无写操作?
- [ ] Phase 5:SQL 是否真实执行成功?结果是否通过合理性检查?自纠正是否在 max_corrections 轮内?
- [ ] Phase 6:回答中的数字是否都能在执行结果中找到出处?空结果是否已说明?
- [ ] 安全:是否用只读账号?是否脱敏?是否留审计日志?
八、执行原则
- 能跑通、答得对,才算成功:终点是正确结果,不是 SQL 文本。
- 先链接后生成:Schema 链接是第一收益点,不可跳。
- 执行反馈是底线:必须真跑,报错就修,不靠模型空想验证。
- 失败如实告知:跑不通就说明原因,绝不伪造结果数字。
- 安全优先:只读账号 + LIMIT + 脱敏 + 审计,生产环境不可妥协。
- 示例库是资产:持续沉淀历史正确 SQL,越用越准(Vanna 式 RAG 自学习)。
- 具体胜过抽象:每个概念都落到可复制的 Prompt 模板,而非空谈方法论。
九、快速启动示例
用户问:"查一下去年每个产品类目的销售额,取前 5。"
按本 SKILL 执行:
- 意图卡:目标=销售额合计;维度=产品类目;时间=2025 全年;排序=降序前 5;过滤=已支付订单。
- Schema 链接:先用只读账号查 information_schema 取元数据;再筛出
orders(order_time, amount, status)、order_items(order_id, product_id)、products(id, category);连接键 order_id / product_id。 - Few-shot:检索到"按月销售额""按类目统计"两条历史 SQL。
- 生成:
sql SELECT p.category, SUM(o.amount) AS total FROM orders o JOIN order_items oi ON o.id = oi.order_id JOIN products p ON oi.product_id = p.id WHERE o.status = 'paid' AND o.order_time >= '2025-01-01' AND o.order_time < '2026-01-01' GROUP BY p.category ORDER BY total DESC LIMIT 5; - 执行验证:只读账号跑通,返回 5 行,数值量级正常。
- 解读:"2025 年销售额前 5 的类目依次是:数码 320 万、家电 280 万……数码领跑,约为第 5 名的 2.3 倍。"
— 这就是一个完整的 Text2SQL 闭环。
关于作者
| 字段 | 内容 |
|---|---|
| 公众号 | @鸢飞鱼游 |
| 专栏/理念 | AI as Dasein |
| 通讯邮箱 | winson_dasein@163.com |
| 版权声明 | 本内容版权归@鸢飞鱼游所有,未经允许任何单位或个人也可使用(若有帮助,欢迎署名)、复制或以任何其他方式使用本文全部或部分。 |