Text-to-SQL 技术设计与注意事项

9 阅读8分钟

image.png

1. Text-to-SQL 是什么?为什么要做?有什么用?

是什么

Text-to-SQL 是一种将自然语言问题自动转换为 SQL 查询语句并执行的技术。用户无需懂 SQL,直接用中文提问,系统返回数据结果。

为什么要做

传统后台管理系统的痛点:

  • 业务人员想看数据 → 必须找开发写 SQL 或加报表页面
  • 开发资源有限,数据需求却是长尾的、频繁变化的
  • 固定报表无法覆盖所有临时数据分析需求

Text-to-SQL 的价值:让业务人员直接提问,系统自动出数,把数据分析的门槛从"会写 SQL"降低到"会说话"。

有什么用(典型场景)

问题示例背后的 SQL 意图
上周新增了多少用户?COUNT + WHERE 时间范围
各角色的用户数量分布?GROUP BY + JOIN
最近登录失败次数最多的 IP?ORDER BY + LIMIT

2. 具体实现步骤

整体数据流:

text
插入
复制
自然语言输入
    ↓
前端 POST /api/ai/text2sql
    ↓
后端构建 Prompt(注入表结构)
    ↓
调用 LLM API(OpenAI 兼容)
    ↓
SQL 安全校验(双重防护)
    ↓
执行 SQL → 推断图表类型
    ↓
前端动态渲染(Table / 数字卡片 / ECharts)

2.1 Schema 动态注入

核心问题:LLM 不认识你的数据库,必须把表结构告诉它。

实现方式:在每次请求时,从 MySQL 的 information_schema 动态提取当前库的表结构:

sql
插入
复制
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ?
  AND COLUMN_NAME NOT IN ('password','token','secret','salt')
ORDER BY TABLE_NAME, ORDINAL_POSITION

两个关键设计决策

  1. 敏感字段在 SQL 层过滤password / token / secret / salt 从查询结果中直接排除。LLM 拿到的 Schema 里根本没有这些字段,从根源杜绝泄露风险。
  2. Redis 缓存:表结构不会频繁变化,查出来的结果序列化为 JSON 缓存至 Redis,Key = ai:schemaTTL = 1h,避免每次请求都查 information_schema 带来的性能损耗。

2.2 Prompt 工程

Prompt 是整个方案的"大脑接口",设计好坏直接决定 SQL 质量。

Prompt 结构

text
插入
复制
你是一个 MySQL 专家。根据以下表结构将自然语言问题转换为 SQL。

【表结构】
{schema}   ← 动态注入,来自 schema_extractor.go

【约束】
1. 只生成 SELECT 语句
2. 不得查询 password、token、secret、salt 字段
3. 结果必须包含 LIMIT,最大 1000
4. 只输出 SQL,不要任何解释

【问题】{question}

设计要点

  • 角色设定:明确告知 LLM 它是 MySQL 专家,而非通用助手
  • 约束显式化:把安全规则写进 Prompt,形成第一道防线
  • 只输出 SQL:避免 LLM 输出多余文字导致解析失败
  • COLUMN_COMMENT 的价值:表结构里带上列注释(如 create_time 创建时间),LLM 能更准确地理解业务语义,生成更精准的 SQL

2.3 SQL 安全校验

核心理念:永远不信任 LLM 的输出,程序层独立做二次验证。

Prompt 约束是"君子协定",安全校验才是"法律"。

