从 Demo 到生产:Text‑to‑SQL 系统架构实战|SpringBoot + 大模型前后端分离完整落地

📅 2026/8/22 18:09:47
从 Demo 到生产:Text‑to‑SQL 系统架构实战|SpringBoot + 大模型前后端分离完整落地
系列第三篇前面我们完成 Text‑to‑SQL 基础搭建、复杂多表关联查询数据库设计本篇聚焦工程化架构。很多同学跑通 Demo 之后直接上线结果遇到安全漏洞、大模型幻觉、接口报错、前端交互混乱等一堆线上问题。本文站在后端工程师视角拆解一套可部署上线的 Text‑to‑SQL 完整系统包含分层架构、代码实现、安全防护、性能调优、未来扩展思路。前言最近在做内部智能问数平台调研和实现了一版 Text‑to‑SQL 系统。相信很多小伙伴和我一样跟着教程跑通 Demo 时感觉一切美好输入一句自然语言大模型唰的一下吐出 SQL数据库返回结果页面展示数据整个流程行云流水。但是一旦往生产环境迁移坑就接踵而至大模型时不时返回DROP、ALTER这类危险语句一旦执行直接事故AI 输出格式乱七八糟带 markdown 代码块、多余前缀注释直接丢给数据库直接报语法错误单表查询、多表关联查询混在同一个接口业务逻辑耦合严重后续维护改不动没有统一异常处理LLM 调用超时、数据库报错直接抛出堆栈到前端页面没有缓存相同业务问题反复调用大模型 APIToken 成本暴涨接口响应慢前端简陋没有示例引导业务人员不知道该怎么提问使用门槛很高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 核心模块职责模块职责关键类 / 文件前端展示层页面渲染、用户交互、示例引导、结果表格渲染、复制 SQLindex.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 控制器层ControllerController 层只做三件事页面跳转路由、接收 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 MapString, Object query(RequestBody MapString, 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 MapString, Object query(RequestBody MapString, 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返回页面PostMappingResponseBody返回 JSONController 只做简单非空校验复杂业务校验、SQL 安全校验交给 Service统一 JSON 返回格式前端只需要一套逻辑处理成功 / 失败场景。2.2 业务服务层ServiceService 层是整个 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 MapString, Object querySingle(String question) { return executeQuery(question, QueryType.SINGLE); } /** * 关联查询入口 */ public MapString, Object queryJoin(String question) { return executeQuery(question, QueryType.JOIN); } /** * 通用查询执行逻辑 * * 流程 * 1. 调用LLM生成SQL * 2. 清理SQL格式 * 3. 安全校验 * 4. 执行查询 * 5. 封装结果 */ private MapString, Object executeQuery(String question, QueryType type) { MapString, 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: 执行查询 ListMapString, 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 ListMapString, Object getAllMembers() { ... } public ListMapString, Object getAllOrders() { ... } public ListMapString, 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 表1member会员表- id, name, level 表2orders订单表- id, member_id, total_amount, status 表3order_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 { // 构建请求 MapString, 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(); HttpResponseString 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展示会员、订单、明细数据单表检索/singlesingle.html会员表单表查询关联检索/joinjoin.html三表关联查询智能问答/chatchat.html通用问答默认关联查询前端的设计目标不仅仅是把请求发出去更要照顾业务用户体验直观展示当前场景下数据表关系让用户知道可以查哪些内容内置示例问题点击直接填充输入框并且发起查询降低使用门槛展示大模型生成后的清洗完成 SQL方便开发人员排查问题业务人员也可以看到背后执行逻辑动态渲染查询结果表格支持一键复制 SQL区分两套主题配色单表绿色渐变关联查询粉色渐变视觉上区分两种查询模式。3.2 关联检索页面实现!DOCTYPE html html langzh-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 classcontainer !-- 页面标题 -- div classheader h1 关联检索/h1 p支持会员、订单、商品多表关联查询/p /div !-- 导航栏 -- div classnav a href/ 数据总览/a a href/single 单表检索/a a href/join classactive 关联检索/a a href/chat 智能问答/a /div !-- 主查询卡片 -- div classcard !-- 表关系图示 -- div classschema-diagram h3 数据表关系/h3 div classtables div classtable-membermember (会员表)/div div classarrow1:N →/div div classtable-ordersorders (订单表)/div div classarrow1:N →/div div classtable-itemorder_item (明细表)/div /div /div !-- 问题输入区 -- div classinput-section textarea idquestionInput placeholder例如查询张三的所有已完成订单/textarea button onclicksubmitQuery() 智能查询/button /div !-- 示例问题 -- div classexamples h4 试试这些问题/h4 div classexample-list span onclickuseExample(this)查询张三的所有订单/span span onclickuseExample(this)统计每个会员的消费总额/span span onclickuseExample(this)查询数码类商品的订单/span /div /div !-- SQL展示区 -- div idsqlResult classsql-box styledisplay:none; h4 生成的SQL语句/h4 pre idsqlCode/pre button onclickcopySql() 复制SQL/button /div !-- 结果展示区 -- div idqueryResult classresult-box styledisplay:none; h4 查询结果/h4 div idresultTable/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 tabletheadtr; columns.forEach(col { html th${col}/th; }); html /tr/theadtbody; 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 >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 SetString 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 MapString, Object handleLlmException(LlmServiceException e) { return Map.of( success, false, error, AI服务异常 e.getMessage() ); } /** * 处理SQL执行异常 */ ExceptionHandler(SqlExecutionException.class) public MapString, Object handleSqlException(SqlExecutionException e) { return Map.of( success, false, error, 数据库错误 e.getMessage() ); } /** * 处理通用异常 */ ExceptionHandler(Exception.class) public MapString, 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.xml5.2 启动与测试命令# 1. 克隆项目 git clone https://github.com/your-repo/text2sql.git # 2. 配置环境变量设置大模型密钥不要硬编码 export SILICONFLOW_API_KEYyour_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 CacheString, ListMapString, Object cache CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(30, TimeUnit.MINUTES) .build(); /** * 带缓存的查询 */ public ListMapString, Object queryWithCache(String sql) { return cache.get(sql, () - jdbcTemplate.queryForList(sql) ); } }异步处理大模型 API 推理时间比较长可以使用 SpringAsync异步接口前端做轮询或者 WebSocket 推送结果用户页面不会卡住等待。Service public class AsyncQueryService { Async(taskExecutor) public CompletableFutureQueryResult 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这就是 RAGText‑to‑SQL 的经典方案。7.2 多模型支持策略模式切换模型使用策略模式抽象模型生成接口可以自由切换在线 API 模型、本地私有化部署开源模型不需要改动上层业务代码。/** * 多模型策略模式 */ public interface TextToSqlModel { String generateSql(String question, ListTableSchema 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 MapString, ListConversationMessage history new ConcurrentHashMap(); /** * 多轮查询 */ public MapString, Object multiTurnQuery( String sessionId, String question) { // 获取历史上下文 ListConversationMessage 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 逻辑。行文仓促定有不足之处欢迎各位朋友在评论区批评指正不胜感激。