SpringBoot+PostgreSQL + 硅基流动大模型从零搭建 Text-to-SQL 智能问答系统

目录

前言

一、项目整体介绍

[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 个实用能力,满足基础业务查询场景:

  1. 自然语言智能问答:中文提问自动生成 SQL 并返回数据表结果
  2. 会员数据总览页面:打开首页直接查看全量会员数据,简单统计总条数
  3. SQL 可视化展示:大模型生成的原始 SQL 清理后完整展示,支持复制复用
  4. SQL 安全拦截机制:强制只允许 SELECT 查询,杜绝删表、改数据等危险操作,防止注入风险
  5. 自适应前端页面:原生 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 开发环境硬性要求

  1. JDK 版本:最低 JDK17,推荐 JDK21(SpringBoot3.x 强制要求高版本 JDK,低版本会直接启动报错)
  2. 构建工具:Maven3.8+ 或者 Gradle8+,本文全程使用 Maven
  3. 数据库:PostgreSQL14 及以上,本地安装或者 Docker 启动均可
  4. 开发工具: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,必须先拿到接口密钥,步骤很简单:

  1. 浏览器打开硅基流动官网,完成手机号注册登录
  2. 进入控制台 - API 密钥管理,复制生成专属 Key(注意妥善保存,只展示一次)
  3. 模型选择:新手推荐tencent/Hunyuan-MT-7BQwen/Qwen2.5-7B-Instruct,对中文 SQL 适配度最高
  4. 计费说明:新用户一般赠送免费调用额度,测试完全够用,正式使用按需充值

三、项目完整目录结构

标准 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();
        }
    }
}

开发重点说明

  1. 提示词设计:完整把表字段、枚举值、语法规则全部传给大模型,是避免生成错误 SQL 的核心;如果后期 SQL 经常出错,优先优化提示词,而非调整代码。
  2. temperature 参数:文本创作场景会调高到 0.7-1,但是 SQL 生成需要精准,设置 0.1 限制模型随机发挥。
  3. 统一错误前缀 :所有异常都用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,整张业务表会直接删除。本文做了两层防护:

  1. 提示词约束大模型禁止生成修改类 SQL;
  2. 代码层二次校验 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 项目核心亮点

  1. 完整可落地:从数据库建表、后端分层、大模型对接、前端页面全套代码,复制即可运行,无缺失模块;
  2. 双层 SQL 安全防护:提示词约束 + 代码关键字拦截,杜绝删改表等高危操作,企业内部使用更放心;
  3. 轻量化无复杂依赖:不引入 Vue、Redis、消息队列等重型组件,小型工具快速开发部署;
  4. 用户友好前端:自带示例快捷提问,自动展示生成 SQL,业务人员可以复制 SQL 复用;
  5. 完善异常捕获:大模型接口报错、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 语法,开发维护成本很低。文章里所有代码都是实际运行调试后的完整版本,大家可以直接复制搭建,有任何搭建报错、功能拓展的问题。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。

相关推荐
xxwl5851 小时前
数据库后端接口测试报告
spring boot·mysql·tomcat
zzzzzz3102 小时前
别让大模型直接碰业务:我在 Spring Boot 里给 AI 操作加了一道“可拒绝的闸门”
人工智能·spring boot·spring
知彼解己2 小时前
Java 版本演进
java·开发语言·spring boot
2501_942389553 小时前
Nifty IT指数的K线犹如断线的风筝
人工智能·postgresql·时序数据库·storm·tdengine
就改了3 小时前
MyBatis核心类用法详解
java·spring boot·后端·mybatis
DarLing丶张皇4 小时前
【源码】JeecgBoot导出Excel模板
java·spring boot
paopaokaka_luck21 小时前
基于springboot3+vue3的智能文库平台(AI智能搜索、AI智能汇总、实时在线状态展示、多格式文档预览与富文本编辑、Echarts图形化分析)
前端·网络·spring boot·网络协议·echarts
zzzll11111 天前
Spring Boot 入门指南:从零开始构建 Java Web 应用
java·前端·spring boot
OK_boom1 天前
C# Dapper匹配postgresql的jsonb类型
开发语言·postgresql·c#