06、RAG

LLM的知识仅限于它所接受到的训练数据。如果我们希望让它了解特定领域的专有知识,则可以使用下面的方式操作:

  • 使用RAG
  • 使用专有数据对LLM进行微调
  • RAG与数据微调方式结合使用

什么是RAG

简单地说,RAG就是把数据发送给LLM之前从数据中查找相关信息片段并把它注入到提示符的方法。这样的话LLM获得相关信息,并可以使用这些信息进行回复。

那么最为重要的是我们要可以检索到信息,可以使用下面的一些方法:

  1. 全文(关键字)检索,这咱方法使用IF-IDF和BM25之类的技术,通过在文档数据库中匹配查询关键字(用户询问的内容)来检索文档。它会根据这些关键字在每个文档中的频率和相关性对结果进行排序。
  2. 向量搜索,有称为“语义搜索”。使用嵌入模型把长文本文档转换为数字向量。然后,根据查询向量和文档向量之间的余弦相似度或其他相似度,来查找和排序文档,从而获得更深层次的语义。
  3. 混合检索,结合多种搜索方法(如:全文 + 向量),通常这样子可以提高检索的效率。

RAG的两阶段处理

RAG的过程是分为两个阶段的:索引和检索

索引

索引阶段就是为了后一阶段检索而进行的对文档的预处理

在这个阶段会根据所使用的信息检索方法而有变化。对于向量检索,通常包含清理文档、用额外的数据和元数据丰富它们、把它们分割为更小的段/块,最后把它们存储到存储区(向量数据库)中。

索引阶段通常是脱机进行的,这意味着它不需要最终用户等待它的操作完成。这样的话我们就可以使用定时任务在固定时间进行重新索引知识库文档,负责进行索引的代码也可以是一个单独的代码也可以是一个单独的应用程序,专门只用来处理索引任务。

在有些情况下,最终用户可能希望上传他们自己的文档,便于LLM访问到它们。在这种情况下索引阶段应该是在线执行的,并且是主应用程序的一部分。

关于索引阶段官方示意图如下:

检索

检索阶段通常是在线进行的,它处于用户提交应该使用索引文档回答问题时。

在这个过程中会根据所使用的信息检索方法而变化。对于向量搜索,通常来说涉及到用户的查询并在嵌入存储中地相似性搜索。然后把这个片段(原始文档的片段)注入提示并发送给LLM。

关于检索的官方示意图如下:

三种不同的RGA

  • Easy RAG:使用RAG最简单的方法
  • Native RAG:一个使用矢量搜索的RAG基本实现
  • Advanced RAG:一个模块化的RAG框架,它允许额的步骤,如查询转换、多个源检索和重新排序

Easy RAG

LangChain4j有一个简单的RAG实现,使用这个不需要额外去了解嵌入、矢量存储、嵌入模型以及了解如何解析和分割文档等等,我们只需要指向指定的文档,LangChain4j就会去处理。

注意:这种简单的RAG在质量上肯定是会低于定制RAG的

使用步骤

第一步:我们要使用Easy RAG功能,必须要添加相对应的依赖

<dependency>    <groupId>dev.langchain4j</groupId><artifactId>langchain4j-easy-rag</artifactId>
</dependency>

第二步:加载指定文档

List<Document> documents = FileSystemDocumentLoader.loadDocuments("E://myDocument");

第二步会加载指定目录下的所有文档,它的实现依赖于Apache Tika库支持,Tika库支持多种类型文档的解析,由于这里没有指定具体的DocumentParser,那么FileSystemDocumentLoader会加载一个ApacheTikaDocumentParser,这是由langchain4j-easy-rag依赖项通过SPI提供的。

我们了可以自定义需要解析哪些个文档,可以给定一个匹配规则,然后解析目录下满足规则的文档

PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:**" + ".txt");
FileSystemDocumentLoader.loadDocuments("E://myDocument", pathMatcher);

上面的话给定了一个规则,则只会解.txt的文档

第三步:我们需要一个专门的嵌入存储(矢量数据库)对文档进行预处理和存储

InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
EmbeddingStoreIngestor.ingest(documents, embeddingStore);

EmbeddingStoreIngestor通过SPI从langchain4j-easy-rag依赖项加载一个DocumentSplitter。每个文档被分割为更小的部分,每个部分由不超过300个令牌组成,并有30个令牌的重叠。

EmbeddingStoreIngestor通过SPI从langchain4j-easy-rag依赖项加载一个EmbeddingModel,使用这个EmbeddingModel把每个TextSegment转为嵌入

最终所有的TextSegment-Embedding对都会存储到EmbeddingStore

第四步:定义和创建AiService

具体示例

1、pom依赖项添加

<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-easy-rag</artifactId>
</dependency>

2、创建一个Document的工具类用来封装加载文档的方法

