使用 Spring AI 实现 Text-to-SQL 聊天机器人

1、概览

现代应用日益采用自然语言界面简化系统交互,这对数据检索尤为实用 —— 非技术用户可直接用自然语言提问。

Text-to-SQL 聊天机器人便是典型实现,它充当人类语言与数据库间的桥梁。通常借助大语言模型(LLM)将自然语言问题转换为可执行 SQL 查询,随后在数据库中执行并返回结果。

本文将带你了解如何使用 Spring AI 构建 Text-to-SQL 聊天机器人。

2、项目设置

在实现聊天机器人前,需引入必要依赖并正确配置应用。

本文基于 AnthropicClaude 模型构建 Text-To-Sql 聊天机器人。注意:本实现不限定特定 AI 模型,也可选用 Hugging FaceOllama 的本地 LLM 作为替代方案

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 Key 和聊天模型:

spring:
  ai:
    anthropic:
      api-key: ${ANTHROPIC_API_KEY}
      chat:
        options:
          model: claude-opus-4-20250514

我们通过 ${} 属性占位符从环境变量加载 API Key。

同时指定当前最智能的 Claude 4 Opus 模型(使用 claude-opus-4-20250514 模型 ID)。你可根据需求切换其他模型。

完成上述配置后,Spring AI 将自动创建 ChatModel 类型的 Bean,用于与指定模型交互。

2.2、使用 Flyway 定义数据库表结构

接下来配置数据库 Schema。我们将使用 Flyway 管理数据库迁移脚本,在 MySQL 数据库中创建基础 wizard management schema。需注意:与 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)
);

此处创建了两个表:

  1. hogwarts_houses 表存储霍格沃茨学院信息。
  2. 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),
-- ...更多其他学院巫师的 INSERT 语句

迁移脚本定义完成后,Flyway 将在应用启动时自动发现并执行这些脚本。

3、配置 AI 提示词

为确保 LLM 能针对我们的数据库 schema 生成准确 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 占位符将在后续替换为实际数据库 schema

作为额外防护措施,应为配置的 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、构建 Text-to-SQL 聊天机器人

完成配置后,我们将基于已配置的 Claude 模型构建 text-to-SQL 聊天机器人。

4.1、定义 Chatbot 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 注解注入系统提示词模板文件和数据库 schema DDL 迁移脚本,并将 ddl 占位符替换为实际的数据库 schema 内容。这能确保 LLM 生成 SQL 查询时始终知晓数据库结构。

接着创建 ChatClient Bean,它组合 ChatModelPromptTemplate Bean,作为与已配置 Claude 模型交互的主入口。

4.2、实现 Service

现在实现处理 SQL 生成与执行的 Service 类。

首先创建 SqlGenerator 服务类,用于将自然语言问题转换为 SQL 查询:

@Service
class SqlGenerator {

    private final ChatClient chatClient;

    //  TODO 构造函数注入

    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

接着创建 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

完成 Service 层实现后,我们基于其构建 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 record 返回。

5、与聊天机器人交互

最后,我们通过已暴露的 API 端点与 Text-To-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"
        ]
    ]
}

如你所见,聊天机器人成功理解了查询 “Slytherin 学院巫师” 的请求,返回了三位巫师及其血统状态。

最后查看应用日志,观察 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 表查找 Slytherin 学院的巫师,并按要求将结果限制为三条记录。

6、总结

本文介绍了如何使用 Spring AI 实现 Text-To-SQL 聊天机器人:

  1. 完成 AI 与数据库的必要配置。
  2. 构建能将自然语言问题转换为可执行 SQL 查询的聊天机器人(基于巫师管理数据库 Schema)。
  3. 通过 REST API 验证功能正确性。

Ref:https://www.baeldung.com/spring-ai-text-to-sql