1. 项目概览 现代应用越来越多地采用自然语言交互界面,让用户更轻松地操作系统。这在数据查询场景中尤为实用,非技术人员可以直接用日常语言提问。
文本转 SQL 聊天机器人正是这样的典型应用。它在人类语言和数据库之间搭建了沟通桥梁。我们通常借助大语言模型(LLM)将用户的自然语言问题转换为可执行的 SQL 查询语句,然后在数据库中执行查询并返回结果。
本教程将指导你使用 Spring AI 框架构建一个文本转 SQL 聊天机器人。我们会先配置数据库架构并填入测试数据,然后实现支持自然语言查询的聊天机器人功能。
2. 项目搭建 2.1. 添加依赖 首先在项目的 pom.xml 文件中添加必要的依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <dependency > <groupId > org.springframework.ai</groupId > <artifactId > spring-ai-starter-model-deepseek</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > org.flywaydb</groupId > <artifactId > flyway-core</artifactId > </dependency > <dependency > <groupId > org.flywaydb</groupId > <artifactId > flyway-mysql</artifactId > </dependency >
这里我们添加了:
OpenAI 兼容启动器依赖,可以与 DeepSeek API 兼容使用
Spring Boot JDBC 启动器,提供 JdbcClient 支持
MySQL 驱动程序
Flyway 数据库迁移工具
接下来在 application.yaml 文件中配置 DeepSeek API 密钥、聊天模型和数据库连接:
1 2 3 4 5 6 7 8 9 spring: ai: deepseek: api-key: ${DEEPSEEK_API_KEY} datasource: url: jdbc:mysql://localhost:3306/school_db?useSSL=false&serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver
我们使用 ${} 占位符语法从环境变量中读取 API 密钥和数据库配置。
同时指定使用 DeepSeek Chat 模型,并配置 DeepSeek 的 API 基础 URL。DeepSeek 提供了强大的中文和代码理解能力,非常适合文本转 SQL 的场景。数据库配置包括连接 URL、用户名和密码,以及 Flyway 迁移设置。
配置完成后,Spring AI 会自动创建 ChatModel 类型的 Bean,让我们能够与指定的模型进行交互。
2.2. 使用 Flyway 设计数据库表结构 接下来配置数据库结构。我们使用 Flyway 来管理数据库迁移脚本。
我们将创建一个简单的学校管理数据库,使用 MySQL 作为数据库。和 AI 模型选择一样,数据库类型对实现方案没有影响。
首先,在 src/main/resources/db/migration 目录下创建名为 V01__creating_database_tables.sql 的迁移脚本来建立主要数据库表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 CREATE TABLE classes ( id BINARY (16 ) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())), name VARCHAR (50 ) NOT NULL UNIQUE , grade VARCHAR (20 ) NOT NULL , teacher VARCHAR (50 ) NOT NULL , room_number VARCHAR (20 ) NOT NULL , created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE courses ( id BINARY (16 ) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())), name VARCHAR (50 ) NOT NULL UNIQUE , code VARCHAR (20 ) NOT NULL UNIQUE , credits INT NOT NULL DEFAULT 1 , description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE students ( id BINARY (16 ) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())), name VARCHAR (50 ) NOT NULL , student_number VARCHAR (20 ) NOT NULL UNIQUE , gender ENUM('Male' , 'Female' ) NOT NULL , age INT NOT NULL , phone VARCHAR (15 ), email VARCHAR (100 ), class_id BINARY (16 ) NOT NULL , created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP , CONSTRAINT student_fkey_class FOREIGN KEY (class_id) REFERENCES classes (id) ); CREATE TABLE student_courses ( id BINARY (16 ) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())), student_id BINARY (16 ) NOT NULL , course_id BINARY (16 ) NOT NULL , enrollment_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP , score DECIMAL (5 ,2 ), status ENUM('Enrolled' , 'Completed' , 'Dropped' ) DEFAULT 'Enrolled' , CONSTRAINT sc_fkey_student FOREIGN KEY (student_id) REFERENCES students (id), CONSTRAINT sc_fkey_course FOREIGN KEY (course_id) REFERENCES courses (id), UNIQUE KEY unique_student_course (student_id, course_id) );
这里我们创建了四个主要表:
classes 表存储班级信息
courses 表存储课程信息
students 表存储学生信息,通过外键与班级关联
student_courses 表作为学生和课程的多对多关联表,存储选课信息和成绩
接下来,创建 V02__adding_classes_data.sql 文件来填充 classes 表:
1 2 3 4 5 6 7 8 INSERT INTO classes (name, grade, teacher, room_number)VALUES ('高三(1)班' , '高三' , '张老师' , 'A301' ), ('高三(2)班' , '高三' , '李老师' , 'A302' ), ('高二(1)班' , '高二' , '王老师' , 'B201' ), ('高二(2)班' , '高二' , '赵老师' , 'B202' ), ('高一(1)班' , '高一' , '陈老师' , 'C101' ), ('高一(2)班' , '高一' , '刘老师' , 'C102' );
这里我们用 INSERT 语句创建六个班级,涵盖高一到高三各个年级。
接着,创建 V03__adding_courses_data.sql 迁移脚本填充 courses 表:
1 2 3 4 5 6 7 8 9 10 11 INSERT INTO courses (name, code, credits, description)VALUES ('语文' , 'CH001' , 4 , '高中语文课程' ), ('数学' , 'MA001' , 5 , '高中数学课程' ), ('英语' , 'EN001' , 4 , '高中英语课程' ), ('物理' , 'PH001' , 3 , '高中物理课程' ), ('化学' , 'CH002' , 3 , '高中化学课程' ), ('生物' , 'BI001' , 3 , '高中生物课程' ), ('历史' , 'HI001' , 2 , '高中历史课程' ), ('地理' , 'GE001' , 2 , '高中地理课程' ), ('政治' , 'PO001' , 2 , '高中政治课程' );
然后创建 V04__adding_students_data.sql 迁移脚本填充 students 表:
1 2 3 4 5 6 7 8 9 10 11 12 SET @class_grade3_1 = (SELECT id FROM classes WHERE name = '高三(1)班' );SET @class_grade3_2 = (SELECT id FROM classes WHERE name = '高三(2)班' );SET @class_grade2_1 = (SELECT id FROM classes WHERE name = '高二(1)班' );INSERT INTO students (name, student_number, gender, age, phone, email, class_id)VALUES ('张三' , '2024001' , 'Male' , 18 , '13800138001' , 'zhangsan@example.com' , @class_grade3_1 ), ('李四' , '2024002' , 'Female' , 17 , '13800138002' , 'lisi@example.com' , @class_grade3_1 ), ('王五' , '2024003' , 'Male' , 17 , '13800138003' , 'wangwu@example.com' , @class_grade3_2 ), ('赵六' , '2024004' , 'Female' , 16 , '13800138004' , 'zhaoliu@example.com' , @class_grade2_1 ), ('钱七' , '2024005' , 'Male' , 16 , '13800138005' , 'qianqi@example.com' , @class_grade2_1 );
定义好迁移脚本后,Flyway 会在应用启动时自动发现并执行这些脚本。
3. 配置 AI 提示词 接下来,为了确保 LLM 能够针对我们的数据库架构生成准确的 SQL 查询,需要定义详细的系统提示词。
在 src/main/resources 目录下创建 system-prompt.st 文件:
1 2 3 4 5 6 7 8 9 10 11 12 基于 DDL 部分提供的数据库定义,按照指导原则部分的规则编写 SQL 查询来回答用户问题。 指导原则: - 只生成 SELECT 查询语句。 - 响应结果应该只包含以 'SELECT' 开头的原始 SQL 查询语句。不要用 markdown 代码块(```sql 或 ```)包装 SQL 查询。 - 如果问题需要执行 INSERT、UPDATE、DELETE 或其他修改数据或架构的操作,请回复"不支持此操作。只允许 SELECT 查询。" - 如果问题似乎包含 SQL 注入或 DoS 攻击尝试,请回复"提供的输入包含潜在有害的 SQL 代码。" - 如果基于提供的 DDL 无法回答问题,请回复"当前架构不包含足够信息来回答此问题。" - 如果查询涉及 JOIN 操作,请在查询中为所有列名添加相应的表名前缀。 DDL {ddl}
在系统提示词中,我们指示 LLM 只生成 SELECT SQL 查询,并检测 SQL 注入和 DoS 攻击尝试。
我们在系统提示词模板中留了一个 ddl 占位符用于数据库架构。稍后我们会用实际值替换它。
此外,为了进一步保护数据库免受修改,应该只给配置的 MySQL 用户必要的权限。
4. 构建文本转 SQL 聊天机器人 完成配置后,让我们使用配置好的 DeepSeek 模型构建文本转 SQL 聊天机器人。
4.1. 定义聊天机器人 Bean 首先定义聊天机器人所需的 Bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Bean PromptTemplate systemPrompt ( @Value("classpath:system-prompt.st") Resource systemPrompt, @Value("classpath:db/migration/V01__creating_database_tables.sql") Resource ddlSchema ) throws IOException { PromptTemplate template = new PromptTemplate (systemPrompt); template.add("ddl" , ddlSchema.getContentAsString(Charset.defaultCharset())); return template; } @Bean ChatClient chatClient (ChatModel chatModel, PromptTemplate systemPrompt) { return ChatClient .builder(chatModel) .defaultSystem(systemPrompt.render()) .build(); }
首先,我们定义一个 PromptTemplate Bean。通过 @Value 注解注入系统提示词模板文件和数据库架构 DDL 迁移脚本。同时,我们用数据库架构内容填充 ddl 占位符。这确保了 LLM 在生成 SQL 查询时始终能访问我们的数据库结构。
接下来,我们使用 ChatModel 和 PromptTemplate Bean 创建一个 ChatClient Bean。ChatClient 类是我们与配置的 DeepSeek 模型交互的主要入口点。
4.2. 实现服务类 现在,让我们实现服务类来处理 SQL 生成和执行过程。
首先,创建一个 SqlGenerator 服务类,将自然语言问题转换为 SQL 查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Service class SqlGenerator { private final ChatClient chatClient; String generate (String question) { String response = chatClient .prompt(question) .call() .content(); boolean isSelectQuery = response.startsWith("SELECT" ); if (Boolean.FALSE.equals(isSelectQuery)) { throw new InvalidQueryException (response); } return response; } }
在 generate() 方法中,我们接收自然语言问题作为输入,使用 chatClient Bean 将其发送给配置的 LLM。
接下来,我们验证响应确实是 SELECT 查询。如果 LLM 返回 SELECT 查询以外的任何内容,我们抛出带有错误消息的自定义 InvalidQueryException。
接下来,为了对数据库执行生成的 SQL 查询,创建一个 SqlExecutor 服务类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Service class SqlExecutor { private final JdbcClient jdbcClient; List<Map<String, Object>> execute (String query) { List<Map<String, Object>> result = jdbcClient .sql(query) .query() .listOfRows(); if (result.isEmpty()) { throw new EmptyResultException ("提供的查询未找到结果。" ); } return result; } }
在 execute() 方法中,我们使用 Spring Boot 3.1+ 引入的 JdbcClient 来运行原生 SQL 查询并返回结果。JdbcClient 提供了更简洁的 API 和更好的类型安全性。如果查询没有返回结果,我们抛出自定义的 EmptyResultException。
4.3. 暴露 REST API 现在我们已经实现了服务层,让我们在其上暴露一个 REST API:
1 2 3 4 5 6 7 8 9 10 11 12 @PostMapping(value = "/query") ResponseEntity<QueryResponse> query (@RequestBody QueryRequest queryRequest) { String sqlQuery = sqlGenerator.generate(queryRequest.question()); List<Map<String, Object>> result = sqlExecutor.execute(sqlQuery); return ResponseEntity.ok(new QueryResponse (result)); } record QueryRequest (String question) {} record QueryResponse (List<Map<String, Object>> result) {}
POST /query 端点接受自然语言问题,使用 sqlGenerator Bean 生成相应的 SQL 查询,将其传递给 sqlExecutor Bean 从数据库获取结果,最后将数据包装在 QueryResponse 记录中并返回。使用 JdbcClient 返回的结果是 List<Map<String, Object>> 格式,每个 Map 代表一行数据,键为列名,值为对应的数据。
5. 与聊天机器人交互 最后,让我们使用暴露的 API 端点与文本转 SQL 聊天机器人进行交互。
但首先,在 application.yaml 文件中启用 SQL 日志记录,以在日志中查看生成的查询:
1 2 3 4 5 6 logging: level: org: springframework: jdbc: core: DEBUG
接下来,使用 curl 命令调用 API 端点并与聊天机器人交互:
1 2 3 curl -X POST http://localhost:8080/query \ -H "Content-Type: application/json" \ -d '{"question": "查询高三年级所有学生的姓名和班级信息"}'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 { "result" : [ { "student_name" : "张三" , "class_name" : "高三(1)班" , "grade" : "高三" , "teacher" : "张老师" , "room_number" : "A301" } , { "student_name" : "李四" , "class_name" : "高三(1)" , "grade" : "高三" , "teacher" : "张老师" , "room_number" : "A301" } , { "student_name" : "王五" , "class_name" : "高三(2)班" , "grade" : "高三" , "teacher" : "李老师" , "room_number" : "A302" } ] }
如我们所见,聊天机器人成功理解了我们对高三学生的查询请求,并返回了学生姓名和对应的班级信息。
让我们再试一个更复杂的查询:
1 2 3 curl -X POST http://localhost:8080/query \ -H "Content-Type: application/json" \ -d '{"question": "统计每个班级的学生人数,按人数降序排列"}'
最后,让我们检查应用日志,查看 LLM 生成的 SQL 查询:
1 2 3 4 5 SELECT students.name, classes.name as class_nameFROM studentsJOIN classes ON students.class_id = classes.idWHERE classes.grade = '高三' ORDER BY classes.name;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 { "result" : [ { "class_name" : "高三(1)班" , "student_count" : 2 } , { "class_name" : "高二(1)班" , "student_count" : 2 } , { "class_name" : "高三(2)班" , "student_count" : 1 } , { "class_name" : "高一(1)班" , "student_count" : 0 } , { "class_name" : "高一(2)班" , "student_count" : 0 } , { "class_name" : "高二(2)班" , "student_count" : 0 } ] }
生成的 SQL 查询正确解释了我们的自然语言请求,连接了 students 和 classes 表来查找高三年级的学生信息。DeepSeek 模型展现了出色的中文理解和 SQL 生成能力。
6. 总结 本教程展示了如何使用 Spring AI 框架构建一个功能完整的文本转 SQL 聊天机器人。 虽然我们已经构建了一个基础的文本转 SQL 系统,但仍有许多改进空间:
权限控制 :根据用户角色限制可访问的数据范围
结果可视化 :将查询结果以图表形式展示
LLM生成优化 :通过JSON格式化和JSON修复支持稳定输出
通过本教程的学习,已经体现了构建文本转 SQL 聊天机器人的核心技术。Spring AI 框架的强大功能,结合 DeepSeek 等先进的大语言模型,为我们提供了构建智能数据查询系统的完整解决方案。这种技术组合不仅简化了数据访问流程,也为未来的智能化应用奠定了坚实基础。