从 Demo 到生产:Text‑to‑SQL 系统架构实战|SpringBoot + 大模型前后端分离完整落地
系列第三篇:前面我们完成 Text‑to‑SQL 基础搭建、复杂多表关联查询数据库设计,本篇聚焦工程化架构。很多同学跑通 Demo 之后直接上线,结果遇到安全漏洞、大模型幻觉、接口报错、前端交互混乱等一堆线上问题。本文站在后端工程师视角,拆解一套可部署上线的 Text‑to‑SQL 完整系统,包含分层架构、代码实现、安全防护、性能调优、未来扩展思路。
前言
最近在做内部智能问数平台,调研和实现了一版 Text‑to‑SQL 系统。相信很多小伙伴和我一样,跟着教程跑通 Demo 时感觉一切美好:输入一句自然语言,大模型唰的一下吐出 SQL,数据库返回结果,页面展示数据,整个流程行云流水。
但是一旦往生产环境迁移,坑就接踵而至:
- 大模型时不时返回
DROP、ALTER这类危险语句,一旦执行直接事故; - AI 输出格式乱七八糟,带 markdown 代码块、多余前缀注释,直接丢给数据库直接报语法错误;
- 单表查询、多表关联查询混在同一个接口,业务逻辑耦合严重,后续维护改不动;
- 没有统一异常处理,LLM 调用超时、数据库报错直接抛出堆栈到前端页面;
- 没有缓存,相同业务问题反复调用大模型 API,Token 成本暴涨,接口响应慢;
- 前端简陋,没有示例引导,业务人员不知道该怎么提问,使用门槛很高CSDN博...。
Demo 可以只关注功能实现,生产系统必须兼顾可维护、安全、可观测、性能、用户体验。
这套系统采用 SpringBoot + Thymeleaf 前端 + 硅基流动大模型 API + PostgreSQL,严格遵循 MVC 分层思想,把请求接收、业务编排、大模型调用、数据库执行、安全校验做模块隔离。本篇会把整体架构、核心源码、配置、踩坑优化全部分享出来。
一、系统架构概览
整套系统遵循标准后端分层思想:前端展示层 → Controller 控制层 → Service 业务层 → 数据访问层,外部对接大模型 API 与 PostgreSQL 数据库。为了区分简单查询和复杂关联查询,我们把单表检索、关联检索做接口与代码隔离,两种场景使用完全不同的 Prompt,提升 SQL 生成准确率。
1.1 整体架构图

