使用 Spring AI 實現文字到 SQL 的轉換
1.概述
現代應用程式越來越多地使用自然語言介面來簡化使用者與系統的互動。這對於資料檢索尤其有用,因為非技術使用者可以使用簡單的英語提問。
文字轉 SQL 的聊天機器人就是這樣一個例子。它充當了人類語言和資料庫之間的橋樑。我們通常利用大型語言模型 (LLM) 將使用者的自然語言問題轉換為可執行的 SQL 查詢。然後,該查詢會在資料庫中運行,以取得並顯示所需的資訊。
在本教程中,我們將使用 Spring AI 建立一個文字轉 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 啟動器依賴項是Anthropic Message API 的包裝器,我們將使用它在我們的應用程式中與 Claude 模型進行互動。
接下來,讓我們在application.yaml
檔案中設定我們的Anthropic API 金鑰和聊天模型:
spring:
ai:
anthropic:
api-key: ${ANTHROPIC_API_KEY}
chat:
options:
model: claude-opus-4-20250514
我們使用${}
屬性佔位符從環境變數載入 API 金鑰的值。
此外,我們指定了 Anthropic 開發的Claude 4 Opus模型,這是本文撰寫時最聰明的模型,模型 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
wizards
關聯起來,從而建立起一對多的關係。
接下來,讓我們建立一個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),
-- ...more insert statements for wizards from other houses
透過定義我們的遷移腳本, 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 查詢並偵測 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 命令中,我們建立一個 MySQL 使用者並授予其對所需資料庫表的唯讀權限。
4. 建立我們的文字到 SQL 聊天機器人
配置完成後,讓我們使用配置的 Claude 模型建立一個文字到 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 ChatClient
是我們與已配置的 Claude 模型互動的主要入口點。
4.2. 實作服務類
現在,讓我們實作服務類別來處理 SQL 產生和執行過程。
首先,讓我們建立一個將自然語言問題轉換為 SQL 查詢的SqlGenerator
服務類別:
@Service
class SqlGenerator {
private final ChatClient chatClient;
// standard constructor
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。
接下來,我們驗證response
是否確實是 SELECT 查詢。如果 LLM 傳回的不是 SELECT 查詢,我們將拋出自訂的InvalidQueryException
異常,並顯示錯誤訊息。
接下來,為了對我們的資料庫執行產生的 SQL 查詢,讓我們建立一個SqlExecutor
服務類別:
@Service
class SqlExecutor {
private final EntityManager entityManager;
// standard constructor
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
bean 產生對應的 SQL 查詢,將其傳遞給sqlExecutor
bean 以從資料庫中取得結果,最後將資料包裝並傳回QueryResponse
記錄中。
5. 與我們的聊天機器人互動
最後,讓我們使用我們公開的 API 端點與我們的文字到 SQL 聊天機器人進行互動。
但首先,讓我們在application.yaml
檔案中啟用 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"
在這裡,我們向聊天機器人發送一個簡單的question
,讓我們看看我們收到的答案:
{
"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 查詢正確地解釋了我們的自然語言請求,連接了wizards
和hogwarts_houses
表以查找來自 Slytherin 學院的巫師,並根據要求將結果限制為三筆記錄。
6. 結論
在本文中,我們探討如何使用 Spring AI 實作文字到 SQL 聊天機器人。
我們介紹了必要的 AI 和資料庫配置。然後,我們建立了一個聊天機器人,它能夠根據嚮導管理資料庫模式將自然語言問題轉換為可執行的 SQL 查詢。最後,我們公開了一個 REST API 來與聊天機器人交互,並驗證了它是否正常運作。
與往常一樣,本文中使用的所有程式碼範例均可在 GitHub 上找到。