告别手写复杂查询,用自然语言和数据库对话
引言:当业务经理说“我要看份销售报表”
周一早上,产品经理急匆匆地跑来,甩给你一个需求:“帮我查一下上季度各区域复购率最高的产品有哪些,按复购率降序排列。”
你心想:这个查询得关联 customers、orders、products 三张表,还得计算各用户的订单次数和复购率,SQL 写起来少说要半小时。而这已经是今天接到的第五个类似需求了。
手写 SQL 的效率瓶颈,正成为数据驱动团队越来越突出的痛点。如果能让业务人员直接用自然语言提出数据问题,系统自动生成并执行 SQL,整个数据团队的效率就能大幅释放。
这正是 Text-to-SQL(自然语言转 SQL)技术试图解决的问题。本文将在 Spring Boot 3.x 项目中,使用 LangChain4j + Ollama 搭建一套完整的 Text-to-SQL 智能查询系统,实现“用大白话问数据库”的能力。
本文将沿用 《Java 开发者上手 Cursor》 中使用的 VS Code + WSL2 环境,在此基础上集成 Spring Boot 和数据库,逐步构建一个支持自然语言查询的 Spring Boot 服务。
一、Text-to-SQL 的核心挑战
Text-to-SQL 并非简单的“翻译问题”。当产品经理问“上季度各区域复购率最高的产品是什么”时,真正的难点在于:
其一,结构认知的断层。 数据库设计遵循第三范式,各表之间通过外键关联,而业务问题往往是跨表关联的模糊需求。比如复购率这个指标,需要同时关联 customers(客户)、orders(订单)和 products(产品)三张表,涉及用户等级的定义、购买频率的计算、商品分类的归类等多层业务规则,LLM 仅凭原始表结构很难直接推理出来。
其二,生成 SQL 的执行容错。 即便 LLM 生成了 SQL,也常因表名大小写敏感、函数不兼容、JOIN 条件缺失等细微问题执行失败。实测数据显示,即使是最先进的闭源模型 GPT-4o 在 BIRD 复杂查询评测基准上,整体准确率也只有 52.54%,一旦遇到需要多步推理的复杂查询,准确率会直接跌到 35%。
其三,安全风险不容忽视。 AI 生成的 SQL 如果未经严格审查,误写的 DELETE 或 DROP 语句可能造成不可逆的数据破坏。
LangChain4j 的 SqlDatabaseContentRetriever 组件正是为了解决上述挑战而设计的。它不仅是简单的“翻译器”,而是一个具备元数据感知、智能重试和安全防护的完整查询引擎。
二、技术选型:为什么是 LangChain4j + Ollama?
| 组件 | 作用 | 选型理由 |
|---|---|---|
| LangChain4j | 自然语言转 SQL 的 Java 框架 | 专为 Java 生态设计,完美集成 Spring Boot,提供 SqlDatabaseContentRetriever 开箱组件 |
| Ollama | 本地模型推理服务 | 免费、隐私安全,支持 Llama3/DeepSeek/SQLCoder 等 50+ 模型,部署简单 |
| DeepSeek-Coder:6.7B | SQL 生成模型 | 中文友好,代码推理强,70 亿参数仅需约 2GB 内存,个人环境也能跑 |
| MySQL / PostgreSQL | 业务数据库 | 用于存储真实业务数据,演示查询能力 |
LangChain4j 诞生于 2023 年,填补了 Python LangChain 在 Java 生态中的空白。2025 年 7 月发布的 1.2.0 版本标志着框架走向成熟,同年 Red Hat 将 LangChain4j 作为 Quarkus 官方扩展正式集成;目前 GitHub Stars 已达 11.8k,在 Java 开发者中的采用率已超过 68%。框架支持 Ollama、OpenAI、Azure OpenAI、Google Vertex AI Gemini 等多种模型实现,上层业务逻辑无需为切换模型而改动代码。
本地模型选用 DeepSeek-Coder 6.7B,原因在于它在约 2 万亿 token 上训练(其中 87% 是代码),在中英文代码生成和推理任务上表现均衡,资源消耗适中。如果硬件配置充足,也可选择 SQLCoder 7B,后者在 sql-eval 评测框架上的表现略微优于 GPT-3.5-Turbo,尤其擅长处理多表 JOIN 查询。
三、环境准备与模型部署
在开始编码之前,需要依次完成数据库准备、Ollama 模型部署和 Spring Boot 项目初始化。
3.1 数据库准备:初始化示例数据
在 MySQL 中创建一个测试数据库 text2sql_demo,并准备 customers、products、orders 三张表用于演示:
CREATE DATABASE IF NOT EXISTS text2sql_demo;
USE text2sql_demo;
-- 客户表
CREATE TABLE customers (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
region VARCHAR(20) NOT NULL,
created_at DATE NOT NULL
);
-- 产品表
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price DECIMAL(10,2) NOT NULL
);
-- 订单表
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
customer_id INT NOT NULL,
product_id INT NOT NULL,
order_date DATE NOT NULL,
quantity INT NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
-- 插入测试数据
INSERT INTO customers (name, region, created_at) VALUES
('张三', '华东', '2024-01-15'),
('李四', '华南', '2024-02-20'),
('王五', '华东', '2024-03-10');
INSERT INTO products (name, price) VALUES
('笔记本电脑', 5999.00),
('机械键盘', 399.00),
('无线鼠标', 89.00);
INSERT INTO orders (customer_id, product_id, order_date, quantity) VALUES
(1, 1, '2024-04-01', 1), (1, 2, '2024-04-15', 2),
(2, 1, '2024-04-10', 1), (2, 3, '2024-04-20', 3),
(3, 2, '2024-05-01', 1), (3, 3, '2024-05-10', 2);
3.2 部署 Ollama 并拉取模型
Ollama 的安装极为简单:从 ollama.com/download 下载对应操作系统的安装包并完成安装。2026 年 2 月发布的 v0.15.6 版本修复了模型加载机制和上下文处理限制,是当前推荐使用的稳定版本。
安装完成后执行以下命令拉取并运行模型(保持终端运行状态):
# 拉取 DeepSeek-Coder 6.7B(推荐)
ollama pull deepseek-coder:6.7b-instruct
# 可选:拉取 SQLCoder 7B(更强但资源消耗更高)
ollama pull sqlcoder:7b
# 验证模型可用
ollama run deepseek-coder:6.7b-instruct "生成一个查询所有订单总数的 SQL"
模型拉取完成后,Ollama 会默认在 http://localhost:11434 上启动 API 服务,LangChain4j 将通过该端点调用模型。
💡 实际测试中,DeepSeek-Coder 6.7B 在标准 SQL 生成场景下每秒可以输出 20–30 个 token,响应延迟通常在 2–5 秒,完全能满足个人开发和学习场景的需求。
3.3 Spring Boot 项目配置
创建 Spring Boot 项目,并在 pom.xml 中添加以下依赖:
<properties>
<langchain4j.version>1.15.0</langchain4j.version>
<spring-ai.version>1.0.0-M6</spring-ai.version>
</properties>
<dependencies>
<!-- Spring Boot 基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- LangChain4j 核心依赖 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- LangChain4j Ollama 集成 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- LangChain4j 实验性 SQL 模块(核心) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-experimental-sql</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
四、核心实现:用 SqlDatabaseContentRetriever 搭建 Text-to-SQL 引擎
4.1 工作原理概览
SqlDatabaseContentRetriever 作为 LangChain4j 实验性 SQL 模块的核心组件,其工作流程如下:
- 元数据提取:通过 JDBC 从数据源中提取表名、列名、数据类型和约束信息,生成数据库结构描述(DDL 风格)
- SQL 生成:将用户自然语言问题和数据库结构拼接成优化后的提示词,调用 LLM 生成 SQL
- 安全校验:使用 JSqlParser 校验生成的 SQL 是否为
SELECT语句,拦截写操作 - 执行与重试:执行 SQL 查询,失败时根据错误信息触发智能重试,指导 LLM 修正
- 结果返回:将查询结果打包返回,供下游业务处理
整个流程形成一个闭环,确保从“用户问题”到“数据答案”的高效转化。
4.2 完整 Java 实现代码
package com.macs.text2sql.service;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.experimental.rag.content.retriever.sql.SqlDatabaseContentRetriever;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.rag.content.Content;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class TextToSqlService {
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${spring.datasource.username}")
private String dbUsername;
@Value("${spring.datasource.password}")
private String dbPassword;
/**
* 初始化 Ollama 模型(连接到本地服务)
*/
private ChatLanguageModel initModel() {
return OllamaChatModel.builder()
.baseUrl("http://localhost:11434")
.modelName("deepseek-coder:6.7b-instruct")
.temperature(0.1) // 低温度让输出更确定
.build();
}
/**
* 初始化只读数据源(关键安全措施!)
*/
private DataSource initReadOnlyDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(dbUrl);
config.setUsername(dbUsername);
config.setPassword(dbPassword);
config.setReadOnly(true); // 只读连接
config.setConnectionInitSql("SET SESSION TRANSACTION READ ONLY");
return new HikariDataSource(config);
}
/**
* 自然语言查询转 SQL 并执行
* @param question 用户自然语言问题,如:"华东地区销量最高的产品是哪款?"
* @return 查询结果(SQL 查询返回的表格数据)
*/
public String askDatabase(String question) {
DataSource dataSource = initReadOnlyDataSource();
ChatLanguageModel model = initModel();
// 构建 Text-to-SQL 检索器
SqlDatabaseContentRetriever retriever = SqlDatabaseContentRetriever.builder()
.dataSource(dataSource)
.chatModel(model)
.sqlDialect("MySQL") // 根据实际数据库类型配置
.maxRetries(2) // 最多重试 2 次
.build();
// 执行检索(自动完成:解析问题 → 生成 SQL → 执行 → 返回结果)
List<Content> contents = retriever.retrieve(question);
// 将结果转换为可读字符串
return contents.stream()
.map(Content::text)
.collect(Collectors.joining("\n"));
}
}
4.3 API 接口暴露
package com.macs.text2sql.controller;
import com.macs.text2sql.service.TextToSqlService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/query")
public class TextToSqlController {
private final TextToSqlService textToSqlService;
public TextToSqlController(TextToSqlService textToSqlService) {
this.textToSqlService = textToSqlService;
}
@PostMapping("/nl")
public String naturalLanguageQuery(@RequestBody QueryRequest request) {
return textToSqlService.askDatabase(request.getQuestion());
}
public static class QueryRequest {
private String question;
public String getQuestion() { return question; }
public void setQuestion(String question) { this.question = question; }
}
}
五、效果测试与优化
编写 Spring Boot 主启动类并运行服务后,通过以下 curl 命令调用接口进行测试:
curl -X POST http://localhost:8080/api/query/nl \
-H "Content-Type: application/json" \
-d '{"question": "华东地区销量最高的产品是哪款?"}'
基于测试数据,系统返回的结果类似:
根据数据库查询结果,华东地区销量最高的产品是:机械键盘,总销量为2台。
尽管目前的实现能够应对单表查询等简单场景,但在实际应用中仍存在明显局限:对于包含 JOIN 和嵌套子查询的复杂业务问题,准确率仍有较大提升空间。以下是几个关键的优化方向:
| 优化方向 | 实现方案 | 预期收益 |
|---|---|---|
| 动态元数据过滤 | 只提取与查询相关的表结构,而非全库所有表 | 缩短提示词长度,提升生成准确率约 15% |
| 表别名和列名语义增强 | 在 DDL 描述中添加表的业务中文注释 | 帮助 LLM 更好理解字段语义 |
| 多轮自校正重试 | 提取 SQL 执行错误信息(如列名拼错),反馈 LLM 修正 | 解决约 40% 的可修正错误 |
| Few-shot 示例嵌入 | 在提示模板中加入 3–5 个查询示例(Query → SQL) | 大幅提升复杂查询准确率 |
| 提示词模板微调 | 针对特定数据库方言优化 promptTemplate 参数 | 减少语法不兼容问题 |
六、安全警示:绝对不能忽视的底线
LangChain4j 官方文档对 SqlDatabaseContentRetriever 有一段非常直白的警告:
WARNING! 虽然这个组件很有趣也很令人兴奋,但生产环境严禁使用。尽管组件会使用 JSqlParser 验证生成的 SQL 是否为 SELECT 语句,但无法保证 SQL 绝对无害。请务必使用只读权限的数据库账户!
这段警告不是危言耸听。AI 生成的 SQL 存在以下真实风险:
- 统计查询中的
CROSS JOIN可能在毫秒级内产生数亿条中间结果,导致数据库 CPU 飙升,甚至造成线上服务不可用 - 如果 AI 错误理解了问题,“查询数量前三的产品”可能生成
SELECT ... LIMIT 3(这是安全的),但“删除重复数据”这种带有暗示的问题,在提示词设计不当的情况下有可能诱导 LLM 生成DELETE或DROP语句
生产环境部署必须严格执行以下安全措施:
- 专属只读账户:为 LLM 查询创建独立的数据库用户,仅授予
SELECT权限,严禁任何写权限 - 连接池强制只读:如代码所示,设置
HikariConfig.setReadOnly(true),双重保障 - SQL 预校验:在 AI 生成的 SQL 执行前,使用正则或 JSqlParser 二次校验,禁止执行
DELETE/UPDATE/INSERT/DROP/CREATE等非SELECT语句 - 查询隔离:将查询执行放在独立的线程池或沙箱环境中,配置超时限制,避免慢查询拖垮主服务
七、写在最后
本文使用 LangChain4j + Ollama + DeepSeek-Coder 搭建了一套完整的 Text-to-SQL 智能查询系统。LangChain4j 作为 Java AI 开发框架,将复杂的 AI 集成标准化,让 Java 开发者无需成为 AI 专家就能构建智能应用。读者完全可以将本文的代码作为起点,根据自身业务场景进行扩展和优化。
结合本系列之前的文章,现在已先后完成了以下工程化实践:
- 《Java 开发者上手 Cursor:AI 代码补全与重构实测》——利用 AI 提升编码效率
- 《WSL2 + Docker Desktop:Windows 下的完美 Java 开发环境》——搭建本地开发环境
- [《用 AI 生成复杂 SQL:LangChain4j + 本地模型实践》](本文)——借助 AI 生成 SQL 查询
后续的文章将继续深入,探索 Java 应用接入 Prometheus + Grafana 全记录和 Spring Boot 3.4 Docker 镜像最佳实践(含分层构建),构建一套完整的现代 Java 工程化技术栈。
如果你的项目中有数据分析类需求,不妨用本文的方案先跑一个原型试一试——用自然语言查询数据库,远比想象中的更触手可及。
本文为 MACS Dev Hub 原创,如需转载请联系授权。









