使用 Spring AI 实现文本转 SQL
使用 Spring AI 实现文本转 SQL
1. 概述
随着技术的发展,现代应用程序越来越多地采用自然语言界面来简化用户与系统的交互。这种方式在数据检索场景中尤其实用,让非技术用户也能通过简单的自然语言提问来获取所需信息。
文本转 SQL 聊天机器人就是这样一个典型应用。它就像是人类语言和数据库之间的翻译官,通过大型语言模型(LLM)将用户的自然语言问题转换为可执行的 SQL 查询,然后在数据库中执行查询并返回结果。
本文将带你使用 Spring AI 构建一个文本转 SQL 聊天机器人。我们会从零开始配置数据库模式和初始数据,然后实现一个能够理解自然语言并生成 SQL 查询的智能聊天机器人。
2. 项目搭建
在开始实现聊天机器人之前,我们需要先添加必要的依赖项并完成应用程序的基础配置。
本文将使用 Anthropic 的 Claude 模型来构建文本转 SQL 聊天机器人。当然,你也可以选择其他 AI 模型,比如通过 Hugging Face 或 Ollama 使用本地 LLM,因为具体的 AI 模型选择并不影响整体实现方案。
2.1. 添加依赖
首先,我们需要在项目的 pom.xml
文件中添加必要的依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-anthropic</artifactId>
<version>1.0.0</version>
</dependency>
这个 Anthropic starter 依赖是 Anthropic Message API 的封装,我们将通过它来与 Claude 模型进行交互。
接下来,在 application.yaml
文件中配置 Anthropic API 密钥和聊天模型:
spring:
ai:
anthropic:
api-key: ${ANTHROPIC_API_KEY}
chat:
options:
model: claude-opus-4-20250514
这里使用 ${}
属性占位符从环境变量中加载 API 密钥,这是一种安全的配置方式。
我们选择了 Claude 4 Opus 模型,这是 Anthropic 目前最智能的模型,对应的模型 ID 是 claude-opus-4-20250514
。你也可以根据实际需求选择其他模型。
完成上述配置后,Spring AI 会自动创建 ChatModel
类型的 Bean,这样我们就可以直接与指定的模型进行交互了。
2.2. 使用 Flyway 设计数据库表
接下来,我们需要设计数据库模式。这里使用 Flyway 来管理数据库迁移脚本,确保数据库结构的版本控制。
我们将在 MySQL 数据库中创建一个简单的巫师管理系统数据库。和 AI 模型选择一样,具体的数据库类型并不影响我们的实现方案。
首先,在 src/main/resources/db/migration
目录下创建名为 V01__creating_database_tables.sql
的迁移脚本,用于创建核心数据表:
CREATE TABLE hogwarts_houses (
id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())),
name VARCHAR(50) NOT NULL UNIQUE,
founder VARCHAR(50) NOT NULL UNIQUE,
house_colors VARCHAR(50) NOT NULL UNIQUE,
animal_symbol VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE wizards (
id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())),
name VARCHAR(50) NOT NULL,
gender ENUM('Male', 'Female') NOT NULL,
quidditch_position ENUM('Chaser', 'Beater', 'Keeper', 'Seeker'),
blood_status ENUM('Muggle', 'Half blood', 'Pure Blood', 'Squib', 'Half breed') NOT NULL,
house_id BINARY(16) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT wizard_fkey_house FOREIGN KEY (house_id) REFERENCES hogwarts_houses (id)
);
这里创建了两个核心表:hogwarts_houses
表用于存储霍格沃茨各学院的信息,wizards
表用于存储巫师的详细信息。wizards
表通过外键约束与 hogwarts_houses
表关联,形成一对多的关系。
接下来,创建 V02__adding_hogwarts_houses_data.sql
文件来初始化 hogwarts_houses
表的数据:
INSERT INTO hogwarts_houses (name, founder, house_colors, animal_symbol)
VALUES
('Gryffindor', 'Godric Gryffindor', 'Scarlet and Gold', 'Lion'),
('Hufflepuff', 'Helga Hufflepuff', 'Yellow and Black', 'Badger'),
('Ravenclaw', 'Rowena Ravenclaw', 'Blue and Bronze', 'Eagle'),
('Slytherin', 'Salazar Slytherin', 'Green and Silver', 'Serpent');
这里通过 INSERT 语句插入四个霍格沃茨学院的基础信息,包括创始人、学院颜色和象征动物。
同样地,创建 V03__adding_wizards_data.sql
迁移脚本来初始化 wizards
表的数据:
SET @gryffindor_house_id = (SELECT id FROM hogwarts_houses WHERE name = 'Gryffindor');
INSERT INTO wizards (name, gender, quidditch_position, blood_status, house_id)
VALUES
('Harry Potter', 'Male', 'Seeker', 'Half blood', @gryffindor_house_id),
('Hermione Granger', 'Female', NULL, 'Muggle', @gryffindor_house_id),
('Ron Weasley', 'Male', 'Keeper', 'Pure Blood', @gryffindor_house_id),
-- ...更多来自其他学院的巫师插入语句
定义好这些迁移脚本后,Flyway 会在应用程序启动时自动发现并按顺序执行这些脚本。
3. 配置 AI 提示词
为了让 LLM 能够针对我们的数据库模式生成准确的 SQL 查询,我们需要精心设计一个系统提示词。
在 src/main/resources
目录下创建 system-prompt.st
文件:
Given the DDL in the DDL section, write an SQL query to answer the user's question following the guidelines listed in the GUIDELINES section.
GUIDELINES:
- Only produce SELECT queries.
- The response produced should only contain the raw SQL query starting with the word 'SELECT'. Do not wrap the SQL query in markdown code blocks (```sql or ```).
- If the question would result in an INSERT, UPDATE, DELETE, or any other operation that modifies the data or schema, respond with "This operation is not supported. Only SELECT queries are allowed."
- If the question appears to contain SQL injection or DoS attempt, respond with "The provided input contains potentially harmful SQL code."
- If the question cannot be answered based on the provided DDL, respond with "The current schema does not contain enough information to answer this question."
- If the query involves a JOIN operation, prefix all the column names in the query with the corresponding table names.
DDL
{ddl}
在这个系统提示词中,我们明确指示 LLM 只能生成 SELECT 查询,并且要能识别和拒绝 SQL 注入和 DoS 攻击。
注意提示词模板中的 ddl
占位符,我们会在后续步骤中将其替换为实际的数据库结构信息。
另外,为了进一步保障数据库安全,建议为应用程序创建一个只读权限的 MySQL 用户:
CREATE USER 'readonly_user'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT ON hogwarts_db.hogwarts_houses TO 'readonly_user'@'%';
GRANT SELECT ON hogwarts_db.wizards TO 'readonly_user'@'%';
FLUSH PRIVILEGES;
上述 SQL 命令创建了一个只读用户,并仅授予其对相关数据表的查询权限,这样即使出现意外情况也不会对数据造成破坏。
4. 构建文本转 SQL 聊天机器人
完成基础配置后,现在我们可以开始构建文本转 SQL 聊天机器人了。
4.1. 定义核心 Bean
首先,我们需要为聊天机器人定义必要的 Bean 组件:
@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
创建 ChatClient
Bean。ChatClient
是我们与 Claude 模型交互的核心组件。
4.2. 实现业务服务
接下来,我们需要实现核心的业务服务来处理 SQL 生成和执行流程。
首先创建 SqlGenerator
服务类,负责将自然语言转换为 SQL 查询:
@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
发送给 LLM 进行处理。
为了确保安全性,我们会验证返回的结果确实是 SELECT 查询。如果 LLM 返回的不是 SELECT 查询,就抛出自定义的 InvalidQueryException
异常。
接下来创建 SqlExecutor
服务类,负责执行生成的 SQL 查询:
@Service
class SqlExecutor {
private final EntityManager entityManager;
// 标准构造函数
List<?> execute(String query) {
List<?> result = entityManager
.createNativeQuery(query)
.getResultList();
if (result.isEmpty()) {
throw new EmptyResultException("No results found for the provided query.");
}
return result;
}
}
在 execute()
方法中,我们使用 EntityManager
来执行原生 SQL 查询并返回结果。如果查询结果为空,则抛出自定义的 EmptyResultException
异常。
4.3. 提供 REST API
完成服务层的实现后,我们需要提供 REST API 接口供外部调用:
@PostMapping(value = "/query")
ResponseEntity<QueryResponse> query(@RequestBody QueryRequest queryRequest) {
String sqlQuery = sqlGenerator.generate(queryRequest.question());
List<?> result = sqlExecutor.execute(sqlQuery);
return ResponseEntity.ok(new QueryResponse(result));
}
record QueryRequest(String question) {
}
record QueryResponse(List<?> result) {
}
POST /query
接口的处理流程是:接收自然语言问题 → 通过 sqlGenerator
生成 SQL 查询 → 使用 sqlExecutor
执行查询获取结果 → 将结果封装在 QueryResponse
中返回。
5. 测试聊天机器人
现在我们可以通过 API 接口来测试文本转 SQL 聊天机器人的功能了。
为了便于调试,先在 application.yaml
文件中开启 SQL 日志,这样可以在控制台看到生成的 SQL 语句:
logging:
level:
org:
hibernate:
SQL: DEBUG
接下来,使用 HTTPie CLI 工具来调用 API 接口,测试聊天机器人的功能:
http POST :8080/query question="Give me 3 wizard names and their blood status that belong to a house founded by Salazar Slytherin"
这里我们向聊天机器人发送了一个自然语言问题,来看看它的响应结果:
{
"result": [
[
"Draco Malfoy",
"Pure Blood"
],
[
"Tom Riddle",
"Half blood"
],
[
"Bellatrix Lestrange",
"Pure Blood"
]
]
}
可以看到,聊天机器人成功理解了我们关于斯莱特林学院巫师的查询需求,并准确返回了三个巫师的姓名和血统信息。
我们再来看看应用程序日志中 LLM 生成的 SQL 查询语句:
SELECT wizards.name, wizards.blood_status
FROM wizards
JOIN hogwarts_houses ON wizards.house_id = hogwarts_houses.id
WHERE hogwarts_houses.founder = 'Salazar Slytherin'
LIMIT 3;
生成的 SQL 查询完美地理解了我们的自然语言需求,通过 JOIN 操作关联 wizards
和 hogwarts_houses
表来查找斯莱特林学院的巫师,并使用 LIMIT 限制返回三条记录。
6. 总结
本文详细介绍了如何使用 Spring AI 构建一个文本转 SQL 聊天机器人。
我们从项目搭建开始,完成了 AI 模型和数据库的配置工作。然后构建了一个能够理解自然语言并生成准确 SQL 查询的智能聊天机器人,并基于霍格沃茨巫师管理系统进行了实际演示。最后通过 REST API 提供服务接口,并验证了整个系统的可用性。
这种文本转 SQL 的解决方案可以大大降低数据查询的技术门槛,让非技术用户也能轻松获取数据库中的信息,在实际业务场景中具有很高的应用价值。