Text2SQL 实战:用大模型把自然语言转成可靠 SQL

一、Text2SQL 是什么,又不是什么

Text2SQL(也称 NL2SQL,自然语言转 SQL)是指让大模型把人的一句业务问话,比如”上个月上海用户下单金额超过 1000 的订单有哪些”,直接翻译成一条可执行的 SQL 查询。它不是把数据库交给模型随意操作,而是把”理解意图”这一步交给大模型,把”真正执行”牢牢攥在应用手里。本文聚焦工程落地:如何写出稳定、可控、不会乱删库的 Text2SQL 流水线。

很多团队一上来就让模型”连上数据库自己查”,结果模型顺手补了一句 UPDATE 或 DROP。正确的边界是:模型只负责生成文本形式的 SELECT 语句,执行、取值、权限全部由你的服务控制。下面从单表 prompt 讲到多表 RAG,再给一份生产检查清单。

二、最小可用:单表查询的提示词工程

先别上框架。把数据库 schema 原样塞进 prompt,要求模型只输出一条 PostgreSQL 的 SELECT,不要解释、不要 markdown 包裹,是成本最低、最可控的方案。schema 越精确,生成越准。

SCHEMA = """
表 orders(id INT, user_id INT, amount DECIMAL, status VARCHAR, created_at TIMESTAMP)
表 users(id INT, name VARCHAR, city VARCHAR)
"""

def build_prompt(question: str) -> str:
    return f"""你是一个 SQL 生成器。只输出一条可执行的 PostgreSQL SELECT 语句,
不要解释、不要使用 markdown 代码块。

数据库 schema:
{SCHEMA}

问题: {question}
SQL:
"""

关键点有两个:temperature=0 让输出稳定可复现;明确要求”只输出 SQL”,避免模型多嘴。PostgreSQL 的字段类型与函数写法可参考《PostgreSQL 实战:与 SQL Server 的核心差异》,能少踩很多语法坑。

三、调通接口:原生调用 OpenAI 兼容端点

本地可用 Ollama、vLLM,云端可用任意 OpenAI 兼容接口。下面用原生 requests 调用,不依赖重框架,方便你塞进现有服务。注意模型有时会多套一层 ```sql,取回后要做清洗。

import requests, os

def nl_to_sql(question: str) -> str:
    resp = requests.post(
        "https://api.your-llm.com/v1/chat/completions",
        headers={"Authorization": f"Bearer {os.environ['LLM_KEY']}"},
        json={
            "model": "your-sql-model",
            "messages": [{"role": "user", "content": build_prompt(question)}],
            "temperature": 0,
        },
        timeout=30,
    )
    sql = resp.json()["choices"][0]["message"]["content"].strip()
    # 去掉模型偶尔多输出的 ```sql 包裹与前缀
    return sql.strip("`").removeprefix("sql").strip()

四、进阶:多表关联与 schema 约束

当表超过十几张,把所有 DDL 全塞进 prompt 会撑爆上下文,模型也更容易”幻觉”出不存在的字段。这时要缩小范围:要么按业务域拆分多个小而专的 prompt,要么进入下一节的向量检索思路。多表 join 务必在 prompt 里给出外键关系,否则模型常把 orders.user_id = users.id 写成错列。

生成后的 SQL,上线前建议用 《PostgreSQL 慢查询优化实战》里提到的 EXPLAIN ANALYZE 先跑一遍执行计划,避免一条烂 SQL 把库拖垮。复杂聚合(如窗口函数排名)也可对照《SQL 窗口函数实战》校验写法。

五、用向量检索做”表/字段发现”(RAG 思路)

大库场景下,把每张表的 DDL 切片做 embedding,用户提问时先召回最相关的几张表,再拼进 prompt。这就是 Vanna 这类工具的核心思路,能显著降低幻觉率。下面是实现骨架:

# 把每张表的 DDL 切片后 embedding,召回相关表再拼进 prompt
def retrieve_tables(question: str, top_k: int = 3) -> str:
    q_vec = embed(question)
    hits = vector_search(q_vec, table_index, top_k)
    return "\n".join(h["ddl"] for h in hits)

