基于LangChain4j从0到1搭建自己的的AI智能体-2
上篇
基于LangChain4j从0到1搭建自己的的AI智能体并部署上线-1-CSDN博客
检索增强生成RAG
什么是RAG?
Retrieval-Augmented Generation 检索增强生成
将原始问题以及提示词信息发送给大语言模型之前,先通过外部知识库检索相关信息,然后将检索结果和原始问题一起发送给大模型,大模型依据外部知识库再结合自身的训练数据,组织自然语言回答问题。通过这种方式,大语言模型可以获取到特定领域的相关信息,并能够利用这些信息进行回复。
- 优点:数据存储在外部知识库,可以实时更新,不依赖对模型自身的训练,成本更低。
- 缺点:需要两次查询:先查询知识库,然后再查询大模型,性能不如微调大模型。
- 应用场景:适用于知识库规模大且频繁更新的场景,如企业客服、实时新闻查询、法律和医疗领域的最新知识问答等。
RAG的过程
RAG过程分为2个不同的阶段:索引和检索。
索引阶段:
在索引阶段,对知识库文档进行预处理,可实现检索阶段的高效搜索。
以下是索引阶段的简化图:
加载知识库文档==>将文档中的文本分段==>利用向量大模型将分段后的文本转换成向量==>将向量存入向量数据库

为什么要进行文本分段?
大语言模型(LLM)的上下文窗口有限,所以整个知识库可能无法全部容纳其中。
- 你在提问中提供的信息越多,大语言模型处理并做出回应所需的时间就越长。
- 你在提问中提供的信息越多,花费也越多。
- 提问中的无关信息可能会干扰大语言模型,增加产生幻觉(生成错误信息)的几率。
我们可以通过将知识库分割成更小、更易于理解的片段来解决这些问题。
检索阶段:
- 以下是检索阶段的简化图:
通过向量模型将用户查询转换成向量 -> 在向量数据库中根据用户查询进行相似度匹配 -> 将用户查询和向量数据库中匹配到的相关内容一起交给LLM处理

文档加载器
常见文档加载器
- 来自 langchain4j 模块的文件系统文档加载器 (FileSystemDocumentLoader)
- 来自 langchain4j 模块的类路径文档加载器 (ClassPathDocumentLoader)
- 来自 langchain4j 模块的网址文档加载器 (UrlDocumentLoader)
- 来自 langchain4j-document-loader-amazon-s3 模块的亚马逊 S3 文档加载器 (AmazonS3DocumentLoader)
- 来自 langchain4j-document-loader-azure-storage-blob 模块的 Azure Blob 存储文档加载器 (AzureBlobStorageDocumentLoader)
- 来自 langchain4j-document-loader-github 模块的 GitHub 文档加载器 (GitHubDocumentLoader)
- 来自 langchain4j-document-loader-google-cloud-storage 模块的谷歌云存储文档加载器 (GoogleCloudStorageDocumentLoader)
- 来自 langchain4j-document-loader-selenium 模块的 Selenium 文档加载器 (SeleniumDocumentLoader)
- 来自 langchain4j-document-loader-tencent-cos 模块的腾讯云对象存储文档加载器 (TencentCosDocumentLoader)
测试
@SpringBootTest
public class RAGTest {
@Test
public void testReadDocument(){
//使用FileSystemDocumentLoader读取指定目录下的知识库文档
//并使用默认的文档解析器TextDocumentParser对文档进行解析
Document document= FileSystemDocumentLoader.loadDocument("E:\\test.txt");
System.out.println(document.text());
}
}
文档解析器
默认可解析txt、md文件,若想解析其他文件(如pdf)则需要:
引入依赖
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
<version>1.0.0-beta3</version>
</dependency>
测试
@Test
public void testParsePdf(){
Document document= FileSystemDocumentLoader.loadDocument("E:\\医院信息.pdf",
new ApachePdfBoxDocumentParser());
System.out.println(document.text());
}

