RAG系列
本文介绍了RAG以及RAG pipeline的整个流程,包括请求转换、路由和请求构造、索引和检索、生成和评估等,其中引用了大量有价值的论文。
参考Advanced RAG Series: Generation and Evaluation中的5篇文章,并丰富了相关内容。
请求转换
请求转换是为了提高查询结果的准确性而对用户请求进行重构、优化的过程。
为什么需要RAG?
-
问题1:LLMs并不了解你的数据,且无法获取与此相关的最新数据,它们是事先使用来自互联网的公共信息训练好的,因此并不是专有数据库的专家也不会针对该数据库进行更新。
-
问题2:上下文窗口-每个LLM都有一个tokens的最大限制(通常平均为100tokens,约75个单词),用于限制用户每次提交的tokens数据,这会导致丢失上下文窗口之外的上下文,进而影响准确性、产生检索问题和幻觉等。
-
问题3:中间遗失-即使LLMs可以一次性接收所有的数据,但它存在根据信息在文档中的位置来检索信息的问题。研究表明如果相关信息位于文档中间(而非开头或结尾时)时就会导致严重的性能降级。
因此,我们需要RAG。
请求转换
请求分解
由于用户问题可能太含糊、太特殊或缺少上下文,因此LLM可能无法很好地处理这些问题。通常会建议在将请求发送到嵌入模型之前对其进行重构。下面是一些重构方式:
-
重写-检索-读取 : 这种方式注重对用户的查询进行重构(而不仅仅是采用retriever或reader,左侧图示)。它使用一个LLM生成一个查询,然后使用web查询引擎来检索内容(中间图示)。此外还需要在pipeline中使用一个小型语言模型来进一步对齐结果(右侧图示)。
-
对问题进行浓缩或改写:通常用于会话中,通过把对话改写成一个好的独立问题来给聊天机器人提供上下文。Langchain的一个提示模板示例如下:
"Given the following conversation and a follow up question, rephrase the follow up \ question to be a standalone question. Chat History: {chat_history} Follow Up Input: {question} Standalone Question:"
-
RAG Fusion: 将RAG和倒数排名融合(RRF)结合。生成多个查询(从多个角度添加上下文),并使用倒数分数对查询结果重新排序,然后将文档和分数进行融合,从而得到更全面和准确的回答。
-
Step-Back Prompting:这是一种更加技术性的提示技术,通过LLM的抽象来衍生出更高层次的概念和首要原则。这是一个对用户问题的迭代过程,用于生成一个"后退一步"的问题(step back question),然后使用该问题对应的回答来生成最终的回答。
-
Query Expansion:这是一个通过为LLM提供一个查询并生成新的内容来扩展查询的过程。适用于文档检索,特别是Chain-of-Thought(CoT 思维链)提示。
使用生成的答案进行扩展:该方式中,让LLM基于我们的查询生成一个假设的回答,然后将该回答追加到我们的查询中,并执行嵌入搜索。通过使用假设的答案来扩展查询,这样在进行嵌入搜索时可以检索到其他相关向量空间,进而可以提供更准确的回答。
使用多个查询进行扩展 :使用LLM基于我们的查询来生成额外的类似查询,然后将这些额外的查询和原始查询一起传给向量数据库进行检索,从而可以大大提升准确性。注意需要通过对提示进行迭代来评估哪些提示会产生最佳结果。
伪文档(Psuedo documents)
伪文档嵌入 (HyDE):当向向量数据库询问问题时,该数据库有可能不会很好地标记相关性。因此更好的方式是先创建一个假设的回答,然后再查询匹配的向量。需要注意的是,虽然这比直接查询到答案的嵌入匹配要好,但对于高度不相关的问题和上下文,产生幻觉的可能性也会更高,因此需要对过程进行优化,并注意这些边缘情况。
作为RAG流程的第一个步骤,查询转换并不存在正确或错误的方式。这是一个带有实验性质的领域,只有通过构建才能知道哪些方式最适合你的使用场景。
路由和请求构造
路由
路由为了将请求发送到与与请求内容相关的存储。
由于环境中可能存在多个数据库和向量存储,而答案可能位于其中其中任何一个,因此需要对查询进行路由。基于用户查询和预定义的选择,LLM可以决定:
-
正确的数据源
-
需要执行的动作:例如,概括 vs 语义搜索
-
是否并行执行多个选择,并校对结果(多路由功能)
下面是一些路由请求的方式:
-
逻辑路由:在这种情况下,我们让LLM根据预定义的路径来决定参考知识库的哪个部分。这对于创建非确定性链非常有用,一个步骤的输出产生下一个步骤。该方式用于通过LLM来选择知识库。
下面是一个逻辑路由的例子,用于根据用户的编程语言来选择合适的数据源:
pythonfrom typing import Literal from langchain_core.prompts import ChatPromptTemplate from langchain_core.pydantic_v1 import BaseModel, Field from langchain_openai import ChatOpenAI # Data model class RouteQuery(BaseModel): """Route a user query to the most relevant datasource.""" datasource: Literal["python_docs", "js_docs", "golang_docs"] = Field( ..., description="Given a user question choose which datasource would be most relevant for answering their question", ) # LLM with function call llm = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0) structured_llm = llm.with_structured_output(RouteQuery) # 构造提示,根据用户问题中的编程语言类型类选择合适的数据源 system = """You are an expert at routing a user question to the appropriate data source. Based on the programming language the question is referring to, route it to the relevant data source.""" prompt = ChatPromptTemplate.from_messages( [ ("system", system), ("human", "{question}"), ] ) # 定义一个 router,通过给LLM(structured_llm)输入提示(prompt)来产生结果 router = prompt | structured_llm
下面是使用方式。用户给出
question
,然后调用router.invoke
让LLM找出合适的数据源,最终会在result.datasource
中返回python_docs
。pyquestion = """Why doesn't the following code work: from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_messages(["human", "speak in {language}"]) prompt.invoke("french") """ result = router.invoke({"question": question})
-
语义路由:使用基于上下文的提示来增强用户查询的强大方法。可以帮助LLMs快速、经济地选择路由(可以预定义或自定义选项),产生确定性的结果。该方式用于通过LLM选择提示。
下面是一个使用语义路由的例子,它嵌入了两个提示模板,分别用于处理物理和数学问题,然后通过匹配用户问题和提示模板的相似性程度来选择合适的提示模板,然后应用到LLM中。
pyfrom langchain.utils.math import cosine_similarity from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import PromptTemplate from langchain_core.runnables import RunnableLambda, RunnablePassthrough from langchain_openai import ChatOpenAI, OpenAIEmbeddings # 创建两个提示模板,一个用于解决物理问题,另一个用于解决数学问题 physics_template = """You are a very smart physics professor. \ You are great at answering questions about physics in a concise and easy to understand manner. \ When you don't know the answer to a question you admit that you don't know. Here is a question: {query}""" math_template = """You are a very good mathematician. You are great at answering math questions. \ You are so good because you are able to break down hard problems into their component parts, \ answer the component parts, and then put them together to answer the broader question. Here is a question: {query}""" # 嵌入提示 embeddings = OpenAIEmbeddings() prompt_templates = [physics_template, math_template] prompt_embeddings = embeddings.embed_documents(prompt_templates) # Route question to prompt def prompt_router(input): # 嵌入查询 query_embedding = embeddings.embed_query(input["query"]) # 计算查询和提示模板的相似度 similarity = cosine_similarity([query_embedding], prompt_embeddings)[0] most_similar = prompt_templates[similarity.argmax()] # 根据相似度来选择最合适的提示模板 print("Using MATH" if most_similar == math_template else "Using PHYSICS") return PromptTemplate.from_template(most_similar) chain = ( {"query": RunnablePassthrough()} | RunnableLambda(prompt_router) | ChatOpenAI() | StrOutputParser() ) # 用户提问,什么是黑洞 print(chain.invoke("What's a black hole"))
请求构造
请求构造是为了解决针对特定类型数据库查询的问题。
在定义好路由之后是否就可以跨数据存储发送请求?如果使用的是非结构化数据存储就可以,但实际中,大部分数据都保存在结构化数据库中,因此在构造请求的同时需要考虑到数据库的类型。
我们很容易会假设和LLMs交互使用的是自然语言,但这是不对的,查询采用的语言类型取决于和数据存储的交互方式,因此在构建查询时需要牢记数据库的查询语言(从使用SQL的关系型数据库到使用相关结构化元数据的非结构化数据)。
a. 自查询检索器-Self-query retriever (文本->元数据的过滤器):向量数据库中带有清晰元数据文件的非结构化数据可以启用此类retriever。任何用户问题(自然语言格式)都可以被拆分为一个查询和一个过滤器(如根据年、电影、艺术家)。通过提升发送到LLM的数据质量,可以大大提升Q&A的工作流表现。
b. 文本-> SQL: 通常LLMs对Text2SQL的表现不佳,去年有很多初创公司就将焦点放在如何解决此类问题上。从创建虚构的表和字段到用户查询中的拼写错误等原因都可能导致LLMs执行失败。鉴于这种数据库相当普遍,因此出现了很多帮助LLM准确创建SQL查询的方式,但都与特定的知识库相关,因此不存在可以用于多种数据库的通用方式。对于一个新的数据库,只能通过构建、迭代、修复来优化LLMs的表现。
- ++Create Table + Select 3++:在了解更多高级技术之前,这是获得模型在数据库上的表现基线的最直接的方法。在Prompt设计中,对于每张表,可以包括一个CREATE TABLE描述,并在一个SELECT语句中提供三个示例行。
-
++少量样本示例++:为LLM提供少量Q&A示例来帮助它理解如何构建请求,通过这种方式可以提升10~15%的准确性。根据示例的质量和使用的模型,添加更多的示例可以获得更高的准确性。在有大量示例的情况下,可以将其保存在向量存储中,然后通过对输入查询进行语义搜索,动态选择其中的一部分。
-
此外还有一篇不错的博客展示了fine-tuning带来的巨大提升:
-
++RAG + Fine-tuning++ :相比于只是为了让语言模型能够理解而将整个schema添加到提示中,使用经过调优的schema RAG和ICL的模型可以将准确性提高20%以上。
-
++用户拼写错误++:通过搜索合适的名词而不是使用包含正确拼写的向量存储,是减少用户特定错误的一种好方法。这在早期的Text2SQL中是一个特别令人烦恼的问题。
c. ++文本-> Cypher++:与图数据库有关,用于表达无法使用表格形式表示的关系。Cypher是这类数据库的查询语言。text-2-Cypher 是一项复杂的任务,对于此类任务,推荐使用GPT4。
知识图谱可以有效提高检索的准确性,因此数据格式不应该局限于表和2D结构。
下面是构造请求的例子:
Examples Data source References Text-to-metadata-filter Vectorstores Docs Text-to-SQL SQL DB Docs, blog, blog Text-to-SQL+ Semantic PGVecvtor supported SQL DB Cookbook Text-to-Cypher Graph databases Blog, Blog, Docs
请求转换、构造和路由可以帮助我们和请求的数据库进行交互。接下来,我们将进入称为索引(Indexing)的工作流程部分,在那里,我们将更深入地研究拆分、索引嵌入以及如何通过配置这些功能来实现准确检索。
索引
在LLM中,文档会以chunk进行切分,索引用于找到与查询相关的chunk
在上文中,我们讨论了在构造查询时需要考虑到与数据库交互所使用的语言。这里要讲的索引类似被查询的数据,索引的实现方式有很多种,但目的都是为了在不丢失上下文的情况下方便LLM的理解。由于对应用户问题的答案可能位于文档中的任何地方,且考虑到LLMs在实时数据、上下文窗口和"中间遗失"问题中的不足,因此有效划分chunk并将其添加到上下文中非常重要。
chunk划分和嵌入是实现准确检索的索引核心部分。简单来说,嵌入向量是将一个大而复杂的数据集转化为一组能够捕捉所嵌入的数据本质的数字。这样就可以将用户请求转化为一个嵌入向量(一组数字),然后基于语义相似性来检索信息。它们在高纬度空间的呈现如下(相似的词的距离相近):
回到chunk划分,为了方便理解,假设有一个大型文档,如电子书。你希望从这本书中解答问题,但由于它远超LLMs的上下文窗口,我们可能需要对其分块,然后将于用户问题相关的部分提供给LLM。而当执行分块时,我们不希望因为分块而丢失故事的角色上下文,此时就会用到索引。
下面是一些索引方式:
chunk优化
首先需要考虑数据本身的长度,它定义了chunk的划分策略以及使用的模型。例如,如果要嵌入一个句子,使用sentence transformer可能就足够了,但对于大型文档,可能需要根据tokens来划分chunk,如使用 text-embedding-ada-002。
其次需要考虑的是这些嵌入向量的最终使用场景。你需要创建一个Q&A机器人?一个摘要工具?还是作为一个代理工具,将其输出导入到其他LLM中进一步处理?如果是后者,可能需要限制传递到下一个LLM的上下文窗口的输出长度。
下面提供了几种实现策略:
基于规则
使用分隔符(如空格、标点符号等)来切分文本:
-
固定长度 :最简单的方式是根据固定数目的字符来划分chunk。在这种方式下,缓解上下文遗失的方式是在每个chunk中添加重叠的部分(可自定义)。但这种方式并不理想,可以使用langchain的CharacterTextSplitter进行测试:
然后是所谓的结构感知拆分器,即基于句子、段落等划分chunk。
-
NLTK语句分词器( Sentence Tokenizer):将给定的文本切分为语句。这种方式虽然简单,但仍然受限于对底层文本的语义理解,即便在一开始的测试中表项良好,但在上下文跨多个语句或段落的场景下仍然不理想(而这正是我们希望LLM查询的文本)。
-
Spacy语句分割器( Sentence Splitter**)**:另一种是基于语句进行拆分,在希望引用小型chunks时有用。但仍存在和NLTK类型的问题。
递归结构感知拆分(Recursive structure aware splitting)
结合固定长度和结构感知策略可以得到递归结构感知拆分策略。Langchain文档中大量采用了这种方式,其好处是可以更好地控制上下文。为了方便语义搜索,此时chunk大小不再相等,但仍然不适合结构化数据。
内容感知拆分
对于非结构化数据来说,上面几种策略可能就足够了,但对于结构化数据,就需要根据结构本身类型进行拆分。这就是为什么有专门用于Markdown、LaTeX、HTML、带表格的pdf、多模式(即文本+图像等)的文本拆分器。
多表示索引
相比于将整个文档进行拆分,然后根据语义相似性检索出 top-k的结果,那如果将文本转换为压缩的检索单元会怎样?例如,压缩为摘要。
父文档(Parent Document)
这种场景下可以根据用户的请求检索到最相关的chunk并传递该chunk所属的父文档,而不是仅仅将某个chunk传递给LLM。通过这种方式可以提升上下文并增强检索能力。但如果父文档比LLM的上下文窗口还要大怎么办?为了匹配上下文窗口,我们可以将较大的chunks和较小的chunks一起传递给LLM(而不是整个父文档)。
上面vectorstores中存储的是较小的 chunks,使用InMemoryStore存储较大的 chunk。
在使用时文档会被分为大小两种chunk,其中小chunk都有其所属的大chunk。由于"chunk越小,其表达的语义精确性更高",因此在查询时,首先检索到较小的chunk,而较小的chunk的元数据中保存了其所属的大chunk,因而可以将小chunk和其所属的大chunk一起传递给LLM。
密集检索(Dense X Retrieval)
密集检索是一种使用非语句或段落chunk进行上下文检索的新方法。在下面论文中,作者将其称之为"proposition",一个proposition包含:
- 文本中的不同含义:需要捕获这些含义,这样所有propositions一起就能在语义上覆盖整个文本。
- 最小单元:即不能再进一步拆分propositions。
- 上下文相关和自包含:即每个propositions应该包含所有必需的上下文。
Proposition级级别的检索比语句级别的检索和篇章级别的检索分别高35%和22.5%(显著提高)。
特定嵌入
领域特定和/或高级嵌入模型。
-
Fine-tuning:对嵌入模型进行微调可以帮助改进RAG pipeline检索相关文档的能力。这里,我们使用LLM生成的查询、文本语料库以及两者之间的交叉参考映射来磅数嵌入模型理解需要查找的语料库。微调嵌入模型可以帮助提升大约5~10%的表现。
下面是Jerry Liu对微调嵌入模型的建议:
- 在项目开始时,需要在嵌入文档前对基础模型进行微调
- 由于生产环境中的文档分布可能会发生变化,因此在微调查询适配器(query adapter)时需要确保嵌入的文档不会发生变化。
-
ColBERT:这是一个检索模型,它能够基于BERT在大规模数据集上实现可扩展的搜索(毫秒级别)。这里,快速和准确检索是关键。
它会将每个段落编码为token级嵌入的矩阵(如下蓝色所示)。当执行一个搜索时,它会将用户查询编码为另一个token级嵌入的矩阵(如下绿色所示)。然后基于上下文匹使用"可扩展的向量相似性(MaxSim)操作"来匹配查询段落。
后期交互是实现快速和可扩展检索的关键。虽然交叉编码器会对每个可能的查询和文档对进行评估,从而提高准确性,但对于大规模应用程序而言,该特性会导致计算成本不断累积。为了实现大型数据集的快速检索,需要提前计算文档嵌入,因而需要平衡计算成本和检索质量。
分层索引
斯坦福大学研究人员基于不同层次的文档摘要树提出了RAPTOR模型,即通过对文本块的聚类进行摘要来实现更准确的检索。文本摘要涵盖了更大范围的上下文,跨越不同的尺度,包括主题理解和细粒度的内容。检索增强的文本摘要涵盖了更大范围不同主题理解和粒度的上下文。
论文声称,通过递归摘要进行检索可以显著提升模型表现。"在涉及复杂多步骤推理的问答任务中,我们展示了最佳结果。例如,通过将RAPTOR检索与GPT-4相结合,我们可以在QuALITY基准测试的最佳表现上提高20%的绝对准确率。"
llamaindex提供了这种实现。
检索
检索可以看做是对索引到的数据的进一步提炼。
在完成数据检索之后,下一步需要根据用户的请求来获取相关数据。最常见和最直接的方法是从之前索引的数据(最近的邻居)中识别并获取与用户查询在语义上最接近的chunks。类似如下向量空间:
检索并不是一步到位的,而是从查询转换开始的一些列步骤,以及如何通过检索来提升获取到相关的chunks之后的检索质量。假设我们已经根据相似性搜索,从向量数据库中找到前k个chunks,但由于大部分chunks存在重复内容或无法适应LLM的上下文窗口。因此在将chunks传递给LLM之前,我们需要通过一些检索技术来提升上下文的质量。
LLMs的世界中,并不存在一劳永逸的方法,需要根据使用场景和chunks的特性来找到合适的技术。下面是一些典型的方法:
Ranking
Reranking
如果我们想要从数据库的chunk中查找答案,可以选择Reranking,它是一种可以给LLM提供最相关上下文的有效策略。有多种实现方式:
-
提升多样性:最常见的方法是最大边缘相关性(Maximum Marginal Relevance-MMR),这可以通过因子a)与请求的相似度或b)与已选的文档的距离等因子来实现。
这里有必要提一下Haystack的DiversityRanker:
- 首先计算每个文档的嵌入向量,然后使用一个sentence-transformer模型进行查询搜索
- 将语义上和查询最接近的文档作为第一个选择的文档
- 对于剩下的每个文档,会计算和所选择文档的平均相似性
- 然后选择和所选择文档最不相似的文档
- 然后重复上述步骤,直到选出所有文档,这样就得到了一个整体多样性从高到低排序的文档列表。
下面介绍了几种重排序的方式,称为reranker。
-
LostInTheMiddleReranker:这是Haystack的另一个解决LLM不擅长从文档中间检索信息的问题的方法。它通过重排序来让最佳的文档位于上下文窗口的开头和结尾。建议在相关性和多样性之后再使用该Reranker。
-
CohereRerank:通过Cohere的Rerank endpoint实现,它会将一开始的搜索结果和用户的请求进行比较,并基于请求文本和文档之间的语义相似性来重新计算结果,而不仅仅根据向量的查询结果。
-
++bge-rerank++:除了要选择出最适合你的数据的嵌入模型外,还需要重点关注可以和该嵌入模型配合使用的检索器(retriever)。我们使用命中率和平均倒数排名(Mean Reciprocal Rank-MRR)作为retriever的评估指标。命中率是指在前k个检索到的chunks中找到正确答案的频率,MRR是排名中最相关的文档在排名中的的位置。从下面可以看到,JinaAI-Base嵌入中用到的bge-rerank-large 或 cohere-reranker 看起来非常适用于该数据集。下表中需要注意的是,嵌入模型并不是最好的rerankers。Jina最擅长嵌入,而bge reranker则最擅长重新排序。
-
++mxbai-rerank-v1++:最新的一种重排序模型是由Mixedbread团队开发的开源项目,称为SOTA。其表现声称要优于Cohere和bge-large。
-
++RankGPT++:这是使用现有的LLMs(如GPT3.5)重排检索文档的方法之一,且重排质量好于Cohere。为了解决检索上下文大于LLM的上下文窗口的问题,该方法使用了一种"滑动窗口"策略,即在滑动窗口内实现渐进排名。这种方式击败了其他大部分reranker,其中包括Cohere。这种方式下需要注意的是延迟和成本,因此适用于优化小型开源模型。
提示压缩(Prompt Compression)
这里有必要介绍一下Prompt Compression,它和Reranking关系密切。这是一种通过压缩无关信息(即和用户查询无关)来降低检索到的文档的技术,有如下方式:
-
LongLLMLingua:该方式基于Selective-Context 和 LLMLingua 框架,是用于提示压缩的SOTA方法,针对成本和延迟进行了优化。除此之外,LongLLMLingua采用了一种"采用问题感知由粗到细的压缩方法、文档重新排序机制、动态压缩比例以及压缩后的子序列恢复等策略来提升大型语言模型对关键信息的感知能力 "的方式来提升检索质量。该方法特别适用于长上下文文档,解决"中间遗失"问题。
-
RECOMP:使用"压缩器",该方法使用文本摘要作为LLMs的上下文来降低成本并提升推理质量。两个压缩器分别表示: a)提取压缩器,用于从检索的文档中选择相关语句;b)抽象压缩器,用于根据多个文档的合成信息来创建摘要。
-
++Walking Down the Memory Maze++:该方法引入了MEMWALKER的概念,即按照树的格式来处理上下文。该方法会对上下文划分chunk,然后对每个部分进行概括。为了回答一个查询,该模型会沿着树结构来迭代提示,以此来找出包含问题答案的段。特别适用于较长序列。
RAG-fusion
这是 Adrian Raudaschl提出的一种方法,使用Reciprocal Rank Fusion (RRF)和生成查询来提高检索质量。该方法会使用一个LLM根据输入生成多个用户查询,并为每个查询运行一个向量搜索,最后根据RRF聚合并优化结果。在最后一步中,LLM会使用查询和重排序列表来生成最终输出。这种方法在响应查询时具有更好的深度,因此非常流行。
改进
CRAG (Corrective Retrieval Augmented Generation)
该方法旨在解决从静态和有限数据中进行次优检索的局限性,它使用一个轻量的检索评估器以及外部数据源(如web搜索)来补充最终的生成步骤。检索评估器会对检索的文档和输入进行评估,并给出一个置信度,用于触发下游的knowledge动作。
从下面报告结果可以看出其极大提升了基于RAG方法的表现:
FLARE (Forward Looking Active Retrieval)
该方法特别适用于长文本生成,它会生成一个临时的"下一个语句",并根据该语句是否包含低概率tokens来决定是否执行检索。该方法可以很好地决定什么时候进行检索,可以降低幻觉以及非事实的输出。
上图展示了FLARE的工作原理。当用户输入x
,并检索出结果Dx,FLARE会迭代生成一个临时的"下一句"(灰色字体),然后检查它是否包含低概率的tokens(下划线表示),如果包含(步骤2和3),则系统会检索相关的文档并重新生成语句。
考虑到需要优化的变量数目,检索可能比较棘手,但通过对用户场景和数据类型进行映射,可以大大降低成本、延迟并提升准确性。
下面将面对文本生成(Generation),并思考一些评估策略。
生成和评估
生成(generation)
使用索引和检索可以保证输出的完整性,接下来,需要在为用户生成结果之前对结果进行评估,并通过决策步骤来触发相应的动作。Language Agents (CoALA) 的认知架构是一个很好的框架,可以将其放在上下文中,通过对检索到的信息的评估来获得一组可能性。如下图所示:
有如下几种方法可以实现这一动作选择决策程序:
Corrective RAG (CRAG)
在上一章(检索)中有提到过该方法,但它与本章节内容有所重叠,因此有必要展开介绍一下。
CRAG使用轻量级"检索评估器"来增强generation,检索评估器会为每个检索到的文档生成一个置信值。该值确定了需要触发的检索动作。例如,评估器可以根据置信值来为检索到的文档标记到三个桶(正确、模糊、不正确)中的某个桶中。
如果所有检索到的文档的置信值都低于阈值,retriever会认为"不正确",然后会通过触发外部知识源(如web搜索)动作来生成合格的结果。
如果至少有一个检索到的文档的置信值大于阈值,则会假设此次检索是"正确的",然后会触发 knowledge refinement来改进检索到的文档。 knowledge refinement需要将文档拆分为"知识条(knowledge strips)",然后根据相关性对每个strip打分,最相关的知识会被重新组合为generation使用的内部知识。
当检索评估器对自己的判断没有信心时会标记为"模糊",此时会同时采用上述策略。见如下决策树:
上述方法跨了四个数据来生成最佳结果。下表中可以看到,CRAG显著优于RAG。self-CRAG则使这些差距更加明显,并且展示了CRAG作为RAG流水线"即插即用"的适应性。另外一个CRAG由于其他方法(如Self-RAG)的点是它可以灵活地替换底层LLM,如果未来可能要采用更加强大的LLM,这一点至关重要。
CRAG的一个明显限制是它严重依赖检索评估器的质量,并且容易受到网络搜索可能引入的偏见的影响。因此可能需要对检索评估器进行微调,且需要分配必要的"护栏"来确保输出的质量和准确性。
Self-RAG
这是另一种可以提升LLM的质量和事实性,同时保持其多功能性的框架。与其检索固定数目的段落(不管这些段落是相关还是不相关的),该框架则更关注按需检索和自我反省。
- 将任一LLM训练为可以对自己生成的结果进行自我反省的模型(通过一种称为reflection tokens(retrieval 和 critique) 的特殊tokens)。检索是由retrieval token触发的,该token是由LLM根据输入提示和之前的生成结果输出的。
- 然后LLM会通过并行处理这些检索到的段落来并发生成结果。该步骤会触发LLM生成用于评估这些结果的critique tokens。
- 最后根据"真实性"和"相关性"来选择用于最终generation的最佳结果。论文中描述了算法,其中tokens的定义如下:
下面Langchain的示意图展示了基于上述定义的reflection token的Self-RAG 推理决策算法。
就表现而言,无论是否进行检索,Self-RAG都显著优于基线(参考CRAG的Benchmark)。需要注意的是,可以通过在CRAG之上构建Self-CRAG来进一步提升模型表现。
该框架的成本和延迟受到LLM调用的数量的限制。可以通过一些变通方法,比如一次生成一代,而不是两次为每个块生成一代进行优化。
RRR (Rewrite-Retrieve-Read)
RRR 模式*[Ma et al., 2023a]* 引入了 重写-检索-读写 流程,利用LLM来强化rewriter模块,从而实现对检索查询的微调,进而提高reader的下游任务表现。
该框架假设用户查询可以被LLM进一步优化(及重写),从而实现更精确的检索。虽然这种方式可以通过LLMs的查询重写过程来提升表现,但也存在推理错误或无效查询之类的问题,因此可能不适用于部署在生产环境中。
为了解决上图中描述的问题,需要在rewriter上添加一个可训练的小型LLM(如上图红色部分所示)来持续提升训练的表现,使之兼具可扩展性和高效性。这里的训练包含两个阶段:"预热"和增强学习,其关键是将可训练模块集成到较大的LLM中。
评估
RAG pipeline中实现评估的方式有很多种,如使用一组Q&A作为测试数据集,并将输出与实际答案进行对比验证。但这种方式的缺点是不仅需要花费大量时间,并且增加了针对数据集本身优化pipeline的风险(而非可能存在很多边缘场景的现实世界)。
RAGAs
RAGAs(RAG Assessment的简称)是一个用于评估RAG pipeline的开源框架,它可以通过如下方式评估pipeline:
- 提供基于"ground truth"(真实数据)来生成测试数据的方式
- 为检索和generation阶段以独立或端到端的方式提供基于指标的评估
它使用如下RAG特征作为指标:
- ++真实性++:事实一致性
- ++回答相关性++:问题和查询的相关性
- ++上下文准确性++:校验相关chunks的排名是否更高
- ++批判(Critique)++:根据预定义的特征(例如无害和正确性)评估提交
- ++上下文回顾++:通过比较ground truth和上下文来校验是否检索到所有相关信息
- ++上下文实体回顾++:评估上下文检索到的和ground truth中的实体数目
- ++上下文相关性++:检索到的上下文和提示的相关性
- ++回答语义相似性++:生成的答案和实际的语义相似性
- ++回答正确性++:评估生成答案与实际答案的准确性和一致性
上面提供了一组可以全面评估RAG的列表,推荐阅读Ragas文档。
Langsmith
Langchain的Langsmith可以作为上面上下文中的可观测性和监控工具。
LangSmith 是一个可以帮助调试、测试、评估和监控基于任何LLM空间构建的chains和agents平台。
通过将Langsmith和RAG进行结合可以帮助我们更深入地了解结果。通过Langsmith评估logging和tracing部分可以更好地了解到对哪个阶段进行优化可以提升retriever或generator。
DeepEval
另一种值得提及的评估框架称为DeepEval。它提供了14种以上涵盖RAG和微调的指标,包括G-Eval、RAGAS、Summarization、Hallucination、Bias、Toxicity 等。
这些指标的自解释信很好地说明了指标评分的能力,使之更易于调试。这是跟上面的RAGAs的关键区别。
此外,它还有一些不错的特性,如集成Pytest (开发者友好),其模块化组件等也是开源的。
Evaluate → Iterate → Optimize
通过生成和评估阶段,我们不仅可以部署一个RAG系统,还可以评估、迭代并针对我们设计的特定场景进行优化。