前言
在日常开发中,我们经常遇到这样的需求:产品经理扔过来一句“查询上个月订单金额超过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 JOIN | 7B 明显更优 |
结论:有 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 等危险语句
- 正则白名单:只通过以
SELECT、WITH开头的 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:11434 | Ollama 未启动 | 执行 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 应用。核心步骤:
- 安装 Ollama,下载 Qwen2 模型
- 创建 Spring Boot 项目,集成 Spring AI Ollama Starter
- 编写 Prompt 模板,引导模型输出规范 SQL
- 提供 REST API,供前端或其他服务调用
- 生产化改进:动态 schema、安全校验、缓存降级
可以继续扩展的方向:
- 自然语言生成 MongoDB 查询:修改 Prompt 中的语法指示
- 生成 Spring Data JPA 方法名:例如 “根据用户名和邮箱查找” →
findByUsernameAndEmail - 对话式 schema 查询:用户追问 “这个表的 created_at 字段是什么格式?”
最后,再次提醒:LLM 生成的 SQL 必须在测试环境验证后,才能用于生产。你可以结合本系列后续的 Java 应用监控文章 对 SQL 执行进行追踪和审计。
Spring AI Ollama 集成、本地大模型生成 SQL、Java 调用开源 LLM、自然语言转 MySQL、qwen2 Java 代码生成、Ollama Spring Boot 实战、本地 LLM 数据隐私方案