public class DocumentUtils {/*** 根据指定的目录加载目录下所有文档* @param dir 目录* @return 文档列表*/public static List<Document> loadDocuments(String dir) {return FileSystemDocumentLoader.loadDocuments(dir);}/*** 根据指定的目录加载目录下指定类型的文件* @param dir 指定要加载的目录* @param filter 文件类型* @return 文档列表*/public static List<Document> loadDocuments(String dir, String filter) {PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:**" + filter);return FileSystemDocumentLoader.loadDocuments(dir, pathMatcher);}/*** 根据指定的目录加载目录下所有文档* @param dir 目录* @param containsSubDir 是否包含子目录* @return 文档列表*/public static List<Document> loadDocuments(String dir,Boolean containsSubDir) {if (containsSubDir) {return FileSystemDocumentLoader.loadDocumentsRecursively(dir);}return FileSystemDocumentLoader.loadDocuments(dir);}/*** 根据指定的目录加载目录下指定类型的文件* @param dir 指定要加载的目录* @param filter 文件类型* @param containsSubDir 是否包含子目录* @return 文档列表*/public static List<Document> loadDocuments(String dir,String filter,Boolean containsSubDir) {// 多级目录时写**PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:**" + filter);if (containsSubDir) {return FileSystemDocumentLoader.loadDocumentsRecursively(dir, pathMatcher);}return FileSystemDocumentLoader.loadDocuments(dir, pathMatcher);}
}

3、创建AiService接口

public interface Assistant {Flux<String> chat(@MemoryId String memoryId, @UserMessage String message);
}

4、创建AiService实例Bean

@Configuration
public class AssistantConfig {@Resourceprivate StreamingChatLanguageModel streamingChatLanguageModel;@Beanpublic Assistant assistant() {InMemoryEmbeddingStore<TextSegment> embeddingStore = getInMemoryEmbeddingStore("E:\\project\\IdeaProjects\\AI\\LangChain4j\\rag\\src\\main\\resources\\documents", ".txt");return AiServices.builder(Assistant.class).chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)).contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore)).streamingChatLanguageModel(streamingChatLanguageModel).build();}private InMemoryEmbeddingStore<TextSegment> getInMemoryEmbeddingStore(String path, String filter) {InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();List<Document> documents = DocumentUtils.loadDocuments(path, filter);if (documents.isEmpty()) {throw new RuntimeException("没有找到文件");}EmbeddingStoreIngestor.ingest(documents, embeddingStore);return embeddingStore;}}

5、controller中进行调用

public interface Assistant {Flux<String> chat(@MemoryId String memoryId, @UserMessage String message);
}

RAG核心API介绍

Document

Document代表了整个文档,比如单个PDF文件或网页。注意:Document现在只能表示文本信息

Document.text();    // 返回文档的文本Document.metadata();    // 返回文档的元数据MetadataDocument.toTextSegment();    // 把文档转为TextSegmentDocument.from(String, Metadata);    // 使用文本+Metadata来创建文档Docunent.from(String);    // 通过文本来创建文档,此时Metadata为null

Metadata

每个文档都包含了Metadata。它存储了文档的一些元信息,如:文档名称、来源、上次更新日期、文档所有者以及其它的一些信息。

元数据以键值对的形式进行存储,其中键为String类型,值可以是String,Integer,Long,Float,Double。

基于以下几点来看使用元数据是非常有用的:

  • 在向LLM提供提示符时包含Document的内容时,可以包含元数据条目,从而为LLM提供需要考虑的附加信息,如:提供文档名称和来源可以帮助提高LLM对内容的理解
  • 我们在搜索包含在提示符中的相关内容时,可以通过Metadata条目进行过滤。这样可以把语义搜索范围缩小到特定的文档
  • 当文档的源被更新时,可以通过元数据条目(如:id,resource...)找到相应文档,并在EmbeddingStore中更新它以保持同步
Metadata.from(Map);    // 通过指定的map来创建MetadataMetadata.put(String key, String value);    // 添加指定的条目到元数据Metadata.getString(String key);
Metadata.getInteger(String key);    // 通过指定的key来获取value值,并且这个值是按调的方法不同直接专为对应的类型Metadata.containsKey(String key);    // 检查Metadata是否包含某个指定的keyMetadata.remove(String key);    // 根据指定的key来移除一个条目Metadata.copy();    // 返回Metadata的一个副本Metadata.toMap();    // 把Metadata转为一个map

DocumentLoader

我们可以使用文本来创建一个Document,但是更为常见的是使用一个文档加载器来实现

下面列举了langchang4j中的文档加载器

  • FileSystemDocumentLoader:在langchain4j模块中
  • ClassPathDocumentLoader:在langchain4j模块中
  • UrlDocumentLoader:在langchain4j模块中
  • AmazonS3DocumentLoader:在langchain4j-docunent-loader-amazon-s3模块中
  • AzureBlobStorageDocumentLoader:在langchain4j-document-loader-azure-storage-blob模块中
  • GitHubDocumentLoader:在langchain4j-document-loader-azure-github
  • GoogleCloudStorageDocumentLoader:在langchain4j-docuement-loader-google-cloud-storage模块中
  • SeleniumDocumentLoader:在langchain4j-document-loader-selenium模块中
  • TencentCosDocumentLoader:在langchain4j-document-loader-tencent-cos模块中

DocumentParser

文档可以表示为各种格式的文件,如:PDF,DOC,TXT,MD...,为了解析它们,有一个DocumentParser接口,其中包含了以下一些实现:

  • TextDocumentParser:来自langchain4j模块,可以解析普通文本文件
  • ApachePdfBoxDocumentParser:来自langchain4j-document-parser-apache-poi模块,可以解析office文件
  • ApacheTikaDocumentParser:来自langchain4j-document-parser-apache-tika模块,可以自动检测文档的类型并且几乎可以解析所有现有文件格式
