欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

LangChain4j实战全系列链接

  1. 准备工作
  2. 极速开发体验
  3. 细说聊天API
  4. 集成到spring-boot
  5. 图像模型
  6. 聊天记忆,低级API版
  7. 聊天记忆,高级API版
  8. 响应流式传输
  9. 高级API(AI Services)实例的创建方式
  10. 结构化输出之一,用提示词指定输出格式
  11. 结构化输出之二,function call
  12. 结构化输出之三,json模式
  13. 函数调用,低级API版本
  14. 函数调用,低级API版本
  15. RAG (检索增强生成),Easy RAG
  16. RAG (检索增强生成),Naive RAG
  17. RAG (检索增强生成),clickhouse存储向量数据

前面实战的遗留问题

  • 经过前面的学习,咱们已经学会了RAG的存储和使用,看起来可以把自己的本地资源用于LLM对话了,但有个问题使得之前的方案不具备实用性,且看下图,这是之前方案的代码,功能是把切分好的文本转为向量再保存到内存中,之后和LLM对话时就会在内存中做向量查询
    在这里插入图片描述
  • 聪明的您应该发现问题所在:
  1. 当前的方案是把向量保存在内存中的,文本要是多的话,内存不够用咋办?
  2. 要是进程出问题了向量就丢了,所以每次启动都要重新加载吗?
  3. 文本转向量较慢(五百行需要十多秒),每次启动要等很久才能使用
  • 为了解决上述问题,需要用到持久化存储,目前LangChain4j可以支持多种存储方式,如下表所示
Embedding Store Storing Metadata Filtering by Metadata Removing Embeddings
In-memory
AlloyDB for Postgres
Amazon S3 Vectors
Astra DB
Azure AI Search
Azure CosmosDB Mongo vCore
Azure CosmosDB NoSQL)
Cassandra
Chroma
ClickHouse
Cloud SQL for Postgres
Coherence
Couchbase
DuckDB
Elasticsearch
Infinispan
JVector
Mariadb
Milvus
MongoDB Atlas
Neo4j
OceanBase
OpenSearch
Oracle
PGVector
Pinecone
Qdrant
Redis
SQL Server
Tablestore
Vearch
Vespa
Weaviate
YugabyteDB
  • 从易于部署和使用的角度来看,本次实战选择了clickhouse,您也可以选择自己熟悉的类型,代码都差不多,主要是配置对象的差别

本篇概览

  • 本次选用clickhouse作为存储服务,实现在和LLM对话中使用RAG能力,而数据来自clickhouse中存储的向量
  • 整个实战要创建两个springboot应用
  1. rag-clickhouse-index:提供一个接口,调用该接口会触发本地文档的加载并转为向量存入clickhouse
  2. rag-clickhouse-query:提供一个接口,该接口使用RAG能力与LLM对话,RAG所需数据在clickhouse中查询得到
  • 之所以把索引和对话分成两个应用,是为了更贴近实际场景:职责不同,应该分开发展
  • 简单画了下架构图,如下所示
    在这里插入图片描述
  • 接下来是准备工作,咱们先把clickhouse部署好,为了简单实现,这里选用docker来部署
  • 这里要说明的是,咱们的目标是有一个可用的clickhouse,用docker只是因为操作简单,如果您有现成的clickhouse就直接用吧,也可以直接在物理机上部署,总之能用就行

注意docker镜像代理问题

  • 如果是docker部署,就一定要注意docker镜像代理问题,目前不用代理直接拉镜像经常拉取失败,而之前一些常用的代理也不能用了,目前使用以下代理配置可以拉到镜像,配置文件是/etc/docker/daemon.json
{
  "registry-mirrors": ["https://hub.rat.dev", "https://docker.m.daocloud.io"]
}
  • 配置好之后记得执行以下命令让配置生效
sudo systemctl daemon-reload
sudo systemctl restart docker

用docker部署clickhouse并验证

  • 下载镜像
sudo docker pull clickhouse/clickhouse-server:latest
  • 执行以下命令即可完成部署和运行,注意这里密码设置的是123456
sudo docker run -d \
  --name clickhouse-server \
  --ulimit nofile=262144:262144 \
  -p 8123:8123 \
  -p 9000:9000 \
  -v clickhouse-data:/var/lib/clickhouse \
  -e CLICKHOUSE_USER=admin \
  -e CLICKHOUSE_PASSWORD=123456 \
  -e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 \
  clickhouse/clickhouse-server
  • 运行完毕后,这样验证服务是否正常
