Modern Architecture
& Coding Solutions

Spring AI + Ollama:本地运行 LLM 生成 SQL 语句(附代码)

前言

在日常开发中,我们经常遇到这样的需求:产品经理扔过来一句“查询上个月订单金额超过1000元的用户”,然后你得把这个自然语言翻译成 SQL。如果有一百个类似的需求,你就要写一百次 SQL。

如果让大语言模型来做这件事呢? 传统的方案是调用 OpenAI API,但敏感数据要传到云端,有隐私顾虑,而且还有持续的成本。本文将介绍一个完全本地的方案:Spring AI + Ollama,在本地运行开源大模型,用自然语言生成 SQL 语句。

所有代码均在 Windows 11 + WSL2 + Docker 环境下测试通过。环境配置参考本系列前文 WSL2 + Docker Desktop 配置指南

本文属于 MACS Dev Hub“现代架构与编码解决方案”系列。后续还将推出 Java 应用接入 Prometheus + Grafana 监控 等相关文章,欢迎持续关注。

一、方案概述:为什么选择 Ollama + 本地模型?

1.1 架构示意图

整个方案的运行流程如下:

1.2 为什么选择这套方案?

对比项OpenAI API本地 Ollama + 开源模型传统规则解析
数据隐私数据上传云端,有泄露风险完全本地,数据不出服务器无风险
成本$0.01-0.03/1K tokens免费(仅需硬件)免费
网络要求需要互联网完全离线可用无要求
SQL 生成质量高(GPT-4)中到高(qwen2:7b 等模型)低(仅简单模式)
响应速度1-3 秒0.5-2 秒(取决于硬件)毫秒级
可控性低(模型黑盒)(可切换模型、调参数)

本地运行 LLM 的核心价值:敏感业务数据(表结构、字段含义、真实查询条件)无需离开公司内网,满足金融、医疗等行业的合规要求。

1.3 适用场景

  • BI 报表系统:业务人员用自然语言自助查询数据
  • 数据库管理工具:辅助生成复杂 SQL
  • 开发辅助功能:根据 JPA 实体类自动生成建表语句
  • 教学场景:演示大语言模型的代码生成能力

不适用场景:需要极高准确率(100%)的生产核心链路,LLM 生成 SQL 仍需人工审核。

二、环境搭建

2.1 安装 Ollama(Windows/WSL2 均可)

Ollama 是一个开源的 LLM 运行框架,支持 Windows、Linux、macOS。

方案一(推荐):在 WSL2 内安装(性能更好,与 Docker 共存)

# 进入 WSL2 Ubuntu 终端
curl -fsSL https://ollama.com/install.sh | sh

# 启动服务(后台运行)
ollama serve &

方案二:Windows 直接安装

访问 ollama.com 下载 Windows 安装包,安装后任务栏会出现羊驼图标。

验证安装:

ollama --version
# 输出示例: ollama version 0.23.3

2.2 下载并运行模型

推荐使用中文能力强的 Qwen2:7B(阿里通义千问 2,约 4GB)或轻量级的 qwen2:1.5b(约 1GB)。首次运行会自动下载模型:

# 下载 Qwen2:7B 模型(约 4.4GB,推荐 N卡 8GB 显存以上)
ollama pull qwen2:7b

# 或下载更轻量的 qwen2:1.5b(约 1GB,CPU 可运行)
ollama pull qwen2:1.5b

# 下载完成后,运行模型(测试用)
ollama run qwen2:1.5b

进入交互模式后,输入 "Hello" 测试响应。输入 /bye 退出。

查看已下载模型:

ollama list

2.3 测试 Ollama API 是否正常

Ollama 默认在 http://localhost:11434 提供 API。用 curl 测试:

curl http://localhost:11434/api/generate -d '{
  "model": "qwen2:1.5b",
  "prompt": "用 MySQL 语法,查询用户表中年龄大于18岁的用户"
}'

如果返回 JSON 格式的响应(包含 response 字段),说明 API 服务正常。

三、Spring Boot 集成 Spring AI

3.1 创建项目并添加依赖

使用 Spring Initializr(或 IDE)创建一个 Spring Boot 3.x 项目,pom.xml 核心依赖如下:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
    <relativePath/>
</parent>

<properties>
    <java.version>21</java.version>
    <spring-ai.version>1.0.0-M4</spring-ai.version>
</properties>

<dependencies>
    <!-- Spring AI Ollama 支持 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
        <version>${spring-ai.version}</version>
    </dependency>

    <!-- Web 接口 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- 可选:MySQL 驱动,用于执行生成的 SQL 验证 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- 可选:JDBC Template -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