String dir = "E:\\project\\IdeaProjects\\AI\\LangChain4j\\rag\\src\\main\\resources\\documents";
// 解析单个指定的文档
Document document = FileSystemDocumentLoader.loadDocument(dir + "\\my.txt", new TextDocumentParser());
log.info("document: {}", document.text());
System.out.println("========================================================");
Metadata metadata = document.metadata();
for (String key : metadata.toMap().keySet()) {System.out.println(key + ":" + metadata.getString(key));
}// 解析指定目录下的所有文档
System.out.println("================  解析目录下所有文档  =================");
List<Document> documentList = FileSystemDocumentLoader.loadDocuments(dir, new TextDocumentParser());
if (!documentList.isEmpty()) {for (Document doc : documentList) {Metadata docMetadata = doc.metadata();for (String key : docMetadata.toMap().keySet()) {System.out.println(key + ":" + docMetadata.getString(key));if (key.equals("file_name") && docMetadata.getString(key).equals("my.txt")) {System.out.println(doc.text());}}System.out.println("=================================================");}
}// 解析指定目录下的指定类型文档
System.out.println("================  解析目录下指定类型文档  =================");
PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:*.txt");    // 目录下txt文件
List<Document> documentList2 = FileSystemDocumentLoader.loadDocuments(dir, pathMatcher, new TextDocumentParser());
if (!documentList2.isEmpty()) {for (Document document1 : documentList2) {System.out.println(document1.text());System.out.println("=========================================================");}
}// 加载目录及其子目录下的所有文档
System.out.println("=============  解析目录及子目录下所有文档  =================");
List<Document> documents = FileSystemDocumentLoader.loadDocumentsRecursively(dir, new TextDocumentParser());
System.out.println("加载到的文档数:" + documents.size());

在我们解析文档时,也可以不明确指定DocumentParser,这个时候会使用默认DocumentParser。它默认是通过SPI加载的(比如从langchain4j-document-parser-apache-tika或者langchain4j-easy-rag,如果其中有一个被导入的话),如果没有通过SPI找到DocumentParser则会使用TextDocumentParser。

DocumentTransformer

这个用来实现各种文档的转换,有如下的一些动作

  • 清理:把文档中的不必要的噪音删除,节省token及减少干扰
  • 过滤:从搜索中完全排除特定的文档
  • 增强:可以向文本中添加其它的信息,从而潜在地增强搜索结果
  • 总结:可以对文档进行简短的总结,这个简短的总结可以存在元数据中,可以改进搜索
  • ...

在转换时还可以增加、删除、修改metadata条目 

当前提供的唯一开箱即用的实现是:langchain4j-document-transformer-jsoup模块中的HtmlToTextDocumentTransformer

TextSegment

一旦文档加载完成,这个时候它们会被分割为更小的部分,langchain4j中有一个TextSegment类,它表示文档的一个片段。它只能表示文本信息。

我们为什么要进行分割呢?

  • LLM有一个有限的上下文窗口,如果一次性放入整个知识库不合适
  • 在提示中提供更多的信息,LLM在处理和响应的时候就越慢
  • 在提示中提供更多的信息,我们调用LLM的成本越高
  • 在提示中给出不相关的信息这可能增加产生幻觉的机会,这个时候有过多的不相关信息干扰
  • 在提示中给出信息越多,就越难理解LLM是根据哪些信息做出的响应

我们可以通过把知识库分成更小、更容易理解的部分来解决上面的问题,这个要分多细?这个还得要视情况而定。

有两种常用的使用方法:

方法一:每个文档(pdf,网页...)都是原子的,不可分割。在RAG检索期间,检索N个最相关的文档并把它注入提示符,在这种情况下需要使用长上下文LLM,因为文档可能是相当长的,如果对于检索完整的文档很重要,这种方法是可取的。

对于方法一的优缺点如下:

优点:没有上下文丢失

缺点:消耗token会多,长文档可能包含多个主题,并非所有主题都与查询有关,各种大小的完整文档被压缩到一个固定长度的矢量中,矢量搜索的质量会受到影响

方法二:文档被分为更小的部分,RAG检索期间,检索N个最相关的段并把它注入提示符。这里要保证每个部分提供足够的上下文/信息以便于LLM理解它。为了减少LLM误解给定的片段并产生幻觉,一种常见的做法就是把文档分成有重叠的部分,注意,这种做法并不是万能的

对于方法二的优缺点如下:

优点:矢量搜索质量好,减少token的消耗

缺点:一些上下文可能丢失

TextSegment.text();    // 返回TextSegment的文本信息TextSegment.metadata();    // 返回TextSegment的metadataTextSegment.from(String, Metadata);    // 根据文本与metadata来创建TextSegmentTextSegment.from(String);    // 根据文本创建TextSegment,此时metadata为null

DocumentSplitter

LangChain4j有一个DocumentSplitter接口,它有几个开箱即用的实现

  • DocumentByParagraphSplitter
  • DocumentByLineSplitter
  • DocumentBySentenceSplitter
  • DocumentByWordSplitter
  • DocumentByCharacterSplitter
  • DocumentByRegexSplitter
  • HierarchicalDocumentSplitter

