目录
4.2.3 配置InMemoryEmbeddingStore
一、前言
尽管AI大模型(如GPT-4、DeepSeek等)在自然语言处理任务中表现出色,但它们仍然存在一些局限性,而RAG(Retrieval-Augmented Generation,检索增强生成)技术可以很好地弥补这些不足。举例来说,企业或个人都希望拥有一款属于自己的AI助手,能够帮自己随时解决一些特定场景或特定领域的问题,在这种场景下,AI大模型中的RAG技术就派上用场了,简单来说,它就是一款可以问你量身打造的大模型知识库,当你需要某个知识的时候为你提供更贴合实际业务场景的回答。
二、LangChain4j 介绍
2.1 什么是LangChain4j
LangChain4j作为一款专注于AI大模型集成的开源库,近年来受到了广泛关注。它旨在为开发者提供一种简单且高效的方式来接入和利用各种AI大模型,从而提升应用程序的智能化水平。LangChain4j的核心优势在于其高度的灵活性和易用性,使得开发者可以在不改变现有架构的前提下,快速实现AI功能的集成。
开发者文档地址:Introduction | LangChain4j
2.2 LangChain4j 主要特点
LangChain4j 主要具备如下特点:
模块化设计
LangChain4j 采用模块化架构,允许开发者根据需要选择和使用特定功能,如模型集成、数据加载、链式调用等。
多模型支持:
支持多种 LLM 提供商,如 OpenAI、Hugging Face 等,方便切换和集成不同模型。
链式调用:
提供链式调用功能,允许将多个任务串联,如文本生成后自动进行情感分析。
数据加载与处理:
内置多种数据加载器和处理器,支持从不同来源加载数据并进行预处理。
扩展性好
提供丰富的 API 和扩展点,开发者可以自定义组件以满足特定需求。
社区活跃
拥有活跃的社区和详细的文档,便于开发者获取支持和学习。
2.3 Langchain4j 核心组件
Langchain4j 的强大之处正是在于其内置了丰富的可以做到开箱即用的组件能力,相比Spring AI,Langchain4j 的组件更加丰富,在使用的时候也更灵活,下面是一张关于Langchain4j 的组件全景图。
三、RAG介绍
3.1 什么是RAG
RAG(Retrieval-Augmented Generation)的核心思想是:将传统的信息检索(IR)技术与现代的生成式大模型(如chatGPT)结合起来。
具体来说,RAG模型在生成答案之前,会首先从一个大型的文档库或知识库中检索到若干条相关的文档片段。再将这些检索到的片段作为额外的上下文信息,输入到生成模型中,从而生成更为准确和信息丰富的文本。
3.2 RAG工作流程
RAG的工作原理可以分为以下几个步骤:
接收请求:首先,系统接收到用户的请求(例如提出一个问题)。
信息检索(R):系统从一个大型文档库中检索出与查询最相关的文档片段。这一步的目标是找到那些可能包含答案或相关信息的文档。
生成增强(A):将检索到的文档片段与原始查询一起输入到大模型(如chatGPT)中,注意使用合适的提示词,比如原始的问题是XXX,检索到的信息是YYY,给大模型的输入应该类似于:请基于YYY回答XXXX。
输出生成(G):大模型基于输入的查询和检索到的文档片段生成最终的文本答案,并返回给用户。
3.2.1 补充说明
第2步骤中的信息检索,不一定必须使用向量数据库,可以是关系型数据库(如MySQL,PG)、全文搜索引擎(如Elasticsearch, ES),非关系数据库(Mongodb,Redis等)
- 大模型应用场景广泛使用向量数据库的原因是:在大模型RAG的应用场景中,主要是要查询相似度高的某几个文档,而不是精确的查找某一条(MySQL、ES擅长)。
- 相似度高的两个文档,可能不包含相同的关键词。例如,句子1: "他很高兴。" 句子2: "他感到非常快乐。" 虽然都是描述【他】很开心快乐的心情,但是不包含相同的关键词;
- 包含相同的关键词的两个文档可能完全没有关联,例如:句子1: "他喜欢苹果。" 句子2: "苹果是一家大公司。" 虽然都包含相同的关键词【苹果】,但两句话的相似度很低。
3.3 Embedding模型
3.3.1 RAG实际使用步骤
基于上面的描述,在实际落地RAG应用的时候,通常的思路是,大模型+本地(私有化)向量数据库,简单来说分为下面两步:
搭建私有化的向量数据库,可以是PG,es等,或者使用云上的付费向量数据库;
基于企业自身的文档资料,或其他业务资料,将这些原始的资料加载到私有向量数据库;
应用问题或知识检索时,大模型能力驱动,参考私有的向量数据库文档资料综合回答,这样给出的答案更符合企业的实际情况;
3.3.2 什么是Embedding
在上一步中,将本地的各类文档加载到向量数据库的过程中,向量数据库完成这一步骤的关键技术中,Embedding 技术在其中承担着非常重要的角色。
Embedding 技术是一种将高维数据映射到低维空间的方法,通常用于将离散的、非连续的数据转换为连续的向量表示,以便于计算机进行处理。
3.3.3 Embedding 技术优缺点
Embedding 技术优点:
- 语义信息捕捉
- Embedding 技术能够捕捉数据的语义信息,使得相似的数据在嵌入空间中更接近,有助于模型更好地理解数据之间的关系。
- 维度约减
- Embedding 技术将高维数据映射到低维空间,减少了计算和内存需求,提高了模型的效率。
- 上下文感知
- 嵌入向量通常是上下文感知的,可以考虑数据点与其周围数据点的关系,这对于自然语言处理等任务非常有用。
- 可训练
- 嵌入向量通常是可训练的,可以与模型一起训练,从而适应特定任务和数据集。
- 泛化能力
- 适当训练的嵌入可以提高模型的泛化能力,从而使其能够处理新数据和未知情况。
Embedding 技术缺点:
- 数据依赖性:
- Embedding 技术的性能高度依赖于训练数据的质量和多样性。如果训练数据不足或不具代表性,嵌入可能不准确。
- 维度选择:
- 选择适当的嵌入维度可以是挑战性的,太低的维度可能丧失信息,太高的维度可能增加计算成本。
- 过拟合:
- 嵌入可以过度拟合训练数据,特别是在小数据集上。这可能导致模型在未见过的数据上表现不佳。
- 计算复杂性:
- 在训练嵌入时,可能需要大量的计算资源和时间,尤其是对于大规模数据集和高维度嵌入。
- 可解释性差:
- 嵌入向量通常是抽象的,难以解释。这使得难以理解模型为什么做出特定的预测或推荐。
3.3.4 Embedding 技术在大模型中的价值
大模型在文本生成上的优势很明显,但是大模型的特性也带来的一定的弱势;
训练数据不实时(如ChatGPT是基于2021年9月之前的数据训练),重新训练成本过高,不现实。
输入文本长度有限制,通常限制在几千到数万个tokens之间。
无法访问不能公开的文档。
OpenAI发布了一篇文档,说明如何基于embedding使用两步搜索的方式来解决GPT无法处理长文本和最新数据的问题,即 RAG 技术。所以说Embedding是RAG应用落地的核心技术。其在很大程度上决定了搜索相关文本的质量以及影响大模型的输出效果。
下面是一张关于LangChain4j目前支持的嵌入模型
四、LangChain4j整合RAG操作实战
下面通过实际案例完整演示下如何在SpringBoot项目中使用LangChain4j完成RAG的完整使用过程。
4.1 前置准备
4.1.1 导入核心依赖
创建一个springboot工程,并导入下面的核心依赖
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<langchain.version>1.0.0-beta1</langchain.version>
<springboot.version>3.2.4</springboot.version>
</properties>
<dependencies>
<!-- JSON 处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
<version>1.0.0-M3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.35</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.16.9</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.32</version> <!-- 使用最新版本 -->
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-web-search-engine-searchapi</artifactId>
<version>${langchain.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-pgvector</artifactId>
<version>${langchain.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
<version>${langchain.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${springboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-bom</artifactId>
<version>${langchain.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<build>
<finalName>boot-docker</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
4.1.2 搭建pgVector向量数据库
向量数据库的选择有很多,比如pgVector,es,Milvus,Weaviate,Redis Stack,Chroma等,这里选择pgVector,首先使用docker命令快速部署一个pgVector服务
docker run -d --name pgvector -p 5432:5432 -v /root/pgv/data:/var/lib/postgresql/data -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres pgvector/pgvector:pg17
4.1.3 添加配置信息
在工程的配置文件中,添加如下信息,在下面的配置文件中,为了后面使用向量数据库的能力,主要包括下面几块核心的配置信息:
langchain4j核心配置,基于langchain4j连接大模型的能力;
这里选择了阿里云百炼大模型平台中的qwen-max这个大模型,apikey也是这个平台的;
选择的模型,建议直接使用那些支持嵌入模型的;
pgvector配置;
本地的文档将会加载到pgvector的向量数据库中进行存储;
server:
port: 8081
langchain4j:
community:
dashscope:
chat-model:
api-key: 你的阿里云百炼平台的apikey
model-name: qwen-max
pgvector:
database: springai
host: pgVector的服务器地址
port : 5432
user: postgres
password: postgres
table: my_table
spring:
ai:
dashscope:
api-key: sk-你的阿里云百炼平台的apikey
embedding:
options:
model: text-embedding-v2
4.1.4 Document Loader介绍
用于读取文件内容并写入到向量数据库中,在这里Langchain4j中集成了apache tika, 能够从许多种文件格式中提取文本内容。像功能比较强大的 Apache tika,入口:Introduction | LangChain4j
tika 内置以下几种读取文件的方式:
FileSystemDocumentLoader : 根据文件绝对路径来读取
ClassPathDocumentLoader : 根据classpath来读取
UrlDocumentLoader : 根据提供的url来读取
其他一些loader示例
Document Loaders | LangChain4j
4.2 基于本地内存作为向量数据库操作过程
如果是为了快速验证效果,或者开发做测试,可以使用基于本地内存的方式进行模拟测试,在Langchain4j中提供了 InMemoryEmbeddingStore 这个对象,可用于作为内存级的向量数据库使用,参考下面的执行步骤。
4.2.1 添加配置信息
在配置文件中添加下面的配置信息
embedding-model 一定要配置,这里选择的是阿里云百炼大平台中的支持向量数据库的大模型;
server:
port: 8081
langchain4j:
community:
dashscope:
chat-model:
api-key: 阿里云百炼平台apikey
model-name: qwen-max
embedding-model:
api-key: 阿里云百炼平台apikey
model: multimodal-embedding-v1
#model-name: text-embedding-v2
spring:
ai:
dashscope:
api-key: 阿里云百炼平台apikey
embedding:
options:
model: multimodal-embedding-v1
#model: text-embedding-v2
4.2.2 添加测试文档
在工程的resources目录下增加一个用于加载到向量数据库的txt文档,内容如下:
4.2.3 配置InMemoryEmbeddingStore
增加一个自定义配置类,将InMemoryEmbeddingStore 配置进去
@Configuration
@RequiredArgsConstructor
public class AssistantConfig {
final ChatLanguageModel chatLanguageModel;
@Bean
public EmbeddingStore<TextSegment> initEmbeddingStore() {
return new InMemoryEmbeddingStore<>();
}
@Bean
public Assistant assistant(SearchApiWebSearchEngine engine, EmbeddingStore<TextSegment> embeddingStore){
return AiServices.builder(Assistant.class)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(15))
.contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))
.tools(new WebSearchTool(engine))
.chatLanguageModel(chatLanguageModel)
.build();
}
}
4.2.4 增加测试接口
增加两个接口,加载文档到向量数据库的接口,和用于对话的接口,参考下面的代码
package com.congge.langchain4j;
import com.congge.service.Assistant;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.splitter.DocumentByLineSplitter;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/rag")
@RequiredArgsConstructor
public class RagController {
final ChatLanguageModel chatLanguageModel;
final EmbeddingStore<TextSegment> embeddingStore;
final EmbeddingModel embeddingModel;
/**
* 加载文件到向量数据库
* localhost:8081/api/rag/load
*
* @return
*/
@GetMapping("/load")
public Object load() {
List<Document> documents = FileSystemDocumentLoader.loadDocuments("E:\\code-self\\boot-consist\\src\\main\\resources\\file");
EmbeddingStoreIngestor.ingest(documents, embeddingStore);
;
return "success";
}
final Assistant assistant;
/**
* 聊天对话
* localhost:8081/api/rag/high/chat?message=AI去中心化与AI伦理成为焦点是什么
*
* @return
*/
@GetMapping("/high/chat")
public Object highChat(@RequestParam(value = "message") String message) {
return assistant.chat( message);
}
}
4.2.5 效果测试
调用第一个接口进行文档加载
再调用第二个接口进行对话,不难看出,本次回答问题的时候明显是参考了本地加载的文档中的内容
4.3 基于pgVector作为向量数据库操作过程
下面来看如何在代码中进行整合集成,参考下面的步骤。
4.3.1 增加配置类
该配置类主要设置一些初始化的内置对象,配置到全局的bean中,提供其他地方直接调用
package com.congge.config;
import com.congge.langchain4j.Calculator;
import com.congge.langchain4j.HighLevelCalculator;
import com.congge.service.Assistant;
import com.congge.util.DateCalculator;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
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.web.search.WebSearchTool;
import dev.langchain4j.web.search.searchapi.SearchApiWebSearchEngine;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class AssistantConfig {
final ChatLanguageModel chatLanguageModel;
@Bean
public Assistant assistant(SearchApiWebSearchEngine engine, EmbeddingStore<TextSegment> embeddingStore){
return AiServices.builder(Assistant.class)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(15))
.contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))
.tools(new WebSearchTool(engine))
.chatLanguageModel(chatLanguageModel)
.build();
}
}
4.3.2 增加pgVector配置类
由于本次使用的是pgVector作为向量数据库,因此需要覆盖默认的向量数据库,如下:
1)读取配置文件中的pg配置类
@Configuration
@ConfigurationProperties(prefix = "pgvector")
@Data
public class PgConfig {
private String host;
private int port;
private String database;
private String user;
private String password;
private String table;
}
2)自定义EmbeddingStore覆盖默认
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class EmbeddingStoreInit {
final PgConfig pgConfig;
@Bean
public EmbeddingStore<TextSegment> initEmbeddingStore() {
return PgVectorEmbeddingStore.builder()
.table(pgConfig.getTable())
.dropTableFirst(true)
.createTable(true)
.host(pgConfig.getHost())
.port(pgConfig.getPort())
.user(pgConfig.getUser())
.password(pgConfig.getPassword())
.dimension(384)
.database(pgConfig.getDatabase())
.build();
}
}
4.3.3 准备几个文档
在本地随机提供几个文档,可以是txt,或者md、PDF等文件
4.3.4 提供文档加载接口
提供第一个接口,用于加载本地文档到向量数据库
final EmbeddingStore<TextSegment> embeddingStore;
/**
* 加载文件到向量数据库
* localhost:8081/api/rag/load
*
* @return
*/
@GetMapping("/load")
public Object load() {
List<Document> documents = FileSystemDocumentLoader.loadDocuments("E:\\code-self\\boot-consist\\src\\main\\resources\\file");
EmbeddingStoreIngestor.ingest(documents, embeddingStore);
;
return "success";
}
接口执行成功后,连接pg数据库之后,可以看到本地的文档数据成功被加载到表中进行存储了
4.3.5 提供一个对话接口
像上文那样再提供一个对话接口,用于后面测试检索
final Assistant assistant;
/**
* 聊天对话
* localhost:8081/api/rag/high/chat?message=勒布朗·詹姆斯获得过哪些荣誉
*
* @return
*/
@GetMapping("/high/chat")
public Object highChat(@RequestParam(value = "message") String message) {
return assistant.chat( message);
}
基于上一步文档加载接口之后,调用一下对话接口
4.3.6 文档加载优化补充
在上面的加载文档接口中,我们使用了 EmbeddingStoreIngestor.ingest(documents, embeddingStore) 这个方法,对读取的文档进行向量数据库的加载,即没有对内部的参数进行特殊的配置,其实来说,在实际使用过程中,为了更合理的对拆分后存储到向量数据库中的文本长度进行控制,以兼顾存储容量和检索的效果,需要适当的做一些参数设置。
此处主要是用于对需要写入向量数据库的文本进行一定策略上的切分,可以让rag的检索效果更好,如果需要自定义Spliter策略的话可以实现DocumentSplitter接口
以下 是langchain4j内置提供的一些切分策略:
DocumentByCharacterSplitter 基于指定的字符进行切分
DocumentByLineSplitter 基于行切分
DocumentByParagraphSplitter 基于段落切分,默认的切分方式
DocumentByRegexSplitter 基于正则进行切分
DocumentBySentenceSplitter 基于语义进行切分,需要依赖语义切分的模型
DocumentByWordSplitter 基于单词进行切分
常用的拆分参数:
maxSegmentSizeInChars : 每个文本段最大的长度
maxOverlapSizeInChars :两个段之间重叠的数量
在上面的参数设置中,需要结合特定的向量数据库类型进行适配,如下这个参数:
下面是一个具体的自定义的文档切分参数后的测试接口
final ChatLanguageModel chatLanguageModel;
final EmbeddingStore<TextSegment> embeddingStore;
final EmbeddingModel embeddingModel;
/**
* 加载文件到向量数据库
* localhost:8081/api/rag/load
*
* @return
*/
@GetMapping("/load")
public Object load() {
List<Document> documents = FileSystemDocumentLoader.loadDocuments("E:\\code-self\\boot-consist\\src\\main\\resources\\file");
EmbeddingStoreIngestor.builder().embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.documentSplitter(new DocumentByLineSplitter(30,20))
.build().ingest(documents);
;
return "success";
}
重新调用一下文档加载接口,调用成功后,不难发现,这一次设置并进行文档拆分存储之后,文档的片段长度更小了;
五、写在文末
本文通过较大的篇幅详细介绍了基于Langchain4j结合目前主流的AI大模型实现RAG的完整过程,并通过实际案例进行了演示,希望对看到的同学有用,本篇到此结束,感谢观看。