专栏名称: 吃果冻不吐果冻皮
专注于AI工程化(LLM、MLOps、LLMOps、RAG、Agent)落地。
目录
相关文章推荐
MINISO名创优品  ·  名创×时空中的绘旅人 | 线下活动全攻略 ·  昨天  
玩物志  ·  曾在风口浪尖的明星 AI ... ·  2 天前  
刀法研究所  ·  人群越小生意越大?悦鲜活靠什么1年卖出35亿? ·  2 天前  
刀法研究所  ·  被小红书女孩买断货,大疆Pocket3用这个 ... ·  3 天前  
51好读  ›  专栏  ›  吃果冻不吐果冻皮

关于RAG优化的几个小技巧

吃果冻不吐果冻皮  · 公众号  ·  · 2024-07-22 18:45

正文

【点击】 加入大模型技术交流群

原文:https://zhuanlan.zhihu.com/p/702196014

一、背景说明

RAG技术为大型语言模型(LLMs)提供了从数据源检索到的信息,以支撑其生成的答案。简而言之,RAG就是搜索加上LLM提示,你让模型在提供的信息上下文中回答问题。查询和检索到的上下文都被注入到发送给LLM的提示中。

传统的建模方式 :

典型的RAG向量化应用程序有两个主要组件:

索引 :从源中获取数据并对其进行向量化索引。

检索和生成 :运行时接受用户查询转化成向量化数据,并从索引中检索相关数据,然后将其传递给LLM,生成内容。

实际使用过程中,上面简单的流程经常效果不能满足业务需求,核心问题在于如何检索并处理好传给LLM的内容,下面介绍几种优化思路以及LangChain的实现。

二、优化方法

2.1 多层索引

多层索引技术主要是针对海量的文档检索的处理策略,对面临需要从海量的文档中搜索、找到相关信息,并将这类信息综合成一个答案,并应用其来源。一般有2种方式建立多层索引来优化这类问题。

一种是为文档建立多级索引,比如 建立文档的类别标签,在处理指定业务场景下的问答时,只需要对该类别下的知识库进行检索 ,如:

处理医疗领域的RAG问答业务时,可能会涉及很多的医学知识库,假设有1000篇文档,包括10个不同的医学研究方向。
当涉及的业务问题范畴只涉及外科相关知识时,可以通过类别标签进行筛选过滤,而无需从1000篇文档中进行全局检索。

第二种是从两个索引——一个是由该文档的摘要组成,一个由文档切片组成。在处理学术论文时,这类方法一般有较好的表现。这类方法的思路是分为两个步骤进行检索,首先通过摘要部分进行召回检索,过滤出可能和当前问题相关的候选文档集,然后再利用切片索引,从候选的文档集中进行检索,召回相关的知识切片。

2.2 假设性问题

首先解释一下什么是假设性问题: 假设性问题是一种提问方式,它基于一个或多个假设的情况或前提来提出问题。在对知识库中文档内容进行切片时,是可以 以该切片为假设条件,利用LLM预先设置几个候选的相关性问题的,也就是说,这几个候选的相关性问题是和切片的内容强相关的。

在构建切片索引时,可以将对应的假设性问题进行向量化,并同时存储于向量库中。在RAG中的召回步骤中,对这个问题向量索引进行查询搜索,然后路由到原始文本块, 并将它们作为上下文发送给LLM以获取答案。这种方法提高了搜索质量,因为 与原始文本块相比,查询和假设问题之间的语义相似性更高

除此以外,还有一种候选的假设性问题解决方案,和上述策略是对应的,它的核心思想可以表示为:

在要求LLM给定查询信息时, 用LLM预先生成一个假设的响应,然后将其向量和查询向量一起使用 ,进一步提升检索的质量。示例如下:

问题: 感冒了怎么办?
大模型预设响应: 感冒了需要多休息,喝水,必要时就医。
召回时用问题和大模型预设响应进行拼接,形成新的prompt内容进行召回。

2.3 拓展上下文

检索较小的块以获得更好的搜索质量,但将周围的上下文一起拼接起来传给LLM推理。当然在索引的时候就要把数据处理好,搜索的时候才能找到周围的上下文。这么做的好处,就是 小的文本块可以提升搜索的准确率,而扩展上下文则可以给LLM更丰富的内容 。这里有两种拓展上下文的方式可以作为参考:

一种处理的方案是,通过分割和存储小块数据来实现,在检索过程中,它首先获取小块,然后 查找这些块的父ID,并返回那些较大的文档 。这种方式在langchain里有进行封装。

from langchain.retrievers import ParentDocumentRetriever

from langchain.storage import InMemoryStore
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

# This text splitter is used to create the child documents
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)
# The vectorstore to use to index the child chunks
vectorstore = Chroma(
collection_name="full_documents", embedding_function=OpenAIEmbeddings()
)
# The storage layer for the parent documents
store = InMemoryStore()
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=store,
child_splitter=child_splitter,
)

第二种是对更大的块进行检索,有时,完整文档可能太大而无法按原样检索它们。在这种情况下,我们真正想做的是首先将原始文档分割成更大的块,然后将其分割成更小的块。然后我们索引较小的块,但在检索时我们检索较大的块(但仍然不是完整的文档)。

# This text splitter is used to create the parent documents
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)
# This text splitter is used to create the child documents
# It should create documents smaller than the parent
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)
# The vectorstore to use to index the child chunks
vectorstore = Chroma(
collection_name="split_parents", embedding_function=OpenAIEmbeddings()
)
# The storage layer for the parent documents
store = InMemoryStore()

retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=store,
child_splitter=child_splitter,
parent_splitter=parent_splitter,
)

retriever.add_documents(docs)

2.4 融合检索

将检索到的结果与不同的相似性分数适当地结合起来——这个问题通常是在倒排融合算法的帮助下解决的,该算法 将检索得到的结果重新排序以获得最终输出 。在LangChain中,这是在EnsembleRetriever类中实现的,它结合了您定义的检索器列表,例如faiss矢量索引和基于BM25的检索器,并使用RRF进行重新排序。混合或融合搜索通常会提供更好的检索结果,因为 两种互补的搜索算法结合在一起,同时考虑了查询和存储文档之间的语义相似性和关键字匹配

from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

doc_list_1 = [
"I like apples",
"I like oranges",
"Apples and oranges are fruits",
]

# initialize the bm25 retriever and faiss retriever
bm25_retriever = BM25Retriever.from_texts(
doc_list_1, metadatas=[{"source": 1}] * len(doc_list_1)
)
bm25_retriever.k = 2

doc_list_2 = [
"You like apples",
"You like oranges",
]

embedding = OpenAIEmbeddings()
faiss_vectorstore = FAISS.from_texts(
doc_list_2, embedding, metadatas=[{"source": 2}] * len(doc_list_2)
)
faiss_retriever = faiss_vectorstore.as_retriever(search_kwargs={"k": 2})

# initialize the ensemble retriever
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, faiss_retriever], weights=[0.5, 0.5]
)

docs = ensemble_retriever.invoke("apples")
docs

2.5 过滤与压缩

检索容易遇到的一个问题,当把数据向量化索引到数据库时,不太可能预想到对应的用户问题会是什么。这意味着,与查询最相关的信息可能隐藏在一个包含大量不相关文本的文档中,如果都一股脑传给LLM,不仅费用高(肉疼)而且效果可能也会受影响。

LLMChainFilter是一个简单的压缩器, 使用LLM链来决定最初检索到的文档中的哪些要过滤掉,哪些要返回 ,而无需所有文档内容。另外,EmbeddingsFilter通过嵌入文档和查询并只返回那些与查询具有足够相似嵌入的文档,提供了一个更便宜、更快的选项。

LLMChainFilter的用法:

from langchain.retrievers.document_compressors import LLMChainFilter

_filter = LLMChainFilter.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=_filter, base_retriever=retriever
)

compressed_docs = compression_retriever.get_relevant_documents(
"What did the president say about Ketanji Jackson Brown"
)
pretty_print_docs(compressed_docs)

EmbeddingsFilter的用法:

from langchain.retrievers.document_compressors import EmbeddingsFilter
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()
embeddings_filter = EmbeddingsFilter(embeddings=embeddings, similarity_threshold=0.76)






请到「今天看啥」查看全文