# 用curl方式可以在clickhouse上执行命令
curl -u admin:123456 'http://localhost:8123/?query=SELECT%20now()'
# 如果服务正常会返回时间信息如下所示
2026-01-17 23:10:45
  • 执行以下命令进入容器
sudo docker exec -it clickhouse-server clickhouse-client --user admin --password 123456
  • 会进入会话模式,输出如下
ClickHouse client version 25.12.3.21 (official build).
Connecting to localhost:9000 as user admin.
Connected to ClickHouse server version 25.12.3.

Warnings:
 * Delay accounting is not enabled, OSIOWaitMicroseconds will not be gathered. You can enable it using `sudo sh -c 'echo 1 > /proc/sys/kernel/task_delayacct'` or by using sysctl.
  • 现在clickhouse已经部署好了,另外就是检查一下用于RAG索引的本地文本是否已经准备好,本篇继续使用删减版wiki百科文本,一共500行,如下所示
    在这里插入图片描述

  • 至此,准备工作已完成,咱们开始编码吧

源码下载(觉得作者啰嗦的,直接在这里下载)

  • 如果您只想快速浏览完整源码,可以在GitHub下载代码直接运行,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):
名称 链接 备注
项目主页 https://github.com/zq2599/blog_demos 该项目在GitHub上的主页
git仓库地址(https) https://github.com/zq2599/blog_demos.git 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  • 这个git项目中有多个文件夹,本篇的源码在langchain4j-tutorials文件夹下,如下图红色箭头所示:
    在这里插入图片描述

编码:父工程调整

  • 《准备工作》中创建了整个《LangChain4j实战》系列代码的父工程,本篇实战会在父工程下新建一个子工程,所以这里要对父工程的pom.xml做少量修改
  1. modules中增加两个子工程,如下图黄框所示
    在这里插入图片描述
  2. 增加一个属性定义,对应着clickhouse的版本
    在这里插入图片描述
  3. 为了读写clickhouse,增加对应库的依赖
      <dependency>
          <groupId>dev.langchain4j</groupId>
          <artifactId>langchain4j-community-clickhouse</artifactId>
          <version>${langchain4j.clickhouse.version}</version>
      </dependency>

编码,rag-clickhouse-index,负责索引

  • 负责索引的应用有个特点,就是不会用到LLM,所以之前工程中与LLM有关的配置(如APIKey)和代码在这里都没有
  • 新增名为rag-clickhouse-index的子工程
  1. langchain4j-totorials目录下新增名rag-clickhouse-index为的文件夹
  2. rag-clickhouse-index文件夹下新增pom.xml,内容如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.bolingcavalry</groupId>
        <artifactId>langchain4j-totorials</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    
    <!-- 配置编译编码 -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    </properties>

    <artifactId>rag-clickhouse-index</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- Spring Boot Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- JUnit Jupiter Engine -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- Mockito Core -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- Mockito JUnit Jupiter -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- LangChain4j Core -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-core</artifactId>
        </dependency>
        
        <!-- LangChain4j OpenAI支持(用于通义千问的OpenAI兼容接口) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</artifactId>
        </dependency>

        <!-- 官方 langchain4j(包含 AiServices 等服务类) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
        </dependency>

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
          <groupId>dev.langchain4j</groupId>
          <artifactId>langchain4j-embeddings-bge-small-zh-v15-q</artifactId>
        </dependency>

        <dependency>
          <groupId>dev.langchain4j</groupId>
          <artifactId>langchain4j-community-clickhouse</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Spring Boot Maven Plugin -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>3.3.5</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <jvmArguments>-Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -Xms2g -Xmx4g</jvmArguments>
                </configuration>
            </plugin>
            
            <!-- Maven Resources Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.3.1</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <propertiesEncoding>UTF-8</propertiesEncoding>
                </configuration>
            </plugin>
            
            <!-- Maven Compiler Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <source>21</source>
                    <target>21</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
        
        <!-- 确保资源文件使用UTF-8编码 -->
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>

</project>
  1. langchain4j-totorials/rag-clickhouse-index/src/main/resources新增配置文件application.properties,内容如下,rag.file.path是存放本地文档的目录(就是前面维基百科那个文件)
# Spring Boot 应用配置
server.port=8080
server.servlet.context-path=/

# 日志配置
logging.level.root=INFO
logging.level.com.bolingcavalry=DEBUG
logging.pattern.console=%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
# 日志编码配置,解决中文乱码问题
logging.charset.console=UTF-8
logging.charset.file=UTF-8
# 应用名称
spring.application.name=rag-clickhouse-index