工作原理:

  1. 实例化一个DocumentSplitter,指定TextSegment所需的大小,并可选择地指定字符或令牌的重叠部分
  2. 调用DocumentSplitter的split(Document),splitAll(List<Document>)
  3. DocumentSplitter把给定的文档拆分为更小的单元,其性质会随着拆分器而产生变化。如:DocumentByParagraphSplitter会把文档分成段落(由两个或多个连接的换行符定义),而DocumentBySentenceSplitter使用OpenNPL库的句子检测器把文档分成句子...
  4. DocumentSplitter然后把这些较小的单元(段落、句子...)组合到TextSegment中,在不超过1中所说的限定大小情况下在单个TextSegment中包含尽可能多的单元。如果某些单元还是过大,则调用子分割器,把不适合理细粒度单元拆分,所有元数据条目从Document复制到TextSegment。有一个唯一的元数据条目“索引”会添加到每个文本段,第一个TextSegment会包含index=0,第二个index=1...

TextSegmentTransformer

它类似于DocumentTransformer,只是说这里转换的是TextSegment

转换是没有万能的方案的,需要根据具体的情况进行实现,一般有效的改进检索技术是在每一个TextSegment当中包含文档标题或简短摘要。

Embedding

它封装了一个数字向量,表示嵌入内容的“语义”(通常是文本,如:TextSegment)

Embedding.dimension();    // 它返回嵌入向量的维度(长度)CosineSimilarity.between(Embedding, Embedding);    // 计算两者之间的余弦相似度Embedding.nomalize();    // 规范化嵌入向量

EmbeddingStore

EmbeddingStore接口表示Embedding的存储,也称为矢量数据库。

EmbeddingStore可以单独或与相应的TextSegment一起存储Embedding

EmbeddingStore.add(Embedding);    // 把给定嵌入添加到存储中并返回一个随机idEmbeddingStore.add(String id, Embedding);    // 指定id的给定嵌入添加到存储区EmbeddingStore.add(Embedding, TextSegment);    // 把给定嵌入与关联的TextSegment添加到存储中,返回一个随机的idEmbeddingStore.addAll(List<Embedding>);    // 把给定嵌入列表添加到存储区,并返回一个随机id列表EmbeddingStore.addAll(List<Embedding>,List<TextSegment>);    // 向存储中添加带有关联TextSegment的给定Embedding列表,返回随机id列表EmbeddingStore.addAll(List<String> ids, List<Embedding>, List<TextSegment>);    // 把带有关联id和textSegment的给定嵌入列表添加到存储中EmbeddingStore.search(EmbeddingSearchRequest);    // 搜索最为相似的嵌入EmbeddingStore.remove(String id);    // 根据id从存储区删除指定的嵌入EmbeddingStore.removeAll(Collection<String> ids);    // 根据列表中的id来批量删除存储区中的嵌入EmbeddingStore.removeAll(Filter);    // 根据指定的筛选器来删除所有匹配的嵌入EmbeddingStore.removeAll();    // 删除所有的嵌入

EmbeddingSearchRequest

这个代表了在EmbeddingStore中搜索的请求,其具有如下属性:

  • Embedding queryEmbedding 作为参数的嵌入
  • int maxResult 要返回的最大结果数,这是一个可选参数,默认值是3
  • double minScore 最低分数,范围是0~1。只有分数>= minScore的嵌入才会返回,可选参数,默认值是0
  • Filter filter 搜索过程中应用于元数据的过滤器。只有元数据匹配过滤器的TextSegment才会返回

Filter

Filter允许在执行矢量搜索时按元数据项进行过滤

目前支持的操作如下:

  • IsEqualTo
  • IsNotEqualTo
  • IsGreaterThan
  • IsGreaterThanOrEqualTo
  • IsLessThan
  • IsLessThanOrEqualTo
  • IsIn
  • IsNotIn
  • ContainsString
  • And
  • Not
  • Or

注意:不是所有的嵌入存储都支持按元数据过滤的,支持的话也不一定是所有的筛选类型或操作都支持!

EmbeddingSearchResult

这个代表了EmbeddingStore中搜索结果,它包含了EmbeddingMatch的列表

EmbeddingMatch

表示匹配的嵌入及相关性评分、ID和原始嵌入数据(一般是TextSegment)

EmbeddingStoreIngestor

EmbeddingStroeIngestor表示一个管道,负责把文档流到EmbeddingStore当中去

最简单的配置中,EmbeddingStoreIngestor使用指定的EmbeddingModel嵌入提供的文档,并把它们的嵌入一起存储在指定的EmbeddingStore当中。

EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder().embeddingModel(embeddingModel).embeddingStore(embeddingStore).build();
ingestor.ingest(document1);
ingestor.ingest(document2,document3);
IngestionResult ingestionResult = ingestor.ingest(List.of(document4,document5,document6));

上面所有的ingest()方法都会返回一个IngestionResut对象,在它当中有一个TokenUsage,显示了用于嵌入的令牌数量。

EmbeddingStoreIngestor可以选择使用指定的DocumentTransformer转换文档。如果希望在嵌入之前清理、丰富或格式化文档,是非常有用的。

EmbeddingStoreIngestor可以使用指定的DocumentSplitter把文档拆分为TextSegment。如果当前文档比较大并希望把它们分割为更小的textSegment,以提高相似性搜索的质量,并减少发送给LLM的提示的大小和成本是非常有用的。