文档分割器
工作方式
- 实例化一个“文档分割器”(DocumentSplitter),指定所需的“文本片段”(TextSegment)大小,并且可以选择指定characters或token的重叠部分。
- “文档分割器”(DocumentSplitter)将给定的文档(Document)分割成更小的单元,这些单元的性质因分割器而异。例如,“按段落分割文档器”(DocumentByParagraphSplitter)将文档分割成段落(由两个或更多连续的换行符定义),而“按句子分割文档器”(DocumentBySentenceSplitter)使用OpenNLP库的句子检测器将文档分割成句子,依此类推。
- 然后,“文档分割器”(DocumentSplitter)将这些较小的单元(段落、句子、单词等)组合成“文本片段”(TextSegment),尝试在单个“文本片段”(TextSegment)中包含尽可能多的单元,同时不超过第一步中设置的限制。如果某些单元仍然太大,无法放入一个“文本片段”(TextSegment)中,它会调用一个子分割器。这是另一个“文档分割器”(DocumentSplitter),能够将不适合的单元分割成更细粒度的单元。会向每个文本片段添加一个唯一的数据条目“index”。第一个“文本片段”(TextSegment)将包含 index=0,第二个是 index=1,依此类推。
LangChain4j 有一个“文档分割器”(DocumentSplitter) 接口,并且提供了几种开箱即用的实现方式:
- 按段落文档分割器 (DocumentByParagraphSplitter)
- 按行文档分割器 (DocumentByLineSplitter)
- 按句子文档分割器 (DocumentBySentenceSplitter)
- 按单词文档分割器 (DocumentByWordSplitter)
- 按字符文档分割器 (DocumentByCharacterSplitter)
- 按正则表达式文档分割器 (DocumentByRegexSplitter)
递归分割:DocumentSplitters.recursive (...)
默认情况下每个文本片段最多不能超过300个token
简单的RAG实现
引入easyRag
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
<version>1.0.0-beta3</version>
</dependency>
@Test
public void testReadDocumentAndStore() {
//使用FileSystemDocumentLoader读取指定目录下的知识库文档
//并使用默认的文档解析器对文档进行解析(TextDocumentParser)
Document document = FileSystemDocumentLoader.loadDocument("E:\\Program\\人工智能.md");
//为了简单起见,我们暂时使用基于内存的向量存储
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
//ingest
//1、分割文档,默认使用递归分割器,将文档分割为多个文本片段,每个片段包含不超过 300个token,并且有 30个token的重叠部分保证连贯性
// (DocumentByParagraphSplitter(DocumentByLineSplitter(DocumentBySentenceSplitter(DocumentByWordSplitter))))
//2、文本向量化,使用一个LangChain4j内置的轻量化向量模型对每个文本片段进行向量化
//3、将原始文本和向量存储到向量数据库中 (InMemoryEmbeddingStore)
//查看向量数据库内容
EmbeddingStoreIngestor.ingest(document, embeddingStore);
System.out.println(embeddingStore);
}
在项目中实现RAG(基于内存)
配置AI类
@Bean
public ContentRetriever contentRetrieverXiaozhi(){
//使用FileSystemDocumentLoader读取指定文件目录下的知识库文档
//并使用默认的文档解析器对文档进行解析
Document document1= FileSystemDocumentLoader.loadDocument("E:/Program/knowledge/医院信息.md");
Document document2= FileSystemDocumentLoader.loadDocument("E:/Program/knowledge/科室信息.md");
Document document3= FileSystemDocumentLoader.loadDocument("E:/Program/knowledge/神经内科.md");
List<Document> documents= Arrays.asList(document1,document2,document3);
//使用内存向量存储
InMemoryEmbeddingStore<TextSegment> embeddingStore=new InMemoryEmbeddingStore<>();
//使用默认文档分割器
EmbeddingStoreIngestor.ingest(documents,embeddingStore);
//从嵌入存储里(EmbeddingStore)里检索和查询内容相关信息
return EmbeddingStoreContentRetriever.from(embeddingStore);
}
在智能体中添加向量检索器
@AiService(
wiringMode = EXPLICIT,
chatModel = "qwenChatModel",
chatMemoryProvider = "chatMemoryProviderXiaozhi",
tools = "appointmentTools",
contentRetriever = "contentRetrieverXiaozhi"
)
public interface XiaozhiAgent {
@SystemMessage(fromResource = "xiaozhi-prompt-template.txt")
String chat(@MemoryId Long memoryId, @UserMessage String userMessage);
}
测试


向量模型和向量存储
模型配置
#向量模型
langchain4j.community.dashscope.embedding-model.api-key=${DASH_SCOPE_API_KEY}
langchain4j.community.dashscope.embedding-model.model-name=text-embedding-v3
测试
@SpringBootTest
public class EmbeddingTest {
@Autowired
private EmbeddingModel embeddingModel;
@Test
public void testEmbeddingModel(){
Response<Embedding> embed=embeddingModel.embed("你好");
System.out.println("向量维度"+embed.content().vector().length);
System.out.println("向量输出"+ embed);
}
}

向量存储
这里使用Pinecone来进行向量存储
The vector database to build knowledgeable AI | Pinecone
登录注册后保存API并创建数据库,插入几条示例数据