# rag文件路径
rag.file.path=/home/will/temp/202601/01

# clickhouse数据库连接地址
clickhouse.url=http://localhost:8123
# clickhouse数据库表名
clickhouse.table=langchain4j_table
# clickhouse数据库用户名
clickhouse.username=admin
# clickhouse数据库密码
clickhouse.pswd=123456
  1. 新增启动类,依旧平平无奇
package com.bolingcavalry;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Spring Boot应用程序的主类
 */
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • 准备一个数据结构用来保存clickhouse的配置信息
package com.bolingcavalry.vo;

import lombok.Data;

/**
 * ClickHouse数据库连接信息
 */
@Data
public class IndexConfig {
    /**
     * ClickHouse数据库连接地址
     */
    private String ckURL;

    /**
     * ClickHouse数据库表名
     */
    private String ckTableName;

    /**
     * ClickHouse数据库用户名
     */
    private String ckUsername;

    /**
     * ClickHouse数据库密码
     */
    private String ckPassword;

    /**
     * 索引文件路径
     */
    private String ragFilePath;
}

  • 接着是配置类,很简单,把clickhouse的配置信息准备好即可
package com.bolingcavalry.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.bolingcavalry.vo.IndexConfig;

@Configuration
public class LangChain4jConfig {

    @Value("${rag.file.path}")
    private String ragFilePath;

    @Value("${clickhouse.url}")
    private String clickhouseUrl;

    @Value("${clickhouse.table}")
    private String clickhouseTable;

    @Value("${clickhouse.username}")
    private String clickhouseUsername;

    @Value("${clickhouse.pswd}")
    private String clickhousePswd;

    @Bean
    public IndexConfig clickHouseConfig() {
        IndexConfig config = new IndexConfig();
        config.setCkURL(clickhouseUrl);
        config.setCkTableName(clickhouseTable);
        config.setCkUsername(clickhouseUsername);
        config.setCkPassword(clickhousePswd);
        config.setRagFilePath(ragFilePath);
        return config;
    }
}
  • 接着是本篇的重点了:服务类,提供一个方法用于加载本地文档,转为向量后存入clickhouse
package com.bolingcavalry.service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.bolingcavalry.vo.IndexConfig;
import com.clickhouse.data.ClickHouseDataType;

import dev.langchain4j.community.store.embedding.clickhouse.ClickHouseEmbeddingStore;
import dev.langchain4j.community.store.embedding.clickhouse.ClickHouseSettings;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentParser;
import dev.langchain4j.data.document.DocumentSplitter;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.parser.TextDocumentParser;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.onnx.bgesmallzhv15q.BgeSmallZhV15QuantizedEmbeddingModel;

/**
 * 通义千问服务类,用于与通义千问模型进行交互
 */
@Service
public class QwenService {

    private static final Logger logger = LoggerFactory.getLogger(QwenService.class);

    @Autowired
    private IndexConfig clickHouseConfig;

