langchain4j-(9)-RAG
一、langchain4j构建RAG的步骤
1. 加载文档
使用合适的 DocumentLoader
(文档加载器)和 DocumentParser
(文档解析器)来加载各种格式的文档,比如文本文件、PDF、网页等。这一步是为了获取原始的文档内容,为后续处理做准备。
2. 转换文档(可选)
利用 DocumentTransformer
(文档转换器)对加载的文档进行清理或者增强操作。清理可能包括去除无关的格式、噪声数据等;增强可能是添加额外的元数据、对内容进行预处理等,目的是让文档更适合后续的处理流程。
3. 拆分文档(可选)
通过 DocumentSplitter
(文档拆分器)把文档拆分成更小的片段。因为大的文档直接处理可能效率低下或者效果不好,拆分成小片段后,更便于后续的嵌入和检索操作,能提高相关性和处理的灵活性。
4. 嵌入文档
使用 EmbeddingModel
(嵌入模型)将拆分后的文档片段转换为嵌入向量。嵌入向量是一种将文本语义映射到高维空间中的数值表示,这样可以方便地计算文本之间的语义相似度。
5. 存储嵌入
借助 EmbeddingStoreIngestor
(嵌入存储摄入器)把生成的嵌入向量存储起来。通常会存储到专门的向量数据库中,以便后续快速检索相关的文档片段。
6. 检索相关内容
当有用户查询时,根据用户的查询内容,从存储嵌入向量的 EmbeddingStore
(嵌入存储)中检索出最相关的文档片段。这一步是通过计算用户查询嵌入向量与存储的文档片段嵌入向量的相似度来实现的。
7. 生成响应
将检索到的相关文档内容和用户的查询一起提供给语言模型(如大语言模型),语言模型结合这些相关信息生成最终的响应,这样生成的响应会更准确、更符合特定领域或文档内容的要求。
二、撸代码-Easy RAG
Step1
新的pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<!--qdrant-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-qdrant</artifactId>
</dependency>
<!--easy-rag-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
Step2
重写LLMConfing
package com.xxx.demo.config;
import com.bbchat.demo.service.ChatAssistant;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import dev.langchain4j.store.embedding.qdrant.QdrantEmbeddingStore;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LLMConfig {
@Bean
public ChatModel chatModel()
{
return OpenAiChatModel.builder()
.apiKey(System.getenv("aliqwen-apikey"))
.modelName("qwen-plus")
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.build();
}
/**
* 需要预处理文档并将其存储在专门的嵌入存储(也称为矢量数据库)中。当用户提出问题时,这对于快速找到相关信息是必要的。
* 我们可以使用我们支持的 15 多个嵌入存储中的任何一个,但为了简单起见,我们将使用内存中的嵌入存储:
* https://docs.langchain4j.dev/integrations/embedding-stores/in-memory
* @return
*/
@Bean
public InMemoryEmbeddingStore<TextSegment> embeddingStore() {
return new InMemoryEmbeddingStore<>();
}
@Bean
public ChatAssistant assistant(ChatModel chatModel, EmbeddingStore<TextSegment> embeddingStore)
{
return AiServices.builder(ChatAssistant.class)
.chatModel(chatModel)
.chatMemory(MessageWindowChatMemory.withMaxMessages(50))
.contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))
.build();
}
}
Step3
写一个测试用的controller
package com.xxxx.demo.controller;
import com.bbchat.demo.service.ChatAssistant;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import dev.langchain4j.data.segment.TextSegment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class RAGController {@Resource
InMemoryEmbeddingStore<TextSegment> embeddingStore;
@Resource
ChatAssistant chatAssistant;
// http://localhost:9013/rag/add
@GetMapping(value = "/rag/add")
public String testAdd()
{
Document document = FileSystemDocumentLoader.loadDocument("D:\\alibaba.docx");
EmbeddingStoreIngestor.ingest(document, embeddingStore);
String result = chatAssistant.chat("命名规范有几种强制");
System.out.println(result);
return result;
}
}
Step4
看下结果
三、Easy RAG、Naive RAG and Advanced RAG
LangChain4j 提供了三种不同层次的 RAG(检索增强生成)实现方式,从简单到复杂依次是:
1. Easy RAG(简单快捷)
特点:一行代码搞定 RAG,适合快速原型验证。
核心优势:
-
自动完成文档加载、分割、嵌入和存储
-
内置默认配置,开箱即用
-
适合初学者和快速验证概念
代码示例:
// 最简单的形式
Rag rag = Rag.from("path/to/documents");
String answer = rag.ask("什么是RAG?");
适用场景:
-
快速原型开发
-
简单问答应用
-
学习和测试
2. Naive RAG(基础但可控)
特点:手动配置关键组件,保留核心功能同时增加灵活性。
核心优势:
-
可以选择不同的嵌入模型和向量存储
-
自定义文档分割策略
-
控制检索参数(如返回数量)
代码示例:
// 创建嵌入模型
OpenAiEmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("text-embedding-3-small")
.build();
// 创建向量存储并摄入文档
EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
ingestor.ingest(documents);
// 创建内容检索器
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(3)
.build();
// 创建语言模型
OpenAiChatModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
// 构建RAG
Rag rag = Rag.builder()
.contentRetriever(contentRetriever)
.chatModel(chatModel)
.build();
适用场景:
-
需要定制基础功能
-
生产环境的基础实现
-
学习 RAG 内部工作原理
3. Advanced RAG(高级灵活)
特点:精细控制 RAG 流程的每个环节,支持复杂业务逻辑。
核心优势:
-
多步检索和重排
-
条件分支和动态策略
-
混合检索(向量 + 关键词)
-
元数据过滤和路由
-
支持工具调用和多模态
代码示例:
// 创建检索器链
RetrievalChain retrievalChain = RetrievalChain.builder()
.retriever(retriever)
.build();
// 创建生成链
GenerationChain generationChain = GenerationChain.builder()
.chatModel(chatModel)
.promptTemplate(promptTemplate)
.build();
// 手动执行链
String question = "什么是RAG?";
List<TextSegment> segments = retrievalChain.execute(question);
String answer = generationChain.execute(question, segments);
高级功能示例:
-
混合检索:结合向量检索和全文检索
-
检索重排:先检索候选文档,再用重排模型优化顺序
-
元数据过滤:基于文档元数据(如日期、类别)筛选
-
查询扩展:使用 LLM 生成多个相关查询提高召回率
-
多步检索:根据中间结果动态调整检索策略
如何选择
-
快速开始:选择 Easy RAG
-
基础可控:选择 Naive RAG
-
复杂需求:选择 Advanced RAG
想了解如何将这些 RAG 实现与特定数据源集成,或者需要针对您的具体业务场景的定制方案吗?
四、三种RAG实现方式的性能差异
LangChain4j 中 Easy RAG、Naive RAG 、Advanced RAG 这三种 RAG 实现方式在性能方面存在多维度的差异,以下从响应时间、资源消耗、检索准确性、扩展性等方面进行分析:
1. 响应时间
-
Easy RAG:
-
由于采用了内置的默认配置,其流程相对固定且简单,在数据量较小、文档结构不太复杂的情况下,能快速完成从文档加载、检索到生成回答的过程,响应时间较短。
-
但当面对大规模文档集或者复杂查询时,由于缺乏针对性的优化配置,可能需要处理过多不必要的数据,导致响应时间变长。
-
-
Naive RAG:
-
手动配置关键组件使得开发者可以根据实际需求选择更合适的嵌入模型、向量存储等。合理配置下,相比 Easy RAG,在大规模数据检索时能更精准地定位相关信息,减少不必要的计算,从而在复杂场景下响应时间表现可能更优。
-
不过,如果配置不当,比如选择了计算资源消耗大但不匹配业务需求的嵌入模型,或者向量存储的索引策略不合理,也可能导致响应时间增加。
-
-
Advanced RAG:
-
虽然提供了最精细的控制和复杂的功能,但也意味着在执行过程中需要进行更多的逻辑判断、多步检索和重排等操作。在简单查询场景下,由于额外的处理步骤,响应时间可能会比前两者长。
-
然而,在处理复杂业务逻辑、多模态数据或者对检索结果准确性要求极高的复杂查询时,通过多步优化检索和生成策略,最终生成高质量回答所花费的整体时间可能更具优势。
-
2. 资源消耗
-
Easy RAG:
-
为了实现简单快捷的特点,通常会使用相对轻量级且默认的配置,在计算资源(如 CPU、GPU)和内存占用上相对较少。
-
但如果文档数据量增长,其默认配置可能无法高效管理资源,导致资源消耗不合理增加。
-
-
Naive RAG:
-
开发者手动选择组件,可以根据实际资源情况进行权衡。例如选择轻量级的嵌入模型以减少计算资源消耗,或者根据数据规模选择合适的向量存储以优化内存占用。
-
不过,如果选择了过于高性能但资源消耗大的组件,或者没有对资源使用进行有效管理(如未及时释放不再使用的向量存储连接等),可能会导致资源消耗过高。
-
-
Advanced RAG:
-
由于支持多步检索、混合检索、工具调用等复杂功能,在运行过程中需要同时处理多个任务和数据流动,对计算资源和内存的需求通常是最高的。
-
比如多步检索过程中可能需要多次查询向量存储和处理中间结果,工具调用可能还会涉及与外部服务的交互,这些都会增加资源的使用。
-
3. 检索准确性
-
Easy RAG:
-
依靠默认配置进行检索,在数据和查询较为简单的场景下能满足基本需求,检索到的相关文档片段与问题有一定相关性。
-
但对于复杂语义的查询或者文档中存在语义模糊的情况,由于缺乏自定义的检索优化策略,检索准确性可能较低。
-
-
Naive RAG:
-
开发者可以通过调整检索参数(如设置合适的返回文档数量、优化嵌入模型以更好捕捉语义等)来提高检索准确性。相比 Easy RAG,在处理复杂查询和大规模文档时,能够更灵活地优化检索策略,检索准确性通常更高。
-
然而,其优化程度受限于手动配置的精细度,如果开发者对业务场景理解不足或者配置经验欠缺,可能无法充分发挥其检索能力。
-
-
Advanced RAG:
-
通过多步检索、检索重排、元数据过滤等高级功能,可以从多个角度对检索结果进行优化。例如,先通过向量检索获取一批候选文档,再利用重排模型根据文档与问题的语义相关性进行重新排序,能显著提高检索到的文档与问题的匹配度。
-
对于复杂多条件的查询,还可以利用元数据过滤掉不相关的文档,进一步提升检索准确性,在三种方式中,检索准确性通常是最高的。
-
4. 扩展性
-
Easy RAG:
-
扩展性相对较差,因为其内部实现较为固定,当需要添加新的功能(如支持新的文档格式、引入新的检索策略等)时,可能需要对整个框架进行较大改动。
-
-
Naive RAG:
-
由于是手动配置组件,在一定程度上具备扩展性。开发者可以相对容易地更换嵌入模型、向量存储等组件以适应新的需求。但对于一些复杂的功能扩展,如添加多步检索逻辑或者复杂的查询扩展功能,仍然需要进行较多的代码编写和调整。
-
-
Advanced RAG:
-
设计上就考虑了高度的灵活性和扩展性,支持添加各种复杂功能模块(如多模态数据处理、动态查询策略调整等)。通过模块化的设计,开发者可以方便地插入新的检索步骤、工具调用逻辑等,以满足不断变化的业务需求,扩展性最强。
-
五、Advanced RAG的特性
下面提供几个使用 LangChain4j 实现 Advanced RAG 的具体代码示例,展示其主要特性:
1. 基础 Advanced RAG 实现
// 1. 创建嵌入模型和向量存储
OpenAiEmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("text-embedding-3-small")
.build();
EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
// 2. 摄入文档
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
ingestor.ingest(documents);
// 3. 创建检索器
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(5)
.build();
// 4. 创建语言模型
OpenAiChatModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
// 5. 创建自定义提示模板
PromptTemplate promptTemplate = PromptTemplate.from(
"根据以下上下文信息回答问题。如果无法从上下文中找到答案,请直接说明不知道。\n"
+ "上下文:\n"
+ "{{context}}\n"
+ "问题:\n"
+ "{{question}}\n"
+ "回答:");
// 6. 构建 Advanced RAG
Rag rag = Rag.builder()
.contentRetriever(contentRetriever)
.chatModel(chatModel)
.promptTemplate(promptTemplate)
.build();
// 7. 执行查询
String answer = rag.ask("什么是RAG?");
System.out.println(answer);
2. 多步检索与重排
// 1. 创建检索链
RetrievalChain retrievalChain = RetrievalChain.builder()
.retriever(contentRetriever)
.build();
// 2. 创建重排链
RerankingChain rerankingChain = RerankingChain.builder()
.chatModel(chatModel)
.build();
// 3. 创建生成链
GenerationChain generationChain = GenerationChain.builder()
.chatModel(chatModel)
.promptTemplate(promptTemplate)
.build();
// 4. 执行多步流程
String question = "LangChain4j有哪些高级特性?";
// 第一步:初步检索
List<TextSegment> segments = retrievalChain.execute(question);
// 第二步:重排
List<TextSegment> rerankedSegments = rerankingChain.execute(question, segments);
// 第三步:生成回答
String answer = generationChain.execute(question, rerankedSegments);
System.out.println(answer);
3. 元数据过滤
// 1. 创建带元数据的文档
Document document = Document.from("LangChain4j 是一个强大的Java LLM框架...",
Metadata.metadata()
.put("category", "technology")
.put("date", "2024-06-15"));
// 2. 创建带过滤的检索器
ContentRetriever filteredRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(3)
.filter(textSegment -> {
Metadata metadata = textSegment.metadata();
// 只检索技术类文档
return "technology".equals(metadata.get("category"));
})
.build();
// 3. 使用过滤检索器构建RAG
Rag rag = Rag.builder()
.contentRetriever(filteredRetriever)
.chatModel(chatModel)
.build();
// 4. 执行查询
String answer = rag.ask("LangChain4j有哪些特性?");
System.out.println(answer);
4. 混合检索(向量 + 关键词)
// 1. 创建向量检索器
ContentRetriever vectorRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(3)
.build();
// 2. 创建关键词检索器(假设已实现)
ContentRetriever keywordRetriever = new KeywordContentRetriever(index);
// 3. 创建混合检索器
ContentRetriever hybridRetriever = HybridContentRetriever.builder()
.vectorRetriever(vectorRetriever)
.keywordRetriever(keywordRetriever)
.build();
// 4. 使用混合检索器构建RAG
Rag rag = Rag.builder()
.contentRetriever(hybridRetriever)
.chatModel(chatModel)
.build();
// 5. 执行查询
String answer = rag.ask("LangChain4j的RAG功能如何工作?");
System.out.println(answer);
5. 查询扩展
// 1. 创建查询扩展链
QueryExpansionChain queryExpansionChain = QueryExpansionChain.builder()
.chatModel(chatModel)
.build();
// 2. 扩展原始查询
String originalQuestion = "LangChain4j如何实现RAG?";
List<String> expandedQueries = queryExpansionChain.execute(originalQuestion);
// 3. 执行多查询检索
List<TextSegment> allSegments = new ArrayList<>();
for (String query : expandedQueries) {
allSegments.addAll(retrievalChain.execute(query));
}
// 4. 去重和重排
List<TextSegment> uniqueSegments = allSegments.stream()
.distinct()
.collect(Collectors.toList());
List<TextSegment> finalSegments = rerankingChain.execute(originalQuestion, uniqueSegments);
// 5. 生成回答
String answer = generationChain.execute(originalQuestion, finalSegments);
System.out.println(answer);
6. 工具调用
// 1. 创建计算器工具
CalculatorTool calculatorTool = new CalculatorTool();
// 2. 创建工具执行链
ToolExecutionChain toolExecutionChain = ToolExecutionChain.builder()
.tools(calculatorTool)
.chatModel(chatModel)
.build();
// 3. 创建条件路由链
ConditionalChain conditionalChain = ConditionalChain.builder()
.condition(question -> question.contains("计算") || question.contains("+") || question.contains("-"))
.ifTrue(toolExecutionChain)
.ifFalse(generationChain)
.build();
// 4. 执行查询
String answer1 = conditionalChain.execute("计算 12345 * 67890");
String answer2 = conditionalChain.execute("解释一下什么是RAG");
System.out.println(answer1);
System.out.println(answer2);
7. 动态提示调整
// 1. 创建动态提示生成器
PromptGenerator promptGenerator = (question, segments) -> {
String context = segments.stream()
.map(TextSegment::text)
.collect(Collectors.joining("\n\n"));
if (segments.size() < 2) {
return "上下文信息有限,请基于已有信息简要回答问题:\n"
+ "上下文:\n" + context + "\n"
+ "问题:\n" + question + "\n"
+ "回答:";
} else {
return "根据以下上下文详细回答问题:\n"
+ "上下文:\n" + context + "\n"
+ "问题:\n" + question + "\n"
+ "回答:";
}
};
// 2. 创建自定义生成链
Chain<String, String> dynamicGenerationChain = input -> {
String[] parts = input.split("\\|\\|", 2);
String question = parts[0].trim();
String context = parts[1].trim();
String prompt = promptGenerator.generate(question,
List.of(TextSegment.from(context)));
return chatModel.generate(prompt);
};
// 3. 执行查询
String context = retrievalChain.execute("LangChain4j特性").stream()
.map(TextSegment::text)
.collect(Collectors.joining("\n\n"));
String answer = dynamicGenerationChain.execute("LangChain4j特性||" + context);
System.out.println(answer);
更多推荐
所有评论(0)