用 LangChain4j+Ollama 打造 Text-to-SQL AI Agent,数据库想问就问

简介: 本文介绍了如何利用AI技术简化SQL查询操作,让不懂技术的用户也能轻松从数据库中获取信息。通过本地部署PostgreSQL数据库和Ollama模型,结合Java代码,实现将自然语言问题自动转换为SQL查询,并将结果以易懂的方式呈现。整个流程简单直观,适合初学者动手实践,同时也展示了AI在数据查询中的潜力与局限。

想象一下:老板拿着 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” 和 “把数据转成人话” 这两步,咱们纯属边玩边学~

准备工作:先搭好 “工具箱”

在写代码之前,得先把需要的工具准备好,就像做饭前要先买菜一样:

  1. 本地起个 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

记好这三个信息,后面要用到!

  1. 装 Ollama(本地跑 AI 模型的工具)

直接去官网下载:https://ollama.com/download,跟装微信一样简单。

装完后,在终端里敲一句

ollama run llama3.2:1b

就能启动目前最小的 Ollama 模型(才 1.3GB)。虽然这模型不算 “聪明”,但胜在跑得快,练手刚好合适~ 后面想换厉害的模型,直接换个命令就行(官网有各种免费模型可选)。

  1. 建个 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 {
    @UserMessage("""
                 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(@V("question") String question,  @V("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 类加个这玩意儿:

@UserMessage("""
      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(@V("question") String question, @V("schema") String schema, @V("results") String results);

搞定啦!咱来捋一捋:

  1. 用户提个问题
  2. 生成数据库结构描述
  3. 把问题和结构描述传给模型
  4. 模型生成能回答用户问题的 SQL 查询
  5. 校验查询
  6. 执行查询
  7. 整理结果
  8. 把结果、数据库结构和用户问题传给模型
  9. 得到人话版答案

咱们把这些零件组装到一个简单的 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 代理的理念,简化展示了底层的工作原理。

相关文章
|
1月前
|
存储 人工智能 前端开发
Qoder + ADB Supabase :5分钟GET超火AI手办生图APP
本文介绍如何利用Qoder、阿里云ADB Supabase和通义千问图像编辑模型,快速搭建AI手办生图Flutter应用。无需传统后端,实现从前端生成到数据存储、AI服务集成的全链路敏捷开发,展现Vibe Coding的高效实践。
Qoder + ADB Supabase :5分钟GET超火AI手办生图APP
|
1月前
|
存储 人工智能 NoSQL
AI大模型应用实践 八:如何通过RAG数据库实现大模型的私有化定制与优化
RAG技术通过融合外部知识库与大模型,实现知识动态更新与私有化定制,解决大模型知识固化、幻觉及数据安全难题。本文详解RAG原理、数据库选型(向量库、图库、知识图谱、混合架构)及应用场景,助力企业高效构建安全、可解释的智能系统。
|
2月前
|
人工智能 缓存 监控
使用LangChain4j构建Java AI智能体:让大模型学会使用工具
AI智能体是大模型技术的重要演进方向,它使模型能够主动使用工具、与环境交互,以完成复杂任务。本文详细介绍如何在Java应用中,借助LangChain4j框架构建一个具备工具使用能力的AI智能体。我们将创建一个能够进行数学计算和实时信息查询的智能体,涵盖工具定义、智能体组装、记忆管理以及Spring Boot集成等关键步骤,并展示如何通过简单的对话界面与智能体交互。
862 1
|
1月前
|
人工智能 运维 关系型数据库
云栖大会|AI时代的数据库变革升级与实践:Data+AI驱动企业智能新范式
2025云栖大会“AI时代的数据库变革”专场,阿里云瑶池联合B站、小鹏、NVIDIA等分享Data+AI融合实践,发布PolarDB湖库一体化、ApsaraDB Agent等创新成果,全面展现数据库在多模态、智能体、具身智能等场景的技术演进与落地。
|
2月前
|
人工智能 Java API
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
本文介绍AI大模型的核心概念、分类及开发者学习路径,重点讲解如何选择与接入大模型。项目基于Spring Boot,使用阿里云灵积模型(Qwen-Plus),对比SDK、HTTP、Spring AI和LangChain4j四种接入方式,助力开发者高效构建AI应用。
1290 122
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
|
2月前
|
人工智能 Java API
构建基于Java的AI智能体:使用LangChain4j与Spring AI实现RAG应用
当大模型需要处理私有、实时的数据时,检索增强生成(RAG)技术成为了核心解决方案。本文深入探讨如何在Java生态中构建具备RAG能力的AI智能体。我们将介绍新兴的Spring AI项目与成熟的LangChain4j框架,详细演示如何从零开始构建一个能够查询私有知识库的智能问答系统。内容涵盖文档加载与分块、向量数据库集成、语义检索以及与大模型的最终合成,并提供完整的代码实现,为Java开发者开启构建复杂AI智能体的大门。
1311 58
|
1月前
|
人工智能 运维 NoSQL
云栖大会|AI浪潮下的NoSQL演进:下一代数据库的破局之道
AI浪潮下的NoSQL演进:下一代数据库的破局之道
|
2月前
|
SQL 人工智能 Linux
SQL Server 2025 RC1 发布 - 从本地到云端的 AI 就绪企业数据库
SQL Server 2025 RC1 发布 - 从本地到云端的 AI 就绪企业数据库
339 5
SQL Server 2025 RC1 发布 - 从本地到云端的 AI 就绪企业数据库
|
1月前
|
SQL 存储 监控
SQL日志优化策略:提升数据库日志记录效率
通过以上方法结合起来运行调整方案, 可以显著地提升SQL环境下面向各种搜索引擎服务平台所需要满足标准条件下之数据库登记作业流程综合表现; 同时还能确保系统稳健运行并满越用户体验预期目标.
172 6