告別復雜SQL!用Spring AI + DeepSeek構建自然語言查詢系統
1. 項目概覽

現代應用越來越多地采用自然語言交互界面,讓用戶更輕松地操作系統。這在數據查詢場景中尤為實用,非技術人員可以直接用日常語言提,文本轉 SQL 聊天機器人正是這樣的典型應用。它在人類語言和數據庫之間搭建了溝通橋梁。我們通常借助大語言模型(LLM)將用戶的自然語言問題轉換為可執行的 SQL 查詢語句,然后在數據庫中執行查詢并返回結果。
本教程將指導你使用 Spring AI 框架構建一個文本轉 SQL 聊天機器人。我們會先配置數據庫架構并填入測試數據,然后實現支持自然語言查詢的聊天機器人功能。
2. 項目搭建
2.1. 添加依賴
首先在項目的 pom.xml 文件中添加必要的依賴:
<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>接下來在 application.yaml 文件中配置 DeepSeek API 密鑰、聊天模型和數據庫連接:
spring:
ai:
deepseek:
api-key: ${DEEPSEEK_API_KEY}
datasource:
url: jdbc:mysql://localhost:3306/school_db?useSSL=false&serverTimeznotallow=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver我們使用 ${} 占位符語法從環境變量中讀取 API 密鑰和數據庫配置。
同時指定使用 DeepSeek Chat 模型,并配置 DeepSeek 的 API 基礎 URL。DeepSeek-Chat-0324 提供了強大的中文和代碼理解能力,非常適合文本轉 SQL 的場景。數據庫配置包括連接 URL、用戶名和密碼,以及 Flyway 遷移設置。
配置完成后,Spring AI 會自動創建 ChatModel 類型的 Bean,讓我們能夠與指定的模型進行交互。
2.2. 使用 Flyway 設計數據庫表結構
接下來配置數據庫結構。我們使用 Flyway 來管理數據庫遷移腳本。
我們將創建一個簡單的學校管理數據庫,使用 MySQL 作為數據庫。和 AI 模型選擇一樣,數據庫類型對實現方案沒有影響。
首先,在 src/main/resources/db/migration 目錄下創建名為 V01__creating_database_tables.sql 的遷移腳本來建立主要數據庫表:
CREATE TABLE classes (
id BINARY(16) PRIMARY KEYDEFAULT (UUID_TO_BIN(UUID())),
name VARCHAR(50) NOT NULLUNIQUE,
grade VARCHAR(20) NOT NULL,
teacher VARCHAR(50) NOT NULL,
room_number VARCHAR(20) NOT NULL,
created_at TIMESTAMPDEFAULTCURRENT_TIMESTAMP
);
CREATE TABLE courses (
id BINARY(16) PRIMARY KEYDEFAULT (UUID_TO_BIN(UUID())),
name VARCHAR(50) NOT NULLUNIQUE,
code VARCHAR(20) NOT NULLUNIQUE,
credits INTNOT NULLDEFAULT1,
description TEXT,
created_at TIMESTAMPDEFAULTCURRENT_TIMESTAMP
);
CREATE TABLE students (
id BINARY(16) PRIMARY KEYDEFAULT (UUID_TO_BIN(UUID())),
name VARCHAR(50) NOT NULL,
student_number VARCHAR(20) NOT NULLUNIQUE,
gender ENUM('Male', 'Female') NOT NULL,
age INTNOT NULL,
phone VARCHAR(15),
email VARCHAR(100),
class_id BINARY(16) NOT NULL,
created_at TIMESTAMPDEFAULTCURRENT_TIMESTAMP,
CONSTRAINT student_fkey_class FOREIGN KEY (class_id) REFERENCES classes (id)
);
CREATE TABLE student_courses (
id BINARY(16) PRIMARY KEYDEFAULT (UUID_TO_BIN(UUID())),
student_id BINARY(16) NOT NULL,
course_id BINARY(16) NOT NULL,
enrollment_date TIMESTAMPDEFAULTCURRENT_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 表:
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 表:
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 表:
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 文件:
基于 DDL 部分提供的MYSQL數據庫定義,按照指導原則部分的規則編寫 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:
@Bean
PromptTemplate systemPrompt(
@Value("classpath:system-prompt.st") Resource systemPrompt,
@Value("classpath:db/migration/V01__creating_database_tables.sql") Resource ddlSchema
)throws IOException {
PromptTemplatetemplate=newPromptTemplate(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 查詢:
@Service
classSqlGenerator {
privatefinal ChatClient chatClient;
// 標準構造函數
String generate(String question) {
Stringresponse= chatClient
.prompt(question)
.call()
.content();
booleanisSelectQuery= response.startsWith("SELECT");
if (Boolean.FALSE.equals(isSelectQuery)) {
thrownewInvalidQueryException(response);
}
return response;
}
}在 generate() 方法中,我們接收自然語言問題作為輸入,使用 chatClient Bean 將其發送給配置的 LLM。
接下來,我們驗證響應確實是 SELECT 查詢。如果 LLM 返回 SELECT 查詢以外的任何內容,我們拋出帶有錯誤消息的自定義 InvalidQueryException。
接下來,為了對數據庫執行生成的 SQL 查詢,創建一個 SqlExecutor 服務類:
@Service
classSqlExecutor {
privatefinal JdbcClient jdbcClient;
// 標準構造函數
List<Map<String, Object>> execute(String query) {
List<Map<String, Object>> result = jdbcClient
.sql(query)
.query()
.listOfRows();
if (result.isEmpty()) {
thrownewEmptyResultException("提供的查詢未找到結果。");
}
return result;
}
}在 execute() 方法中,我們使用 Spring Boot 3.1+ 引入的 JdbcClient 來運行原生 SQL 查詢并返回結果。JdbcClient 提供了更簡潔的 API 和更好的類型安全性。如果查詢沒有返回結果,我們拋出自定義的 EmptyResultException。
4.3. 暴露 REST API
現在我們已經實現了服務層,讓我們在其上暴露一個 REST API:
@PostMapping(value = "/query")
ResponseEntity<QueryResponse> query(@RequestBody QueryRequest queryRequest) {
StringsqlQuery= sqlGenerator.generate(queryRequest.question());
List<Map<String, Object>> result = sqlExecutor.execute(sqlQuery);
return ResponseEntity.ok(newQueryResponse(result));
}
recordQueryRequest(String question) {
}
recordQueryResponse(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 日志記錄,以在日志中查看生成的查詢:
logging:
level:
org:
springframework:
jdbc:
core: DEBUG接下來,使用 curl 命令調用 API 端點并與聊天機器人交互:
curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d '{"question": "查詢高三年級所有學生的姓名和班級信息"}'{
"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"
}
]
}如我們所見,聊天機器人成功理解了我們對高三學生的查詢請求,并返回了學生姓名和對應的班級信息。
讓我們再試一個更復雜的查詢:
curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d '{"question": "統計每個班級的學生人數,按人數降序排列"}'最后,讓我們檢查應用日志,查看 LLM 生成的 SQL 查詢:
SELECT students.name, classes.name as class_name
FROM students
JOIN classes ON students.class_id = classes.id
WHERE classes.grade = '高三'
ORDER BY classes.name;{
"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 等先進的大語言模型,為我們提供了構建智能數據查詢系統的完整解決方案。這種技術組合不僅簡化了數據訪問流程,也為未來的智能化應用奠定了堅實基礎。



