    private void doIndex() {
        // 每个分块创建嵌入向量,模型是智源 bge-small-zh-v1.5 量化版,中文 C-MTEB 第一梯队
        EmbeddingModel embeddingModel = new BgeSmallZhV15QuantizedEmbeddingModel();
            // 将元数据键映射到 ClickHouse 数据类型
            Map<String, ClickHouseDataType> metadataTypeMap = new HashMap<>();

            logger.info("ClickHouse配置:{}", clickHouseConfig);
            
            ClickHouseSettings clickHouseSettings = ClickHouseSettings.builder()
                    .url(clickHouseConfig.getCkURL())
                    .table(clickHouseConfig.getCkTableName())
                    .username(clickHouseConfig.getCkUsername())
                    .password(clickHouseConfig.getCkPassword())
                    .dimension(embeddingModel.dimension())
                    .metadataTypeMap(metadataTypeMap)
                    .build();

        // DocumentParser的作用是把磁盘上的文件转为Document对象,以便后面的分块处理,
        // TextDocumentParser处理文本类文件,如txt、md等,如果要处理更多类型,可以用ApacheTikaDocumentParser,代价是包更大,启动更慢
        DocumentParser documentParser = new TextDocumentParser();
        long start = System.currentTimeMillis();
        logger.info("开始加载索引文件:{}", clickHouseConfig.getRagFilePath());
        List<Document> documents = FileSystemDocumentLoader.loadDocuments(clickHouseConfig.getRagFilePath(), documentParser);
        logger.info("加载索引文件完成,耗时: {}毫秒, 文件数量: {}",
                (System.currentTimeMillis() - start), documents.size());

        start = System.currentTimeMillis();
        logger.info("开始对文档分块,共{}个文档", documents.size());
        List<TextSegment> segments = new ArrayList<>();

        // 每个文档分块
        for (int i = 0; i < documents.size(); i++) {
            Document document = documents.get(i);
            try {
                logger.info("处理第{}个文档", i + 1);
                // 文档分块, 每个分块300个字符, 重叠0个字符
                try {
                    // 尝试使用递归分割器
                    DocumentSplitter splitter = DocumentSplitters.recursive(300, 0);
                    segments.addAll(splitter.split(document));
                    logger.info("第{}个文档加载完成", i + 1);
                } catch (RuntimeException e) {
                    logger.error("处理第{}个文档时出错: {}", i + 1, e.getMessage());
                    // 如果递归分割器失败(比如遇到特殊字符),使用手动分割
                    if (e.getMessage().contains("doesn't fit into the maximum segment size")) {
                        logger.warn("递归分割器失败,使用手动分割: {}", e.getMessage());
                        // 手动分割文档
                        String text = document.text();
                        int maxSegmentSize = 300;
                        for (int j = 0; j < text.length(); j += maxSegmentSize) {
                            int end = Math.min(j + maxSegmentSize, text.length());
                            String segmentText = text.substring(j, end);
                            // 检查segmentText是否为空或空白
                            if (segmentText != null && !segmentText.trim().isEmpty()) {
                                TextSegment segment = TextSegment.from(segmentText, document.metadata());
                                segments.add(segment);
                            }
                        }
                    } else {
                        // 其他错误,直接抛出
                        throw e;
                    }
                }
            } catch (Exception e) {
                logger.error("处理文档时出错: {}", e.getMessage());
                logger.error("文档内容长度: {}", document.text().length());
                // 如果文档内容很长,只打印前100个字符
                if (document.text().length() > 100) {
                    logger.error("文档前100个字符: {}", document.text().substring(0, 100));
                } else {
                    logger.error("文档内容: {}", document.text());
                }
                throw e;
            }
        }
        logger.info("文档分块完成,共{}个分块,耗时: {}毫秒", segments.size(), (System.currentTimeMillis() - start));

        start = System.currentTimeMillis();
        logger.info("开始将文档分块转为向量");
        
        List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
        ClickHouseEmbeddingStore embeddingStore = ClickHouseEmbeddingStore.builder()
                .settings(clickHouseSettings)
                .build();
        
        embeddingStore.addAll(embeddings, segments);


        logger.info("文档分块转为向量完成,共{}个向量,耗时: {}秒", embeddings.size(), (System.currentTimeMillis() - start) / 1000);
    }

    /**
     * 开始索引
     * 
     * @return
     */
    public String startIndex() {
        // 在新的线程中操作,避免阻塞主线程
        new Thread(() -> {
            doIndex();
        }).start();

        return "已开始索引,请通过后台日志确认索引工作进展";
    }
}

  • 最后是controller类,准备一个接口,该接口被调用时执行索引操作
package com.bolingcavalry.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.bolingcavalry.service.QwenService;

import lombok.Data;

/**
 * 通义千问控制器,处理与大模型交互的HTTP请求
 */
@RestController
@RequestMapping("/api/qwen")
public class QwenController {

    private final QwenService qwenService;

    /**
     * 构造函数,通过依赖注入获取QwenService实例
     * 
     * @param qwenService QwenService实例
     */
    public QwenController(QwenService qwenService) {
        this.qwenService = qwenService;
    }

    /**
     * 响应实体类
     */
    @Data
    static class Response {
        private String result;

        public Response(String result) {
            this.result = result;
        }
    }

    /**
     * 开始索引
     * 
     * @return
     */
    @PostMapping("/rag/startindex")
    public ResponseEntity<Response> startIndex() {
        String response = qwenService.startIndex();
        return ResponseEntity.ok(new Response(response));
    }
}

  • 至此代码就全部写完了,现在把工程运行起来试试,在rag-clickhouse-index目录下执行以下命令即可启动服务
mvn spring-boot:run
  • 用vscode的 REST Client插件发起http请求,参数如下,和前文用提示词指定JSON不同,这里并没有要求LLM返回JSON格式