校验逻辑(sql_validator.go

text
插入
复制
Step 1: 解析 SQL 首个 token
        → 非 SELECT 直接返回错误,拒绝执行

Step 2: 关键字黑名单扫描
        → INSERT / UPDATE / DELETE / DROP / TRUNCATE
          CREATE / ALTER / EXEC 任意出现 → 拒绝

Step 3: 敏感字段扫描
        → SQL 文本中包含 password / token / secret / salt → 拒绝

Step 4: LIMIT 检查
        → 若 LLM 漏写 LIMIT,强制注入 LIMIT 1000

为什么这样设计:LLM 存在幻觉,即便 Prompt 明确约束,极端情况下仍可能生成危险 SQL。双重校验(Prompt 约束 + 程序校验)形成纵深防御。


2.4 图表类型自动推断

问题:用户不知道自己的问题适合用什么图表展示,不应该让用户选。

方案:后端根据查询结果的结构特征自动推断,返回 chartType 字段:

结果特征chartType说明
1 列 + 1 行number单一聚合值,大数字卡片最直观
2 列 + 行数 ≤ 8pie类目少,饼图看占比
2 列 + 行数 > 8bar类目多,柱状图看对比
含时间列(date/time/year)line时序数据,折线图看趋势
其他多列table兜底,通用表格展示

推断逻辑完全在后端,前端只需按 chartType 切换渲染组件,职责清晰。


2.5 前端动态渲染

页面三区布局

text
插入
复制
┌─────────────────────────────────────┐
│  输入区                              │
│  [文本框] [快捷问题标签] [发送]       │
├─────────────────────────────────────┤
│  SQL 展示区(只读代码块)             │
│  SELECT COUNT(*) AS cnt FROM ...    │
├─────────────────────────────────────┤
│  结果区(动态渲染)                   │
│  根据 chartType 切换:               │
│  number → 大数字卡片                 │
│  bar/line/pie → ECharts 图表        │
│  table → el-table 分页              │
└─────────────────────────────────────┘

SQL 展示区的意义:不隐藏生成的 SQL,用户可以看到系统"是怎么查的",增加透明度,也帮助用户学习 SQL。这是信任建立的关键设计。

技术实现

  • vue-echarts 封装 ECharts,响应式渲染
  • API 模块独立 axios 实例,超时时间 30s(LLM 响应较慢)
  • 路由懒加载:() => import('@/views/ai/TextToSQL.vue')

3. 安全设计亮点 / 系统稳定性 / 企业级 vs Demo 级

安全五层防御

层级风险防护措施
1LLM 生成恶意 SQL首 token 校验 + 关键字黑名单(程序层)
2泄露敏感字段Schema 提取时过滤 + Prompt 约束(双保险)
3大批量查询打垮 DB强制注入 LIMIT 1000
4未授权访问JWT 中间件鉴权,接口需登录
5Prompt 注入攻击用户输入长度限制 ≤ 500 字符

为什么不是 Demo 级,而是企业级?

Demo 级系统通常的问题:

问题本方案的处理
直接把用户输入塞进 SQL 执行中间有 LLM 隔离层 + 安全校验层
全部表结构暴露给 LLMSchema 提取时过滤敏感字段
没有访问控制JWT 鉴权,权限与主系统统一
没有限流/防护LIMIT 强制注入,输入长度限制
每次都查 information_schemaRedis 缓存,TTL 1h
图表类型写死自动推断,自适应结果结构
LLM 配置硬编码yaml 配置 + 环境变量,支持切换模型

稳定性设计

  • Redis 缓存 Schema:避免 information_schema 查询成为瓶颈
  • axios 30s 超时:LLM 响应慢时前端不会无限等待
  • MaxTokens: 512:控制 LLM 输出长度,防止超长响应
  • LIMIT 兜底:防止意外的全表扫描
  • 模型可配置gpt-4o-mini 成本低,生产可按需切换更强模型

针对"无法理解的自然语言查询"如何引导用户?

这是企业级系统必须考虑的体验问题,方案中设计了快捷问题标签

text
插入
复制
前端输入区预置常用问题标签,例如:
[上周新增用户数] [各角色用户分布] [今日登录次数]

用户点击标签直接填充问题,降低输入门槛,同时隐式告知用户"可以问什么类型的问题"。

其他引导策略(可扩展):

  • SQL 展示区透明展示生成的 SQL → 用户理解系统能力边界
  • 查询失败时返回友好提示(如"请尝试更具体的问题描述")
  • 可在标签上补充领域提示(如"仅支持查询用户、角色相关数据")

未来可扩展的方向

方向一:SQL 修正对话(提升准确率)

当 SQL 执行失败或结果为空时,把错误信息反馈给 LLM,让它自动修正:

text
插入
复制
Round 1: 用户问 → LLM 生成 SQL → 执行失败
Round 2: 把原问题 + 错误信息 +SQL → 再次请求 LLM 修正

最多重试 2 次,超出则提示用户换一种问法。这把准确率从"一次猜中"提升到"多轮收敛"。

方向二:查询历史 + 收藏

持久化用户的查询记录(问题 + 生成的 SQL + 结果摘要),支持:

  • 历史查询一键重跑
  • 收藏常用查询,生成"我的看板"
  • 团队共享收藏查询(相当于轻量级报表系统)

方向三:Schema 变更主动失效

监听 DDL 变更事件(MySQL binlog 或定时 diff),表结构变更时主动删除 Redis 缓存的 Key,而不是等 TTL 过期。

方向四:用户可切换图表类型

后端推断 chartType 作为默认值,前端提供切换按钮(柱/折/饼/表),用户可手动覆盖。低成本的体验提升。

方向五:数据行级权限

在 SQL 执行前,根据当前用户的角色,自动向 SQL 注入 WHERE 条件(如 AND dept_id = ?),实现行级数据隔离。这是从"能用"到"可上生产"的关键一步。

方向六:可观测性

  • 记录每次 LLM 调用的耗时、token 消耗、成功/失败状态
  • 监控 SQL 执行时间,超过阈值告警
  • 统计哪些问题类型失败率高 → 优化 Prompt 或补充 Schema 注释

方向七:Schema 语义增强

当前 Schema 只注入表结构,可以进一步注入业务术语词典

text
插入
复制
【业务术语说明】
- 活跃用户:最近 7 天内有登录记录的用户
- 新用户:注册时间在最近 30 天内的用户

这样 LLM 对业务概念的理解更准确,SQL 质量显著提升。