简单梳理完整请求链路:
用户打开网页输入自然语言问题 → 前端 fetch 发送 POST 请求 → Controller 接收,做基础参数校验 → 调用
Text2SqlService→ 调用LlmService,传入对应场景的 System Prompt,请求大模型 API 拿到原始 SQL → 清洗 AI 返回的多余 markdown 标记 → 执行 SQL 安全校验,拦截危险操作 → JdbcTemplate 执行只读查询,捕获全部异常 → 组装统一格式返回给前端 → 前端渲染生成的 SQL 语句和表格数据。
1.2 核心模块职责
| 模块 | 职责 | 关键类 / 文件 |
|---|---|---|
| 前端展示层 | 页面渲染、用户交互、示例引导、结果表格渲染、复制 SQL | index.html, single.html, join.html, chat.html |
| 后端控制层 | 页面路由跳转、接收 AJAX 请求、基础参数非空校验、统一响应封装 | SingleQueryController, JoinQueryController |
| 业务服务层 | 业务流程编排、调用大模型、SQL 清洗、安全校验、异常捕获、结果组装;独立封装大模型 API 调用 | Text2SqlService, LlmService |
| 数据访问层 | 动态 SQL 执行、数据库交互,返回结构化数据集 | JdbcTemplate |
| 外部依赖 | 大模型推理能力、业务数据存储 | 硅基流动 API, PostgreSQL |
设计思考:为什么要把单表和关联查询分开两套 Prompt 和接口? 如果把全部三张表结构一股脑全部丢给大模型做单表查询,上下文 Token 变多,模型容易混淆表之间字段,出现幻觉,明明只查 member 表,却莫名其妙带上 JOIN 关联其他表。分开之后,单表场景只传入 member 表结构,关联场景传入三张表 + 关联关系,针对性 Prompt,有效提升 SQL 准确率,降低上下文长度,减少 API 成本。
二、后端分层实现
2.1 控制器层(Controller)
Controller 层只做三件事:页面跳转路由、接收 http 请求、基础参数校验,不处理复杂业务逻辑,业务全部下沉到 Service 层。这是 SpringMVC 最佳实践,避免 Controller 写大量业务代码,后续单元测试、接口复用都会很麻烦。
单表检索控制器
/**
* 单表检索控制器
*
* 职责:
* - 提供单表检索页面路由
* - 接收前端AJAX请求
* - 参数校验和响应封装
*/
@Controller
@RequestMapping("/single")
public class SingleQueryController {
private final Text2SqlService text2SqlService;
public SingleQueryController(Text2SqlService text2SqlService) {
this.text2SqlService = text2SqlService;
}
/**
* 页面路由 - 渲染单表检索界面
*/
@GetMapping
public String singlePage(Model model) {
return "single"; // 返回Thymeleaf模板名称
}
/**
* API接口 - 处理单表查询请求
*
* 请求体: { "question": "查询所有金卡会员" }
* 响应体: { "success": true, "data": [...], "sql": "..." }
*/
@PostMapping("/api/query")
@ResponseBody
public Map<String, Object> query(@RequestBody Map<String, String> request) {
String question = request.get("question");
// 参数校验
if (question == null || question.trim().isEmpty()) {
return Map.of("success", false, "error", "问题不能为空");
}
// 委托给Service层处理
return text2SqlService.querySingle(question);
}
}
关联检索控制器
/**
* 关联检索控制器
*
* 职责:
* - 提供关联检索页面路由
* - 接收前端AJAX请求
* - 参数校验和响应封装
*/
@Controller
@RequestMapping("/join")
public class JoinQueryController {
private final Text2SqlService text2SqlService;
public JoinQueryController(Text2SqlService text2SqlService) {
this.text2SqlService = text2SqlService;
}
/**
* 页面路由 - 渲染关联检索界面
*/
@GetMapping
public String joinPage(Model model) {
return "join";
}
/**
* API接口 - 处理关联查询请求
*/
@PostMapping("/api/query")
@ResponseBody
public Map<String, Object> query(@RequestBody Map<String, String> request) {
String question = request.get("question");
if (question == null || question.trim().isEmpty()) {
return Map.of("success", false, "error", "问题不能为空");
}
return text2SqlService.queryJoin(question);
}
}
设计要点总结:
- 单一职责原则:每个 Controller 只负责一类查询,后续如果要对关联查询做权限拦截,只需要修改 JoinQueryController;
- 页面渲染和 API 接口共存:
@GetMapping返回页面,@PostMapping+@ResponseBody返回 JSON; - Controller 只做简单非空校验,复杂业务校验、SQL 安全校验交给 Service;
- 统一 JSON 返回格式,前端只需要一套逻辑处理成功 / 失败场景。
2.2 业务服务层(Service)
Service 层是整个 Text‑to‑SQL 的核心编排层,这里串联整个业务链路:分发查询类型、调用大模型、清洗 AI 输出、安全校验、执行 SQL、捕获异常、组装返回结果。
Text2SqlService - 核心业务编排
/**
* Text-to-SQL核心业务服务
*
* 职责:
* - 协调LLM服务和数据库操作
* - SQL安全校验
* - 结果格式化封装
*/
@Service
public class Text2SqlService {
private final LlmService llmService;
private final JdbcTemplate jdbcTemplate;
private final ObjectMapper objectMapper;
// 构造器注入
public Text2SqlService(LlmService llmService,
JdbcTemplate jdbcTemplate,
ObjectMapper objectMapper) {
this.llmService = llmService;
this.jdbcTemplate = jdbcTemplate;
this.objectMapper = objectMapper;
}
/**
* 单表查询入口
*/
public Map<String, Object> querySingle(String question) {
return executeQuery(question, QueryType.SINGLE);
}
/**
* 关联查询入口
*/
public Map<String, Object> queryJoin(String question) {
return executeQuery(question, QueryType.JOIN);
}
/**
* 通用查询执行逻辑
*
* 流程:
* 1. 调用LLM生成SQL
* 2. 清理SQL格式
* 3. 安全校验
* 4. 执行查询
* 5. 封装结果
*/
private Map<String, Object> executeQuery(String question, QueryType type) {
Map<String, Object> result = new HashMap<>();
result.put("question", question);
result.put("queryType", type.name().toLowerCase());
try {
// Step 1: 调用LLM生成SQL
String sql;
if (type == QueryType.SINGLE) {
sql = llmService.textToSqlSingle(question);
} else {
sql = llmService.textToSqlJoin(question);
}
result.put("generatedSql", sql);
// 检查LLM返回错误
if (sql.startsWith("ERROR")) {
result.put("success", false);
result.put("error", sql);
return result;
}
// Step 2: 清理SQL格式
sql = cleanSql(sql);
result.put("cleanedSql", sql);
// Step 3: 安全校验
if (!isSelectOnly(sql)) {
result.put("success", false);
result.put("error", "安全限制:只允许执行SELECT查询");
return result;
}
// Step 4: 执行查询
List<Map<String, Object>> data = jdbcTemplate.queryForList(sql);
result.put("success", true);
result.put("data", data);
result.put("count", data.size());
} catch (Exception e) {
result.put("success", false);
result.put("error", "SQL执行错误:" + e.getMessage());
}
return result;
}
/**
* SQL清理 - 去除AI可能返回的格式标记
* 大模型经常输出```sql ... ```markdown代码块,必须清洗,否则数据库执行报错
*/
private String cleanSql(String sql) {
if (sql == null) return "";
sql = sql.replaceAll("```sql", "").replaceAll("```", "");
sql = sql.trim();
if (sql.toLowerCase().startsWith("sql:")) {
sql = sql.substring(4).trim();
}
return sql;
}
/**
* 安全校验 - 防止SQL注入
* 只允许SELECT和WITH开头的查询,WITH对应CTE递归查询语法
*/
private boolean isSelectOnly(String sql) {
String upper = sql.toUpperCase().trim();
return upper.startsWith("SELECT") || upper.startsWith("WITH");
}
// 辅助方法:获取会员、订单、明细列表
public List<Map<String, Object>> getAllMembers() { ... }
public List<Map<String, Object>> getAllOrders() { ... }
public List<Map<String, Object>> getAllOrderItems() { ... }
}
/**
* 查询类型枚举
*/
enum QueryType {
SINGLE, // 单表查询
JOIN // 关联查询
}
这里有一个非常容易踩坑点:大模型输出不会永远是干净的 SQL,经常附带 markdown 标记、注释、前缀文字。如果不做 cleanSql 清洗,直接交给 JdbcTemplate 执行,直接抛出 SQL 语法异常,这是很多 Demo 上线之后高频报错点。
LlmService - AI 模型服务封装
把所有和大模型 API 交互的逻辑全部封装到LlmService,做到解耦。未来如果想要切换模型,比如换成本地部署开源模型,只需要修改这个类,上层业务代码几乎不用改动,符合适配器模式。
/**
* 大语言模型服务
*
* 职责:
* - 封装与硅基流动API的交互
* - 管理系统提示词
* - 解析API响应、捕获网络异常
*/
@Service
public class LlmService {
@Value("${siliconflow.api.url}")
private String apiUrl;
@Value("${siliconflow.api.key}")
private String apiKey;
@Value("${siliconflow.api.model}")
private String model;
private final ObjectMapper objectMapper;
/**
* 单表查询 - 仅提供member表结构
*/
public String textToSqlSingle(String question) {
String systemPrompt = buildSinglePrompt();
return callApi(question, systemPrompt);
}
/**
* 关联查询 - 提供三表结构和关联关系
*/
public String textToSqlJoin(String question) {
String systemPrompt = buildJoinPrompt();
return callApi(question, systemPrompt);
}
/**
* 构建单表查询提示词
*/
private String buildSinglePrompt() {
return """
你是SQL生成助手。根据以下member表结构,生成单表查询SQL:
表:member(会员信息表)
- id: 会员ID
- name: 会员姓名
- level: 等级(普通/银卡/金卡/钻石)
- points: 积分
- status: 状态(活跃/冻结/注销)
要求:
1. 只返回SQL语句
2. 只能使用member表
3. 中文值用单引号
示例:查询金卡会员
SELECT * FROM member WHERE level = '金卡会员'
""";
}
/**
* 构建关联查询提示词
*/
private String buildJoinPrompt() {
return """
你是SQL生成助手。根据以下表结构和关联关系,生成查询SQL:
表1:member(会员表)- id, name, level
表2:orders(订单表)- id, member_id, total_amount, status
表3:order_item(明细表)- id, order_id, product_name
关联关系:
- orders.member_id = member.id
- order_item.order_id = orders.id
要求:
1. 只返回SQL语句
2. 使用JOIN关联
3. 使用表别名
示例:查询张三的订单
SELECT o.* FROM orders o
JOIN member m ON o.member_id = m.id
WHERE m.name = '张三'
""";
}
/**
* 通用API调用方法
*/
private String callApi(String question, String systemPrompt) {
try {
// 构建请求
Map<String, Object> body = new HashMap<>();
body.put("model", model);
body.put("messages", List.of(
Map.of("role", "system", "content", systemPrompt),
Map.of("role", "user", "content", question)
));
body.put("max_tokens", 512);
// temperature设置0.1,降低随机性,SQL生成任务不适合高温度
body.put("temperature", 0.1);
String json = objectMapper.writeValueAsString(body);
// HTTP请求
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(60))
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
// 解析响应
if (response.statusCode() == 200) {
JsonNode root = objectMapper.readTree(response.body());
return root.get("choices").get(0)
.get("message").get("content").asText().trim();
}
return "ERROR:API调用失败";
} catch (Exception e) {
return "ERROR:" + e.getMessage();
}
}
}
提示词小技巧:SQL 生成任务,
temperature不要设置太高,推荐 0.1‑0.2,降低大模型随机创造性,尽量输出稳定可执行 SQL。同时 Few‑shot 示例一定要写在 Prompt 内部,给模型参考输出格式,显著降低幻觉概率CSDN博...。
三、前端页面实现
本项目前端使用 Thymeleaf 模板 + 原生 JS 实现,不需要 Vue、React 这类重型前端框架,快速开发原型工具。整套系统 4 个页面:
| 页面 | 路由 | 模板文件 | 功能说明 |
|---|---|---|---|
| 数据总览 | / |
index.html | 展示会员、订单、明细数据 |
| 单表检索 | /single |
single.html | 会员表单表查询 |
| 关联检索 | /join |
join.html | 三表关联查询 |
| 智能问答 | /chat |
chat.html | 通用问答(默认关联查询) |
前端的设计目标不仅仅是把请求发出去,更要照顾业务用户体验:
- 直观展示当前场景下数据表关系,让用户知道可以查哪些内容;
- 内置示例问题,点击直接填充输入框并且发起查询,降低使用门槛;
- 展示大模型生成后的清洗完成 SQL,方便开发人员排查问题,业务人员也可以看到背后执行逻辑;
- 动态渲染查询结果表格,支持一键复制 SQL;
- 区分两套主题配色,单表绿色渐变,关联查询粉色渐变,视觉上区分两种查询模式。
3.2 关联检索页面实现
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>关联检索 - Text-to-SQL</title>
<style>
/* 页面背景 - 粉色渐变 */
body {
background: linear-gradient(-135deg, #f093fb 0%, #f5576c 100%);
min-height: 100vh;
}
/* 导航栏样式 */
.nav a.active {
background: linear-gradient(135deg, #f5576c, #f093fb);
color: white;
}
/* 卡片立体效果 */
.card {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
}
</style>
</head>
<body>
<div class="container">
<!-- 页面标题 -->
<div class="header">
<h1>🔗 关联检索</h1>
<p>支持会员、订单、商品多表关联查询</p>
</div>
<!-- 导航栏 -->
<div class="nav">
<a href="/">📊 数据总览</a>
<a href="/single">📋 单表检索</a>
<a href="/join" class="active">🔗 关联检索</a>
<a href="/chat">💬 智能问答</a>
</div>
<!-- 主查询卡片 -->
<div class="card">
<!-- 表关系图示 -->
<div class="schema-diagram">
<h3>📐 数据表关系</h3>
<div class="tables">
<div class="table-member">member (会员表)</div>
<div class="arrow">1:N →</div>
<div class="table-orders">orders (订单表)</div>
<div class="arrow">1:N →</div>
<div class="table-item">order_item (明细表)</div>
</div>
</div>
<!-- 问题输入区 -->
<div class="input-section">
<textarea id="questionInput"
placeholder="例如:查询张三的所有已完成订单"></textarea>
<button onclick="submitQuery()">🚀 智能查询</button>
</div>
<!-- 示例问题 -->
<div class="examples">
<h4>💡 试试这些问题:</h4>
<div class="example-list">
<span onclick="useExample(this)">查询张三的所有订单</span>
<span onclick="useExample(this)">统计每个会员的消费总额</span>
<span onclick="useExample(this)">查询数码类商品的订单</span>
</div>
</div>
<!-- SQL展示区 -->
<div id="sqlResult" class="sql-box" style="display:none;">
<h4>🔍 生成的SQL语句</h4>
<pre id="sqlCode"></pre>
<button onclick="copySql()">📋 复制SQL</button>
</div>
<!-- 结果展示区 -->
<div id="queryResult" class="result-box" style="display:none;">
<h4>📊 查询结果</h4>
<div id="resultTable"></div>
</div>
</div>
</div>
<script>
// AJAX提交查询
async function submitQuery() {
const question = document.getElementById('questionInput').value.trim();
if (!question) return;
try {
const response = await fetch('/join/api/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question })
});
const data = await response.json();
displayResult(data);
} catch (error) {
alert('查询失败:' + error.message);
}
}
// 展示查询结果
function displayResult(data) {
if (!data.success) {
alert('错误:' + data.error);
return;
}
// 展示SQL
document.getElementById('sqlResult').style.display = 'block';
document.getElementById('sqlCode').textContent = data.cleanedSql;
// 展示数据表格
document.getElementById('queryResult').style.display = 'block';
renderTable(data.data);
}
// 动态渲染表格
function renderTable(data) {
const container = document.getElementById('resultTable');
if (!data || data.length === 0) {
container.innerHTML = '<p>暂无数据</p>';
return;
}
// 获取列名
const columns = Object.keys(data[0]);
// 构建HTML表格
let html = '<table><thead><tr>';
columns.forEach(col => {
html += `<th>${col}</th>`;
});
html += '</tr></thead><tbody>';
data.forEach(row => {
html += '<tr>';
columns.forEach(col => {
html += `<td>${row[col] ?? ''}</td>`;
});
html += '</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
}
// 使用示例问题
function useExample(el) {
document.getElementById('questionInput').value = el.textContent;
submitQuery();
}
// 复制SQL
function copySql() {
const sql = document.getElementById('sqlCode').textContent;
navigator.clipboard.writeText(sql);
alert('SQL已复制!');
}
</script>
</body>
</html>
单表页面逻辑大体一致,只是更换主题配色、只展示 member 单张表、示例问题换成单表相关提问,接口请求地址改为/single/api/query,这里就不再重复贴完整代码。
四、配置与安全策略
⚠️ 安全是 Text‑to‑SQL 系统重中之重!千万不要直接把大模型输出的 SQL 直接丢给数据库执行! 大模型会受提示词注入攻击,可能生成
DROP TABLE、DELETE FROM等高危语句,如果数据库账号权限过大,直接会造成数据灾难。很多网上 Demo 完全忽略安全校验,只能本地玩,绝对不能直接上线。
4.1 应用配置 application.yml
# application.yml
server:
port: 8080
spring:
application:
name: text2sql
datasource:
url: jdbc:postgresql://localhost:5432/text2sql_db
username: postgres
password: your_password
driver-class-name: org.postgresql.Driver
sql:
init:
mode: always
schema-locations:
- classpath:schema.sql
- classpath:schema_order.sql
data-locations:
- classpath:data.sql
- classpath:data_order.sql
# 硅基流动AI配置
siliconflow:
api:
url: https://api.siliconflow.cn/v1/chat/completions
key: ${SILICONFLOW_API_KEY}
model: Qwen/Qwen2.5-7B-Instruct
生产环境建议 API Key 使用环境变量注入,不要硬编码写死在配置文件,防止密钥泄露。数据库账号尽量使用只读账号,即使安全校验被绕过,也无法修改、删除业务数据,这是最后一道防线。
4.2 SQL 安全校验器
这里独立抽离一个SqlSecurityValidator组件,多层防护:白名单校验开头语句、黑名单拦截危险关键字、SQL 长度限制。注意简单判断字符串包含关键字有局限(注释绕过等),更严谨的生产方案可以引入JSQLParser做 SQL 语法 AST 解析,真正解析 SQL 抽象语法树判断操作类型。
/**
* SQL安全校验器
*
* 多层安全防护:
* 1. 白名单验证 - 只允许SELECT/WITH开头
* 2. 关键字过滤 - 禁止DROP、DELETE、UPDATE
* 3. 长度限制 - 防止超长SQL攻击
*/
@Component
public class SqlSecurityValidator {
private static final Set<String> DANGEROUS_KEYWORDS = Set.of(
"DROP", "DELETE", "UPDATE", "INSERT",
"ALTER", "CREATE", "TRUNCATE", "GRANT", "EXEC"
);
/**
* 验证SQL安全性
*/
public ValidationResult validate(String sql) {
// 1. 非空检查
if (sql == null || sql.trim().isEmpty()) {
return ValidationResult.fail("SQL语句不能为空");
}
// 2. 长度限制
if (sql.length() > 10000) {
return ValidationResult.fail("SQL语句过长");
}
// 3. 只允许SELECT查询
String upperSql = sql.toUpperCase().trim();
if (!upperSql.startsWith("SELECT") && !upperSql.startsWith("WITH")) {
return ValidationResult.fail("只允许SELECT查询语句");
}
// 4. 危险关键字检查
for (String keyword : DANGEROUS_KEYWORDS) {
if (upperSql.contains(keyword)) {
return ValidationResult.fail("SQL中包含危险操作");
}
}
return ValidationResult.success();
}
}
@Data
class ValidationResult {
private boolean valid;
private String message;
public static ValidationResult success() {
return new ValidationResult(true, "验证通过");
}
public static ValidationResult fail(String msg) {
return new ValidationResult(false, msg);
}
}
4.3 全局异常处理
如果没有全局异常处理器,当 LLM 调用超时、数据库报错,会直接把 Java 堆栈抛给前端,用户体验差,还泄露内部代码信息。增加@RestControllerAdvice统一捕获异常,返回标准化 JSON 错误信息。
/**
* 全局异常处理器
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理LLM调用异常
*/
@ExceptionHandler(LlmServiceException.class)
public Map<String, Object> handleLlmException(LlmServiceException e) {
return Map.of(
"success", false,
"error", "AI服务异常:" + e.getMessage()
);
}
/**
* 处理SQL执行异常
*/
@ExceptionHandler(SqlExecutionException.class)
public Map<String, Object> handleSqlException(SqlExecutionException e) {
return Map.of(
"success", false,
"error", "数据库错误:" + e.getMessage()
);
}
/**
* 处理通用异常
*/
@ExceptionHandler(Exception.class)
public Map<String, Object> handleException(Exception e) {
return Map.of(
"success", false,
"error", "系统异常:" + e.getMessage()
);
}
}
五、系统集成与部署
5.1 完整项目结构
text2sql/
├── src/main/
│ ├── java/com/example/text2sql/
│ │ ├── controller/
│ │ │ ├── SingleQueryController.java
│ │ │ ├── JoinQueryController.java
│ │ │ └── DataOverviewController.java
│ │ ├── service/
│ │ │ ├── Text2SqlService.java
│ │ │ └── LlmService.java
│ │ ├── config/
│ │ │ └── WebConfig.java
│ │ ├── security/
│ │ │ └── SqlSecurityValidator.java
│ │ └── Text2SqlApplication.java
│ └── resources/
│ ├── templates/
│ │ ├── index.html # 数据总览
│ │ ├── single.html # 单表检索
│ │ ├── join.html # 关联检索
│ │ └── chat.html # 智能问答
│ ├── schema.sql # 会员表结构
│ ├── schema_order.sql # 订单表结构
│ ├── data.sql # 会员数据
│ ├── data_order.sql # 订单数据
│ └── application.yml
└── pom.xml
5.2 启动与测试命令
# 1. 克隆项目
git clone https://github.com/your-repo/text2sql.git
# 2. 配置环境变量,设置大模型密钥,不要硬编码
export SILICONFLOW_API_KEY=your_api_key
# 3. 构建项目
cd text2sql
mvn clean package
# 4. 运行应用
java -jar target/text2sql-1.0.0.jar
# 5. 访问页面
# 数据总览:http://localhost:8080/
# 单表检索:http://localhost:8080/single
# 关联检索:http://localhost:8080/join
# 智能问答:http://localhost:8080/chat
六、性能优化建议
这套基础架构跑 Demo 没问题,一旦并发上来,就会遇到大模型接口慢、数据库查询慢、重复提问消耗大量 Token 等问题,下面从数据库、应用层、AI 模型三个维度给出优化方案。
6.1 数据库层面优化
- 索引优化:针对 JOIN 关联字段、高频过滤条件建立索引,避免全表扫描。
-- 为常用JOIN字段创建索引
CREATE INDEX idx_orders_member_id ON orders(member_id);
CREATE INDEX idx_order_item_order_id ON order_item(order_id);
-- 为常用WHERE条件创建索引
CREATE INDEX idx_member_level ON member(level);
CREATE INDEX idx_orders_status ON orders(status);
- 业务上尽量避免
SELECT *,只查询需要字段; - 使用
EXPLAIN分析大模型生成 SQL 的执行计划,识别全表扫描风险; - 复杂统计类查询,可提前构建物化视图,减少实时计算压力。
6.2 应用层面优化
- 缓存策略:相同业务问题重复提问,直接缓存 SQL + 结果,避免重复调用付费大模型 API。这里使用 Guava Cache 做本地缓存,生产环境可以替换 Redis 分布式缓存。
@Service
public class QueryCacheService {
private final Cache<String, List<Map<String, Object>>> cache =
CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build();
/**
* 带缓存的查询
*/
public List<Map<String, Object>> queryWithCache(String sql) {
return cache.get(sql, () ->
jdbcTemplate.queryForList(sql)
);
}
}
- 异步处理:大模型 API 推理时间比较长,可以使用 Spring
@Async异步接口,前端做轮询或者 WebSocket 推送结果,用户页面不会卡住等待。
@Service
public class AsyncQueryService {
@Async("taskExecutor")
public CompletableFuture<QueryResult> asyncQuery(String question) {
QueryResult result = text2SqlService.queryJoin(question);
return CompletableFuture.completedFuture(result);
}
}
- 增加接口超时控制,防止大模型接口卡死,线程耗尽。

6.3 AI 模型层面优化
- 提示词缓存:系统 SystemPrompt 固定不变,可以预编译缓存,不需要每次调用 API 都重新构建字符串;
- 超时设置:大模型 API 超时设置 30‑60 秒,根据业务调整;
- 重试机制:网络抖动、限流场景,增加 2‑3 次自动重试;
- Token 控制:不要把全量库表 Schema 全部塞进 Prompt,表多之后 token 暴涨,会降低生成准确率,后续可以引入 RAG,根据用户问题检索相关表结构,只把相关 Schema 丢给大模型。
七、系统扩展方向
这套架构预留了很好的扩展空间,当业务越来越复杂,可以往下面几个方向迭代:
7.1 多表扩展,接入 RAG 元数据检索
现在我们是硬编码写死两套 Prompt,当业务库表增长到十几张、几十张表,写死 Prompt 完全不可行。可以读取数据库元数据,把表名、字段、注释、关联关系存入向量库。用户提问之后,通过向量检索取出和问题相关的几张表结构,动态组装 Prompt,这就是 RAG+Text‑to‑SQL 的经典方案。
7.2 多模型支持,策略模式切换模型
使用策略模式抽象模型生成接口,可以自由切换在线 API 模型、本地私有化部署开源模型,不需要改动上层业务代码。
/**
* 多模型策略模式
*/
public interface TextToSqlModel {
String generateSql(String question, List<TableSchema> schemas);
}
@Component
public class SiliconFlowModel implements TextToSqlModel { ... }
@Component
public class LocalModel implements TextToSqlModel { ... }
@Configuration
public class ModelConfig {
@Bean
public TextToSqlModel textToSqlModel(
@Value("${ai.model.type}") String type) {
if ("local".equals(type)) {
return localModel;
}
return siliconFlowModel;
}
}
7.3 多轮对话,上下文记忆
实现会话 session,保存历史提问和生成 SQL,支持上下文理解。例如第一轮问 “查询金卡会员”,第二轮接着问 “统计他们积分总和”,系统结合上一轮上下文理解用户意图,不需要用户重复描述条件。
/**
* 上下文感知的多轮对话
*/
@Service
public class ConversationService {
private final Map<String, List<ConversationMessage>> history =
new ConcurrentHashMap<>();
/**
* 多轮查询
*/
public Map<String, Object> multiTurnQuery(
String sessionId, String question) {
// 获取历史上下文
List<ConversationMessage> context = history.getOrDefault(
sessionId, new ArrayList<>());
// 构建带上下文的提示词
String prompt = buildContextPrompt(context, question);
// 调用AI生成SQL
String sql = llmService.callApi(question, prompt);
// 更新历史
context.add(new ConversationMessage(question, sql));
history.put(sessionId, context);
// 执行查询
return executeSql(sql);
}
}
7.4 企业级增强(生产平台必备)
- 审计日志:记录每一次用户提问、原始问题、生成 SQL、耗时、是否成功,方便排查问题;
- 权限控制:用户行级、表级权限,不同用户只能查询授权的数据;
- SQL 预检:执行 SQL 之前调用 EXPLAIN 预估扫描行数,拦截会全表扫描、消耗巨大资源的 SQL,保护数据库;
- 错误自动重试:SQL 语法报错时,把报错信息丢回给大模型,让 AI 自动修正 SQL 再尝试一次;
- 结果可视化:返回数据自动生成简单图表。
八、总结
本篇完整讲解一套可落地的 Text‑to‑SQL 系统架构,核心要点复盘:
- 分层设计,职责隔离:Controller 接收请求,Service 编排业务逻辑,LlmService 封装大模型调用,每个模块只干自己的事,便于维护、单元测试;
- 区分单表 / 关联查询场景:两套独立 Prompt,减少上下文噪声,提升 SQL 生成准确率;
- 安全永远第一位:不要信任大模型输出!多层校验,数据库账号使用只读权限,规避删库风险;
- 做好容错处理:AI 输出格式混乱要清洗,网络、数据库异常统一捕获,不要把堆栈直接抛给前端;
- 兼顾用户体验:前端示例引导、展示生成 SQL,降低业务人员使用门槛;
- 预留扩展能力:策略模式、抽象接口,后续扩展多模型、RAG 元数据检索、多轮对话,不需要大规模重构原有代码。
Text‑to‑SQL 不是银弹,大模型幻觉问题客观存在,生产环境更多适合内部业务人员做自助分析,不建议直接面向外部用户。对于关键业务报表,依然需要人工核对 SQL 逻辑。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。
更多推荐
所有评论(0)