添加依赖
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-pinecone</artifactId>
<version>1.0.0-beta3</version>
</dependency>
配置向量存储对象
@Configuration
public class EmbeddingStoreConfig {
@Autowired
private EmbeddingModel embeddingModel;
@Bean
public EmbeddingStore<TextSegment> embeddingStore(){
//创建向量存储
EmbeddingStore<TextSegment> embeddingStore= PineconeEmbeddingStore.builder()
.apiKey(System.getenv("PINECONE_API_KEY"))
.index("xiaozhi-index")//若不存在新建
.nameSpace("xiaozhi-namespace")//若不存在新建
.createIndex(PineconeServerlessIndexConfig.builder()
.cloud("AWS")
.region("us-east-1")
.dimension(embeddingModel.dimension())
.build())
.build();
return embeddingStore;
}
}
测试
@Test
public void testPineconeEmbeded(){
//将文本转换成向量
TextSegment segment1=TextSegment.from("我喜欢羽毛球");
System.out.println(segment1);
Embedding embedding1=embeddingModel.embed(segment1).content();
//存入向量数据库
embeddingStore.add(embedding1,segment1);
TextSegment segment2=TextSegment.from("今天天气很好");
Embedding embedding2=embeddingModel.embed(segment2).content();
embeddingStore.add(embedding2,segment2);
}
相似度匹配
/**
* Pinecone-相似度匹配
*/
@Test
public void embeddingSearch() {
// 提问,并将问题转成向量数据
Embedding queryEmbedding = embeddingModel.embed("你最喜欢的运动是什么?").content();
// 创建搜索请求对象
EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(1) // 匹配最相似的一条记录
.build();
// 根据搜索请求 searchRequest 在向量存储中进行相似度搜索
EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest);
// searchResult.matches(), 获取搜索结果中的匹配项
// .get(0): 从匹配项列表中获取第一个匹配项
EmbeddingMatch<TextSegment> embeddingMatch = searchResult.matches().get(0);
// 获取匹配项的相似度得分
System.out.println(embeddingMatch.score()); // 0.8144288515898701
// 返回文本结果
System.out.println(embeddingMatch.embedded().text());
}

在项目中整合向量数据库
上传知识库到pinecone
@Test
public void testUploadKnowledgeLibrary() {
// 使用 FileSystemDocumentLoader 读取指定目录下的知识库文档
// 并使用默认的文档解析器对文档进行解析
Document document1 = FileSystemDocumentLoader.loadDocument("E:/Program/knowledge/医院信息.md");
Document document2 = FileSystemDocumentLoader.loadDocument("E:/Program/knowledge/科室信息.md");
Document document3 = FileSystemDocumentLoader.loadDocument("E:/Program/knowledge/神经内科.md");
List<Document> documents = Arrays.asList(document1, document2, document3);
// 文本向量化并存入向量数据库, 将每个片段进行向量化, 得到一个嵌入向量
EmbeddingStoreIngestor
.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.build()
.ingest(documents);
}
修改配置类
@Bean
public ContentRetriever contentRetrieverXiaozhiPinecone() {
// 创建一个 EmbeddingStoreContentRetriever 对象, 用于从嵌入存储中检索内容
return EmbeddingStoreContentRetriever
.builder()
// 设置用于生成嵌入向量的嵌入模型
.embeddingModel(embeddingModel)
// 指定要使用的嵌入存储
.embeddingStore(embeddingStore)
// 设置最大检索结果数量, 这里表示最多返回 1 条匹配结果
.maxResults(1)
// 设置最小得分阈值, 只有得分大于等于 0.8 的结果才会被返回
.minScore(0.8)
// 构建最终的 EmbeddingStoreContentRetriever 实例
.build();
}
@AiService(
wiringMode = EXPLICIT,
chatModel = "qwenChatModel",
chatMemoryProvider = "chatMemoryProviderXiaozhi",
tools = "appointmentTools",
contentRetriever = "contentRetrieverXiaozhiPinecone"
)
public interface XiaozhiAgent {
@SystemMessage(fromResource = "xiaozhi-prompt-template.txt")
String chat(@MemoryId Long memoryId, @UserMessage String userMessage);
}
流式输出改造
引入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>1.0.0-beta3</version>
</dependency>
配置流式输出模型
# 在application.properties中配置流式输出大模型
# 集成阿里云千问-流式输出
langchain4j.community.dashscope.streaming-chat-model.api-key=${DASH_SCOPE_API_KEY}
langchain4j.community.dashscope.streaming-chat-model.model-name=qwen-plus
修改在智能体中所要使用的模型

运行前端工程

![]()

至此项目完成,后续可以优化前端显示适配MD格式,看起来会更加美观
更多推荐



所有评论(0)