Text-to-SQL AI 数据分析实战:构建安全可靠的自然语言查询数据库管道
为什么 Text-to-SQL AI 数据分析值得认真对待
让业务人员直接用中文问数据库"上个季度华东区 GMV 比同期高多少",这个需求存在已久,但真正能投入生产的方案一直很稀缺。原因不复杂:早期模型生成的 SQL 准确率在复杂 join 场景下不足 50%,而一旦出错,轻则返回错误结果,重则跑出全表扫描拖垮线上库。
现在情况不同了。GPT-5.4、Claude Sonnet 4.6 在标准 Spider benchmark 上的 execution accuracy 已经超过 87%,处理多表关联和窗口函数的能力大幅提升。更重要的是,工程侧有了更成熟的方法论:把模型能力和安全护栏分开处理,各司其职。
这篇文章拆解一套可以真正上生产的 Text-to-SQL 管道,核心三层:schema 注入、SQL 校验、权限控制。
第一层:schema 注入——让模型看到它需要看到的
模型生成 SQL 的质量,70% 取决于它拿到的 schema 信息质量。把整个数据库的 DDL 全部塞进 prompt 是最直接的办法,但一个中型数仓动辄几百张表,token 消耗会爆炸,而且无关表的噪声反而会降低准确率。
实际可行的做法是语义检索 + 精准注入:
- 离线阶段:把每张表的
CREATE TABLE语句、列注释、业务含义描述向量化,存入 pgvector 或 Qdrant。 - 在线阶段:用户输入的自然语言问题先做向量检索,召回 Top-K(通常 K=5~8)相关表,再把这些表的 schema 拼入 system prompt。
# 伪代码示意
relevant_tables = vector_search(user_query, top_k=6)
schema_context = "\n\n".join([
f"-- {t.name}: {t.description}\n{t.ddl}"
for t in relevant_tables
])
system_prompt = f"""你是一个 SQL 专家,只能查询以下表:
{schema_context}
规则:只输出 SELECT 语句,禁止 INSERT/UPDATE/DELETE/DROP。"""
这一步有几个细节容易被忽略:
- 列注释比列名本身更重要。gmt_create 对模型意义不大,但 -- 订单创建时间,UTC+8 就清晰多了。
- 枚举值要显式说明。如果 status 字段只有 1/2/3 三个值,注释里必须写清楚它们的含义,否则模型大概率会生成 WHERE status = 'paid' 这种无效过滤。
- 外键关系要用注释标出,别指望模型从列名推断 order_id 和 orders.id 是同一个东西。
第二层:SQL 校验——生成之后必须过的三道门
模型输出 SQL 之后,不能直接执行。三道校验缺一不可。
第一道:语法检查
用数据库自带的 EXPLAIN 或 PARSE 接口,而不是自己写正则。PostgreSQL 用 EXPLAIN (FORMAT JSON),MySQL 用 EXPLAIN FORMAT=JSON。语法错误会直接抛异常,捕获后返回给模型重试,通常一次重试可以解决 90% 的语法问题。
第二道:白名单语义检查
用 sqlglot 或 sqlparse 解析 AST,检查以下几点:
import sqlglot
def validate_sql(sql: str, allowed_tables: list[str]) -> tuple[bool, str]:
try:
ast = sqlglot.parse_one(sql, dialect="mysql")
except Exception as e:
return False, f"解析失败: {e}"
# 只允许 SELECT
if not isinstance(ast, sqlglot.exp.Select):
return False, "只允许 SELECT 语句"
# 检查表名白名单
tables_in_sql = {t.name for t in ast.find_all(sqlglot.exp.Table)}
illegal = tables_in_sql - set(allowed_tables)
if illegal:
return False, f"访问了未授权的表: {illegal}"
# 检查是否有子查询访问敏感表(防绕过)
subqueries = list(ast.find_all(sqlglot.exp.Subquery))
# ... 递归检查
return True, "ok"
第三道:性能预估
全表扫描是生产环境最常见的事故源头。在正式执行前跑一次 EXPLAIN,检查执行计划里是否有 type: ALL(MySQL)或 Seq Scan on large_table(PostgreSQL)。如果预估扫描行数超过阈值(比如 1000 万行),直接拒绝执行并提示用户缩小查询范围。
这三道门加起来,可以拦截绝大多数危险查询,整个校验流程加起来通常在 50ms 以内,对用户体验几乎无感知。
第三层:权限控制——最容易被跳过、最不能被跳过
很多团队做 Text-to-SQL 的时候,权限控制是事后补的,或者根本没做。这是个严重的错误。
数据库层面:给 Text-to-SQL 服务单独开一个只读账号,GRANT SELECT ON analytics.* TO 'textsql_ro'@'%',物理上不可能写入。不要用 DBA 账号或应用主账号,哪怕加了软件层校验也不行,因为提示注入(prompt injection)是真实存在的攻击面。
应用层面:每个用户或角色绑定一张"可见表"白名单,在 schema 注入阶段就只传入该用户有权查看的表,这样模型压根不知道其他表的存在,无法生成访问它们的 SQL。
数据行级别:如果需要行级权限(比如销售只能看自己区域的数据),在最终 SQL 外面套一层 wrapper 自动追加 WHERE region = 'east',不依赖模型自觉添加过滤条件。
一个完整的权限检查顺序如下:
| 检查层 | 实现位置 | 失效后果 |
|---|---|---|
| Schema 可见性 | prompt 构建阶段 | 模型不知道表存在 |
| SQL 表白名单 | sqlglot AST 检查 | 拒绝执行 |
| 数据库账号权限 | DB 层 GRANT | 执行报错 |
| 行级过滤 | SQL wrapper | 数据隔离 |
四层冗余,任何一层被绕过,后面还有保险。
把管道串起来:从用户输入到结果返回
一个完整请求的生命周期大概是这样的:
- 用户输入自然语言问题(~10ms)
- 向量检索相关表,构建 schema 上下文(~30ms)
- 调用 LLM 生成 SQL,Claude Sonnet 4.6 或 GPT-5.4-mini 在这个任务上延迟通常在 1~3s
- 三道 SQL 校验(~50ms)
- 执行 SQL,返回结果集(取决于查询复杂度)
- 可选:LLM 对结果做自然语言解读(~1s)
总端到端延迟控制在 5s 以内,对于数据分析场景是完全可接受的。如果步骤 3 失败(校验不通过),把错误信息回传给模型自动重试,最多重试 2 次,超过则返回友好提示。
生产环境还有一点值得注意:把所有生成的 SQL 和执行结果记录下来,这份日志是持续改进 schema 注释和 prompt 的黄金数据。每周花一小时看一遍失败案例,准确率可以稳定往上走。
构建这套管道的时候,LLM API 的稳定性和调用成本是绕不开的问题——Text-to-SQL 链路里 LLM 调用频率很高,schema 检索阶段可能用轻量模型,SQL 生成阶段需要旗舰模型,有时还要调用 embedding 接口,多模型混用很常见。我现在用的是 XycAi 词元平台,一个接口覆盖 200+ 全球模型,Claude Sonnet 4.6、GPT-5.4 都能直接调,价格低至官方价 1.4 折起,而且支持 Claude Code 和 Codex CLI 一键接入,本地开发和生产环境用的是同一套 API,省去了很多切换成本。企业用还有合规备案和全球发票,对接财务比较顺畅。
一个 API 接入 200+ 全球 AI 模型
GPT · Claude · Gemini 官方模型低至 1.4 折起,持大模型算法备案号,CN2 直连低至 5ms,可开全球合规发票。
立即体验 XycAi →