LangChain4j实战之十七:RAG (检索增强生成),clickhouse存储向量数据及其使用
·
欢迎访问我的GitHub
这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
LangChain4j实战全系列链接
- 准备工作
- 极速开发体验
- 细说聊天API
- 集成到spring-boot
- 图像模型
- 聊天记忆,低级API版
- 聊天记忆,高级API版
- 响应流式传输
- 高级API(AI Services)实例的创建方式
- 结构化输出之一,用提示词指定输出格式
- 结构化输出之二,function call
- 结构化输出之三,json模式
- 函数调用,低级API版本
- 函数调用,低级API版本
- RAG (检索增强生成),Easy RAG
- RAG (检索增强生成),Naive RAG
- RAG (检索增强生成),clickhouse存储向量数据
前面实战的遗留问题
- 经过前面的学习,咱们已经学会了RAG的存储和使用,看起来可以把自己的本地资源用于LLM对话了,但有个问题使得之前的方案不具备实用性,且看下图,这是之前方案的代码,功能是把切分好的文本转为向量再保存到内存中,之后和LLM对话时就会在内存中做向量查询

- 聪明的您应该发现问题所在:
- 当前的方案是把向量保存在内存中的,文本要是多的话,内存不够用咋办?
- 要是进程出问题了向量就丢了,所以每次启动都要重新加载吗?
- 文本转向量较慢(五百行需要十多秒),每次启动要等很久才能使用
- 为了解决上述问题,需要用到持久化存储,目前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应用
- rag-clickhouse-index:提供一个接口,调用该接口会触发本地文档的加载并转为向量存入clickhouse
- 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做少量修改
- modules中增加两个子工程,如下图黄框所示

- 增加一个属性定义,对应着clickhouse的版本

- 为了读写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的子工程
- langchain4j-totorials目录下新增名rag-clickhouse-index为的文件夹
- 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>
- 在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
- 新增启动类,依旧平平无奇
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中的向量
- langchain4j-totorials目录下新增名rag-clickhouse-query为的文件夹
- 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>
- 在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
- 新增启动类,依旧平平无奇
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计划提供一些参考
你不孤单,欣宸原创一路相伴
更多推荐



所有评论(0)