EmbeddingStoreIngestor可以使用指定的TextSegmentTransformer转换TextSegment,如果希望在嵌入TextSegment之前进行清理、丰富或格式化它们,这是非常有用的。

看下面的代码:

private InMemoryEmbeddingStore<TextSegment> getInMemoryEmbeddingStoreTransformer(String path, String filter) {InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();List<Document> documents = DocumentUtils.loadDocuments(path, filter);if (documents.isEmpty()) {throw new RuntimeException("没有找到文件");}EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()// 向每个document添加一个userId元数据条目,后面可能的话可以通过它来进行过滤.documentTransformer(document -> {document.metadata().put("userId", "xiaoxie");return document;})// 把每个文档拆分为每个文本段,每个文本段有200个令牌,其中有10个令牌重叠.documentSplitter(DocumentSplitters.recursive(200, 10))//.textSegmentTransformer(textSegment -> TextSegment.from(textSegment.metadata().getString("file_name") + "\n" + textSegment.text(),textSegment.metadata())).embeddingModel(embeddingModel).embeddingStore(embeddingStore).build();ingestor.ingest(documents);return embeddingStore;
}

注意,我们存储使用的是:InMemoryEmbeddingStore,它的向量维度是384,如我们此时使用的是百炼平台的 text-embedding-v3,它的向量维度是1024,这两者会对不上最终请求时会出错的。

所以我们不要使用百炼的这个embeddingModel,也就是直接都不使用.embedding(embeddingModel),最终改为如下:

在config类中我们创建一个Bean

@Bean("transformerAssistant")
public Assistant assistant2() {InMemoryEmbeddingStore<TextSegment> embeddingStore = getInMemoryEmbeddingStoreTransformer("E:\\project\\IdeaProjects\\AI\\LangChain4j\\rag\\src\\main\\resources\\documents", ".md");return AiServices.builder(Assistant.class).chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)).contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore)).streamingChatLanguageModel(streamingChatLanguageModel).build();
}private InMemoryEmbeddingStore<TextSegment> getInMemoryEmbeddingStoreTransformer(String path, String filter) {InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();List<Document> documents = DocumentUtils.loadDocuments(path, filter);if (documents.isEmpty()) {throw new RuntimeException("没有找到文件");}EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()// 向每个document添加一个userId元数据条目,后面可能的话可以通过它来进行过滤.documentTransformer(document -> {document.metadata().put("userId", "xiaoxie");return document;})// 把每个文档拆分为每个文本段,每个文本段有200个令牌,其中有10个令牌重叠.documentSplitter(DocumentSplitters.recursive(200, 10))//.textSegmentTransformer(textSegment -> TextSegment.from(textSegment.metadata().getString("file_name") + "\n" + textSegment.text(),textSegment.metadata()))// 这里不要加上embeddingModel,因为InMemoryEmbeddingStore向量维度是384。现在百炼平台引入embeddingModel向量维度无法对应// .embeddingModel(embeddingModel).embeddingStore(embeddingStore).build();ingestor.ingest(documents);return embeddingStore;
}

controller中进行请求如下:

@Resource(name = "transformerAssistant")
private Assistant assistant2;@GetMapping(value = "/chat2",produces = "text/plain;charset=UTF-8")
public Flux<String> chat2(@RequestParam("message") String message, @RequestParam("memoryId") String memoryId) {return assistant2.chat(memoryId, message);
}

Native RAG

一旦我们对文档进行ingested,我们就可以创建一个EmbeddingStoreContentRetriever,在其中可以指定EmbeddingStroe,EmbeddingModel

config类中创建Bean的代码改为如下:

@Bean("transformerAssistant")
public Assistant assistant2() {InMemoryEmbeddingStore<TextSegment> embeddingStore = getInMemoryEmbeddingStoreTransformer("E:\\project\\IdeaProjects\\AI\\LangChain4j\\rag\\src\\main\\resources\\documents", ".md");EmbeddingStoreContentRetriever embeddingStoreContentRetriever = EmbeddingStoreContentRetriever.builder().embeddingModel(embeddingModel).embeddingStore(embeddingStore).maxResults(5).minScore(0.75).build();return AiServices.builder(Assistant.class).chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))// .contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore)).contentRetriever(embeddingStoreContentRetriever).streamingChatLanguageModel(streamingChatLanguageModel).build();
}private InMemoryEmbeddingStore<TextSegment> getInMemoryEmbeddingStoreTransformer(String path, String filter) {InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();List<Document> documents = DocumentUtils.loadDocuments(path, filter);if (documents.isEmpty()) {throw new RuntimeException("没有找到文件");}EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()// 向每个document添加一个userId元数据条目,后面可能的话可以通过它来进行过滤.documentTransformer(document -> {document.metadata().put("userId", "xiaoxie");return document;})// 把每个文档拆分为每个文本段,每个文本段有200个令牌,其中有10个令牌重叠.documentSplitter(DocumentSplitters.recursive(200, 10))//.textSegmentTransformer(textSegment -> TextSegment.from(textSegment.metadata().getString("file_name") + "\n" + textSegment.text(),textSegment.metadata())).embeddingModel(embeddingModel).embeddingStore(embeddingStore).build();ingestor.ingest(documents);return embeddingStore;
}

