每日新闻 / 2026-05-14

Text-to-SQL AI 数据分析实战:构建安全可靠的自然语言查询数据库管道

XycAi
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 消耗会爆炸,而且无关表的噪声反而会降低准确率。

实际可行的做法是语义检索 + 精准注入

  1. 离线阶段:把每张表的 CREATE TABLE 语句、列注释、业务含义描述向量化,存入 pgvector 或 Qdrant。
  2. 在线阶段:用户输入的自然语言问题先做向量检索,召回 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_idorders.id 是同一个东西。


第二层:SQL 校验——生成之后必须过的三道门

模型输出 SQL 之后,不能直接执行。三道校验缺一不可。

第一道:语法检查

用数据库自带的 EXPLAINPARSE 接口,而不是自己写正则。PostgreSQL 用 EXPLAIN (FORMAT JSON),MySQL 用 EXPLAIN FORMAT=JSON。语法错误会直接抛异常,捕获后返回给模型重试,通常一次重试可以解决 90% 的语法问题。

第二道:白名单语义检查

sqlglotsqlparse 解析 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 数据隔离

四层冗余,任何一层被绕过,后面还有保险。


把管道串起来:从用户输入到结果返回

一个完整请求的生命周期大概是这样的:

  1. 用户输入自然语言问题(~10ms)
  2. 向量检索相关表,构建 schema 上下文(~30ms)
  3. 调用 LLM 生成 SQL,Claude Sonnet 4.6 或 GPT-5.4-mini 在这个任务上延迟通常在 1~3s
  4. 三道 SQL 校验(~50ms)
  5. 执行 SQL,返回结果集(取决于查询复杂度)
  6. 可选: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 →