目录
[1.1 什么是 Text-to-SQL](#1.1 什么是 Text-to-SQL)
[1.2 系统核心功能](#1.2 系统核心功能)
[1.3 技术选型说明](#1.3 技术选型说明)
[2.1 开发环境硬性要求](#2.1 开发环境硬性要求)
[2.2 PostgreSQL 数据库初始化](#2.2 PostgreSQL 数据库初始化)
[2.3 硅基流动 API Key 申请步骤](#2.3 硅基流动 API Key 申请步骤)
[四、Maven 依赖与全局配置](#四、Maven 依赖与全局配置)
[4.1 pom.xml 完整依赖](#4.1 pom.xml 完整依赖)
[4.2 application.yml 配置详解](#4.2 application.yml 配置详解)
[5.1 schema.sql 建表语句](#5.1 schema.sql 建表语句)
[5.2 data.sql 初始化测试数据](#5.2 data.sql 初始化测试数据)
[6.1 Member.java 实体映射类](#6.1 Member.java 实体映射类)
[6.2 MemberRepository 数据访问接口](#6.2 MemberRepository 数据访问接口)
[七、核心:硅基流动大模型 API 对接服务 LlmService](#七、核心:硅基流动大模型 API 对接服务 LlmService)
[八、核心业务整合服务 Text2SqlService](#八、核心业务整合服务 Text2SqlService)
[九、Controller 页面路由与 API 接口](#九、Controller 页面路由与 API 接口)
[10.1 首页 index.html(会员数据总览)](#10.1 首页 index.html(会员数据总览))
[10.2 问答页面 chat.html(核心交互页面)](#10.2 问答页面 chat.html(核心交互页面))
[11.1 项目核心亮点](#11.1 项目核心亮点)
[11.2 开发踩坑 FAQ](#11.2 开发踩坑 FAQ)
[Q1:启动项目时报 PostgreSQL 连接失败?](#Q1:启动项目时报 PostgreSQL 连接失败?)
[Q2:大模型返回 ERROR:API 调用失败?](#Q2:大模型返回 ERROR:API 调用失败?)
[Q3:生成的 SQL 查询不出数据?](#Q3:生成的 SQL 查询不出数据?)
前言
最近公司内部有个需求:业务同事不会写 SQL,每次查会员数据都要找后端开发帮忙,来回沟通效率极低。想着能不能做一套自然语言转 SQL 的小工具,业务人员输入中文描述就能自动查数据库,不用懂任何数据库语法。调研了一圈方案,本地部署大模型硬件成本太高、推理速度慢,国内可直接调用的公有大模型 API 里硅基流动性价比不错,对中文 SQL 生成适配很好,搭配 SpringBoot+PostgreSQL 就能快速落地。

本篇文章我会把完整搭建流程、踩坑细节、安全处理逻辑全部整理出来,从环境准备、数据库建表、后端分层开发、大模型 API 对接,再到前端问答页面,一套完整流程直接照着跑就能出效果,新手也能跟着实现。
一、项目整体介绍
1.1 什么是 Text-to-SQL
简单说就是自然语言转结构化 SQL 语句 ,非技术人员只用大白话描述查询需求,后端对接大模型自动翻译成标准 SQL,执行后把表格数据返回前端展示。 举个实际场景:业务输入 "查询所有积分超过 1 万的金卡会员",系统自动生成SELECT * FROM member WHERE level = '金卡会员' AND points > 10000并展示匹配数据,完全不用人工写查询语句。
1.2 系统核心功能
这套小系统主要做了 5 个实用能力,满足基础业务查询场景:
- 自然语言智能问答:中文提问自动生成 SQL 并返回数据表结果
- 会员数据总览页面:打开首页直接查看全量会员数据,简单统计总条数
- SQL 可视化展示:大模型生成的原始 SQL 清理后完整展示,支持复制复用
- SQL 安全拦截机制:强制只允许 SELECT 查询,杜绝删表、改数据等危险操作,防止注入风险
- 自适应前端页面:原生 JS+Thymeleaf 实现,电脑、平板打开都能正常使用
1.3 技术选型说明
选型没有追求花里胡哨的框架,选用成熟稳定、上手门槛低的技术,方便后续二次改造:
| 分层 | 技术栈 | 版本 | 选择理由 |
|---|---|---|---|
| 后端主框架 | SpringBoot | 3.2.0 | 生态完善,Web、JPA、JDBC 一键集成,企业主流技术栈 |
| 数据库 | PostgreSQL | 14 及以上 | 开源免费,支持复杂查询、中文注释、自增序列,企业数据分析常用 |
| ORM 层 | Spring Data JPA | 无指定版本 | 简化单表 CRUD 开发,不用手写基础 SQL |
| 原生 SQL 执行 | JdbcTemplate | 内置 | 大模型生成动态 SQL 后,需要原生执行工具返回表格数据 |
| 页面模板 | Thymeleaf | 内置 | 服务端渲染页面,不用前后端分离,小型工具开发更轻量化 |
| 大模型服务 | 硅基流动 API | 在线调用 | 国内合规大模型服务,中文理解强,SQL 生成准确率高,不用本地部署 |
| 前端交互 | 原生 JavaScript | 无框架 | 无需引入 Vue/React,减少打包、跨域等额外问题,快速实现问答交互 |
| 代码简化工具 | Lombok | 最新稳定版 | 省去实体类 get/set/toString 冗余代码 |
二、本地环境准备
2.1 开发环境硬性要求
- JDK 版本:最低 JDK17,推荐 JDK21(SpringBoot3.x 强制要求高版本 JDK,低版本会直接启动报错)
- 构建工具:Maven3.8+ 或者 Gradle8+,本文全程使用 Maven
- 数据库:PostgreSQL14 及以上,本地安装或者 Docker 启动均可
- 开发工具:IDEA2023 以上版本最佳,VS Code 搭配 Java 插件也能开发
2.2 PostgreSQL 数据库初始化
本地装好 PostgreSQL 后,打开数据库客户端(pgAdmin/psql 命令行)执行下面 SQL,新建专属数据库和用户,避免和本地其他业务库冲突:
sql
-- 创建项目专用数据库
CREATE DATABASE llm_texttosql;
-- 创建数据库访问用户
CREATE USER postgres WITH PASSWORD 'postgres';
-- 给用户分配该库全部操作权限
GRANT ALL PRIVILEGES ON DATABASE llm_texttosql TO postgres;
踩坑提醒:很多新手直接用 postgres 默认库存业务表,后期多项目开发容易表名冲突,单独建库是好习惯。
2.3 硅基流动 API Key 申请步骤
想要调用大模型生成 SQL,必须先拿到接口密钥,步骤很简单:
- 浏览器打开硅基流动官网,完成手机号注册登录
- 进入控制台 - API 密钥管理,复制生成专属 Key(注意妥善保存,只展示一次)
- 模型选择:新手推荐
tencent/Hunyuan-MT-7B或Qwen/Qwen2.5-7B-Instruct,对中文 SQL 适配度最高 - 计费说明:新用户一般赠送免费调用额度,测试完全够用,正式使用按需充值
三、项目完整目录结构
标准 SpringBoot 分层架构,严格按照 Controller-Service-Repository 分层,资源文件分类存放,后续扩展多表、多接口不会混乱:
bash
llm-text-to-sql/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/text2sql/
│ │ │ ├── Text2SqlApplication.java # 项目启动类
│ │ │ ├── config/
│ │ │ │ └── WebConfig.java # Web扩展配置(本文基础版暂未拓展,预留扩展)
│ │ │ ├── controller/
│ │ │ │ └── Text2SqlController.java # 页面路由+前后端API接口
│ │ │ ├── entity/
│ │ │ │ └── Member.java # 会员数据库实体映射类
│ │ │ ├── repository/
│ │ │ │ └── MemberRepository.java # JPA数据访问层
│ │ │ └── service/
│ │ │ ├── LlmService.java # 硅基流动大模型API对接核心类
│ │ │ └── Text2SqlService.java # 业务核心整合服务(LLM+SQL执行+安全校验)
│ │ └── resources/
│ │ ├── application.yml # 全局配置文件:数据库、大模型参数全部写在这里
│ │ ├── schema.sql # 项目启动自动执行建表语句
│ │ ├── data.sql # 测试会员初始化数据
│ │ └── templates/
│ │ ├── index.html # 首页:会员数据总览页面
│ │ └── chat.html # 智能问答交互页面
└── pom.xml # Maven依赖管理文件
四、Maven 依赖与全局配置
4.1 pom.xml 完整依赖
所有用到的依赖全部贴出,直接复制替换项目 pom.xml 即可,无多余冗余包:
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>text2sql</artifactId>
<version>1.0.0</version>
<name>text2sql</name>
<description>Text-to-SQL会员智能问答系统</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- SpringBoot Web容器,提供接口访问能力 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf页面模板引擎,渲染前端html -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Spring Data JPA,简化单表CRUD开发 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- JdbcTemplate,执行大模型生成的动态SQL -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- PostgreSQL数据库驱动,runtime运行时生效 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok简化实体类代码,不用手动写get/set -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2 application.yml 配置详解
所有配置做了详细注释,新手能看懂每一项作用,注意替换自己的硅基流动 API Key:
javascript
# 服务端口配置,访问地址localhost:8080
server:
port: 8080
spring:
application:
name: text2sql-llm-demo
# PostgreSQL数据库连接配置,和前面初始化库对应
datasource:
url: jdbc:postgresql://localhost:5432/llm_texttosql
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
# JPA持久化配置
jpa:
hibernate:
ddl-auto: none # 关闭自动建表,统一使用schema.sql脚本管理表结构,线上更安全
show-sql: true # 控制台打印执行的SQL,调试排错很方便
properties:
hibernate:
format_sql: true # 格式化打印SQL,不会挤成一行
dialect: org.hibernate.dialect.PostgreSQLDialect
# 项目启动自动执行初始化SQL脚本
sql:
init:
mode: always # 每次重启项目都执行脚本,测试环境使用;生产建议改成embedded
schema-locations: classpath:schema.sql
data-locations: classpath:data.sql
# Thymeleaf页面配置,开发环境关闭缓存,修改html不用重启项目
thymeleaf:
cache: false
prefix: classpath:/templates/
suffix: .html
mode: HTML
encoding: UTF-8
# 硅基流动大模型API专属配置,重点替换api.key
siliconflow:
api:
url: https://api.siliconflow.cn/v1/chat/completions
key: 你的硅基流动后台复制的API Key
model: tencent/Hunyuan-MT-7B
开发踩坑点:线上环境一定要把
sql.init.mode改成embedded,否则每次重启都会重复插入测试数据;同时不要把 API Key 硬编码提交到代码仓库,正式环境推荐使用环境变量注入。
五、数据库表结构与测试数据
5.1 schema.sql 建表语句
设计一张会员业务表,覆盖姓名、等级、积分、入会时间等常用查询维度,增加字段注释、索引提升查询效率:
sql
-- 创建会员信息业务表
CREATE TABLE IF NOT EXISTS member (
id SERIAL PRIMARY KEY, -- 自增主键,会员唯一ID
name VARCHAR(50) NOT NULL, -- 会员姓名,非空
gender VARCHAR(10), -- 性别:男/女
age INTEGER, -- 年龄
phone VARCHAR(20) UNIQUE, -- 手机号,唯一约束,防止重复录入
email VARCHAR(100), -- 联系邮箱
level VARCHAR(20) DEFAULT '普通会员', -- 会员等级:普通/银卡/金卡/钻石
points INTEGER DEFAULT 0, -- 账户积分
join_date DATE, -- 入会日期
status VARCHAR(20) DEFAULT '活跃', -- 账号状态:活跃/冻结/注销
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- PostgreSQL专属字段中文注释,数据库客户端可直接查看业务含义
COMMENT ON TABLE member IS '门店会员信息主表';
COMMENT ON COLUMN member.id IS '会员主键ID';
COMMENT ON COLUMN member.name IS '会员真实姓名';
COMMENT ON COLUMN member.gender IS '会员性别';
COMMENT ON COLUMN member.age IS '会员年龄';
COMMENT ON COLUMN member.phone IS '绑定手机号';
COMMENT ON COLUMN member.email IS '预留邮箱';
COMMENT ON COLUMN member.level IS '会员会员等级';
COMMENT ON COLUMN member.points IS '累计消费积分';
COMMENT ON COLUMN member.join_date IS '首次入会时间';
COMMENT ON COLUMN member.status IS '账号使用状态';
COMMENT ON COLUMN member.created_at IS '数据创建时间';
-- 建立常用查询字段索引,大数据量下提升查询速度
CREATE INDEX IF NOT EXISTS idx_member_level ON member(level);
CREATE INDEX IF NOT EXISTS idx_member_points ON member(points);
5.2 data.sql 初始化测试数据
批量插入多条测试会员数据,加入ON CONFLICT冲突判断,避免项目重复启动报唯一键冲突:
sql
-- 初始化会员数据(使用ON CONFLICT避免重复插入)
INSERT INTO member (name, gender, age, phone, email, level, points, join_date, status) VALUES
('张三', '男', 28, '13800138001', 'zhangsan@example.com', '金卡会员', 15800, '2023-01-15', '活跃'),
('李四', '女', 35, '13800138002', 'lisi@example.com', '钻石会员', 32000, '2022-03-20', '活跃'),
('王五', '男', 22, '13800138003', 'wangwu@example.com', '普通会员', 500, '2024-01-10', '活跃'),
('赵六', '女', 41, '13800138004', 'zhaoliu@example.com', '银卡会员', 8200, '2023-06-01', '活跃'),
('钱七', '男', 30, '13800138005', 'qianqi@example.com', '金卡会员', 12500, '2023-08-15', '活跃'),
('孙八', '女', 27, '13800138006', 'sunba@example.com', '普通会员', 1200, '2024-02-20', '活跃'),
('周九', '男', 45, '13800138007', 'zhoujiu@example.com', '钻石会员', 45000, '2021-11-05', '活跃'),
('吴十', '女', 19, '13800138008', 'wushi@example.com', '普通会员', 200, '2024-05-01', '活跃'),
('郑十一', '男', 33, '13800138009', 'zheng11@example.com', '银卡会员', 6800, '2023-04-10', '冻结'),
('冯十二', '女', 38, '13800138010', 'feng12@example.com', '金卡会员', 18900, '2022-12-25', '活跃'),
('陈十三', '男', 25, '13800138011', 'chen13@example.com', '普通会员', 800, '2024-03-18', '活跃'),
('褚十四', '女', 42, '13800138012', 'chu14@example.com', '钻石会员', 52000, '2021-06-30', '活跃'),
('卫十五', '男', 29, '13800138013', 'wei15@example.com', '金卡会员', 14200, '2023-09-12', '活跃'),
('蒋十六', '女', 36, '13800138014', 'jiang16@example.com', '银卡会员', 9500, '2023-05-08', '活跃'),
('沈十七', '男', 23, '13800138015', 'shen17@example.com', '普通会员', 350, '2024-04-22', '注销'),
('韩十八', '女', 48, '13800138016', 'han18@example.com', '钻石会员', 61000, '2020-08-14', '活跃'),
('杨十九', '男', 31, '13800138017', 'yang19@example.com', '金卡会员', 16500, '2023-07-19', '活跃'),
('朱二十', '女', 26, '13800138018', 'zhu20@example.com', '普通会员', 950, '2024-01-05', '活跃'),
('秦廿一', '男', 39, '13800138019', 'qin21@example.com', '银卡会员', 7800, '2023-02-28', '活跃'),
('尤廿二', '女', 34, '13800138020', 'you22@example.com', '金卡会员', 13800, '2023-10-11', '活跃')
ON CONFLICT (phone) DO NOTHING;
六、实体类与数据访问层代码
6.1 Member.java 实体映射类
使用 Lombok 简化代码,字段和数据库一一对应,区分日期、时间类型:
java
package com.example.text2sql.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 会员信息实体类
*
* 对应数据库表:member
*
* 字段说明:
* - id: 会员ID,主键自增
* - name: 会员姓名
* - gender: 性别(男/女)
* - age: 年龄
* - phone: 手机号(唯一)
* - email: 邮箱
* - level: 会员等级(普通会员/银卡会员/金卡会员/钻石会员)
* - points: 积分
* - joinDate: 入会日期
* - status: 状态(活跃/冻结/注销)
* - createdAt: 创建时间
*/
@Entity
@Table(name = "member")
@Data // Lombok注解,自动生成getter/setter/toString等方法
public class Member {
/**
* 会员ID,主键,自增
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 会员姓名,非空,最大长度50
*/
@Column(name = "name", nullable = false, length = 50)
private String name;
/**
* 性别,可选值:男/女,最大长度10
*/
@Column(name = "gender", length = 10)
private String gender;
/**
* 年龄
*/
@Column(name = "age")
private Integer age;
/**
* 手机号,唯一约束,最大长度20
*/
@Column(name = "phone", length = 20)
private String phone;
/**
* 邮箱,最大长度100
*/
@Column(name = "email", length = 100)
private String email;
/**
* 会员等级
* 可选值:普通会员、银卡会员、金卡会员、钻石会员
* 默认值:普通会员
*/
@Column(name = "level", length = 20)
private String level;
/**
* 积分,默认值0
*/
@Column(name = "points")
private Integer points;
/**
* 入会日期
*/
@Column(name = "join_date")
private LocalDate joinDate;
/**
* 会员状态
* 可选值:活跃、冻结、注销
* 默认值:活跃
*/
@Column(name = "status", length = 20)
private String status;
/**
* 记录创建时间,默认当前时间
*/
@Column(name = "created_at")
private LocalDateTime createdAt;
}
6.2 MemberRepository 数据访问接口
继承 JpaRepository 自带基础 CRUD,额外扩展几个常用条件查询方法,方便首页数据统计:
java
package com.example.text2sql.repository;
import com.example.text2sql.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 会员数据访问接口
*
* 功能说明:
* - 基于Spring Data JPA提供Member实体的CRUD操作
* - 继承JpaRepository,自动获得以下方法:
* - findById(id): 根据ID查找会员
* - findAll(): 查找所有会员
* - save(member): 保存会员
* - deleteById(id): 根据ID删除会员
* - count(): 统计会员数量
*
* 自定义查询方法(Spring Data JPA自动生成SQL):
* - findByLevel(level): 根据会员等级查询
* - findByStatus(status): 根据状态查询
* - findByGender(gender): 根据性别查询
* - findByAgeGreaterThan(age): 查询年龄大于指定值的会员
* - findByPointsGreaterThan(points): 查询积分大于指定值的会员
*/
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
/**
* 根据会员等级查询会员列表
*
* @param level 会员等级(普通会员/银卡会员/金卡会员/钻石会员)
* @return 该等级的会员列表
*/
List<Member> findByLevel(String level);
/**
* 根据会员状态查询会员列表
*
* @param status 会员状态(活跃/冻结/注销)
* @return 该状态的会员列表
*/
List<Member> findByStatus(String status);
/**
* 根据性别查询会员列表
*
* @param gender 性别(男/女)
* @return 该性别的会员列表
*/
List<Member> findByGender(String gender);
/**
* 查询年龄大于指定值的会员
*
* @param age 年龄阈值
* @return 年龄大于指定值的会员列表
*/
List<Member> findByAgeGreaterThan(Integer age);
/**
* 查询积分大于指定值的会员
*
* @param points 积分阈值
* @return 积分大于指定值的会员列表
*/
List<Member> findByPointsGreaterThan(Integer points);
}
七、核心:硅基流动大模型 API 对接服务 LlmService
这是整个项目最核心的类,负责组装提示词、发起 HTTP 请求调用大模型、解析返回的 SQL 语句,每一步都加了异常捕获,接口调用失败会返回明确错误标识,方便前端提示用户。
sql
package com.example.text2sql.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 大模型对接服务:封装硅基流动API所有交互逻辑
*/
@Service
public class LlmService {
// 从yml配置文件读取大模型接口参数
@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;
// 构造注入JSON序列化工具
public LlmService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
/**
* 接收用户自然语言,调用大模型生成PostgreSQL标准SQL
* @param question 用户输入中文查询问题
* @return 生成SQL,异常统一返回ERROR:开头错误信息
*/
public String textToSql(String question) {
// 系统提示词:告诉大模型数据库表结构、输出规范,是SQL生成准确率关键
String systemPrompt = """
你是专业PostgreSQL SQL生成助手,严格根据下方member表结构,把用户中文问题转换成可直接执行的SQL语句。
数据库表member完整结构:
- id: 会员ID (SERIAL PRIMARY KEY)
- name: 会员姓名 (VARCHAR(50))
- gender: 性别 (VARCHAR(10)) 可选值仅:男、女
- age: 年龄 (INTEGER)
- phone: 手机号 (VARCHAR(20))
- email: 邮箱 (VARCHAR(100))
- level: 会员等级 (VARCHAR(20)) 可选值:普通会员、银卡会员、金卡会员、钻石会员
- points: 累计积分 (INTEGER)
- join_date: 入会日期 (DATE)
- status: 账号状态 (VARCHAR(20)) 可选值:活跃、冻结、注销
- created_at: 创建时间 (TIMESTAMP)
强制输出要求:
1. 只输出纯SQL语句,不要任何解释、说明文字
2. 严格遵循PostgreSQL语法,中文字符串用单引号包裹
3. 用户问题无法生成有效查询时,直接返回固定文本:ERROR:无法理解的问题
4. 禁止生成DELETE、UPDATE、DROP、ALTER等修改、删除类SQL,只输出SELECT相关语句
""";
try {
// 组装大模型请求体
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", model);
// 系统角色消息:表结构+规则约束
Map<String, String> systemMsg = Map.of("role", "system", "content", systemPrompt);
// 用户提问消息
Map<String, String> userMsg = Map.of("role", "user", "content", question);
requestBody.put("messages", List.of(systemMsg, userMsg));
requestBody.put("max_tokens", 512);
// temperature越低,输出结果越固定、不会随意发挥,SQL场景建议0.1
requestBody.put("temperature", 0.1);
// JSON序列化请求参数
String jsonReq = objectMapper.writeValueAsString(requestBody);
// 创建HTTP客户端发起接口调用
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30))
.build();
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(jsonReq))
.build();
// 接收接口返回结果
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
// 接口正常返回200状态码,解析生成的SQL
if (response.statusCode() == 200) {
JsonNode root = objectMapper.readTree(response.body());
JsonNode choices = root.get("choices");
if (choices != null && choices.isArray() && choices.size() > 0) {
return choices.get(0).get("message").get("content").asText().trim();
}
return "ERROR:大模型返回数据格式异常";
} else {
// 接口调用失败,返回状态码方便排查
return "ERROR:API调用失败,HTTP状态码:" + response.statusCode();
}
} catch (Exception e) {
// 捕获所有网络、序列化异常,统一封装错误信息
return "ERROR:" + e.getMessage();
}
}
}
开发重点说明
- 提示词设计:完整把表字段、枚举值、语法规则全部传给大模型,是避免生成错误 SQL 的核心;如果后期 SQL 经常出错,优先优化提示词,而非调整代码。
- temperature 参数:文本创作场景会调高到 0.7-1,但是 SQL 生成需要精准,设置 0.1 限制模型随机发挥。
- 统一错误前缀 :所有异常都用
ERROR:开头,上层业务服务直接判断前缀就能区分正常 SQL 和错误信息,逻辑更简洁。
八、核心业务整合服务 Text2SqlService
串联大模型调用、SQL 清洗、安全校验、数据库执行整套流程,增加安全拦截逻辑,防止用户诱导大模型生成删改数据 SQL,是保障系统安全的关键层。
sql
package com.example.text2sql.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* Text-to-SQL整合业务服务,串联大模型、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;
}
/**
* 完整问答处理主流程
* 1.调用大模型生成SQL → 2.清理SQL多余标记 → 3.安全校验只允许SELECT → 4.执行查询返回数据
* @param question 用户输入中文问题
* @return 包含原始问题、生成SQL、查询结果、成功/失败标识的Map
*/
public Map<String, Object> query(String question) {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("question", question);
// 第一步:调用大模型获取SQL
String rawSql = llmService.textToSql(question);
resultMap.put("generatedSql", rawSql);
// 判断大模型是否返回错误
if (rawSql.startsWith("ERROR")) {
resultMap.put("success", false);
resultMap.put("error", rawSql);
return resultMap;
}
// 第二步:清洗SQL,去除AI附带的markdown代码块标记
String cleanSql = cleanSqlText(rawSql);
resultMap.put("cleanedSql", cleanSql);
try {
// 第三步:安全校验,拦截非查询类SQL
if (!checkSqlSafe(cleanSql)) {
resultMap.put("success", false);
resultMap.put("error", "安全拦截:系统仅支持SELECT查询语句,禁止修改/删除数据操作");
return resultMap;
}
// 第四步:执行动态SQL,返回表格数据
List<Map<String, Object>> dataList = jdbcTemplate.queryForList(cleanSql);
resultMap.put("success", true);
resultMap.put("data", dataList);
resultMap.put("count", dataList.size());
} catch (Exception e) {
// SQL语法错误、表不存在等数据库异常捕获
resultMap.put("success", false);
resultMap.put("error", "SQL执行失败:" + e.getMessage());
}
return resultMap;
}
/**
* 清理大模型返回SQL附带的多余符号,比如```sql、```、SQL:前缀
*/
private String cleanSqlText(String sql) {
if (sql == null || sql.isEmpty()) return "";
sql = sql.replaceAll("```sql", "").replaceAll("```", "").trim();
if (sql.toLowerCase().startsWith("sql:")) {
sql = sql.substring(4).trim();
}
return sql;
}
/**
* SQL安全校验:仅允许SELECT开头,支持WITH子句CTE查询
* 拦截UPDATE/DELETE/DROP/ALTER等危险操作,规避数据安全风险
*/
private boolean checkSqlSafe(String sql) {
String upperSql = sql.toUpperCase().trim();
return upperSql.startsWith("SELECT") || upperSql.startsWith("WITH");
}
/**
* 查询全部会员数据,首页总览页面使用
*/
public List<Map<String, Object>> getAllMemberData() {
return jdbcTemplate.queryForList("SELECT * FROM member ORDER BY id ASC");
}
}
安全逻辑重点说明
很多新手做 Text-to-SQL 项目会忽略安全问题,直接执行大模型返回的 SQL,一旦有人刻意诱导大模型生成DROP TABLE member,整张业务表会直接删除。本文做了两层防护:
- 提示词约束大模型禁止生成修改类 SQL;
- 代码层二次校验 SQL 开头关键字,双重拦截,避免数据事故。
九、Controller 页面路由与 API 接口
区分页面跳转接口和 JSON 数据接口,使用 @Controller 返回页面,@ResponseBody 返回 JSON,接口注释清晰,方便后续对接前端或者第三方系统。
java
package com.example.text2sql.controller;
import com.example.text2sql.service.Text2SqlService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 系统控制器:页面跳转、前端问答API统一处理
*/
@Controller
public class Text2SqlController {
private final Text2SqlService text2SqlService;
public Text2SqlController(Text2SqlService text2SqlService) {
this.text2SqlService = text2SqlService;
}
/**
* 首页:会员数据总览页面
*/
@GetMapping("/")
public String indexPage(Model model) {
List<Map<String, Object>> allMember = text2SqlService.getAllMemberData();
model.addAttribute("members", allMember);
model.addAttribute("totalCount", allMember.size());
return "index";
}
/**
* 智能问答聊天页面
*/
@GetMapping("/chat")
public String chatPage() {
return "chat";
}
/**
* 问答核心API接口,前端AJAX异步调用
* 请求体:{"question":"查询钻石会员"}
*/
@PostMapping("/api/query")
@ResponseBody
public Map<String, Object> queryData(@RequestBody Map<String, String> request) {
String question = request.get("question");
if (question == null || question.trim().length() == 0) {
return Map.of("success", false, "error", "输入内容不能为空,请描述你的查询需求");
}
return text2SqlService.query(question);
}
/**
* 获取全量会员数据接口,可供第三方调用
*/
@GetMapping("/api/members")
@ResponseBody
public List<Map<String, Object>> getMemberList() {
return text2SqlService.getAllMemberData();
}
}
十、前端页面代码实现
10.1 首页 index.html(会员数据总览)
使用 Thymeleaf 循环渲染会员表格,简单统计会员总数,页面样式简洁适配办公场景。
html
<body>
<div class="header">
<h1>📊 全部会员数据总览</h1>
<div class="nav">
<a href="/chat">进入AI智能问答</a>
</div>
</div>
<div class="count-card">
<h3>当前会员总条数:<span th:text="${totalCount}">0</span></h3>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>性别</th>
<th>年龄</th>
<th>手机号</th>
<th>会员等级</th>
<th>累计积分</th>
<th>账号状态</th>
<th>入会日期</th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${members}">
<td th:text="${item.id}"></td>
<td th:text="${item.name}"></td>
<td th:text="${item.gender}"></td>
<td th:text="${item.age}"></td>
<td th:text="${item.phone}"></td>
<td th:text="${item.level}"></td>
<td th:text="${item.points}"></td>
<td th:text="${item.status}"></td>
<td th:text="${item.join_date}"></td>
</tr>
</tbody>
</table>
</body>
展示效果如下:


10.2 问答页面 chat.html(核心交互页面)
原生 JS 实现表单提交、异步请求、结果渲染,自带示例快捷提问按钮,增加 HTML 转义函数防止 XSS 攻击,展示生成 SQL 和查询表格。
javascript
<script>
// 填充示例问题到输入框
function fillExample(text) {
document.getElementById("questionInput").value = text;
}
// 表单提交监听
const form = document.getElementById("queryForm");
form.addEventListener("submit", async function(e) {
e.preventDefault();
const inputVal = document.getElementById("questionInput").value.trim();
if (!inputVal) {
alert("请输入查询问题!");
return;
}
try {
// 调用后端查询接口
const res = await fetch("/api/query", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({question: inputVal})
});
const result = await res.json();
renderResult(inputVal, result);
} catch (err) {
alert("网络请求失败:" + err.message);
}
});
// 渲染查询结果到页面
function renderResult(question, res) {
let htmlStr = `<div class="result-item">
<div class="question">❓ 你的问题:${htmlEscape(question)}</div>`;
if (res.success) {
htmlStr += `
<div>生成SQL语句:</div>
<div class="sql-block">${htmlEscape(res.cleanedSql)}</div>
<div>匹配到${res.count}条数据</div>
<table>
<thead>
<tr>
${Object.keys(res.data[0] || {}).map(k => `<th>${htmlEscape(k)}</th>`).join("")}
</tr>
</thead>
<tbody>
${res.data.map(row => `
<tr>
${Object.values(row).map(v => `<td>${htmlEscape(v || "")}</td>`).join("")}
</tr>
`).join("")}
</tbody>
</table>
`;
} else {
htmlStr += `<div class="error-text">❌ 查询失败:${htmlEscape(res.error)}</div>`;
}
htmlStr += "</div>";
// 最新结果插入最上方
document.getElementById("resultContainer").insertAdjacentHTML("afterbegin", htmlStr);
}
// HTML转义,防止XSS注入
function htmlEscape(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
</script>


十一、项目总结
11.1 项目核心亮点
- 完整可落地:从数据库建表、后端分层、大模型对接、前端页面全套代码,复制即可运行,无缺失模块;
- 双层 SQL 安全防护:提示词约束 + 代码关键字拦截,杜绝删改表等高危操作,企业内部使用更放心;
- 轻量化无复杂依赖:不引入 Vue、Redis、消息队列等重型组件,小型工具快速开发部署;
- 用户友好前端:自带示例快捷提问,自动展示生成 SQL,业务人员可以复制 SQL 复用;
- 完善异常捕获:大模型接口报错、SQL 语法错误、空输入全部做友好提示,便于排查问题。
11.2 开发踩坑 FAQ
Q1:启动项目时报 PostgreSQL 连接失败?
A:检查 yml 数据库 url、账号密码,确认本地 PostgreSQL 服务正常启动,5432 端口没有被占用;同时确认 llm_texttosql 数据库已提前创建。
Q2:大模型返回 ERROR:API 调用失败?
A:核对硅基流动 API Key 是否复制正确,Key 前后不要带空格;检查本地网络是否能访问硅基流动外网接口;新用户查看是否还有免费调用额度。
Q3:生成的 SQL 查询不出数据?
A:优先检查提示词里的字段枚举值是否和数据库一致(比如 "金卡会员" 不能写成 "金卡");其次优化提示词,增加 1-2 条查询示例给大模型参考,提升匹配准确率。
这套 Text-to-SQL 系统完全适配中小企业内部数据查询场景,不用业务人员学习 SQL 语法,开发维护成本很低。文章里所有代码都是实际运行调试后的完整版本,大家可以直接复制搭建,有任何搭建报错、功能拓展的问题。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。