注意:这里使用的还是内存嵌入存储,为什么此时又可以使用百炼的EmbeddingModel了,这个是因在创建EmbeddingStoreIngestor时指定了EmbeddingModel,在创建AiService时,构造EmbeddingStoreContentRetriever时也指写了EmbeddingModel,这两个EmbeddingModel是一样的,所以它们的向量维度也一定是一样的!!

Advanced RAG

对于高级RAG可以使用LangChain4j中如下核心组件实现

  • QueryTransformer
  • QueryRouter
  • ContentRetriever
  • ContentAggregator
  • ContentInjector

官方提供的下图展示了各个组件的协同工作过程

从这个图上可以看到这个过程大致如下:

  1. 用户生成一个UserMessage,它会被转换为一个Query
  2. QueryTransformer把查询转换为一个或多个查询
  3. 每个查询由QueryRouter路由到一个或多个ContentRetriever
  4. 每一个ContentRetriever为每个查询检索相关的内容
  5. ContentAggregator把所有检索到的内容组合成一个最终排名列表
  6. Content列表会被注入到原始UserMessage中
  7. 最终把包含原始查询和注入的相关内容的UserMessage发送到LLM

RetrievalAugmentor

它是进入RAG管道的入口,负责用各种来源检索到相关内容扩充ChatMessage

每次调用AiService时,会调用指定的RetrievalAugmentor扩展当前UserMessage,有一个RetrievalAugmentor的默认实现,也可以自定义实现。

DefaultRetrievalAugmentor

LangChain4j提供了一个实现DefaultRetrievalAugmentor,它适用于大多数RAG。

Query

它表示RAG管道中的用户查询,包含查询文本和查询元数据

QueryMetadata

查询中的元数据包含可能在RAG管道的各个组件中的有用信息,如:

Metadata.userMessage()  应该被增强的原始UserMessage

Metadata.chatMemoryId()  @MemoryId注解指定的参数值,用来识别用户,并在检索期间应用于访问限制和过滤器

Metadata.chatMemory()  所有之前的聊天信息,有助于理解查询所处的上下文

QueryTransformer

把给定的查询转换为一个或多个查询。目标是通过修改或扩展原始查询来提高检索质量

一些可以改进检索的方法有:

  • 查询压缩:Query compression
  • 查询扩展:Query expansion
  • 查询重写:Query re-writing
  • 退一步提示:Step-back prompting
  • 文档嵌入:Hypothetical document embeddings

DefaultQueryTransformer

它是DefaultRetrievalAugmentor中使用的默认实现,它不会对Query进行任何修改,而只是把它传递出去。

CompressingQueryTransformer

把给定查询和之前的对话压缩为一个独立的查询。当用户可能会提出后续问题,涉及到之前的问题或答案中的信息时会非常有用。

ExpandingQueryTransformer

把给定查询扩展为多个查询。LLM可以以各种方式重新表述和重新制定查询,这有助于检索更相关的内容。

Content

Content表示与用户查询相关的内容,当前只支持文本内容

ContentRetriever

使用给定的查询从底层数据源检索内容,底层数据源可以是任何内容:

  • EmbeddingStore
  • 全文检索引擎
  • 矢量与全文检索的混合
  • 网络搜索引擎
  • 知识图谱
  • SQL数据库
  • ...

由ContentRetriever返回的列表内容会按相关性从高到低排序

EmbeddingStoreContentRetriever

它用EmbeddingModel嵌入查询,从EmbeddingStore检索相关内容,如下所示代码:

