ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot整合讯飞星火大模型:构建自然语言转SQL的数据分析助手

SpringBoot整合讯飞星火大模型:构建自然语言转SQL的数据分析助手 最近在搞内部数据平台的一个小需求运营同学天天找开发写 SQL 拉数重复又机械。我寻思正好把大模型接进来做一个人人都能查数的智能数据分析助手。项目代号就叫“星火助手”技术栈是讯飞星火大模型 SpringBoot核心链路一句话就能说清自然语言问题 - 大模型生成 SQL - 数据库执行 - 结果再交给大模型总结成人话。这个标题听起来挺唬人实际拆开也就三件事怎么把 SpringBoot 和大模型 API 接起来怎么让大模型产出可信的 SQL怎么把查询结果安全地跑出来。下面就从项目设计思路、环境准备、核心代码、安全校验到常见坑位一条龙讲清楚。适合正在做内部数据查询、报表平台或者想在自己系统里塞一个“问数机器人”的后端同学参考。1. 项目概览与整体设计思路1.1 这个助手到底能做什么先给个直观画面。以前运营想查“上个月华东区销量前 10 的商品”流程是提工单、找开发、写 SQL、跑数据、再回邮件运气好半小时运气不好半天。接上星火助手之后运营直接在对话框里把这句话打出来后端把这句话交给星火大模型模型基于我们配置的表结构返回一版 SELECT 语句程序校验通过后去 MySQL 执行再把查询结果交给模型做二次总结最后返回给前端“上个月华东区销量前 10 的商品分别是A 商品 1250 件、B 商品 986 件……整体环比增长 12.3%建议关注榜单头部商品的库存情况。”整个过程不到 10 秒。这就是这个项目的核心能力自然语言转 SQLNL2SQL 查询结果自动解读。它可以嵌入企业微信、飞书机器人也可以做成一个独立的前端页面算是用大模型做内部提效最典型的落地场景之一。1.2 架构设计和技术选型背后的考量先看整体链路用户请求打到 SpringBoot 接口接口组装 system prompt包含表结构说明和用户问题调用讯飞星火 API模型返回 JSON 格式的 SQL后端解析出来SQL 校验器做安全校验只允许 SELECT 类查询、禁止多条语句通过 JdbcTemplate 执行查询结果集转成 ListMap带着 SQL 和查询结果再次调用星火 API让模型生成业务结论返回给前端展示技术选型上我纠结过几个点第一大模型 API 为什么选星火而不是别的核心原因是国内访问稳定、接入门槛低而且它提供了 OpenAI 兼容的 HTTP 接口不需要去啃 WebSocket 那套复杂的鉴权流程。申请之后拿 APIKey 就能调对 SpringBoot 项目非常友好。如果你之前用过 OpenAI 的 message 结构上手星火基本零成本。第二后端框架为什么用 SpringBoot不用多解释生态成熟。数据源、连接池、事务、定时任务、监控都有现成方案团队招人也好招。做数据分析助手这类中后台服务SpringBoot 是最稳妥的选择没有必要为了“新”去冒险。第三为什么不要“前端直接把问题发给大模型、拿到 SQL 后由前端直连数据库”这个一定要控制住。前端直连数据库等于把连接串暴露给浏览器等于裸奔。而且大模型生成的 SQL 可能有幻觉必须有一层后端校验和拦截才能执行。数据库链接、权限、审计这些都应该留在服务端。这里用一个类比帮助你理解大模型就像一个“懂数据库但偶尔犯迷糊的实习生”。你给他一份清楚的表结构说明他能帮你写 SQL但你绝不能直接给他一个生产库账号让他随便跑。你得在旁边加一个“安全员”角色检查他写的每一句话确认没问题了才放行。后面代码里那个 SqlValidator就是这个安全员。2. 环境准备与项目初始化2.1 申请星火API密钥与开通服务这一步没什么技术含量但很多人卡在找入口上。我走了一遍完整流程打开讯飞开放平台控制台用手机号注册登录。在“创建新应用”里随便填个应用名称比如“数据分析助手”类型选 Web 后端创建完成。进入应用的“星火认知大模型”服务页面开通服务。新用户一般有免费额度个人学习完全够用。在服务详情里找到 APIKey、APISecret、APPID 三个值。确认接口调用地址。新版接口的 base 路径是https://spark-api-open.xf-yun.com/v1和 OpenAI 兼容的调用方式在同一个风格线上。这里强调三点APIKey 和 APISecret 等同于账号密码不要写死在代码里更不要提交到 Git。建议用环境变量注入或者接入 Nacos、Apollo 这类配置中心。模型版本要选对。免费额度对应的模型和个人申请擅长的模型可能不一致调用时报 model not found 或鉴权失败八成是这里没对上。可以参考下面这张表模型标识适用场景说明lite轻量问答、简单提取便宜速度快适合测试generalv3.0通用对话、NL2SQL性价比高大多数业务够了generalv3.5复杂推理、长文本准确率更高价格也高一些4.0Ultra复杂任务、高精度最新模型成本最高生产环境慎用我自己的项目中nl2sql 这种偏结构化的任务用generalv3.5效果最好虽然贵一点但 SQL 出错率低能省掉大把排查时间总结结果这种轻任务用generalv3.0就够。2.2 搭建SpringBoot工程与基础依赖工程可以从 IDEA 的 Spring Initializr 或 start.spring.io 生成然后手动加依赖。用 SpringBoot 2.7.18 作为基线版本兼容 JDK 8也方便团队老项目统一。pom.xml里除了常规的 web 和 jdbc 之外我加了 OkHttp 作为 HTTP 客户端Hutool 处理部分工具方法MySQL 驱动用 8.0 版本。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId version4.12.0/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies2.3 关于SpringBoot版本与JDK版本的忠告这里必须单独说一节因为我在部署阶段被版本问题坑得不轻。网上很多教程默认大家用 JDK 8 SpringBoot 2.x但现在新创建的项目一开 IDEA 就是 JDK 21Spring Initializr 默认拉到 SpringBoot 3.3/3.4。版本一下子跳上去两个问题立刻出现第一包名变了。SpringBoot 3.x 用的jakarta.*2.x 用的是javax.*。如果你参照 2.x 的代码写了import javax.annotation.Resource;在 3.x 项目里直接编译不过得改成jakarta.annotation.Resource。第二第三方依赖兼容性。SpringBoot 3.x 基于 Spring Framework 6要求 JDK 17 以上一些老的 MyBatis、ShardingSphere 版本没跟上运行期会报NoSuchMethodError或ClassNotFoundException。所以我的建议非常直接团队如果都是 JDK 8 背景项目用 2.7.18 最稳如果是新团队、新系统直接用 JDK 17 SpringBoot 3.2别上 JDK 21部分第三方工具在 21 上还有兼容性摩擦。版本匹配参考这个表使用场景JDK 版本SpringBoot 版本老项目维护、JDK8 环境82.7.x新内部系统、云原生173.1.x / 3.2.x追求最新特性213.3.x / 3.4.x配置文件我放在application.yml星火相关的配置统一用spark前缀方便后续用ConfigurationProperties装配。数据库账号单独建立一个只读账号密码从环境变量注入。spring: datasource: url: jdbc:mysql://localhost:3306/analytics?useUnicodetruecharacterEncodingutf8useSSLfalse username: readonly_user password: ${DB_PASSWORD} spark: api-key: ${SPARK_API_KEY} url: https://spark-api-open.xf-yun.com/v1/chat/completions model: generalv3.5 max-tokens: 2048 temperature: 0.3配置类的写法也很固定Data Component ConfigurationProperties(prefix spark) public class SparkProperties { private String apiKey; private String url; private String model; private Integer maxTokens; private Double temperature; }顺带说一句ConfigurationProperties是 SpringBoot 自动配置机制里很核心的一环它会把配置文件的属性绑定到 bean 上。原理是ConfigurationPropertiesBindingPostProcessor在 bean 初始化前解析注解再通过 Binder 把属性值写入对象字段。平时不用深究但面试常问知道这个流程就行。3. 核心代码实现对接星火 API3.1 从 HTTP 客户端开始老一代的星火接口是 WebSocket 协议需要先用 HMAC-SHA256 生成带签名的 URL再建立长连接接收流式数据代码非常绕。后来平台推出了 OpenAI 兼容的 HTTP 接口鉴权只需要在请求头加Authorization: Bearer {APIKey}消息体也是标准的messages结构对后端同学友好太多。我这里就用这个 HTTP 接口。HTTP 客户端选型上RestTemplate 虽然 Spring 自带但 OkHttp 在连接复用、超时控制、拦截器扩展上都更顺手所以我选用 OkHttp。先把三个基础的 DTO 定义出来代码很简单但代表请求与响应结构后续所有业务都建立在这几个类上Data public class ChatMessage { private String role; // system / user / assistant private String content; } Data public class ChatRequest { private String model; private Double temperature; private Integer max_tokens; private Boolean stream; private ListChatMessage messages; } Data public class ChatResponse { private String id; private Integer created; private String model; private ListChoice choices; Data public static class Choice { private Integer index; private Message message; } Data public static class Message { private String role; private String content; } }3.2 请求消息体与关键参数调用星火 API 时消息体有几个参数直接影响输出质量值得专门说明temperature控制随机性NL2SQL 这种确定性任务我设 0.1~0.3太高会让模型“发挥”出各种奇怪的 SQL文本总结可以提到 0.5 左右。max_tokens限制生成最大长度SQL 生成任务 1024 足够总结任务设 2048。stream是否流式返回。如果只是后端接口内部调用直接设 false 拿完整结果最省事如果要给前端实时展示打字机效果再开流式。messages与大模型对话的消息列表遵循 OpenAI 的格式。第一轮通常是 system定义大模型的角色和约束后续是 user 和 assistant 交替的上下文。封装好调用方法核心逻辑集中在SparkApiClient.chat()里负责拼装请求、发送、解析响应Service RequiredArgsConstructor public class SparkApiClient { private final SparkProperties properties; private final ObjectMapper objectMapper; private OkHttpClient buildClient() { return new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .build(); } public String chat(ListChatMessage messages) { ChatRequest request new ChatRequest(); request.setModel(properties.getModel()); request.setTemperature(properties.getTemperature()); request.setMax_tokens(properties.getMaxTokens()); request.setStream(false); request.setMessages(messages); String json objectMapper.writeValueAsString(request); RequestBody requestBody RequestBody.create( MediaType.parse(application/json; charsetutf-8), json); Request httpRequest new Request.Builder() .url(properties.getUrl()) .header(Authorization, Bearer properties.getApiKey()) .post(requestBody) .build(); try (Response response buildClient().newCall(httpRequest).execute()) { if (!response.isSuccessful()) { throw new RuntimeException(星火API调用失败: HTTP response.code() , body response.body().string()); } String responseBody response.body().string(); ChatResponse chatResponse objectMapper.readValue(responseBody, ChatResponse.class); if (chatResponse.getChoices() null || chatResponse.getChoices().isEmpty()) { throw new RuntimeException(星火API返回异常: responseBody); } return chatResponse.getChoices().get(0).getMessage().getContent(); } } }注意上面的Response是 OkHttp 的okhttp3.Response别和javax.servlet.http.HttpServletResponse混了不然编译报错半天找不到原因。3.3 流式输出与SSE方案如果你的业务需要“打字机”效果那就得用流式。HTTP 接口开启streamtrue时返回的是 SSE 格式的事件流每一行以data:开头。SpringBoot 里最方便的做法是用SseEmitter配合 OkHttp 的异步回调把模型吐出来的增量文本直接推给浏览器。GetMapping(value /chat-stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter chatStream(RequestParam String question) { SseEmitter emitter new SseEmitter(0L); // 不自动超时 // 异步线程里请求星火API拿到增量内容后 emitter.send()结束 emitter.complete() return emitter; }流式实现要处理的东西不少断线重连、Token 计数、上下文保留、并发 SseEmitter 的数量控制。我个人的建议是第一版先用非流式把链路跑通核心逻辑稳定后再考虑用户体验层面的流式输出。毕竟一个数据分析工具用户最关心的是答案准不准而不是字是不是一个一个蹦出来的。4. 数据分析助手的业务逻辑实现4.1 提示词工程让大模型生成靠谱的 SQL对接完 API真正的重头戏才开始。大模型能不能生成正确的 SQL80% 取决于你给的 system prompt 写得是否清晰。我的做法是维护一份“表结构说明”放进 system prompt 里这份说明要包含字段名、字段类型、字段含义和一个示例。以一张销售明细表为例我实际使用的 system prompt 模板如下你是一个数据分析助手。现在需要根据用户的业务问题生成 MySQL 查询语句。 数据库表结构如下 表名sales_detail 字段 - id BIGINT 主键 - order_no VARCHAR 订单编号 - region VARCHAR 区域如华东、华南、华北 - product_name VARCHAR 商品名称 - category VARCHAR 商品类目 - amount DECIMAL(10,2) 销售金额 - order_time DATETIME 下单时间 约束 1. 只能生成 SELECT 开头的查询语句禁止 DELETE、UPDATE、DROP、INSERT。 2. 只输出 JSON 格式{sql: 完整的SQL语句, reason: 生成依据} 3. 如果问题含义不明确输出{sql: null, reason: 具体不确定点} 4. 默认给查询结果加 LIMIT 100。 5. 当前日期可以认为是 2025-01-15时间筛选尽量结合上下问判断。为什么要把表结构写得这么细因为大模型并不知道你的业务库长什么样。你不告诉它region存的是“华东”而不是“east_china”它就可能写一个WHERE region east_china查出来结果为空还找不到原因。给足字段注释就是给模型一副“视力正常”的眼睛这是整个项目里性价比最高的一项投入。另外有个实用的 trick把用户问题和表名、字段名相关的历史正确 SQL 整理成 few-shot 示例放到 system prompt 末尾。比如“用户问‘华东销量’正确 SQL 是 SELECT SUM(amount) FROM sales_detail WHERE region华东”。模型看到几个样本之后生成质量会明显提升。4.2 安全执行SQL与查询结果处理大模型生成的 SQL 不能盲跑这是安全底线。我在执行前加了一道校验器核心逻辑有三层第一层语句白名单。只允许SELECT、SHOW、DESC、WITH开头的语句其他一律拦截。第二层多条语句拦截。按分号切分如果切分后的片段多于一段且不全是空串直接拒绝防止SELECT ...; DROP TABLE ...这类拼接攻击。第三层关键词黑名单。DELETE、UPDATE、DROP、ALTER、TRUNCATE、INSERT出现就拒绝。public class SqlValidator { private static final ListString DANGEROUS_KEYWORDS Arrays.asList(delete, update, drop, alter, truncate, insert, replace); public void validate(String sql) { String trimmed sql.trim(); String lower trimmed.toLowerCase(); String head lower.split(\\s)[0]; if (!head.startsWith(select) !head.startsWith(with) !head.startsWith(show) !head.startsWith(desc)) { throw new IllegalArgumentException(仅允许 SELECT 查询语句); } long semicolonCount trimmed.chars().filter(ch - ch ;).count(); if (semicolonCount 1) { throw new IllegalArgumentException(检测到多条SQL语句已拦截); } for (String keyword : DANGEROUS_KEYWORDS) { if (lower.contains(keyword)) { throw new IllegalArgumentException(检测到危险关键字: keyword); } } } }这套校验在常规场景够用但如果业务安全性要求高建议用 Druid 的 SQL 解析器做 AST 级校验它能把 SQL 解析成抽象语法树从语法层面判断是不是一条纯查询语句比字符串黑名单健壮得多。另外还有一个非常关键的提醒程序里配置的数据源账号必须是只读账号最好连SELECT INTO OUTFILE这类权限也去掉。即使前面所有校验都被绕过数据库层面上也不会造成破坏。执行 SQL 时我用 Spring 自带的JdbcTemplate查询结果直接转成ListMapString, Object这种结构后面给大模型做总结也方便。Service RequiredArgsConstructor public class DataAnalysisService { private static final String TABLE_SCHEMA_PROMPT ……上面那段表结构描述……; private final SparkApiClient sparkApiClient; private final JdbcTemplate jdbcTemplate; private final SqlValidator sqlValidator; public AnalysisResult analyze(String question) { // 1. 生成 SQL String sql generateSql(question); if (sql null) { return AnalysisResult.fail(问题描述不够明确请补充时间或区域条件); } // 2. 校验并执行 sqlValidator.validate(sql); ListMapString, Object rows jdbcTemplate.queryForList(sql); // 3. 二次调用模型生成结论 String summary summarize(sql, rows); return AnalysisResult.success(sql, rows, summary); } private String generateSql(String question) { ListChatMessage messages Arrays.asList( new ChatMessage(system, TABLE_SCHEMA_PROMPT), new ChatMessage(user, question) ); String content sparkApiClient.chat(messages); // 解析 content 里的 JSON拿 sql 字段 JsonNode node objectMapper.readTree(content); return node.has(sql) ? node.get(sql).asText() : null; } }4.3 把结果交给模型二次加工成结论很多人做到“生成 SQL、执行出结果”就停了返回一坨原始表格给前端。这样用户体验并不好用户是来问“怎么样”的不是来看几十行数字的。所以我对模型做了一个二次调用第一次生成 SQL第二次生成解读。第二次调用的 system prompt 大概是这个意思请根据下面的 SQL 和查询结果用中文生成简洁的业务分析结论。 要求 1. 提取关键数字如总额、TOP3、环比变化。 2. 说明数据反映的趋势或异常。 3. 不要编造数据只基于给定结果。 4. 控制在150字以内。这一步不复杂但带来的体验提升非常大。用户看到的不再是一堆行记录而是一段人话“上个月华东区销量 1250 件环比增长 12.3%其中 A 商品贡献 32%”。而且因为结论是基于真实查询结果生成的幻觉的概率比让大模型直接回答业务问题低得多。5. 常见问题与避坑实录5.1 鉴权失败与参数踩坑这一块基本是所有接星火 API 的人都绕不过去的。我整理了几个高频错误和解决办法直接贴表现象可能原因解决办法HTTP 401APIKey 错误或 Authorization 头格式不对检查授权头是否为Bearer 密钥注意大小写和空格报 model not found模型标识写错或该模型未开通在控制台确认开通的模型把model参数改成对应标识返回 403 code10013IP 白名单限制控制台配置服务器出口 IP或者临时关闭白名单测试输出内容为空 / 被截断max_tokens 太小、内容触发安全过滤增大 max_tokens或者改 prompt 说法调用报超时网络波动或生成时间过长调大 readTimeout关闭流式改同步并做重试我在项目联调阶段遇到最崩溃的一次是 401 持续出现检查了半小时最后发现配置文件里 APIKey 多了一个看不见的换行符。用环境变量注入密钥时务必检查一下是不是有隐形字符。5.2 SpringBoot版本太高引发的依赖冲突这部分是我基于实际踩坑整理的也是搜索热词里最多人求助的点。比如“现在的版本是21想回退到1.8”这个问题如果你用的是 IDEA可以在 Project Structure 里改 Project SDK 为 1.8同时把 pom 里的java.version属性改成 1.8并且确保spring-boot-starter-parent版本是 2.7.x。如果项目已经用了 SpringBoot 3.x 的代码风格直接回退会有一堆javax.*到jakarta.*的报错不是改个版本号就能解决。另外SpringBoot 3.x 里 MyBatis 相关的 starter 要使用mybatis-spring-boot-starter3.x 版本老版本 2.x 在 Spring 6 下会报factoryBeanObjectType之类的初始化错误。OkHttp 本身不依赖 Spring但如果项目里同时有老版本 okhttp 和新版本 okhttp类冲突会让编译器“找到了两个同名的类”运行期随机行为异常。统一用dependencyManagement控制版本是最省心的。5.3 调用超时、并发控制与成本控制接入大模型 API 和调用普通接口不一样响应时间通常在 3~10 秒服务端不能按普通的 1 秒超时去设置。我的 OkHttp 里连接超时 10 秒、读超时 60 秒接口层再配合异步化处理避免长连接占满 Tomcat 线程。成本控制也是生产环境必须考虑的问题。每次 NL2SQL 至少消耗 1~2 千 token二次总结再消耗几百 token并发高的时候一个月费用相当可观。我做了三件事来控制成本对用户问题做简单的规则预筛如果只是查“订单总数”这类固定模板直接走写死的 SQL不调用大模型。同一个用户同一个问题在项目里加了 Redis 缓存10 分钟内复用结果键名就是question model。并发控制用 Semaphore 限制同时调用星火 API 的最大请求数默认 5 个超出就排队或提示稍后重试防止突发的流量把调用额度打爆。6. 效果演示与后续扩展6.1 实际运行效果演示接口我只暴露了一个 POST/api/analysis/query请求体就是一个question字段。下面放两条真实测试记录用户问题生成的 SQL返回结论“今年1月销售总额是多少”SELECT SUM(amount) FROM sales_detail WHERE order_time BETWEEN 2025-01-01 AND 2025-01-312025年1月销售总额为 358.6 万元环比上月增长 6.2%“销量前十的商品是哪些”SELECT product_name, SUM(amount) AS total FROM sales_detail GROUP BY product_name ORDER BY total DESC LIMIT 10销量TOP3商品为A、B、CA贡献份额达28%头部商品集中度较高从维护者视角看最让我惊喜的不是 SQL 多完美而是当用户问题模糊时模型会按照 prompt 里的要求返回reason提示缺少时间范围。这比闷头查出一个空结果再让用户猜原因好太多。6.2 后续可以往哪些方向扩展第一版跑通之后能做的优化方向其实非常多把表结构映射做成可视化配置运营自己维护字段含义不用改代码。引入 RAG 思路把几十张表的 schema 向量化先检索出跟问题相关的表再动态注入 prompt而不是把所有表结构一股脑塞进去。表一多prompt 会超过上下文长度。前端用 ECharts 把查询结果直接渲染成柱状图、折线图结论文案配图表一起展示业务价值更大。在 SQL 执行层增加预计算缓存常见报表查询直接走结果缓存大模型只在缓存失效时才介入。我在实际项目中最大的体会是接入大模型 API 只是整个项目最轻松的一步真正花时间的全在“让模型更懂你的业务”和“让结果更可信”这两件事上。表结构维护得越细few-shot 示例准备得越充分模型的表现就越好。而安全校验、成本控制、结果缓存这些工程化能力才是项目能不能真正上线的关键。最后再分享一个小技巧不要在项目一开始就追求所有表都能查。先挑三张核心业务表手动造 20 组标准问答对测试效果满意后再逐步扩表。这样既能让老业务尽快用起来也方便你观察哪些表结构描述方式容易让模型跑偏迭代 prompt 的效率会高很多。
返回列表