SpringAI 功能体验之SQL智能助手:用自然语言查询数据库

1. 引言

在上一篇文章《SpringAI 入门实战:从 ChatModel 到 FunctionCall 的完整体验》中,我们体验了 SpringAI 如何让大模型与 Java 应用无缝集成。今天,我们将更进一步,探索 SpringAI 的一个非常实用的落地场景------SQL 智能助手

想象一下,你不再需要记住复杂的 SQL 语法,只需要用自然语言提问:"统计出版书籍前三名的出版社",系统就能自动生成并执行 SQL 语句,返回查询结果。这就是我们今天要实现的"SQL 智能助手"。

2. 项目目标与基础思路

2.1 项目目标

我们将使用 SpringAI 搭建一个 RESTful API,实现以下功能:

  • 用户通过 HTTP 接口输入自然语言问题(如"查询所有作者")。
  • 系统将问题与数据库表结构(DDL)一起构建成 Prompt,发送给大模型。
  • 大模型返回对应的 SQL 查询语句。
  • 系统自动执行该 SQL 语句,并将结果返回给用户。

2.2 基础思路

整个流程的核心逻辑非常清晰,可以分为以下几步:

  1. 接收用户提问 :将用户的自然语言问题作为 UserMessage
  2. 构建 Prompt:将用户的问题和本地数据库的表结构(DDL)一起,填充到一个精心设计的提示词模板中。
  3. 调用大模型:将构建好的 Prompt 发送给大模型,让大模型理解数据库结构并生成对应的 SQL 语句。
  4. 执行 SQL:在本地数据库中执行大模型返回的 SQL 语句,获取查询结果。
  5. 返回结果:将 SQL 语句和查询结果一并返回给用户。

3. 环境准备与数据初始化

3.1 准备测试数据

首先,我们需要准备一个简单的图书管理系统数据库。包含三张表:Authors(作者)、Publishers(出版社)和 Books(书籍)。

Schema.sql(数据库表结构):

sql 复制代码
create table Authors (
    id int not null auto_increment,
    firstName varchar(255) not null,
    lastName varchar(255) not null,
    primary key (id)
);

create table Publishers (
    id int not null auto_increment,
    name varchar(255) not null,
    primary key (id)
);

create table Books (
    id int not null auto_increment,
    isbn varchar(255) not null,
    title varchar(255) not null,
    author_ref int not null,
    publisher_ref int not null,
    primary key (id),
    foreign key (author_ref) references Authors(id),
    foreign key (publisher_ref) references Publishers(id)
);

Data.sql(测试数据):

sql 复制代码
insert into Authors (firstName, lastName) values ('Craig', 'Walls');
insert into Authors (firstName, lastName) values ('Josh', 'Long');
insert into Authors (firstName, lastName) values ('Ken', 'Kousen');
insert into Authors (firstName, lastName) values ('Venkat', 'Subramaniam');

insert into Publishers (name) values ('Manning Publications');
insert into Publishers (name) values ('OReilly Media');
insert into Publishers (name) values ('Pragmatic Bookshelf');

insert into Books (isbn, title, author_ref, publisher_ref)
values ('9781617292545', 'Spring Boot in Action', 1, 1);
insert into Books (isbn, title, author_ref, publisher_ref)
values ('9781617297571', 'Spring in Action', 1, 1);
insert into Books (isbn, title, author_ref, publisher_ref)
values ('9781680507256', 'Build Talking Apps for Alexa', 1, 3);
insert into Books (isbn, title, author_ref, publisher_ref)
values ('9781934356401', 'Modular Java', 1, 3);
insert into Books (isbn, title, author_ref, publisher_ref)
values ('9781449374648', 'Cloud Native Java', 2, 2);
insert into Books (isbn, title, author_ref, publisher_ref)
values ('9781680508222', 'Help Your Boss Help You', 3, 3);
insert into Books (isbn, title, author_ref, publisher_ref)
values ('9781492046677', 'Kotlin Cookbook', 3, 2);
insert into Books (isbn, title, author_ref, publisher_ref)
values ('9781680509816', 'Cruising Along with Java', 4, 3);
insert into Books (isbn, title, author_ref, publisher_ref)
values ('9781680506358', 'Programming Kotlin', 4, 3);

3.2 引入依赖与配置

pom.xml 中添加 MySQL 驱动和 MyBatis-Plus(用于简化数据库操作,当然你也可以直接用 JdbcTemplate)的依赖。

xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.7</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.32</version>
</dependency>

application.properties 中配置数据库连接信息:

properties 复制代码
spring.datasource.url=jdbc:mysql://localhost:3306/gpttest?characterEncoding=utf8&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

4. 核心代码实现

4.1 创建 Prompt 模板

这是整个功能的关键。我们需要一个提示词模板,告诉大模型它的角色和任务。创建一个名为 sql-prompt-template.st 的文件,放在 resources 目录下。

text 复制代码
Given the DDL in the DDL section, write an SQL query to answer the question in the QUESTION section.
Only produce select queries. If the question would result in an insert, update, or delete, or if the query would alter the DDL in any way, say that the operation isn't supported. If the question can't be answered, say that the DDL doesn't support answering that question.
Answer with the raw SQL query only; no markdown or other punctuation that isn't part of the query itself.

DDL
{ddl}

QUESTION
{question}

