Text-to-SQL 实战:用大模型把自然语言转成查询语句

Text-to-SQL(也常写作 NL2SQL,即 Natural Language to SQL)是指把人类用自然语言提出的问题,自动转换为可在数据库上执行的 SQL 查询语句。它让不懂 SQL 的业务人员也能直接「问数据」,是增强型数据分析、智能问数与生成式 BI(GBI)的核心能力之一。本文从原理、难点、提示工程到可运行代码,带你完整落地一个 Text-to-SQL 流程。

什么是 Text-to-SQL 与典型场景

Text-to-SQL 的输入是一段自然语言问题与一份数据库 schema(表结构),输出是一条符合该数据库方言的 SQL。典型落地场景包括:

  • 自助数据分析:运营或产品用中文提问,例如「上海的用户上个月下了多少笔订单」,系统直接返回 SQL 与结果。
  • 智能问数助手:嵌入企业 IM 或数据平台,把对话转为查询,自动出图、出报表。
  • 低代码报表搭建:根据描述生成查询雏形,工程师再做微调,缩短开发周期。
  • 数据校验与探索:用自然语言快速验证假设,例如「哪些商品的退货率超过 5%」。

需要说明的是,Text-to-SQL 不等于「让模型随便编一条 SQL」。生产环境要求生成的语句可解析、可执行、且语义与用户意图一致,因此提示工程与执行校验必不可少。

核心难点

Schema 理解

模型必须准确理解表名、字段名、字段类型与主外键关系。现实数据库的字段名常常语义模糊,例如 flagtypeext,模型若不了解其含义,极易写出错误条件。把表结构的 DDL 与字段注释一起提供给模型,是缓解该问题的基础手段。

语义歧义

同一个问题可能有多种合理解读。例如「最近活跃的用户」,模型需要判断:是按最后一次登录时间,还是按近 30 天有下单行为?「消费高」是指总金额高,还是客单价高?这类歧义需要靠约束说明、少样本示例,或在业务层做澄清来收敛。

多表关联与嵌套查询

跨表 JOIN、子查询、聚合分组是准确率下降的主要来源。模型容易漏写 JOIN 条件、用错关联方向,或在 GROUP BYHAVING 上出错。复杂问题建议拆成多步推理:先确定涉及哪些表,再确定关联键,最后生成聚合逻辑。

提示工程方法

要让大模型稳定输出正确 SQL,关键在于把「上下文」喂足。一个实用的提示模板通常包含以下四部分。

表结构 DDL

直接贴出相关表的建表语句,让模型知道有哪些表、有哪些列、类型与约束是什么。

字段注释

在 DDL 之后补充业务语义,尤其是易混淆字段。例如注明 orders.status 的取值为 paid / pending / refunded

少样本示例

给出 3 到 5 条「问题到 SQL」的配对示例,示范你期望的写法风格(是否用别名、是否限定方言、是否避免 SELECT *)。少样本能显著提升风格一致性与复杂查询的正确率。

约束说明

明确硬性规则,例如:只输出一条 SQL,不要解释;使用 SQLite 方言;禁止 SELECT *;时间字段为文本格式 YYYY-MM-DD,需用字符串比较。约束越具体,输出越可控。

下面是一段提示拼接示例,可直接用于中文场景:

你是 SQL 生成助手。请只输出一条可执行的 SQL,不要解释,使用 SQLite 方言。

数据库表结构:
CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  city TEXT,
  created_at TEXT
);
CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  user_id INTEGER,
  amount REAL,
  status TEXT,
  created_at TEXT
);

字段说明:
orders.status 取值为 paid(已支付)、pending(待支付)、refunded(已退款)。

示例:
问:每个城市有多少用户?
SQL:SELECT city, COUNT(*) FROM users GROUP BY city;

问:消费金额最高的前 5 位用户是谁?
SQL:SELECT u.name, SUM(o.amount) AS total
     FROM users u JOIN orders o ON u.id = o.user_id
     GROUP BY u.id ORDER BY total DESC LIMIT 5;

问题:上海的用户总共下了多少笔订单?
SQL:

Python 调用示例

下面给出两段可运行代码:第一段负责调用大模型生成 SQL,第二段负责在本地校验并执行,确保不会因为一条非法 SQL 直接报错崩溃。

生成 SQL 的代码

import os
from openai import OpenAI

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

SCHEMA = """
CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  city TEXT,
  created_at TEXT
);
CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  user_id INTEGER,
  amount REAL,
  status TEXT,
  created_at TEXT
);
"""

FEW_SHOT = """
问:每个城市有多少用户?
SQL:SELECT city, COUNT(*) FROM users GROUP BY city;

问:消费金额最高的前 5 位用户是谁?
SQL:SELECT u.name, SUM(o.amount) AS total
     FROM users u JOIN orders o ON u.id = o.user_id
     GROUP BY u.id ORDER BY total DESC LIMIT 5;
"""