# 召回结果替换掉 build_prompt 里的固定 SCHEMA
prompt = build_prompt(question).replace(SCHEMA, retrieve_tables(question))

六、主流方案横向对比

方案适用场景优点主要风险
纯提示词工程单库小表、内部 demo零依赖、完全可控schema 大时易错字段
LangChain SQL Agent多表 join、快速验证自动选表、能迭代易生成危险写操作
Vanna / 自建 RAG大库多表生产召回准、可沉淀知识需维护向量库与索引
微调小模型高频固定问法延迟低、成本低训练与迭代成本高

七、生产落地:防注入与权限边界

Text2SQL 上生产,安全是第一位。永远不要让模型生成具体查询值——值由应用层提供,模型只输出带占位符的语句,用参数化查询执行,既防注入又防越权。

# 让模型只生成带占位符的语句,真实值由应用填(psycopg 风格)
sql = "SELECT * FROM orders WHERE city = $1 AND created_at >= $2"
params = [city, start_date]
cur.execute(sql, params)   # 参数化,杜绝 SQL 注入

# 执行前强制白名单校验:只允许 SELECT
import re
if not re.match(r"^select\s", sql, re.IGNORECASE):
    raise PermissionError("仅允许只读查询")

建议搭配只读账号运行,并在 VS Code 里用 SQL 插件先把生成结果人工跑通再接入自动化。CI 侧可用 GitHub Actions 对核心问法做回归测试,模型一升级就跑一遍黄金 SQL 集合。

八、上线前检查清单

检查项推荐做法
只允许 SELECT执行前正则校验 ^select\s,拒绝一切写操作
值参数化模型输出占位符,应用层填值,杜绝拼接
账号最小化独立只读账号,必要时加行级权限
审计日志记录”问题 / 生成 SQL / 用户 / 耗时”
失败兜底解析不出就返回”暂不支持”,绝不瞎编字段

九、把查询结果直接变成图表

Text2SQL 的终点是”人能看懂”。生成 SQL 执行拿到结果后,用几行代码就能出图,让业务方不必自己跑查询。下面用 Python 把”各城市高额订单”直接画成柱状图:

import matplotlib.pyplot as plt
import pandas as pd

df = pd.read_sql(sql, conn)          # sql 即模型生成的 SELECT 语句
top = (df.groupby("city")["amount"]
         .sum().sort_values(ascending=False).head(10))
plt.bar(top.index, top.values)
plt.title("各城市订单总额 Top10")
plt.xticks(rotation=45)
plt.tight_layout(); plt.savefig("orders_by_city.png")

十、常见失败模式排查

上线后最常见的几类问题:字段名幻觉(模型用了不存在的列)、类型不匹配(把 VARCHAR 当日期比较)、join 列写错、以及响应超时。排查时先打印模型原始输出,再对照真实 DDL 逐列核对;多表场景优先确认外键关系是否正确。

现象可能原因对策
报错 column does not exist模型幻觉字段prompt 附真实 DDL,或上向量召回缩小范围
查询结果为空类型 / 时区不匹配显式 CAST,统一时区后再比
查询极慢缺索引、全表扫描EXPLAIN 看执行计划,按需加索引
接口超时模型或库响应慢加 timeout,失败降级到固定模板

十一、小结

Text2SQL 的价值不在”让模型连库”,而在把意图理解自动化、把执行权留在应用侧。从小表纯提示词起步,表多了上向量召回,上生产时守住”只读 + 参数化 + 审计”三条线,就能把一个炫酷 demo 变成靠谱的内部查询入口。它尤其适合运营、财务、产品这类”会提需求但不写 SQL”的团队:把自然语问题直接变成可信查询,既解放研发,又缩短数据到决策的链路。下一步可以接入图表渲染,让”查出来”直接变成”看得到”。

上一篇 RocketMQ 实战:消息发送、消费与高可用架构
下一篇 HikariCP 连接池调优实战:从连接泄漏到科学配置
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]