想象一下:老板拿着 Excel 表问你 “咱们家最畅销的三款产品是啥?”,你却对着复杂的 SQL 查询语句抓头发 —— 这种尴尬场面,以后有 AI 帮忙就再也不会出现啦!
其实很多职场人(甚至有些开发)都搞不定 SQL,更别说非技术出身的管理者了。要是能直接用大白话问数据库问题,比如 “谁买的东西最多?”“哪个产品最赚钱?”,那工作效率不得直接起飞?
先给大家看个实际效果:
问题:咱们最畅销的三款产品是啥?
AI 给出的答案:根据数据库数据,top3 畅销产品是:
笔记本电脑 —— 卖了 3 台
鼠标 —— 卖了 2 台
键盘 —— 卖了 2 台
这三款产品的销量都在 2 台以上,是目前的销量冠军。
不过先说明哈:这方法不算 “最优解”,主要是带大家玩一玩,感受下 LLM(大语言模型)的能力。毕竟先摸清 AI 的 “脾气”,才能解锁更多骚操作嘛~(要是真要上生产环境,还是得用 MCP 或者 Langchain4j 的 @Tool 工具调用功能,专业的事得靠专业工具!)
核心思路:让 AI 当你的 “SQL 翻译官”
整个流程其实超简单,就像 “传话游戏”,只不过中间多了个 AI 帮忙:
你用大白话提问题(比如 “最畅销的产品是啥?”)
给 Ollama 模型(一款本地能跑的 AI)看数据库的 “结构说明书”,让它生成对应的 SQL 查询语句
用这个 SQL 去数据库里捞数据
再把捞到的数据和你的问题一起丢给 AI,让它给你一个人话版答案
全程没什么 “黑科技”,就是靠 AI 帮你搞定 “把大白话转成 SQL” 和 “把数据转成人话” 这两步,咱们纯属边玩边学~
准备工作:先搭好 “工具箱”
在写代码之前,得先把需要的工具准备好,就像做饭前要先买菜一样:
- 本地起个 PostgreSQL 数据库(用 Docker 超方便)
复制下面这行命令,在终端里敲一下,就能自动下载并启动数据库,连安装步骤都省了:
docker run --name tts-db \ -e POSTGRES_USER=tts-user \ -e POSTGRES_PASSWORD=tts-pass \ -e POSTGRES_DB=tts-db \ -p 5432:5432 -d postgres
给大家划重点:
数据库名:tts-db
用户名:tts-user
密码:tts-pass
记好这三个信息,后面要用到!
- 装 Ollama(本地跑 AI 模型的工具)
直接去官网下载:https://ollama.com/download,跟装微信一样简单。
装完后,在终端里敲一句
ollama run llama3.2:1b
就能启动目前最小的 Ollama 模型(才 1.3GB)。虽然这模型不算 “聪明”,但胜在跑得快,练手刚好合适~ 后面想换厉害的模型,直接换个命令就行(官网有各种免费模型可选)。
- 建个 Java 项目(用 Gradle 举例)
先建个普通的 Java Gradle/Maven 项目,然后在build.gradle里加三个依赖(相当于给项目装 “插件”):
implementation 'org.postgresql:postgresql:42.7.7' implementation 'dev.langchain4j:langchain4j:1.0.1' implementation 'dev.langchain4j:langchain4j-ollama:1.0.1-beta6'
动手实操:一步步搭起 “AI 查数据库” 功能
准备工作做完,就到最有意思的环节了!咱们一步步来,别怕,代码都给你写好了~
第一步:给数据库填点 “测试数据”
先建个init.sql文件(放在src/main/resources文件夹下),里面写好建表和插数据的 SQL。我让 ChatGPT 帮我编了个简化的电商数据库,大家直接用就行:
-- 先删旧表,避免重复 DROP TABLE IF EXISTS orders; DROP TABLE IF EXISTS products; DROP TABLE IF EXISTS customers; -- 顾客表 CREATE TABLE customers ( id INT PRIMARY KEY, name VARCHAR(100), -- 姓名 email VARCHAR(100), -- 邮箱 city VARCHAR(50) -- 城市 ); -- 产品表 CREATE TABLE products ( id INT PRIMARY KEY, name VARCHAR(100), -- 产品名 price DECIMAL(8,2) -- 价格 ); -- 订单表(记录谁买了啥) CREATE TABLE orders ( id INT PRIMARY KEY, customer_id INT, -- 关联顾客表 product_id INT, -- 关联产品表 quantity INT, -- 购买数量 order_date DATE, -- 下单日期 FOREIGN KEY (customer_id) REFERENCES customers(id), FOREIGN KEY (product_id) REFERENCES products(id) ); -- 插点测试顾客 INSERT INTO customers VALUES (1, '张三', 'zhangsan@email.com', '北京'), (2, '李四', 'lisi@email.com', '上海'), (3, '王五', 'wangwu@email.com', '广州'), (4, '赵六', 'zhaoliu@email.com', '深圳'), (5, '钱七', 'qianqi@email.com', '杭州'); -- 插点测试产品 INSERT INTO products VALUES (1, '笔记本电脑', 9999.99), (2, '鼠标', 99.99), (3, '键盘', 299.99), (4, '显示器', 1999.99), (5, '耳机', 799.99); -- 插点测试订单 INSERT INTO orders VALUES (1, 1, 1, 1, '2024-01-15'), -- 张三买1台笔记本 (2, 2, 2, 2, '2024-01-16'), -- 李四买2个鼠标 (3, 1, 3, 1, '2024-01-17'), -- 张三买1个键盘 (4, 3, 1, 1, '2024-01-18'), -- 王五买1台笔记本 (5, 4, 4, 1, '2024-01-19'), -- 赵六买1个显示器 (6, 2, 5, 1, '2024-01-20'), -- 李四买1个耳机 (7, 5, 2, 3, '2024-01-21'), -- 钱七买3个鼠标 (8, 1, 4, 1, '2024-01-22'), -- 张三买1个显示器 (9, 3, 3, 1, '2024-01-23'), -- 王五买1个键盘 (10, 4, 2, 1, '2024-01-24'); -- 赵六买1个鼠标
有了这些数据,就能问 “谁买的鼠标最多?”“笔记本卖了多少台” 这类问题啦~
第二步:写个 “数据库管理器”(帮你连数据库 + 初始化数据)
建个DatabaseManager类,负责两件事:连数据库,以及执行上面的init.sql文件初始化数据。代码里都加了注释,看不懂的地方看注释就行:
package com.tsvetkov.db; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; public class DatabaseManager { // 数据库连接信息(就是之前让你记的那三个!) private static final String URL = "jdbc:postgresql://localhost:5432/tts-db"; private static final String USER = "tts-user"; private static final String PASSWORD = "tts-pass"; // 连数据库并初始化表和数据 public static Connection getInitialConnection() throws SQLException, IOException { // 1. 连数据库 Connection connection = DriverManager.getConnection(URL, USER, PASSWORD); System.out.println("成功连到PostgreSQL啦!"); // 2. 读init.sql文件,执行里面的SQL return new String(Files.readAllBytes(Paths.get("src/main/resources/init.sql"))); }
第三步:初始化 AI 助手(让 Ollama 模型跑起来)
建个AIUtils类,负责启动 Ollama 模型,相当于给你找个 “AI 小助手”:
import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.service.AiServices; public class AIUtils { // 生成AI助手(能帮你转SQL、写答案) public static AIAssistant getAIAssistant() { return AiServices.builder(AIAssistant.class) .chatModel(initOllamaChatModel()) // 连Ollama模型 .build(); } // 启动Ollama的llama3.2:1b模型 private static ChatModel initOllamaChatModel() { return OllamaChatModel.builder() .baseUrl("http://localhost:11434") // Ollama默认地址 .modelName("llama3.2:1b") // 用的模型名 .build(); } }
第四步:定义 AI 助手的 “能力”(让它知道该干啥)
建个AIAssistant接口,用注解告诉 AI:“你要帮我把大白话转成 SQL”“你要帮我把数据转成人话”。这就像给 AI 发 “任务清单”:
import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; public interface AIAssistant { (""" You are a senior SQL engineer. Given the database schema and user question below, write a syntactically correct and schema-valid SQL SELECT query. Database Schema (Use only columns and tables listed here) {{schemaDescription}} Rules: Only valid syntax queries - meaning it must start with SELECT Only use tables and columns from the schema above — do not guess Only use SELECT statements (no INSERT, UPDATE, DELETE) Use explicit JOINs, not subqueries unless necessary Add LIMIT 100 to large result sets if not specified in the question Use aggregate functions (COUNT, SUM, etc.) only if the question requires it Return only the SQL query, no explanation, no comments The query must be valid SQL and executable without syntax errors User Question {{question}} """) String getQuery(("question") String question, ("schema") String schemaDescription); }
第五步:写个 “数据库结构分析器”(给 AI 看 “说明书”)
AI 得知道数据库里有哪些表、每个表有哪些字段,才能写出正确的 SQL。所以建个SchemaAnalyzer类,把数据库结构整理成 AI 能看懂的格式(还会加几条测试数据当例子):
import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; // 存字段信息(字段名+字段类型) record ColumnInfo(String name, String type) {} public class SchemaAnalyzer { // 每个表只取3条数据当例子,太多了AI看不过来 private static final int EXAMPLES_LIMIT = 3; // 生成数据库结构描述(给AI看的“说明书”) public static String getSchemaDescription(Connection connection) throws SQLException { if (connection == null) { throw new IllegalArgumentException("数据库没连上,别瞎传!"); } StringBuilder schemaDesc = new StringBuilder(); // 1. 先获取所有表名 List<String> tables = getTables(connection); // 2. 逐个表整理结构和示例数据 for (String table : tables) { schemaDesc.append("表名:").append(table).append("\n"); schemaDesc.append(getTableDetail(connection, table)); } return schemaDesc.toString(); } // 整理单个表的结构(字段名+类型)和示例数据 private static String getTableDetail(Connection connection, String table) throws SQLException { StringBuilder detail = new StringBuilder(); // 获取表的所有字段 List<ColumnInfo> columns = getColumns(connection, table); for (ColumnInfo col : columns) { detail.append(" - ").append(col.name()).append("(类型:").append(col.type()).append(")\n"); } // 获取表的示例数据 List<List<String>> sampleRows = getSampleRows(connection, table, columns); // 把示例数据写成表格格式,AI看得更清楚 detail.append("示例数据:\n"); detail.append(" | "); for (ColumnInfo col : columns) { detail.append(col.name()).append(" | "); } detail.append("\n"); for (List<String> row : sampleRows) { detail.append(" | "); for (String value : row) { detail.append(value).append(" | "); } detail.append("\n"); } return detail.toString(); } // 获取数据库里的所有表名 private static List<String> getTables(Connection connection) throws SQLException { List<String> tables = new ArrayList<>(); try (ResultSet rs = connection.getMetaData().getTables(null, "public", "%", new String[]{"TABLE"})) { while (rs.next()) { tables.add(rs.getString("TABLE_NAME")); } } return tables; } // 获取单个表的所有字段 private static List<ColumnInfo> getColumns(Connection connection, String table) throws SQLException { List<ColumnInfo> columns = new ArrayList<>(); try (ResultSet rs = connection.getMetaData().getColumns(null, "public", table, "%")) { while (rs.next()) { columns.add(new ColumnInfo( rs.getString("COLUMN_NAME"), // 字段名 rs.getString("TYPE_NAME") // 字段类型 )); } } return columns; } private static List<List<String>> getSampleRows(Connection connection, String table, List<ColumnInfo> columns) throws SQLException { var rows = new ArrayList<List<String>>(); try (ResultSet rs = connection.createStatement().executeQuery("SELECT * FROM \"" + table + "\" LIMIT " + EXAMPLES_LIMIT)) { while (rs.next()) { var row = new ArrayList<String>(); for (ColumnInfo col : columns) { row.add(rs.getString(col.name())); } rows.add(row); } } return rows; } }
咱们的操作逻辑其实特简单:先搞清楚数据库里有哪些表,每张表有啥字段、字段是啥类型,然后按照刚才说的格式生成描述,再从每张表里挑几条数据当例子 —— 齐活!
举个例子,要是调用 AIAssistant 的 getQuery 方法,问 “产品目录里最贵的是啥?”,AI 可能会返回这样的 SQL:
SELECT T1.price FROM products AS T1 INNER JOIN orders AS T2 ON T1.id= T2.product_id GROUP BY T1.price ORDER BY SUM(T1.price) DESC LIMIT 1
执行数据库查询
下一步就是执行这个查询,把结果捞出来。咱先简单校验下 SQL 语法,不用搞太复杂的操作。给 DatabaseManager 类加个 validateQuery 方法就行:
public static void validateQuery(String sql) { // 基础校验——只允许SELECT语句 String upperSql = sql.toUpperCase().trim(); if(!upperSql.startsWith("SELECT") || upperSql.contains("DROP") || // 不准删表 upperSql.contains("DELETE") || // 不准删数据 upperSql.contains("INSERT") || // 不准插数据 upperSql.contains("UPDATE") || // 不准改数据 upperSql.contains("ALTER") || // 不准改表结构 upperSql.contains("CREATE")) { // 不准建表 throw new RuntimeException("SQL有问题,不能执行:\n"+sql); } }
校验通过后,就可以执行查询,然后把结果整理一下。
比如问 “能列出所有产品和它们的价格不?”
模型可能会返回这样的查询:
SELECT p.name, p.price FROM products p ORDER BY p.price LIMIT 100
我希望结果能长成这样:
name | price Mouse | 25.99 Keyboard | 75.99 Headphones | 149.99 Monitor | 299.99 Laptop | 999.99
咱们来实现个方法干这事儿。这个方法接收模型生成的 SQL 查询和数据库连接作为参数,就叫 getFormattedResultsFromQuery:
private static String getFormattedResultsFromQuery(Connection conn, String sql) throws SQLException { DatabaseManager.validateQuery(sql); var resultsDescription = new StringBuilder(); try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { int columnCount = rs.getMetaData().getColumnCount(); // 先拼表头 for (int i = 1; i <= columnCount; i++) { resultsDescription.append(rs.getMetaData().getColumnName(i)); if (i < columnCount) resultsDescription.append(" | "); } resultsDescription.append("\n"); // 再拼数据行 while (rs.next()) { for (int i = 1; i <= columnCount; i++) { resultsDescription.append(rs.getString(i)); if (i < columnCount) resultsDescription.append(" | "); } resultsDescription.append("\n"); } } return resultsDescription.toString(); }
用 SQL 查询结果生成最终答案
现在可以给模型定义个模板,把数据库结构、查询结果和用户的问题一起传过去 —— 这样模型应该就能给出靠谱的答案了。给 AIAssistant 类加个这玩意儿:
(""" You are a data analyst. Based on the database schema, the user’s question, the SQL query and the SQL query results, generate a clear, concise, human-readable answer. Focus on answering the user's question directly using the data provided — do not describe the SQL or repeat the table structure. Give a straight answer. ### Database Schema {{schema}} ### User Question {{question}} ### SQL Query {{ query }} ### SQL Query Results {{results}} ### Answer """) String explainAnswer(("question") String question, ("schema") String schema, ("results") String results);
搞定啦!咱来捋一捋:
- 用户提个问题
- 生成数据库结构描述
- 把问题和结构描述传给模型
- 模型生成能回答用户问题的 SQL 查询
- 校验查询
- 执行查询
- 整理结果
- 把结果、数据库结构和用户问题传给模型
- 得到人话版答案
咱们把这些零件组装到一个简单的 main 方法里:
public static void main(String[] args) throws SQLException, IOException { System.out.println("Starting our text-to-sql application..."); AIAssistant aiAssistant = AIUtils.getAIAssistant(); try (Connection conn = DatabaseManager.getInitialConnection()) { String schemaDescription = SchemaAnalyzer.getSchemaDescription(conn); String question = "What are our top 3 most ordered products?"; String query = aiAssistant.getQuery(question, schemaDescription); String formattedQueryResults = getFormattedResultsFromQuery(conn, query); String humanAnswer = aiAssistant.explainAnswer(question, schemaDescription, query, formattedQueryResults); System.out.println(humanAnswer); } }
最后,运行这个例子,会得到类似这样的结果:
启动咱们的文本转SQL应用... 成功连到PostgreSQL啦! 数据库表和数据都初始化好咯~ 张三总共下了3单。
得说明一下,用我这个例子里的模型,结果不一定总能完美无缺。不过用来瞎折腾折腾够够的了,而且试试不同的模型模板,看啥样能让模型给出更好的答案,还挺有意思的。要是用更厉害的模型,几乎每次都能得到不错的结果。
你可能还会发现,模型生成的查询经常有问题,所以整个重试逻辑是个不错的练习。玩得开心就好!
如今用 Java 在本地折腾大语言模型其实特简单。轻量级模型加上用熟悉的语言写的结构清晰的代码,让这些想法快速原型化又快又好玩!就像文章开头说的,这个方法不算优化,也不适合实际生产应用,但它基于 AI 代理的理念,简化展示了底层的工作原理。