一、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”的团队:把自然语问题直接变成可信查询,既解放研发,又缩短数据到决策的链路。下一步可以接入图表渲染,让”查出来”直接变成”看得到”。




