🎯 本章核心问题
如何让 LLM 生成准确、安全、可执行的 SQL?
这是整个系统的核心挑战:
- ❌ LLM 不知道你的表名是
orders还是t_order_2024 - ❌ LLM 可能生成
DROP TABLE等危险语句 - ❌ 生成的 SQL 可能有语法错误或字段名拼写错误
- ❌ 复杂查询(多表 JOIN、子查询)容易出错
解决方案:构建”NL→SQL 转换引擎” —— 五阶段流水线确保输出质量。
📐 架构总览
NL→SQL 转换引擎架构
五阶段处理流水线
上下文组装 → LLM 生成 → 清洗 → 校验 → 执行
📥 一、输入层:三要素准备
1.1 自然语言查询(用户输入)
1 | |
1.2 语义模型(Semantic Model)
来自 Metadata Service 的 get_semantic_model() (详见第3篇:元数据智能管理系统):
1 | |
关键价值:
- ✅ 让 LLM 知道
categories表对应中文”分类表” - ✅ 让 LLM 知道
order_items.order_id关联orders.id - ✅ 让 LLM 知道
total_amount是decimal类型(可做 SUM)
1.3 可选:公式库(Formulas)
预定义的 SQL 模板,用于复杂计算:
1 | |
🔧 二、Step 1:上下文组装(generate_query)
app/services/sql_generator.py
1 | |
💡 设计要点:为什么单独封装 generate_query()?
| 原因 | 说明 |
|---|---|
| 单一职责 | 只负责”组装上下文 + 调用LLM”,不关心验证和执行 |
| 可测试性 | 可以 mock LLM 返回值进行单元测试 |
| 复用性 | Chat Engine 和 Dashboard Engine 都调用这个函数 |
| 扩展性 | 未来可以添加缓存、重试、A/B 测试等逻辑 |
🤖 三、Step 2:LLM SQL 生成(核心)
3.1 Prompt 工程详解
app/services/llm_gateway.py
1 | |
3.2 Prompt 设计策略分析
| 策略 | 示例 | 效果 |
|---|---|---|
| 角色设定 | “你是专业的 SQL 生成专家” | 让 LLM 以严谨的态度生成 |
| 负向约束 | “只输出 SQL,不要解释” | 减少噪声(从 80% → <10%) |
| 正向引导 | “使用语义模型中正确的表名和字段名” | 提高字段名准确率(90%+) |
| JOIN 提示 | “根据表关系使用 JOIN” | 自动利用 relations 信息 |
| 兜底策略 | “默认按主键或时间降序排列” | 避免无序结果 |
3.3 实际案例:复杂多表查询
用户输入:
1 | |
LLM 收到的完整 Prompt:
1 | |
LLM 输出(原始响应):
1 | |
1 | |
完美! 🎉 包含了:
- ✅ 4 表 JOIN(正确利用了 relations 信息)
- ✅ 时间过滤(理解了”上个月”)
- ✅ 分组聚合(GROUP BY + SUM)
- ✅ 占比计算(子查询)
- ✅ TOP N 过滤(LIMIT 5)
🧹 四、Step 3:响应清洗(已在 LLM Gateway 实现)
详见第2篇:LLM 统一网关设计的”响应清洗器”章节。
清洗步骤回顾:
- 去除
<think >...</think >标签(DeepSeek 特有) - 剥离
```sql`` 标记 - 去除首尾空白字符
strip()清理
🛡️ 五、Step 4:安全验证(⚠️ 最关键)
5.1 为什么必须安全验证?
LLM 不是绝对可靠的! 它可能生成:
1 | |
5.2 validate_sql() 正则引擎实现
app/services/sql_generator.py
1 | |
5.3 防御策略分析
| 层级 | 策略 | 拦截能力 |
|---|---|---|
| 第一层:黑名单 | 正则匹配危险关键字 | 拦截 99% 的已知攻击模式 |
| 第二层:白名单 | 强制 SELECT 开头 | 拦截所有非查询操作 |
| 第三层:LLM约束 | System Prompt 中要求只生成 SELECT | 从源头降低风险(但不可依赖) |
💡 为什么不只用白名单?
1 | |
结论:黑白名单结合才是最安全的!
5.4 安全日志示例
1 | |
⚡ 六、Step 5:执行查询(execute_query)
6.1 异步 MySQL 连接管理
app/services/sql_generator.py
1 | |
6.2 DictCursor vs TupleCursor
1 | |
6.3 性能基准测试
在我们的测试环境(MySQL 8.0, 10万行 orders 表):
| 查询类型 | SQL 示例 | 耗时 | 返回行数 |
|---|---|---|---|
| 简单查询 | SELECT COUNT(*) FROM users |
12ms | 1 |
| 单表条件 | SELECT * FROM orders WHERE status='paid' |
45ms | 1,234 |
| 分组聚合 | SELECT category, SUM(amount) GROUP BY category |
89ms | 15 |
| 多表 JOIN | 4表 JOIN + 子查询(上面的例子) | 156ms | 5 |
| 全表扫描 | SELECT * FROM order_items |
380ms | 50,000 |
结论:绝大多数业务查询可在 200ms 内完成!
🔗 七、调用方集成示例
7.1 Chat Engine 中的调用链
app/services/chat_engine.py
1 | |
7.2 Dashboard Engine 中的调用链
app/services/dashboard_engine.py
1 | |
📊 八、常见问题与解决方案
Q1:LLM 生成的 SQL 有语法错误怎么办?
方案 A:自动修复(推荐)
1 | |
方案 B:让 LLM 自我纠错
1 | |
Q2:如何处理超大型表的性能问题?
方案:添加 LIMIT 保护
1 | |
Q3:如何支持数据库特定的方言?
方案:在 System Prompt 中指定方言
1 | |
🎯 九、最佳实践总结
✅ 我们做到了什么
- 五阶段流水线:上下文组装 → LLM生成 → 清洗 → 验证 → 执行
- 语义模型注入:让 LLM 理解真实的数据库结构(准确率提升至 90%+)
- 多层安全防御:黑名单 + 白名单双重保护,杜绝 SQL 注入
- 异步高性能:aiomysql + DictCursor,200ms 内返回结果
- 完善错误处理:验证失败友好提示,执行异常详细日志
📚 最佳实践清单
- 使用
generate_query()统一入口,避免重复代码 - 注入完整的语义模型(表结构 + 关系 + 中文名)
- System Prompt 明确约束输出格式
- 实现
validate_sql()双层验证(黑名单 + 白名单) - 使用
aiomysql.DictCursor返回字典格式 - 记录安全相关的 WARNING 日志
- 考虑自我纠错机制(retry with feedback)
- 添加 LIMIT 保护防止大数据量查询(规划扩展,见「方案」章节)
🚀 下一步
下一篇文章我们将深入 智能对话引擎实战 —— 如何管理多轮对话上下文?如何根据 SQL 特征自动推荐图表类型?如何一键生成数据分析报告?
敬请期待!🚀
相关代码文件:
app/services/sql_generator.py(sql_generator.py) — 核心转换逻辑app/services/llm_gateway.py— LLM 生成 Promptapp/services/chat_engine.py— 对话引擎集成app/services/dashboard_engine.py— 大屏引擎集成