###  用提示词实现json格式的输出
###  开始索引,数据保存在clickhouse中
POST http://localhost:8080/api/qwen/rag/startindex
Content-Type: application/json
Accept: application/json

{}
  • 此时已经应用已经开始执行索引操作了,观察日志,可以确认是本次请求触发了索引操作,将向量写入了clickhouse
01:18:03.788 [http-nio-8080-exec-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
01:18:03.788 [http-nio-8080-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
01:18:03.789 [http-nio-8080-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 1 ms
01:18:08.384 [Thread-1] INFO  ai.djl.util.Platform - Found matching platform from: jar:file:/home/will/.m2/repository/ai/djl/huggingface/tokenizers/0.31.1/tokenizers-0.31.1.jar!/native/lib/tokenizers.properties
01:18:08.417 [Thread-1] WARN  a.d.h.t.HuggingFaceTokenizer - maxLength is not explicitly specified, use modelMaxLength: 512
01:18:08.419 [Thread-1] INFO  c.bolingcavalry.service.QwenService - ClickHouse配置:IndexConfig(ckURL=http://localhost:8123, ckTableName=langchain4j_table, ckUsername=admin, ckPassword=123456, ragFilePath=/home/will/temp/202601/01)
01:18:08.422 [Thread-1] INFO  c.bolingcavalry.service.QwenService - 开始加载索引文件:/home/will/temp/202601/01
01:18:08.430 [Thread-1] INFO  c.bolingcavalry.service.QwenService - 加载索引文件完成,耗时: 8毫秒, 文件数量: 1
01:18:08.430 [Thread-1] INFO  c.bolingcavalry.service.QwenService - 开始对文档分块,共1个文档
01:18:08.430 [Thread-1] INFO  c.bolingcavalry.service.QwenService - 处理第1个文档
01:18:08.492 [Thread-1] INFO  c.bolingcavalry.service.QwenService - 第1个文档加载完成
  • 至此,把文本文件索引到clickhouse的操作就全部完成了,接下来就是使用clickhouse数据库来完成RAG的操作,咱们新增一个子工程来做这事情

编码,rag-clickhouse-query,负责对话

  • 新增名为rag-clickhouse-query的子工程,作用是负责LLM对话,且对话期间会查询clickhouse中的向量
  1. langchain4j-totorials目录下新增名rag-clickhouse-query为的文件夹
  2. rag-clickhouse-query文件夹下新增pom.xml,内容如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.bolingcavalry</groupId>
        <artifactId>langchain4j-totorials</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    
    <!-- 配置编译编码 -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    </properties>

    <artifactId>rag-clickhouse-index</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- Spring Boot Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- JUnit Jupiter Engine -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- Mockito Core -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- Mockito JUnit Jupiter -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- LangChain4j Core -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-core</artifactId>
        </dependency>
        
        <!-- LangChain4j OpenAI支持(用于通义千问的OpenAI兼容接口) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</artifactId>
        </dependency>

        <!-- 官方 langchain4j(包含 AiServices 等服务类) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
        </dependency>

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
          <groupId>dev.langchain4j</groupId>
          <artifactId>langchain4j-embeddings-bge-small-zh-v15-q</artifactId>
        </dependency>

        <dependency>
          <groupId>dev.langchain4j</groupId>
          <artifactId>langchain4j-community-clickhouse</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Spring Boot Maven Plugin -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>3.3.5</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <jvmArguments>-Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -Xms2g -Xmx4g</jvmArguments>
                </configuration>
            </plugin>
            
            <!-- Maven Resources Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.3.1</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <propertiesEncoding>UTF-8</propertiesEncoding>
                </configuration>
            </plugin>
            
            <!-- Maven Compiler Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <source>21</source>
                    <target>21</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
        
        <!-- 确保资源文件使用UTF-8编码 -->
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>

</project>
  1. langchain4j-totorials/rag-clickhouse-index/src/main/resources新增配置文件application.properties,内容如下,主要是模型的配置信息,注意把your-api-key替换成您自己的APIKey,另外还有clickhouse的配置信息
# Spring Boot 应用配置
server.port=8080
server.servlet.context-path=/

# LangChain4j 使用OpenAI兼容模式配置通义千问模型
# 注意:请将your-api-key替换为您实际的通义千问API密钥
langchain4j.open-ai.chat-model.api-key=your-api-key
# 通义千问模型名称
langchain4j.open-ai.chat-model.model-name=qwen3-max
# 阿里云百炼OpenAI兼容接口地址
langchain4j.open-ai.chat-model.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1

# 日志配置
logging.level.root=INFO
logging.level.com.bolingcavalry=DEBUG
logging.pattern.console=%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
# 日志编码配置,解决中文乱码问题
logging.charset.console=UTF-8
logging.charset.file=UTF-8
# 应用名称
spring.application.name=rag-clickhouse-query

# clickhouse数据库连接地址
clickhouse.url=http://localhost:8123
# clickhouse数据库表名
clickhouse.table=langchain4j_table
# clickhouse数据库用户名
clickhouse.username=admin
# clickhouse数据库密码
clickhouse.pswd=123456
  1. 新增启动类,依旧平平无奇
package com.bolingcavalry;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Spring Boot应用程序的主类
 */
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • 准备一个数据结构用来保存clickhouse的配置信息
package com.bolingcavalry.vo;

import lombok.Data;

/**
 * ClickHouse数据库连接信息
 */
@Data
public class IndexConfig {
    /**
     * ClickHouse数据库连接地址
     */
    private String ckURL;

    /**
     * ClickHouse数据库表名
     */
    private String ckTableName;

    /**
     * ClickHouse数据库用户名
     */
    private String ckUsername;

    /**
     * ClickHouse数据库密码
     */
    private String ckPassword;

    /**
     * 索引文件路径
     */
    private String ragFilePath;
}

  • 查询用到了高级API,所以要有自定义接口
package com.bolingcavalry.service;

public interface Assistant {
    /**
     * 通过提示词range大模型返回JSON格式的内容
     * 
     * @param userMessage 用户消息
     * @return 助手生成的回答
     */
    String byRagNaive(String userMessage);
}
  • 接着是配置类,这是重点,注意assistant方法,这里面先创建Clickhouse的配置类实例,再用这个实例创建查询类ContentRetriever,最后用contentRetriever方法把查询类和自定义接口的实现类绑定,这样LLM对话时LangChain4j就会去Clickhouse做向量查询,拿到本地文档再和LLM对话
package com.bolingcavalry.config;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.bolingcavalry.service.Assistant;
import com.clickhouse.data.ClickHouseDataType;

import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.community.store.embedding.clickhouse.ClickHouseEmbeddingStore;
import dev.langchain4j.community.store.embedding.clickhouse.ClickHouseSettings;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.listener.ChatModelErrorContext;
import dev.langchain4j.model.chat.listener.ChatModelListener;
import dev.langchain4j.model.chat.listener.ChatModelRequestContext;
import dev.langchain4j.model.chat.listener.ChatModelResponseContext;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.onnx.bgesmallzhv15q.BgeSmallZhV15QuantizedEmbeddingModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.service.AiServices;

@Configuration
public class LangChain4jConfig {

    private static final Logger logger = LoggerFactory.getLogger(LangChain4jConfig.class);

    @Value("${langchain4j.open-ai.chat-model.api-key}")
    private String apiKey;

    @Value("${langchain4j.open-ai.chat-model.model-name:qwen-turbo}")
    private String modelName;

    @Value("${langchain4j.open-ai.chat-model.base-url}")
    private String baseUrl;

    @Value("${clickhouse.url}")
    private String clickhouseUrl;

    @Value("${clickhouse.table}")
    private String clickhouseTable;

    @Value("${clickhouse.username}")
    private String clickhouseUsername;

    @Value("${clickhouse.pswd}")
    private String clickhousePswd;

    @Bean
    public OpenAiChatModel chatModel() {

        ChatModelListener listener = new ChatModelListener() {
            @Override
            public void onRequest(ChatModelRequestContext reqCtx) {
                // 1. 拿到 List<ChatMessage>
                List<ChatMessage> messages = reqCtx.chatRequest().messages();
                logger.info("发到LLM的请求: {}", messages);

            }

            @Override
            public void onResponse(ChatModelResponseContext respCtx) {
                // 2. 先取 ChatModelResponse
                ChatResponse response = respCtx.chatResponse();
                // 3. 再取 AiMessage
                AiMessage aiMessage = response.aiMessage();

                // 4. 工具调用
                List<ToolExecutionRequest> tools = aiMessage.toolExecutionRequests();
                for (ToolExecutionRequest t : tools) {
                    logger.info("LLM响应, 执行函数[{}], 函数入参 : {}", t.name(), t.arguments());
                }

                // 5. 纯文本
                if (aiMessage.text() != null) {
                    logger.info("LLM响应, 纯文本 : {}", aiMessage.text());
                }
            }

            @Override
            public void onError(ChatModelErrorContext errorCtx) {
                errorCtx.error().printStackTrace();
            }
        };

        return OpenAiChatModel.builder()
                .apiKey(apiKey)
                .modelName(modelName)
                .baseUrl(baseUrl)
                .listeners(List.of(listener))
                .build();
    }

    @Bean
    public Assistant assistant(OpenAiChatModel chatModel) {
        EmbeddingModel embeddingModel = new BgeSmallZhV15QuantizedEmbeddingModel();

        // 将元数据键映射到 ClickHouse 数据类型
        Map<String, ClickHouseDataType> metadataTypeMap = new HashMap<>();

        ClickHouseSettings settings = ClickHouseSettings.builder()
                .url(clickhouseUrl)
                .table(clickhouseTable)
                .username(clickhouseUsername)
                .password(clickhousePswd)
                .dimension(embeddingModel.dimension())
                .metadataTypeMap(metadataTypeMap)
                .build();

        ContentRetriever contentRetriever = createContentRetriever(settings);

        return AiServices.builder(Assistant.class)
                .chatModel(chatModel)
                .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
                .contentRetriever(contentRetriever)
                .build();
    }

    private ContentRetriever createContentRetriever(ClickHouseSettings settings) {

        // 每个分块创建嵌入向量,模型是智源 bge-small-zh-v1.5 量化版,中文 C-MTEB 第一梯队
        EmbeddingModel embeddingModel = new BgeSmallZhV15QuantizedEmbeddingModel();

        ClickHouseEmbeddingStore embeddingStore = ClickHouseEmbeddingStore.builder()
                .settings(settings)
                .build();

        // 创建内容检索器
        ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
                .embeddingStore(embeddingStore)
                .embeddingModel(embeddingModel)
                .maxResults(2) // 每个查询返回2个最相关的分块
                .minScore(0.5) // 每个分块的相似度阈值
                .build();

        return contentRetriever;
    }
}
  • 然后是服务类,这里很简单,只要使用自定义接口实例即可
package com.bolingcavalry.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 通义千问服务类,用于与通义千问模型进行交互
 */
@Service
public class QwenService {

    private static final Logger logger = LoggerFactory.getLogger(QwenService.class);

    @Autowired
    private Assistant assistant;


    /**
     * 通过提示词range大模型返回JSON格式的内容
     * 
     * @param prompt
     * @return
     */
    public String byRagNaive(String prompt) {
        String answer = assistant.byRagNaive(prompt);
        logger.info("响应:" + answer);
        return answer + "[from byRagNaive]";
    }
}


  • 最后是controller类,准备一个接口,该接口被调用时执行LLM对话操作
package com.bolingcavalry.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.bolingcavalry.service.QwenService;

import lombok.Data;

/**
 * 通义千问控制器,处理与大模型交互的HTTP请求
 */
@RestController
@RequestMapping("/api/qwen")
public class QwenController {

    private final QwenService qwenService;

    /**
     * 构造函数,通过依赖注入获取QwenService实例
     * 
     * @param qwenService QwenService实例
     */
    public QwenController(QwenService qwenService) {
        this.qwenService = qwenService;
    }

    /**
     * 提示词请求实体类
     */
    @Data
    static class PromptRequest {
        private String prompt;
        private int userId;
    }

    /**
     * 响应实体类
     */
    @Data
    static class Response {
        private String result;

        public Response(String result) {
            this.result = result;
        }
    }

    /**
     * 检查请求体是否有效
     * 
     * @param request 包含提示词的请求体
     * @return 如果有效则返回null,否则返回包含错误信息的ResponseEntity
     */
    private ResponseEntity<Response> check(PromptRequest request) {
        if (request == null || request.getPrompt() == null || request.getPrompt().trim().isEmpty()) {
            return ResponseEntity.badRequest().body(new Response("提示词不能为空"));
        }
        return null;
    }

    @PostMapping("/rag/clickhouse")
    public ResponseEntity<Response> byRagNaive(@RequestBody PromptRequest request) {
        ResponseEntity<Response> checkRlt = check(request);
        if (checkRlt != null) {
            return checkRlt;
        }

        try {
            String response = qwenService.byRagNaive(request.getPrompt());
            return ResponseEntity.ok(new Response(response));
        } catch (Exception e) {
            // 捕获异常并返回错误信息
            return ResponseEntity.status(500).body(new Response("请求处理失败: " + e.getMessage()));
        }
    }
}


  • 至此代码就全部写完了,现在把工程运行起来试试,在rag-clickhouse-query目录下执行以下命令即可启动服务
mvn spring-boot:run
  • 用vscode的 REST Client插件发起http请求,参数如下,和前文用提示词指定JSON不同,这里并没有要求LLM返回JSON格式
###  用提示词实现json格式的输出
POST http://localhost:8080/api/qwen/rag/clickhouse
Content-Type: application/json
Accept: application/json

{
  "prompt": "一百字介绍完顏陳和尚"
}
  • 收到响应如下,可见回答内容符合预期
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 02 Feb 2026 09:25:52 GMT
Connection: close

{
  "result": "完顏陳和尚是金朝末年著名將領,忠勇善戰,以抗擊蒙古軍隊聞名。他出身女真貴族,屢立戰功,堅守城池,奮力抵抗蒙古入侵。在金朝危亡之際,他率部浴血奮戰,最終壯烈殉國,展現了忠貞不屈的氣節,為後世所敬仰。[from byRagNaive]"
}

  • 再观察日志,可以确认请求LLM的时候带上了文本内容,而该内容应该来自clickhouse
09:25:46.971 [http-nio-8080-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 1 ms
09:25:47.224 [http-nio-8080-exec-1] INFO  c.b.config.LangChain4jConfig - 发到LLM的请求: [UserMessage { name = null, contents = [TextContent { text = "一百字介绍完顏陳和尚

Answer using the following information:
</doc>

<doc id="132012" url="https://zh.wikipedia.org/wiki?curid=132012" title="完顏陳和尚">
完顏陳和尚" }], attributes = {} }]
09:25:52.085 [http-nio-8080-exec-1] INFO  c.b.config.LangChain4jConfig - LLM响应, 纯文本 : 完顏陳和尚是金朝末年著名將領,忠勇善戰,以抗擊蒙古軍隊聞名。他出身女真貴族,屢立戰功,堅守城池,奮力抵抗蒙古入侵。在金朝危亡之際,他率部浴血奮戰,最終壯烈殉國,展現了忠貞不屈的氣節,為後世所敬仰。


09:25:52.087 [http-nio-8080-exec-1] INFO  c.bolingcavalry.service.QwenService - 响应:完顏陳和尚是金朝末年著名將領,忠勇善戰,以抗擊蒙古軍隊聞名。他出身女真貴族,屢立戰功,堅守城池,奮力抵抗蒙古入侵。在金朝危亡之際,他率部浴血奮戰,最終壯烈殉國,展現了忠貞不屈的氣節,為後世所敬仰。
  • 有种最简单的方法确认RAG查询了clickhouse,就是把数据库停掉再试试查询,我这里clickhouse是用docker部署的,所以用docker命令把它停掉
sudo docker stop clickhouse-server
  • 此时再一次发起对话请求试试,得到响应如下
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 02 Feb 2026 09:33:04 GMT
Connection: close

{
  "result": "请求处理失败: com.clickhouse.client.api.ClientException: Query request failed after attempts: 4 - Duration: 40268236"
}
  • 日志如下所示,确认对话过程中会查询clickhouse数据库
09:33:55.679 [http-nio-8080-exec-3] WARN  com.clickhouse.client.api.Client - Failed to connect to 'localhost': Connect to http://localhost:8123 [localhost/127.0.0.1] failed: Connection refused
09:33:55.679 [http-nio-8080-exec-3] WARN  com.clickhouse.client.api.Client - Retrying.
com.clickhouse.client.api.ClientException: Failed to connect
        at com.clickhouse.client.api.internal.HttpAPIClientHelper.executeRequest(HttpAPIClientHelper.java:438)
        at com.clickhouse.client.api.Client.lambda$query$10(Client.java:1727)
        at com.clickhouse.client.api.Client.runAsyncOperation(Client.java:2156)
        at com.clickhouse.client.api.Client.query(Client.java:1770)
        at com.clickhouse.client.api.Client.queryRecords(Client.java:1818)
        at com.clickhouse.client.api.Client.queryRecords(Client.java:1785)
        at dev.langchain4j.community.store.embedding.clickhouse.ClickHouseEmbeddingStore.search(ClickHouseEmbeddingStore.java:198)
...        
  • 至此,基于数据库的索引和向量查询实战就全部完成了,有了持久化存储,并且将索引和查询完全分离,整体上的实用性也增加了不少,希望能给您的RAG计划提供一些参考

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