知識庫 / Spring / Spring AI RSS 訂閱

基於 Spring AI 的文本到 SQL 實現

Artificial Intelligence,Spring AI
HongKong
8
10:46 AM · Dec 06 ,2025

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 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 Key 的值。

此外,我們指定了 Anthropic 在寫作時提供的最智能模型 Claude 4 Opus,使用 claude-opus-4-20250514 模型 ID。我們可以根據需求使用 不同模型

配置以上屬性時,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 語句來創建四個霍格沃茨學院及其各自的創始人、顏色和象徵。

同樣,讓我們在新的 wizards 表中填充數據,該數據位於 V03__adding_wizards_data.sql 遷移腳本中。

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 查詢時始終能夠訪問我們的數據庫結構

接下來,我們使用 ChatClient Bean,該 Bean 使用 ChatModelPromptTemplate Bean。 ChatClient 類作為我們與配置好的 Claude 模型交互的主要入口

4.2. 實現服務類

現在,讓我們實現服務類來處理 SQL 生成和執行過程。

首先,讓我們創建一個 SqlGenerator 服務類,該類將自然語言問題轉換為 SQL 查詢:

@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 (!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;
    }
}

在我們的 方法中,我們使用自動注入的 實例來執行原生 SQL 查詢並返回結果。如果查詢沒有返回任何結果,我們將拋出自定義的 異常。

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"

在這裏,我們向聊天機器人發送一個簡單的問題,讓我們看看我們收到什麼作為迴應:

{
    "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 查詢正確地解釋了我們的自然語言請求,通過連接 wizardshogwarts_houses 表來查找來自斯萊特林學院的巫師,並將結果限制為三條記錄,如請求所示

6. 結論

在本文中,我們探討了使用 Spring AI 實現文本到 SQL 聊天機器人的方法。

我們完成了必要的 AI 和數據庫配置。然後,我們構建了一個能夠將自然語言問題轉換為針對我們巫師管理數據庫模式的可執行 SQL 查詢的聊天機器人。最後,我們暴露了一個 REST API 與我們的聊天機器人交互,並驗證了其正確運行。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.