[大模型]SpringBoot结合LangChain4J实现Rag检索增强
在大语言模型(LLM)技术飞速发展的今天,如何将其高效集成到企业级应用中,已成为开发者面临的重要课题。SpringBoot 作为 Java 生态中构建微服务和企业级应用的主流框架,以其简化配置、快速开发的特性,成为无数开发者的首选工具;而 LangChain4J 则专为 Java 开发者打造,提供了连接大语言模型与实际业务场景的桥梁,让复杂的 LLM 应用开发变得模块化、可扩展。在索引阶段,文档的
文章目录
前言
在大语言模型(LLM)技术飞速发展的今天,如何将其高效集成到企业级应用中,已成为开发者面临的重要课题。SpringBoot 作为 Java 生态中构建微服务和企业级应用的主流框架,以其简化配置、快速开发的特性,成为无数开发者的首选工具;而 LangChain4J 则专为 Java 开发者打造,提供了连接大语言模型与实际业务场景的桥梁,让复杂的 LLM 应用开发变得模块化、可扩展。
一、Rag检索增强
1.1 索引
在索引阶段,文档的预处理方式可以在检索阶段实现高效搜索。
此过程可能会根据所使用的信息检索方法而有所不同。 对于矢量搜索,这通常涉及清理文档,使用其他数据和元数据丰富它们, 将它们拆分为更小的段(又名分块),嵌入这些段,最后将它们存储在嵌入存储(又名向量数据库)中。
索引阶段通常脱机进行,这意味着它不需要最终用户等待其完成。 例如,这可以通过每周在周末重新索引一次公司内部文档的 cron 作业来实现。 负责索引的代码也可以是仅处理索引任务的单独应用程序。
但是,在某些情况下,最终用户可能希望上传其自定义文档,以便 LLM 可以访问它们。 在这种情况下,索引应在线执行,并成为主应用程序的一部分。
文档解析入库简化图

1.2文档解析器

1.3 文档转换器(粗粒度)
使用步骤文档转换器可以对每一个文档进行细化操作,清洁,过滤,丰富等操作如需要实现此扩展功能,需要自定义实现接口实现业务文档处理,然后进行接入
1.4 文档拆分器(细粒度)
将文档中的文本切割成每一个文本片段,获取每个文本片段进行扩展处理
1.5 检索
检索阶段通常在线进行,当用户提交一个应该使用索引文档回答的问题时。
此过程可能会根据所使用的信息检索方法而有所不同。 对于向量搜索,这通常涉及嵌入用户的查询(问题) 在嵌入存储中执行相似性搜索。 然后,相关片段(原始文档的片段)被注入提示并发送到 LLM。
以下是检索阶段的简化图:

1.6 pom依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mk</groupId>
<artifactId>Spring-LangChain4j</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Spring-LangChain4j</name>
<description>Spring-LangChain4j</description>
<properties>
<java.version>17</java.version>
<langchain.version>1.0.1-beta6</langchain.version>
<druid-version>1.2.23</druid-version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-bom</artifactId>
<version>${langchain.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>${druid-version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-milvus</artifactId>
<version>${langchain.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
<!-- LangChain4j整合boot低阶版本 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>${langchain.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>${langchain.version}</version>
</dependency>
<!-- LangChain4j整合boot高阶版本支持功能更多比如Tools RAG 等 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>${langchain.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- <optional>true</optional>-->
</dependency>
<!-- LangChain4j文档解析器 - 支持PDF -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
<version>${langchain.version}</version>
</dependency>
<!-- LangChain4j文档解析器 - 支持Word等Office文档 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-poi</artifactId>
<version>${langchain.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>aliyunmaven</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
1.7 yml文件
langchain4j:
open-ai:
chat-model:
api-key: api秘钥
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
model-name: qwen-plus
log-requests: true
log-responses: true
max-retries: 3
streaming-chat-model:
api-key: api秘钥
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
model-name: qwen-plus
log-requests: true
log-responses: true
max-retries: 3
embedding-model:
api-key: api秘钥
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
model-name: text-embedding-v4
log-requests: true
log-responses: true
max-retries: 3
1.8 代码示例
- 构建向量存储对象
注意:配置向量数据库Bean时需要将.autoFlushOnInsert(true)代码注释,否则使用程序进行插入数据到向量数据库时会频繁触发阈值导致报错。
package com.mk.springlangchain4j.config;
import com.mk.springlangchain4j.service.CustomAiService;
import com.mk.springlangchain4j.service.VectorAiService;
import com.mk.springlangchain4j.store.message.MessageHistoryRedisStoreProvider;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.milvus.MilvusEmbeddingStore;
import io.milvus.common.clientenum.ConsistencyLevelEnum;
import io.milvus.param.IndexType;
import io.milvus.param.MetricType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LangChain4JConfig {
@Autowired
ChatModel chatModel;
@Autowired
StreamingChatModel streamingChatModel;
@Autowired
MessageHistoryRedisStoreProvider messageHistoryRedisStoreProvider;
@Autowired
EmbeddingModel embeddingModel;
@Bean
public VectorAiService vectorAiService(ContentRetriever contentRetriever) {
return AiServices.builder(VectorAiService.class)
.streamingChatModel(streamingChatModel)
.chatMemoryProvider(chatMemoryProvider)//实现历史回话存储
.contentRetriever(contentRetriever)//实现向量检索
.build();
}
@Bean
public ChatMemoryProvider chatMemoryProvider() {
return memoryId -> MessageWindowChatMemory.builder()
.maxMessages(20)
.id(memoryId)
.chatMemoryStore(messageHistoryRedisStoreProvider)
.build();
}
@Bean
public EmbeddingStore<TextSegment> milvusEmbeddingStore() {
return MilvusEmbeddingStore.builder()
.host("ip地址")
.port(端口号)
.databaseName("default")
.collectionName("data")
.dimension(1024)// 向量维度
.indexType(IndexType.IVF_SQ8)// 索引类型
.metricType(MetricType.COSINE)// 距离度量类型 使用余弦相似度
.consistencyLevel(ConsistencyLevelEnum.BOUNDED) // 一致性级别
//.autoFlushOnInsert(true)// 插入后自动刷新
.idFieldName("id") // ID字段名(INT64,手动指定)
.vectorFieldName("vector") // 向量字段名(Float Vector)
.textFieldName("text") // 文本字段名(VarChar)
.metadataFieldName("metadata")
.build();
}
@Bean
public ContentRetriever contentRetriever(EmbeddingStore<TextSegment> milvusEmbeddingStore){
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(milvusEmbeddingStore)
.embeddingModel(embeddingModel)
.build();
}
}
- 自定义ai接口
package com.mk.springlangchain4j.service;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;
import reactor.core.publisher.Flux;
public interface VectorAiService {
Flux<String> streamingVectorChat(@MemoryId String memoryId, @UserMessage String message);
}
- controller层
package com.mk.springlangchain4j.request;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class VectorDto {
/**
* 用户id
*/
public String memoryId;
/**
* 相似度最小阈值
*/
public String threshold;
/**
*输入内容
*/
public String queryContent;
public String ragContent;
/**
* 查询数量
*/
public Integer queryTotal;
}
package com.mk.springlangchain4j.web;
import com.mk.springlangchain4j.request.VectorDto;
import com.mk.springlangchain4j.response.ResponseResult;
import com.mk.springlangchain4j.response.VectorVo;
import com.mk.springlangchain4j.service.VectorAiService;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.Metadata;
import dev.langchain4j.data.document.parser.TextDocumentParser;
import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser;
import dev.langchain4j.data.document.parser.apache.poi.ApachePoiDocumentParser;
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.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingSearchResult;
import dev.langchain4j.store.embedding.EmbeddingStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;
import java.io.IOException;
import java.util.*;
import java.util.function.Function;
@CrossOrigin
@RestController
@RequestMapping("/api/vector")
public class VectorController {
@Autowired
EmbeddingModel embeddingModel;
@Autowired
EmbeddingStore<TextSegment> milvusEmbeddingStore;
@Autowired
VectorAiService aiService;
@PostMapping("chat")
public Flux<String> ragChat(@RequestBody VectorDto vectorDto) {
Embedding embedding = embeddingModel.embed(vectorDto.getQueryContent()).content();
EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder()
.queryEmbedding(embedding)
.minScore(vectorDto.getThreshold() != null ? Double.parseDouble(vectorDto.getThreshold()) : 0.0)
.maxResults(vectorDto.queryTotal != null ? vectorDto.queryTotal : 10)
.build();
EmbeddingSearchResult<TextSegment> embeddingSearchResult = milvusEmbeddingStore.search(searchRequest);
StringBuilder builder = new StringBuilder();
embeddingSearchResult.matches().forEach(itemVectorRes->{
builder.append(itemVectorRes.embedded().text()).append("。");
});
//向量化文本
vectorDto.ragContent=(vectorDto.queryContent+ builder);
return aiService.streamingVectorChat(vectorDto.getMemoryId(), vectorDto.queryContent);
}
@PostMapping("add")
public ResponseResult add(@RequestBody VectorDto vectorDto) {
Metadata metadata = Metadata.from(
Map.of(
"text", vectorDto.queryContent,
"date", new Date()
)
);
TextSegment textSegment = TextSegment.from(vectorDto.queryContent, metadata);
Embedding embedding = embeddingModel.embed(textSegment).content();
milvusEmbeddingStore.add(embedding, textSegment);
return ResponseResult.builder()
.data("success")
.message("success")
.status(200)
.build();
}
/**
* 知识库文档上传接口
* 支持多种文档格式:TXT、MD、PDF、Word(DOC/DOCX)等
*
* @param file 上传的文档文件
* @return 处理结果,包含成功处理的段落数量等信息
*/
@PostMapping("addDocument")
public ResponseResult addDocument(MultipartFile file) {
// 1. 基础参数验证
if (file.isEmpty()) {
return ResponseResult.builder()
.message("文件不能为空")
.status(400)
.build();
}
String fileName = file.getOriginalFilename();
if (fileName.trim().isEmpty()) {
return ResponseResult.builder()
.message("文件名不能为空")
.status(400)
.build();
}
try {
// 2. 根据文件类型选择合适的文档解析器
Document document = parseDocumentByType(file, fileName);
if (document == null || document.text().trim().isEmpty()) {
return ResponseResult.builder()
.message("文档内容为空或解析失败")
.status(400)
.build();
}
// 3. 添加文档元数据信息
Metadata metadata = createDocumentMetadata(file, fileName);
document = Document.from(document.text(), metadata);
// 4. 智能分割文档为小段落
List<TextSegment> segments = splitDocument(document);
// 5. 批量处理段落:生成向量并存储到知识库
int successCount = processSegmentsWithRateLimit(segments);
// 6. 返回处理结果
return ResponseResult.builder()
.data(Map.of(
"totalSegments", segments.size(),
"successCount", successCount,
"fileName", fileName,
"fileSize", formatFileSize(file.getSize()),
"documentType", getDocumentType(fileName)
))
.message(String.format("文档上传成功!共处理 %d 个段落,成功存储 %d 个段落到知识库",
segments.size(), successCount))
.status(200)
.build();
} catch (Exception e) {
System.err.println("文档处理异常: " + e.getMessage());
e.printStackTrace();
return ResponseResult.builder()
.message("文档处理失败: " + e.getMessage())
.status(500)
.build();
}
}
/**
* 根据文件类型选择合适的文档解析器
*
* @param file 上传的文件
* @param fileName 文件名
* @return 解析后的文档对象
* @throws IOException 文件读取异常
*/
private Document parseDocumentByType(MultipartFile file, String fileName) throws IOException {
String fileExtension = getFileExtension(fileName).toLowerCase();
switch (fileExtension) {
case "pdf":
// PDF文档解析 - 使用Apache PDFBox
System.out.println("正在解析PDF文档: " + fileName);
ApachePdfBoxDocumentParser pdfParser = new ApachePdfBoxDocumentParser();
return pdfParser.parse(file.getInputStream());
case "doc":
case "docx":
case "ppt":
case "pptx":
case "xls":
case "xlsx":
// Microsoft Office文档解析 - 使用Apache POI
System.out.println("正在解析Office文档: " + fileName + " (类型: " + fileExtension.toUpperCase() + ")");
ApachePoiDocumentParser poiParser = new ApachePoiDocumentParser();
return poiParser.parse(file.getInputStream());
case "txt":
case "md":
case "markdown":
case "log":
case "csv":
case "json":
case "xml":
case "html":
case "htm":
default:
// 纯文本文档解析 - 支持TXT、MD、HTML等文本格式
System.out.println("正在解析文本文档: " + fileName + " (类型: " + fileExtension.toUpperCase() + ")");
TextDocumentParser textParser = new TextDocumentParser();
return textParser.parse(file.getInputStream());
}
}
/**
* 创建文档元数据
*
* @param file 上传的文件
* @param fileName 文件名
* @return 元数据对象
*/
private Metadata createDocumentMetadata(MultipartFile file, String fileName) {
return Metadata.from(
Map.of(
"fileName", fileName,
"fileSize", String.valueOf(file.getSize()),
"fileSizeFormatted", formatFileSize(file.getSize()),
"uploadTime", String.valueOf(System.currentTimeMillis()),
"uploadDate", new Date().toString(),
"contentType", file.getContentType() != null ? file.getContentType() : "text/plain",
"documentType", getDocumentType(fileName),
"fileExtension", getFileExtension(fileName)
)
);
}
/**
* 智能分割文档为合适的段落
*
* @param document 文档对象
* @return 分割后的文本段落列表
*/
private List<TextSegment> splitDocument(Document document) {
// 根据文档长度动态调整分割参数
int documentLength = document.text().length();
int maxSegmentSize;
int overlapSize;
if (documentLength < 2000) {
// 短文档:较小的段落,减少分割
maxSegmentSize = 300;
overlapSize = 30;
} else if (documentLength < 10000) {
// 中等文档:标准分割
maxSegmentSize = 500;
overlapSize = 50;
} else {
// 长文档:较大的段落,保持上下文连贯性
maxSegmentSize = 800;
overlapSize = 80;
}
System.out.println(String.format("文档长度: %d 字符,使用分割参数: 最大段落长度=%d, 重叠长度=%d",
documentLength, maxSegmentSize, overlapSize));
return DocumentSplitters.recursive(
maxSegmentSize, // 最大段落长度
overlapSize // 段落重叠长度,保持上下文连贯性
).split(document);
}
/**
* 批量处理文档段落,包含速率限制和重试机制
*
* @param segments 文档段落列表
* @return 成功处理的段落数量
*/
private int processSegmentsWithRateLimit(List<TextSegment> segments) {
int successCount = 0;
int totalSegments = segments.size();
System.out.println("开始处理 " + totalSegments + " 个文档段落...");
for (int i = 0; i < segments.size(); i++) {
TextSegment segment = segments.get(i);
boolean processed = false;
int retryCount = 0;
int maxRetries = 3;
// 重试机制处理单个段落
while (!processed && retryCount < maxRetries) {
try {
// 生成向量嵌入
Embedding embedding = embeddingModel.embed(segment).content();
// 存储到Milvus向量数据库
milvusEmbeddingStore.add(embedding, segment);
successCount++;
processed = true;
// 进度提示
if ((i + 1) % 10 == 0 || (i + 1) == totalSegments) {
System.out.println(String.format("已处理: %d/%d 个段落 (%.1f%%)",
i + 1, totalSegments, (i + 1) * 100.0 / totalSegments));
}
} catch (Exception e) {
retryCount++;
System.err.println(String.format("处理第 %d 个段落失败 (重试 %d/%d): %s",
i + 1, retryCount, maxRetries, e.getMessage()));
if (retryCount < maxRetries) {
try {
// 指数退避重试策略
Thread.sleep(retryCount * 5000L); // 5秒、10秒、15秒
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}
// 速率控制:避免触发Milvus限流
try {
Thread.sleep(100); // 每个段落间延迟100毫秒
// 每处理10个段落后额外延迟
if ((i + 1) % 10 == 0) {
Thread.sleep(2000); // 额外延迟2秒
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
System.out.println(String.format("文档处理完成!总段落: %d, 成功: %d, 失败: %d",
totalSegments, successCount, totalSegments - successCount));
return successCount;
}
/**
* 获取文件扩展名
*
* @param fileName 文件名
* @return 文件扩展名(小写)
*/
private String getFileExtension(String fileName) {
if (fileName == null || !fileName.contains(".")) {
return "";
}
return fileName.substring(fileName.lastIndexOf(".") + 1);
}
/**
* 获取文档类型描述
*
* @param fileName 文件名
* @return 文档类型描述
*/
private String getDocumentType(String fileName) {
String extension = getFileExtension(fileName).toLowerCase();
switch (extension) {
case "pdf":
return "PDF文档";
case "doc":
case "docx":
return "Word文档";
case "ppt":
case "pptx":
return "PowerPoint演示文稿";
case "xls":
case "xlsx":
return "Excel表格";
case "txt":
return "纯文本文档";
case "md":
case "markdown":
return "Markdown文档";
case "html":
case "htm":
return "HTML网页";
case "json":
return "JSON数据";
case "xml":
return "XML文档";
case "csv":
return "CSV表格";
case "log":
return "日志文件";
default:
return "文本文档";
}
}
/**
* 格式化文件大小显示
*
* @param size 文件大小(字节)
* @return 格式化后的文件大小字符串
*/
private String formatFileSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.1f KB", size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return String.format("%.1f MB", size / (1024.0 * 1024.0));
} else {
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
}
}
}
二、演示效果
2.1 rag检索
rag检索
总结
完结…
更多推荐

所有评论(0)