这个模板清晰地定义了大模型的行为:

  • 角色:一个 SQL 生成器。
  • 约束 :只生成 SELECT 查询,拒绝 INSERT/UPDATE/DELETE 操作。
  • 输出格式:只输出纯 SQL,不包含任何 Markdown 标记。

4.2 编写 Controller

创建一个 SqlExecController,注入 ChatClientJdbcTemplate

java 复制代码
package com.roy.sqlgenerate;

import org.springframework.core.io.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.nio.charset.Charset;

@RestController
public class SqlExecController {

    @Value("classpath:/schema.sql")
    private Resource ddlResource;

    @Value("classpath:/sql-prompt-template.st")
    private Resource sqlPromptTemplateResource;

    private final ChatClient aiClient;
    private final JdbcTemplate jdbcTemplate;

    public SqlExecController(ChatClient.Builder aiClientBuilder, JdbcTemplate jdbcTemplate) {
        this.aiClient = aiClientBuilder.build();
        this.jdbcTemplate = jdbcTemplate;
    }

    @GetMapping(path = "/sql")
    public Answer sql(@RequestParam("question") String question) throws IOException {
        // 1. 读取 DDL
        String schema = ddlResource.getContentAsString(Charset.defaultCharset());

        // 2. 调用大模型生成 SQL
        String query = aiClient.prompt()
                .user(userSpec -> userSpec
                        .text(sqlPromptTemplateResource)
                        .param("question", question)
                        .param("ddl", schema)
                )
                .call()
                .content();

        // 3. 清理大模型返回的 SQL(去除可能的 Markdown 标记)
        query = query.replace("```sql", "").replace("```", "").trim();
        System.out.println("Generated SQL: " + query);

        // 4. 执行 SQL 并返回结果
        if (query.toLowerCase().startsWith("select")) {
            return new Answer(query, jdbcTemplate.queryForList(query));
        }
        throw new RuntimeException("Unsupported operation: " + query);
    }

    // 用于封装返回结果的 Record
    public record Answer(String sql, List<Map<String, Object>> result) {}
}

4.3 测试运行

启动项目,使用 curl 或 Postman 进行测试。

请求

bash 复制代码
curl "http://localhost:8080/sql?question=统计出版书籍前三名的出版社"

响应(示例):

json 复制代码
{
  "sql": "SELECT p.name, COUNT(b.id) AS book_count FROM Publishers p JOIN Books b ON p.id = b.publisher_ref GROUP BY p.name ORDER BY book_count DESC LIMIT 3",
  "result": [
    {
      "name": "Manning Publications",
      "book_count": 2
    },
    {
      "name": "Pragmatic Bookshelf",
      "book_count": 4
    },
    {
      "name": "OReilly Media",
      "book_count": 2
    }
  ]
}

5. 应用扩展与思考

一个简单的 SQL 智能助手已经诞生了。但如何让大模型更稳定、更准确地生成结果,是 AI 应用落地中需要重点考虑的问题。以下是一些扩展思路:

  1. 提供更多上下文:除了 DDL,还可以提供一些示例数据(DML)或常见的查询模式,帮助大模型更好地理解数据。
  2. 结果后处理:对于复杂的聚合查询,可以对大模型返回的 SQL 进行语法校验,或者对查询结果进行格式化、图表渲染等。
  3. 多轮对话 :结合 ChatMemory,让用户可以对上一次的查询结果进行追问,例如"再查一下这些出版社的地址"。
  4. 安全审计:在执行 SQL 前,增加一个审核步骤,确保生成的 SQL 不会对数据库造成破坏(虽然我们的 Prompt 已经做了限制,但双重保险更安全)。

6. 总结

通过本文,我们体验了 SpringAI 在"Text-to-SQL"这一经典场景下的强大能力。我们只用了不到 50 行核心代码,就搭建了一个功能完整的 SQL 智能助手。这充分展示了 SpringAI 框架的简洁与高效,让开发者能够将更多精力放在业务逻辑和 AI 应用的创新上。

从 ChatModel 到 FunctionCall,再到今天的 SQL 智能助手,SpringAI 正在不断降低 AI 应用开发的门槛。希望这篇文章能给你带来启发,快去动手试试吧!

相关推荐
程序员老刘17 分钟前
Flutter版本选择指南:3.44系列继续观望 | 2026年6月
flutter·ai编程·客户端
Shockang35 分钟前
AI 设计工作流全景拆解:Figma MCP / Claude Design / Codex / Google Stitch
人工智能
To_OC2 小时前
数据集划分不是随便切:手把手切分大众点评情感数据集
人工智能·llm·agent
冬奇Lab3 小时前
每日一个开源项目(第142篇):android/skills - Google 官方 Android 开发 AI Skill 库
人工智能·开源·资讯
冬奇Lab3 小时前
Skill 系列(06):Skill 工程化与治理——路由准确率 38%、压缩节省 76%
人工智能·开源·agent
IT_陈寒5 小时前
Vue这个坑我跳了两次,原来问题出在这
前端·人工智能·后端
新新技术迷5 小时前
Node给AI接口做SSE代理与鉴权
人工智能
人活一口气5 小时前
Spring Boot与AIGC的完美结合:从零搭建智能内容生成平台
java·spring boot·aigc
洞窝技术6 小时前
构建 AI 增量代码审查系统:AST 语义分析 + 多层约束架构 + LLM 多模型调度的工程实践
ai编程