</dependencies>

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

3.2 配置 application.yml

spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: qwen2:1.5b          # 模型名称,与 ollama list 中的一致
          temperature: 0.3            # 温度越低,输出越确定(0-1)
          top-p: 0.9
  datasource:                         # 可选:目标数据库(用于验证 SQL)
    url: jdbc:mysql://localhost:3306/yourdb
    username: root
    password: root

server:
  port: 8080

temperature=0.3 是一个经验值。SQL 生成需要确定性,过高的温度(如 0.8)会产生语法错误。

3.3 编写核心服务:SqlGenerationService

创建 com.macs.demo.service.SqlGenerationService.java

package com.macs.demo.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class SqlGenerationService {

    @Autowired
    private ChatClient chatClient;

    /**
     * 根据自然语言和表结构生成 SQL
     * @param naturalLanguage 自然语言查询,例如 "查询所有年龄大于18岁的用户"
     * @param tableSchema 表结构信息(可选,不传则使用默认 schema)
     * @return 生成的 SQL 语句
     */
    public String generateSql(String naturalLanguage, String tableSchema) {
        String finalSchema = (tableSchema == null || tableSchema.isEmpty()) 
            ? getDefaultSchema() : tableSchema;

        // 构造 Prompt 模板
        String promptTemplateText = """
            你是一个专业的 SQL 生成助手。根据以下 MySQL 数据库表结构,将用户的自然语言查询转换为 SQL 语句。

            表结构信息:
            {schema}

            用户需求:{question}

            要求:
            1. 只输出 SQL 语句,不要输出任何解释或额外文字
            2. 确保 SQL 语法正确,符合 MySQL 8.0 标准
            3. 如果需求无法实现,输出 "UNSUPPORTED"

            SQL:
            """;

        Map<String, Object> params = new HashMap<>();
        params.put("schema", finalSchema);
        params.put("question", naturalLanguage);

        PromptTemplate template = new PromptTemplate(promptTemplateText, params);
        Prompt prompt = template.create();

        ChatResponse response = chatClient.call(prompt);
        String sql = response.getResult().getOutput().getContent();

        // 清理可能的 markdown 代码块标记
        sql = sql.replaceAll("```sql\n?", "").replaceAll("\n?```", "").trim();
        return sql;
    }

    /**
     * 默认表结构示例(实际使用时替换为你的真实表结构)
     */
    private String getDefaultSchema() {
        return """
            CREATE TABLE users (
                id INT PRIMARY KEY AUTO_INCREMENT,
                username VARCHAR(50) NOT NULL,
                age INT,
                email VARCHAR(100),
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP
            );
            CREATE TABLE orders (
                id INT PRIMARY KEY AUTO_INCREMENT,
                user_id INT,
                amount DECIMAL(10,2),
                status VARCHAR(20),
                order_date DATE
            );
            """;
    }
}

注意:在实际业务中,你应该从数据库元数据中动态获取表结构,而不是硬编码。下文“生产化改进”会介绍。

3.4 创建 REST Controller

创建 com.macs.demo.controller.SqlController.java

package com.macs.demo.controller;

import com.macs.demo.service.SqlGenerationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/sql")
public class SqlController {

    @Autowired
    private SqlGenerationService sqlGenerationService;

    @PostMapping("/generate")
    public Map<String, String> generateSql(@RequestBody Map<String, String> request) {
        String question = request.get("question");
        String schema = request.getOrDefault("schema", "");

        String sql = sqlGenerationService.generateSql(question, schema);

        return Map.of(
            "question", question,
            "generated_sql", sql,
            "status", sql.startsWith("UNSUPPORTED") ? "failed" : "success"
        );
    }
}

四、运行与测试

4.1 启动 Spring Boot 应用

mvn spring-boot:run

看到 Started Application in 2.5 seconds 即表示成功。

4.2 测试 API

使用 curl 或 Postman 发送 POST 请求:

curl -X POST http://localhost:8080/api/sql/generate \
  -H "Content-Type: application/json" \
  -d '{"question": "查询所有年龄大于18岁的用户,按年龄倒序排列"}'

期望响应

{
  "question": "查询所有年龄大于18岁的用户,按年龄倒序排列",
  "generated_sql": "SELECT * FROM users WHERE age > 18 ORDER BY age DESC;",
  "status": "success"
}

再测试一个更复杂的:

{"question": "统计每个用户的订单总金额,只显示总金额超过1000的用户"}

期望输出类似:

SELECT user_id, SUM(amount) as total_amount 
FROM orders 
GROUP BY user_id 
HAVING SUM(amount) > 1000;

效果截图建议:这里截一张 API 响应(Postman 或终端输出)的截图,展示生成的 SQL。

4.3 模型对比测试

自然语言查询Qwen2:1.5B 输出Qwen2:7B 输出评价
查询用户表所有数据SELECT * FROM users;SELECT id, username, age, email, created_at FROM users;7B 输出了显式字段,更规范
删除 age 为空的数据DELETE FROM users WHERE age IS NULL;DELETE FROM users WHERE age IS NULL OR age = '';7B 考虑了空字符串,更健壮
复杂的多表 JOIN偶尔语法错误正确生成 LEFT JOIN7B 明显更优

结论:有 GPU 推荐使用 7B 模型,纯 CPU 环境使用 1.5B 模型。个人开发者用 1.5B 足够了。

五、生产化改进建议

5.1 动态获取表结构

从数据库中读取真实的表结构(而不是硬编码),让模型了解当前数据库的准确 schema。

@Component
public class SchemaFetcher {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public String fetchAllTableSchemas() {
        String sql = """
            SELECT 
                TABLE_NAME, 
                COLUMN_NAME, 
                DATA_TYPE, 
                IS_NULLABLE 
            FROM INFORMATION_SCHEMA.COLUMNS 
            WHERE TABLE_SCHEMA = DATABASE()
            ORDER BY TABLE_NAME, ORDINAL_POSITION
            """;

        List<Map<String, Object>> columns = jdbcTemplate.queryForList(sql);
        // 按 TABLE_NAME 分组,构建 CREATE TABLE 风格的文本
        // ... 省略组装逻辑(约30行)
        return assembledSchema;
    }
}

5.2 增加 SQL 安全校验

  • 仅允许 SELECT 语句:避免 LLM 生成 DELETE/DROP 等危险语句
  • 正则白名单:只通过以 SELECTWITH 开头的 SQL
  • 人工审核层:高风险操作推送到钉钉/企微,人工确认后再执行
public boolean isSafeSql(String sql) {
    String upper = sql.trim().toUpperCase();
    return upper.startsWith("SELECT") || upper.startsWith("WITH");
}

5.3 缓存与性能优化

  • 相同的自然语言 + 表结构 → 缓存 SQL 结果(Redis 或 Caffeine),避免重复调用模型
  • 使用更小的模型(如 qwen2:1.5b)提升响应速度

5.4 错误处理与降级

try {
    return generateSql(question, schema);
} catch (Exception e) {
    log.error("Ollama 调用失败: {}", e.getMessage());
    // 降级方案:返回规则匹配的 SQL 或错误提示
    return "ERROR: 无法生成 SQL,请检查 Ollama 服务状态";
}

六、常见问题

问题原因解决方法
Connection refused: localhost:11434Ollama 未启动执行 ollama serve 或检查 Windows 任务栏
模型下载极慢GitHub/HuggingFace 网络问题使用国内镜像或代理,或提前下载导入
生成的 SQL 语法错误模型能力不足 / temperature 太高降低 temperature 到 0.2,或换用 7B 模型
CPU 运行 7B 模型过慢模型太大改用 1.5B 模型,或增加内存(≥16GB)
Spring AI 版本兼容性Spring Boot 版本不匹配使用 3.2.x + spring-ai 1.0.0-M4

七、总结与扩展

本文完整实现了一个 本地运行 LLM 生成 SQL 语句 的 Spring Boot 应用。核心步骤:

  1. 安装 Ollama,下载 Qwen2 模型
  2. 创建 Spring Boot 项目,集成 Spring AI Ollama Starter
  3. 编写 Prompt 模板,引导模型输出规范 SQL
  4. 提供 REST API,供前端或其他服务调用
  5. 生产化改进:动态 schema、安全校验、缓存降级

可以继续扩展的方向

  • 自然语言生成 MongoDB 查询:修改 Prompt 中的语法指示
  • 生成 Spring Data JPA 方法名:例如 “根据用户名和邮箱查找” → findByUsernameAndEmail
  • 对话式 schema 查询:用户追问 “这个表的 created_at 字段是什么格式?”

最后,再次提醒:LLM 生成的 SQL 必须在测试环境验证后,才能用于生产。你可以结合本系列后续的 Java 应用监控文章 对 SQL 执行进行追踪和审计。


赞(0) 打赏
未经允许不得转载:MACS Dev Hub » Spring AI + Ollama:本地运行 LLM 生成 SQL 语句(附代码)

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫

登录

找回密码

注册