def generate_sql(question: str) -> str:
    prompt = f"""你是 SQL 生成助手。请只输出一条 SQL,不要解释,使用 SQLite 方言。
数据库表结构如下:
{SCHEMA}

示例:
{FEW_SHOT}

问题:{question}
SQL:"""
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
    )
    return resp.choices[0].message.content.strip()

if __name__ == "__main__":
    sql = generate_sql("上海的用户总共下了多少笔订单?")
    print(sql)

校验与执行的代码

import sqlite3

def safe_execute(sql: str, db_path: str = "demo.db"):
    conn = sqlite3.connect(db_path)
    try:
        cur = conn.cursor()
        cur.execute(sql)
        return cur.fetchall()
    except sqlite3.Error as e:
        return f"执行失败:{e}"
    finally:
        conn.close()

def validate_syntax(sql: str, db_path: str = "demo.db") -> bool:
    conn = sqlite3.connect(db_path)
    try:
        conn.execute("EXPLAIN " + sql)
        return True
    except sqlite3.Error:
        return False
    finally:
        conn.close()

if __name__ == "__main__":
    sql = "SELECT COUNT(*) FROM orders o JOIN users u ON o.user_id = u.id WHERE u.city = '上海'"
    if validate_syntax(sql):
        print(safe_execute(sql))
    else:
        print("SQL 语法不通过,已拦截。")

通过先 EXPLAIN 再做真正执行,可以在不改动数据的前提下拦截大部分语法错误,是生产环境的基本防线。

主流方案对比

生态中既有开箱即用的开源框架,也有云厂商提供的托管能力,可按隐私、成本与可控性取舍。

开源框架

  • DB-GPT:由蚂蚁集团发起的开源 AI 原生数据应用框架,内置 Text2SQL 优化、RAG、多智能体与 AWEL 工作流编排,并提供 DB-GPT-Hub 做有监督微调。适合需要私有化部署、与自有数据深度结合的场景。
  • SQLCoder:由 Defog AI 开源的专用 SQL 生成大模型家族,基于 StarCoder 系列微调,官方评测在 sql-eval 框架上超过 gpt-3.5-turbo,可自托管,适合对数据不出域有强要求的团队。

云服务与通用大模型

OpenAI、Anthropic、Google 等通用大模型本身具备不错的 Text-to-SQL 能力,配合完善的提示工程即可快速验证。优势是省去运维,劣势是数据需要出域、按调用量计费,且对私有库结构需要每次携带 schema。

选型建议

数据敏感、需长期沉淀,优先开源自托管(DB-GPT + SQLCoder);只想快速做内部原型,先用通用大模型加提示工程;若 query 高度固定,可考虑用 DB-GPT-Hub 在自有 schema 上微调一个小模型,兼顾成本与准确率。

准确性提升技巧

  • 精简 schema 上下文:只把问题相关的表与字段放进提示,表越多越容易误导模型。
  • 提供字段取值样例:对枚举类字段,列出 status 的可能值,避免模型猜值。
  • 约束方言与习惯:固定使用某种 SQL 方言(如 SQLite 或 PostgreSQL),并禁止 SELECT *、要求使用参数化思路。
  • 引入执行反馈闭环:把执行报错或空结果回灌给模型,让它自检重试一次,往往能修掉明显的关联或聚合错误。
  • 复杂问题分步推理:让模型先输出涉及的表与关联键,再生成最终 SQL,比一步到位更稳。
  • 用基准持续评估:以 Spider 等公开数据集为标尺,量化每次提示或模型切换带来的准确率变化。

小结

Text-to-SQL 把自然语言查询变成可执行的 SQL,是降低数据使用门槛的关键能力。它的落地重点不在「调通一次调用」,而在稳定的提示工程与执行校验:用 DDL 加字段注释把 schema 讲清楚,用少样本与约束把输出管住,再用 EXPLAIN 与异常回灌把风险兜住。对于追求私有化与高准确率的团队,DB-GPT 与 SQLCoder 等开源方案值得优先评估。

参考与延伸阅读

  1. Yu Tao 等人,Spider:Text-to-SQL 与跨领域语义解析的大规模人工标注数据集,EMNLP 2018,arXiv:1809.08887。
  2. Xue Siqiao 等人,DB-GPT:用私有大模型强化数据库交互,arXiv:2312.17449。
  3. DB-GPT 开源项目(蚂蚁集团发起,含 Text2SQL 与 DB-GPT-Hub),GitHub:https://github.com/eosphoros-ai/DB-GPT
  4. SQLCoder 开源项目(Defog AI,专用 SQL 生成大模型家族),GitHub:https://github.com/defog-ai/sqlcoder
  5. Defog 团队发布 SQLCoder 与 sql-eval 评估框架的说明,https://defog.ai/blog/open-sourcing-sqlcoder
本文累计阅读