@Bean("transformerAssistant")public Assistant assistant2() {InMemoryEmbeddingStore<TextSegment> embeddingStore = getInMemoryEmbeddingStoreTransformer("E:\\project\\IdeaProjects\\AI\\LangChain4j\\rag\\src\\main\\resources\\documents", ".md");EmbeddingStoreContentRetriever embeddingStoreContentRetriever = EmbeddingStoreContentRetriever.builder().embeddingModel(embeddingModel).embeddingStore(embeddingStore).maxResults(5)// maxResults是动态的.dynamicMaxResults(query -> 3).minScore(0.75)// minScore是动态的.dynamicMinScore(query -> 0.75)// .filter(metadataKey("userId").isEqualTo("xiaoxie"))// 动态filter.dynamicFilter(query -> {String chatMemoryId = (String) query.metadata().chatMemoryId();return metadataKey("userId").isEqualTo(chatMemoryId);}).build();return AiServices.builder(Assistant.class).chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))// .contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))// .contentRetriever(embeddingStoreContentRetriever).retrievalAugmentor(DefaultRetrievalAugmentor.builder().contentRetriever(embeddingStoreContentRetriever).build()).streamingChatLanguageModel(streamingChatLanguageModel).build();}

QueryRouter

QueryRouter负责把Query路由到适当的ContentRetriever

DefaultQueryRouter

它是DefaultRetrievalAugmentor中使用的默认实现。把每个查询路由到所有配置的Conentretiever

LanguageModelQueryRouter

用LLM来决定把给定的查询路由到哪里

ContentAggregator

负责聚合来自以下内容的多个排名列表

  • 多个Query
  • 多个ContentRetriever
DefaultContentAggregator

DefaultContentAggregator是ContentAggregator是ContentAggregator的默认实现

ReRankingContentAggregator

ReRankingContentAggregator用一个ScoringModel,像Cohere,执行重新排名。

ContentInjector

负责把ContentAggregator返回的内容注入到UserMessage中

DefaultContentInjector

它是ContentInjector的默认实现,它简单地把Contents附加到UserMessage的末尾

我们可以重写default PromptTemplate

@Bean("transformerAssistant")public Assistant assistant2() {InMemoryEmbeddingStore<TextSegment> embeddingStore = getInMemoryEmbeddingStoreTransformer("E:\\project\\IdeaProjects\\AI\\LangChain4j\\rag\\src\\main\\resources\\documents", ".md");EmbeddingStoreContentRetriever embeddingStoreContentRetriever = EmbeddingStoreContentRetriever.builder().embeddingModel(embeddingModel).embeddingStore(embeddingStore).maxResults(5)// maxResults是动态的.dynamicMaxResults(query -> 3).minScore(0.75)// minScore是动态的.dynamicMinScore(query -> 0.75)// .filter(metadataKey("userId").isEqualTo("xiaoxie"))// 动态filter.dynamicFilter(query -> {String chatMemoryId = (String) query.metadata().chatMemoryId();return metadataKey("userId").isEqualTo(chatMemoryId);}).build();return AiServices.builder(Assistant.class).chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))// .contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))// .contentRetriever(embeddingStoreContentRetriever).retrievalAugmentor(DefaultRetrievalAugmentor.builder().contentRetriever(embeddingStoreContentRetriever).contentInjector(DefaultContentInjector.builder().promptTemplate(PromptTemplate.from("{{userMessage}}\n,基于以下回答:{{contents}}")).build()).build()).streamingChatLanguageModel(streamingChatLanguageModel).build();}

看上面在构造AiService的Bean的时候在retrievalAugmentor()中有一个.contentInjector()

注意:PromptTemplate必须包含{{userMessage}}和{{contents}}变量。

并行化

当只有一个查询和一个ContentRetriever,DefaultRetrievalAugmentor在同一个线程中执行查询路由和内容检索,否则会使用Executor来进行并行处理。

默认情况下,使用修改后的(keepAliveTime是1秒而不是60秒)Executors.newCachedThreadPool(),但是你可以在创建DefaultRetrievalAugmentor时提供一个自定义Executor实例

DefaultRetrievalAugmentor.builder()....executor(executor).build;

Accessing Soruces

如查希望在使用AIService时访问Sources,可以通过Result类获取

interface Assistant {Result<String> chat(String userMessage);
}Result<String> result = assistant.chat("How to do Easy RAG with LangChain4j?");String answer = result.content();
List<Content> sources = result.sources();

如果是流式的,可以使用onRetrieved来指定

interface Assistant {TokenStream chat(String userMessage);
}assistant.chat("How to do Easy RAG with LangChain4j?").onRetrieved((List<Content> sources) -> ...).onPartialResponse(...).onCompleteResponse(...).onError(...).start();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/899097.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

自然语言处理:第一百零二章 如何去掉DeepSeek R1思考过程

本人项目地址大全&#xff1a;Victor94-king/NLP__ManVictor: CSDN of ManVictor 写在前面: 笔者更新不易&#xff0c;希望走过路过点个关注和赞&#xff0c;笔芯!!! 写在前面: 笔者更新不易&#xff0c;希望走过路过点个关注和赞&#xff0c;笔芯!!! 写在前面: 笔者更新不易…

flink 安装与访问 ui

官方文档&#xff1a;First steps | Apache Flink 版本&#xff1a;v2.0.0 下载Flink Flink运行在所有类UNIX环境中&#xff0c;即Linux&#xff0c;Mac OS X和Cygwin&#xff08;适用于Windows&#xff09;。您需要安装Java 11。要检查安装的Java版本&#xff0c;请在终端中…

WPF TextBox实现键盘enter后实时读取TextBox中的值

代码 <TextBox Grid.Column"0" x:Name"textBox" Margin"10,5,0,5" TextWrapping"Wrap" Text"{Binding SendMessage,UpdateSourceTriggerPropertyChanged}" VerticalContentAlignment"Center" CaretBrush&qu…

PyTorch实现Transformer模型

首先&#xff0c;我得回顾一下Transformer的基本结构&#xff0c;确保自己没有记错。Transformer由编码器和解码器组成&#xff0c;每个编码器层包含多头自注意力机制和前馈网络&#xff0c;解码器层则还有编码器-解码器注意力。 接下来&#xff0c;用户需要的是手把手的代码解…

详细介绍sentinel的使用,并列举经常出的面试题以及答案

Sentinel 是一款由阿里巴巴开源的分布式系统的流量防卫系统&#xff0c;能够实时响应并满足高并发的流量控制需求。它提供了流量监控、流量控制、熔断降级、系统保护等核心功能&#xff0c;可帮助开发人员实时发现系统的流量异常并快速做出相应的限流策略。 Sentinel 的使用步…

mysql-connector-java-5.1.37.jarJava连接器

mysql-connector-java-5.1.37.jar是MySQL官方提供的Java连接器&#xff0c;用于在Java应用程序中与MySQL数据库进行通信。具体来说&#xff0c;这个JAR文件是MySQLJDBC驱动程序的一个版本&#xff0c;允许Java程序通过JDBC&#xff08;JavaDatabaseConnectivity&#xff09;接口…

Python基于Django的智能旅游推荐系统(附源码,文档说明)

博主介绍&#xff1a;✌IT徐师兄、7年大厂程序员经历。全网粉丝15W、csdn博客专家、掘金/华为云//InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3…

【博客节选】再谈Unity 的 root motion

节选自 【Unity实战笔记】第二十三 root motion变更方向攻击 &#xff08;OnStateMove rootmotion rigidbody 使用的一些问题&#xff09; 小伙伴们应该对root motion非常困惑&#xff0c;包括那个bake into pose。 当xz bake into pose后&#xff0c;角色攻击动画与父节点产…

网站服务器常见的CC攻击防御秘籍!

CC攻击对网站的运营是非常不利的&#xff0c;因此我们必须积极防范这种攻击&#xff0c;但有些站长在防范这种攻击时可能会陷入误区。让我们先了解下CC攻击&#xff01; CC攻击是什么 CC是DDoS攻击的一种&#xff0c;CC攻击是借助代理服务器生成指向受害主机的合法请求&#x…

JAVA:Spring Boot @Conditional 注解详解及实践

1、简述 在 Spring Boot 中&#xff0c;Conditional 注解用于实现 条件化 Bean 装配&#xff0c;即根据特定的条件来决定是否加载某个 Bean。它是 Spring 框架中的一个扩展机制&#xff0c;常用于实现模块化、可配置的组件加载。 本文将详细介绍 Conditional 相关的注解&…

使用python爬取网络资源

整体思路 网络资源爬取通常分为以下几个步骤&#xff1a; 发送 HTTP 请求&#xff1a;使用requests库向目标网站发送请求&#xff0c;获取网页的 HTML 内容。解析 HTML 内容&#xff1a;使用BeautifulSoup库解析 HTML 内容&#xff0c;从中提取所需的数据。处理数据&#xff…

PostgreSQL 数据库源码编译安装全流程详解 Linux 8

PostgreSQL 数据库源码编译安装全流程详解 Linux 8 1. 基础环境配置1.1 修改主机名1.2 配置操作系统yum源1.3 安装操作系统依赖包1.4 禁用SELINUX配置1.5 关闭操作系统防火墙1.6 创建用户和组1.7 建立安装目录1.8 编辑环境变量 2. 源码方式安装&#xff08;PG 16&#xff09;2.…

(基本常识)C++中const与引用——面试常问

作者&#xff1a;求一个demo 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 内容通俗易懂&#xff0c;没有废话&#xff0c;文章最后是面试常问内容&#xff08;建议通过标题目录学习&#xff09; 废话不多…

Java并发编程 什么是分布式锁 跟其他的锁有什么区别 底层原理 实战讲解

目录 一、分布式锁的定义与核心作用 二、分布式锁与普通锁的核心区别 三、分布式锁的底层原理与实现方式 1. 核心实现原理 2. 主流实现方案对比 3. 关键技术细节 四、典型问题与解决方案 五、总结 六、具体代码实现 一、分布式锁的定义与核心作用 分布式锁是一种在分布…

案例:使用网络命名空间模拟多主机并通过网桥访问外部网络

案例目标 隔离性&#xff1a;在同一台物理机上创建两个独立的网络命名空间&#xff08;模拟两台主机&#xff09;&#xff0c;确保其网络配置完全隔离。内部通信&#xff1a;允许两个命名空间通过虚拟设备直接通信。外部访问&#xff1a;通过宿主机的网桥和 NAT 规则&#xff…

AF3 Rotation 类解读

Rotation 类(rigid_utils 模块)是 AlphaFold3 中用于 3D旋转 的核心组件,支持两种旋转表示: 1️⃣ 旋转矩阵 (3x3) 2️⃣ 四元数 (quaternion, 4元向量) 👉 设计目标: 允许灵活选择 旋转矩阵 或 四元数 封装了常用的 旋转操作(组合、逆旋转、应用到点上等) 像 torch.…

DeepSeek面试——模型架构和主要创新点

本文将介绍DeepSeek的模型架构多头潜在注意力&#xff08;MLA&#xff09;技术&#xff0c;混合专家&#xff08;MoE&#xff09;架构&#xff0c; 无辅助损失负载均衡技术&#xff0c;多Token 预测&#xff08;MTP&#xff09;策略。 一、模型架构 DeepSeek-R1的基本架构沿用…

【web3】

检测钱包是否安装 方法一 // npm install metamask/detect-provider import detectEthereumProvider from metamask/detect-provider// 检测钱包是否安装 const isProvider await detectEthereumProvider() if(!isProvider) {proxy.$modal.msgError("请安装钱包")…

husky的简介以及如果想要放飞自我的解决方案

husky 是一个 Git Hooks 管理工具&#xff0c;它的主要作用是 在 Git 提交&#xff08;commit&#xff09;、推送&#xff08;push&#xff09;等操作时执行自定义脚本&#xff0c;比如代码检查&#xff08;Lint&#xff09;、单元测试&#xff08;Test&#xff09;、格式化代码…

JVM之类的加载过程

加载 这一阶段是将类的字节码从外部存储&#xff08;如磁盘&#xff09;加载到JVM的内存中。加载时&#xff0c;JVM会根据类的全限定名&#xff08;包括包名和类名&#xff09;查找相应的字节码文件&#xff08;.class文件&#xff09;&#xff0c;并将其读入内存。 链接 链接…