? ? ? ?检索增强生成(Retrieval-Augmented Generation,又称RAG)通过检索LLMs之外的数据源来支持其生成答案。RAG=搜索+LLM提示,根据用户的查询要求,LLMs会使用搜索算法从外部数据源获取上下文信息,最后,查询和检索到的上下文合成后送入到LLM的提示中。
? ? ? ?RAG是2023年基于LLM的系统中最流行的体系结构。有许多产品几乎完全建立在RAG上——从将web搜索引擎与LLM结合起来的问答服务到数百个与数据聊天的应用程序。
? ? ? ?RAG也带动了向量搜索领域蓬勃发展。诞生了很多向量数据库初创公司,比如chroma、weavaite.io和pinecone,这些都是在现有开源搜索索引(主要是faiss和nmslib)的基础上建立起来的。
? ? ? ?受ChatGPT发布的启发,还有两个基于LLM的管道和应用程序的最著名开源库(LangChain(https://python.langchain.com/docs/get_started/introduction)和LlamaIndex(https://docs.llamaindex.ai/en/stable/))也得到了快速的发展,他们分别于2022年10月和11月成立,并在2023年获得了广泛使用。
? ? ? ?Vanilla RAG 使用最简单的方式大致如下所示:1)将文本分割成块;2)然后使用Transformer编码器模型将这些块编码成向量,并将这些所有向量存储到向量数据库中;3)最后创建一个LLM提示,并让模型根据搜索到的上下文来回答用户的查询。
? ? ? ?在执行交互时,使用相同的编码器模型对用户的查询进行向量化,然后执行向量索引,找到相关性最高的top-k个结果,从向量数据库中检索出这些索引对应的文本块,并将它们作为上下文提供给LLM Prompt。
? ? ? ?Prompt示例如下所示:
def question_answering(context, query):
prompt = f"""
Give the answer to the user query delimited by triple backticks ```{query}```\
using the information given in context delimited by triple backticks ```{context}```.\
If there is no relevant information in the provided context, try to answer yourself,
but tell user that you did not have any relevant context to base your answer on.
Be concise and output the answer of size less than 80 tokens.
"""
?
response = get_completion(instruction, prompt, model="gpt-3.5-turbo")
answer = response.choices[0].message["content"]
return answer
? ? ? ?提高RAG pipeline最经济的方式就是Prompt工程,可以参考OpenAI提示工程指南(https://platform.openai.com/docs/guides/prompt-engineering/strategy-write-clear-instructions)。
? ? ? ?现在我们将深入了解一下高级RAG技术的概述。下图描述了高级RAG的核心步骤和算法。为了保证方案的可读性,省略了一些逻辑循环和复杂的多步骤代理行为。
? ? ? ?方案中的绿色元素是需要进一步讨论的核心RAG技术,蓝色元素是文本。上述方案也并没有包括高级RAG的所有技术,例如,省略了各种上下文扩展方法——我们将在后面深入探讨。
? ? ? ?首先,我们要创建一个表示文档内容的向量索引,然后在运行时会从这些向量索引中搜索与查询向量最小余弦距离的向量。
2.1.1 Chunking
? ? ? ?Transformer模型输入序列长度一般是固定的,即使输入上下文窗口较大,一个句子或几个句子的向量也比几页文本上平均的向量能更好地表示其语义,因此,最好将原始文档分为若干大小的块,尽量不丢失其含义(将文本分为句子或段落,而不是将单个句子分为两部分)。
? ? ? ?分块的大小是一个超参数,它取决于使用的嵌入模型以及在token的容量。标准的transformer编码器模型(如基于BERT的sentence?transformer)最多可以使用512个tokens,OpenAI ada-002能够处理更长的序列,比如8191个tokens,但是,这里需要对LLM在足够的上下文中推理和有效地执行搜索进行权衡。在(https://www.pinecone.io/learn/chunking-strategies/)可以找到分块大小选择问题的研究。在LlamaIndex中,NodeParser类提供了一些高级选项,如定义自己的文本拆分器、元数据、节点/块关系等。
2.1.2 Vectorisation
? ? ? 接下来需要选择一个模型来嵌入分块,优先选择经过搜索优化的模型,比如bge-large(https://huggingface.co/BAAI/bge-large-en-v1.5)或E5 Embeddengs系列(https://huggingface.co/intfloat/multilingual-e5-large)。最新的模型可以查看MTEB排行榜(https://huggingface.co/spaces/mteb/leaderboard)。
? ? ? 对于分块和向量化步骤end2end的实现,可以查看LlamaIndex中完整的示例(https://docs.llamaindex.ai/en/latest/module_guides/loading/ingestion_pipeline/root.html#)。
2.2.1 Vector store index
? ? ? ?RAG管道的关键部分是搜索索引,它存储我们在上一步中获得的矢量化内容。最简单的实现是使用一个平面索引——暴力计算查询向量和所有块向量之间的距离。
? ? ? ?如果向量数量超过10000多个时,可以采用为高效检索而优化的向量索引,如faiss、nmslib或annoy,使用一些近似近邻实现,如clustring、trees或HNSW算法。当然还有一些托管解决方案,如OpenSearch或ElasticSearch和vector数据库(比如Pinecone、Weaviate或Chroma)。
? ? ? ?根据索引选择、数据和搜索需要,还可以将元数据与向量一起存储,然后使用元数据过滤器可以搜索例如:某些日期或源中的信息。
? ? ? LlamaIndex支持许多向量存储索引,但也支持其他更简单的索引实现,如列表索引、树索引和关键字表索引—我们将在融合检索部分讨论后者。
2.2.2 Hierarchical indices
? ? ? ?如果有许多文档要从中检索,需要能够有效地在其中搜索,找到相关信息,并在最终的一个答案中综合这些信息,并引用搜索来源。对于大型数据库,一种有效的方法是创建两个索引(一个由摘要组成,另一个由文档块组成),并分两步进行搜索,首先通过摘要过滤出相关文档,然后在该相关组中进行搜索。
2.2.3 Hypothetical Questions and HyDE
? ? ? ?另一种方法是要求LLM为每个分块生成一个问题并将这些问题嵌入向量中,在运行时对问题向量的索引执行查询搜索(将分块向量替换为索引中的问题向量),然后在检索后路由到原始文本区块并将其作为上下文发送给LLM以获得答案。这种方法提高了搜索质量,因为查询和假设问题之间的语义相似度比实际块更高。
? ? ? 还有一种称为HyDE的反向逻辑方法—您要求LLM生成给定查询的假设响应,然后使用其向量和查询向量来提高搜索质量。
2.2.4 Context enrichment
? ? ? 这里的概念是检索更小的块以获得更好的搜索质量,但要将周围的上下文相加以供LLM推理。
? ? ? 有两种选择—通过围绕较小检索块的语句扩展上下文,或者递归地将文档拆分为若干较大的父块(包含较小的子块)。
a)?Sentence Window Retrieval
? ? ? 在该方案中,文档中的每个句子都被单独嵌入,这为上下文余弦距离搜索提供了很高的查询精度。
? ? ? 为了在获取最相关的单个句子后更好地对所发现的上下文进行推理,我们在检索到的句子前后对上下文窗口进行k个句子的扩展,然后将扩展后的上下文发送给LLM。
? ? ? 绿色部分是在索引中搜索时发现的嵌入句子,整个黑色+绿色段落喂给LLM,以扩大其上下文,同时对提供的查询进行推理
b)?Auto-merging Retriever (aka Parent Document Retriever)
? ? ? ?这里的想法非常类似于句子窗口检索器——搜索更细粒度的信息片段,然后在将所述上下文提供给LLM进行推理之前扩展上下文窗口。文档被分割成更小的子块,引用更大的父块。
? ? ? 首先在检索过程中获取较小的块,然后如果前k个检索到的块中有n个以上的块链接到同一父节点(较大的块),我们将替换由该父节点提供给LLM的上下文-工作方式类似于将几个检索到的块自动合并到较大的父块中,从而得到方法名称。只需注意-搜索仅在子节点索引中执行。关于递归检索器+节点引用,可以参考LlamaIndex教程(https://docs.llamaindex.ai/en/stable/examples/retrievers/recursive_retriever_nodes.html)。
2.2.5 Fusion retrieval or hybrid search
? ? ? 一个相对传统的想法是,可以从两个世界中取其精华-基于关键字的老式搜索-稀疏检索算法,如tf-idf或搜索行业标准BM25-和现代语义或向量搜索,并将其组合到一个检索结果。
? ? ? 这里唯一的技巧是将检索到的结果与不同的相似性分数正确地结合起来——这个问题通常通过使用?Reciprocal Rank Fusion算法来解决,将检索到的结果重新排序以获得最终输出。
? ? ? ? 在LangChain中,这是在Ensemble Retriever类(https://python.langchain.com/docs/modules/data_connection/retrievers/ensemble)中实现的,将用户定义的检索器列表(例如faiss向量索引和基于BM25的检索器)结合起来,并使用RRF重新排序。
? ? ? ?在LlamaIndex中,也是以非常类似的方式完成的,请参考:https://docs.llamaindex.ai/en/stable/examples/retrievers/reciprocal_rerank_fusion.html。
? ? ? ?混合或融合搜索通常通过两种互补的搜索算法相结合,同时考虑查询与存储文档之间的语义相似度和关键字匹配,从而提供更好的检索结果。
? ? ? ?根据上面章节,我们得到了检索结果,但结果可能是错误的或者是冗余的。本小节我们继续介绍一些后处理操作,比如过滤,重新排序或一些转换。在LlamaIndex中,有各种可用的后处理器(https://docs.llamaindex.ai/en/stable/module_guides/querying/node_postprocessors/root.html),根据相似度得分、关键字、元数据过滤出结果,或者使用其他模型对其重新排序,比如LLM,sentence-transformer cross-encoder,重排序端点或者基于元数据,比如日期。常见的方法基本上都可以。
? ? ? ?这是将检索到的上下文提供给LLM以获得结果答案之前的最后一步。
? ? ? ?现在是时候使用更复杂的RAG技术了,比如查询转换和路由,这两种技术都涉及LLM,因此代表了代理行为——在RAG管道中涉及LLM推理的一些复杂逻辑。
? ? ? 查询转换是一系列使用LLM作为推理引擎修改用户输入以提高检索质量的技术。
如果查询很复杂,LLM可以将其分解为多个子查询。例如,如果您询问:
-“What framework has more stars on Github, Langchain or LlamaIndex?”,
而且,我们不太可能在语料库中的某些文本中找到直接比较,因此将此问题分解为两个子查询是有意义的,前提是信息检索更简单、更具体:
-“How many stars does Langchain have on Github?”
-“How many stars does Llamaindex have on Github?”
? ? ? ?它们将并行执行,然后检索到的上下文将组合在一个单独的提示中,供LLM合成初始查询的最终答案。这两个库都实现了这个功能——在Langchain中?使用Multi Query Retriever(https://python.langchain.com/docs/modules/data_connection/retrievers/MultiQueryRetriever?ref=blog.langchain.dev),在Llamaindex中使用Sub Question Query Engine(https://docs.llamaindex.ai/en/stable/examples/query_engine/sub_question_query_engine.html)。
Step-back prompting:使用LLM生成一个更通用的查询,检索时我们会获得一个更通用或更高级的上下文,该上下文有助于确定原始查询的答案。原始查询的检索被优化了,并将优化前后的两个查询上下文都提供给LLM来生成最终的答案。下面是一个LangChain实现(https://github.com/langchain-ai/langchain/blob/master/cookbook/stepback-qa.ipynb?ref=blog.langchain.dev)。
使用LLM对初始查询进行重写以改进检索。LangChain(https://github.com/langchain-ai/langchain/blob/master/cookbook/rewrite.ipynb?ref=blog.langchain.dev)和LlamaIndex(https://llamahub.ai/l/llama_packs-fusion_retriever-query_rewrite)都有实现,但有点不同,发现LlamaIndex中似乎更强大。
? ? ? ?有时间,我们不仅仅要求RAG完成一个任务,也需要进行多轮对话聊天,它与经典的聊天机器人一样可以考虑对话上下文。这需要跟踪会话、回指或记录历史聊天记录。该方法采用查询压缩技术,结合聊天环境和用户查询,解决了该问题。
? ? ? 通常,有几种方法可以实现上述上下文压缩-一种流行且相对简单的ContextChatEngine(https://docs.llamaindex.ai/en/stable/examples/chat_engine/chat_engine_context.html),首先检索与用户查询相关的上下文,然后将其与缓存中的聊天历史一起输入给LLM,以便LLM在生成下一个答案的同时考虑历史聊天记录。
? ? ? 更复杂一点的例子是CondensePlusContextMode(https://docs.llamaindex.ai/en/stable/examples/chat_engine/chat_engine_condense_plus_context.html)——在每个交互中,聊天历史和最后一条消息被压缩成一个新的查询,然后将这个查询建立索引再进行检索,检索到的上下文与原始用户消息一起传递给LLM以生成答案。
? ? ? ?需要注意的是,LlamaIndex中还支持基于OpenAI代理的聊天引擎(https://docs.llamaindex.ai/en/stable/examples/chat_engine/chat_engine_openai.html),提供了更灵活的聊天模式,Langchain也支持OpenAI函数API(https://python.langchain.com/docs/modules/agents/agent_types/openai_multi_functions_agent)。
? ? ? 还有其他聊天引擎类型,比如ReAct Agent(https://docs.llamaindex.ai/en/stable/examples/chat_engine/chat_engine_react.html),我们将在第7节中介绍Agent。
? ? ? ?查询路由是LLM支持的决策步骤,决定在给定用户查询的情况下接下来要做什么——通常是汇总、对某些数据索引执行搜索或尝试多个不同的路由,然后在单个答案中综合它们的输出。
? ? ? ?查询路由器可以选择一个索引或者数据存储来发送用户的查询。或者有多个数据源,例如,经典向量存储和图形数据库或关系数据库,或者您有一个索引层次结构—对于多文档存储来说,一个非常经典的例子是摘要索引和另一个文档块向量索引。
? ? ? ?定义查询路由器包括设置它可以做出的选择。通过LLM调用执行路由选项的选择,以预定义格式返回其结果,将查询路由到给定的索引,或者,如果我们采用不相关行为,则路由到子链或甚至其他代理,如下面的多文档代理方案所示。
LlamaIndex(https://docs.llamaindex.ai/en/stable/module_guides/querying/router/root.html)和LangChain(https://python.langchain.com/docs/expression_language/how_to/routing?ref=blog.langchain.dev)都支持查询路由器。
? ? ? 代理(由Langchain和LlamaIndex支持)几乎自第一个LLM API发布以来就一直存在——其思想是提供一个LLM,能够推理,具有一组工具和要完成的任务。这些工具可能包括一些确定性函数,比如任何代码函数、外部API甚至其他代理——这种LLM链接思想就是LangChain得名的地方。
? ? ? ?代理本身是一个庞大的东西,在RAG概述中不可能对这个主题进行足够深入的研究,因此将继续介绍基于代理的多文档检索案例,在OpenAI助手站稍作停留,因为这是一个相对较新的东西,在最近的OpenAI 的 dev conference as GPTs中介绍(https://openai.com/blog/new-models-and-developer-products-announced-at-devday)的。
? ? ? ?OpenAI助手(https://platform.openai.com/docs/assistants/overview)基本上已经实现了我们以前在开源中使用的LLM所需的许多工具,比如聊天历史、知识存储、文档上传接口,以及最重要的函数调用API。后者提供了将自然语言转换为对外部工具或数据库查询的API调用的功能。
? ? ? 在LlamaIndex中,有一个OpenAIgent类(https://docs.llamaindex.ai/en/stable/examples/agent/openai_agent.html)将这种高级逻辑与ChatEngine和QueryEngine类相结合,提供基于知识和上下文感知的聊天,以及在一次会话中调用多个OpenAI函数的能力,这真正带来了智能代理行为。
? ? ? ?让我们看一看多文档代理方案(https://docs.llamaindex.ai/en/stable/examples/agent/multi_document_agents.html)——一个非常复杂的设置,包括在每个文档上初始化一个代理(OpenAIAgent(https://docs.llamaindex.ai/en/stable/examples/agent/openai_agent.html)),能够进行文档摘要和经典的QA机制,以及一个顶级代理,负责将查询路由到文档代理并进行最终答案合成。
? ? ? ?每个文档代理都有两个工具—向量存储索引和摘要索引,并根据路由查询决定使用哪个工具。对于顶级代理,所有文档代理都是工具。
? ? ? ?该方案展示了一种先进的RAG体系结构,每个代理都会做出大量的路由决策。这种方法的好处是能够比较不同的解决方案或实体,在不同的文档及其摘要中进行描述,以及经典的单文档摘要和QA机制—这基本上涵盖了与文档用例集合最频繁的聊天。
? ? ? ?这样一个复杂方案的缺点可以从图中猜测出来——由于在代理中使用LLM进行多次来回迭代,所以有点慢。以防万一,LLM调用始终是RAG管道中最长的操作-设计优化了搜索速度。因此,对于大型多文档存储,建议考虑对该方案进行一些简化,使其具有可扩展性。
? ? ? ?这是任何RAG管道的最后一步——根据检索的所有上下文和初始用户查询生成答案。
? ? ? ?最简单的方法是将所有获取的上下文(高于某个相关阈值)与查询一起串联并同时提供给LLM。
? ? ? ?但是,还有其他更复杂的选项涉及多个LLM调用,以优化检索到的上下文并生成更好的答案。
响应综合的主要方法有:
1、通过逐块向LLM发送检索到的上下文,迭代地细化答案;
2、总结检索到的上下文以适应提示;
3、根据不同的上下文块生成多个答案,并将其串联或汇总。
有关更多详细信息,请查看响应合成器模块文档(https://docs.llamaindex.ai/en/stable/module_guides/querying/response_synthesizers/root.html)。
? ? ? ?前面两章介绍了直接使用现有模型来完成RAG,那么有时候底座模型的性能未必会满足需求,这时可以考虑对其进行微调。RAG管道中主要涉及到两个模型,分别是负责嵌入质量和上下文检索质量的Transformer编码器,另一个是负责最好地使用所提供的上下文来回答用户查询的LLM——幸运的是,后者是一个很好的少量学习者。
? ? ? 现在的一大优势是可以使用GPT-4等高端LLM生成高质量的合成数据集。
? ? ? ?需要注意的是:使用专业研究团队训练开源模型收集、清理和验证的大型数据集,并使用小型合成数据集进行快速微调,可能会降低模型的通用能力。
? ? ? ?在LlamaIndex notebook(https://docs.llamaindex.ai/en/stable/examples/finetuning/embeddings/finetune_embedding.html)中测试了bge-large-en-v1.5(撰写本文时MTEB排行榜的前4名)的微调所带来的性能提升,结果显示检索质量提高了2%。
? ? ? ?另一个很好的选择是,如果您不完全信任基本编码器,那么可以使用交叉编码器对检索到的结果进行重新排序。它的工作方式如下:将查询和前k个检索到的文本块传递给交叉编码器,并用SEP token分隔,然后对其进行微调,将相关块输出1,将不相关块输出0。这里有个例子(https://docs.llamaindex.ai/en/latest/examples/finetuning/cross_encoder_finetuning/cross_encoder_finetuning.html#),结果显示,对交叉编码器微调,评分提高了4%。
? ? ? ?最近OpenAI开始提供LLM FineTunning API(https://platform.openai.com/docs/guides/fine-tuning),LlamaIndex有一个关于在RAG设置中微调GPT-3.5-turbo的教程(https://docs.llamaindex.ai/en/stable/examples/finetuning/openai_fine_tuning.html),可以“蒸馏”一些GPT-4知识。这里的想法是获取一个文档,使用GPT-3.5-turbo生成许多问题,然后使用GPT-4根据文档内容生成这些问题的答案(构建GPT4支持的RAG管道),然后在该问答对数据集上微调GPT-3.5-turbo。用于RAG管道评估的ragas框架(https://docs.ragas.io/en/latest/index.html)显示忠实性度量增加了5%,这意味着经过微调的GPT 3.5-turbo模型比原始模型更好地利用了提供的上下文来生成其答案。
? ? ? 最近Meta AI的一篇论文《RA-DIT: Retrieval Augmented Dual Instruction Tuning》提出了一种在查询、上下文和答案的三元组上同时微调LLM和检索器的方法,原论文采用一个双编码器结构,实现细节可以参考:https://docs.llamaindex.ai/en/stable/examples/finetuning/knowledge/finetune_retrieval_aug.html#fine-tuning-with-retrieval-augmentation。该技术用于通过微调API和Llama2开源模型来微调OpenAI LLMs,从而使知识密集型任务度量增加了约5%(相比于使用RAG的Llama2 65B),并使常识推理任务增加了两个百分点。
? ? ? ?有几个RAG系统性能评估框架,他们思路类似,都采用以下指标来进行评估:比如总体答案相关性、答案有根据性、可信度和检索到的上下文相关性。
? ? ? ?如前一节所述,Ragas使用信度和答案相关性作为生成的答案质量度量,并使用经典上下文精度P和召回率R作为RAG方案的检索部分。
? ? ? ?在Andrew NG、LlamaIndex和评估框架Truelens(https://github.com/truera/trulens/tree/main)最近发布的一个大型短期课程“构建和评估高级RAG(https://learn.deeplearning.ai/building-evaluating-advanced-rag/)”中,他们提出了RAG三元组——检索到的与查询相关的上下文、有根据性(所提供的上下文支持LLM答案的多少)和与查询相关的答案。
? ? ? ?关键且最可控的度量是检索到的上下文相关性——基本上,上面描述的高级RAG管道的第1–7部分以及编码器和Ranker微调部分旨在改进此度量,而第8部分和LLM微调则侧重于答案相关性和基础性。
? ? ? ?这里(https://github.com/run-llama/finetune-embedding/blob/main/evaluate.ipynb)可以找到一个非常简单的检索器评估Pipeline的例子,并将其应用于编码器微调部分。OpenAI cookbook(https://github.com/openai/openai-cookbook/blob/main/examples/evaluation/Evaluate_RAG_with_LlamaIndex.ipynb)中展示了一种更高级的方法,该方法不仅考虑了命中率,而且还考虑了平均倒数秩(一种常见的搜索引擎度量)以及生成的答案度量(如忠实度和相关性)。
? ? ? ?LangChain有一个非常高级的评估框架LangSmith(https://docs.smith.langchain.com/),其中可以实现定制的评估器,并监视RAG管道中的运行状况,以使系统更加透明。
? ? ? ? ?在LlamaIndex中,有一个rag_evaluator llama pack包(https://github.com/run-llama/llama-hub/tree/dac193254456df699b4c73dd98cdbab3d1dc89b0/llama_hub/llama_packs/rag_evaluator),它提供了一个快速工具,可以使用公共数据集评估管道。
? ? ? ?试图勾勒出RAG的核心算法方法,并举例说明其中的一些方法,希望这可能会激发一些新的想法,尝试在RAG管道中,或将一些系统引入到今年发明的各种各样的技术中——对我来说,2023年是ML迄今为止最激动人心的一年。
? ? ?还有很多其他的东西需要考虑,比如基于web搜索的RAG(由LlamaIndex、webLangChain等开发的RAG),深入研究代理架构(以及最近在游戏中的OpenAI stake),以及一些关于LLMs长期记忆的想法。
? ? ? ?对于RAG系统来说,除了答案的相关性和忠实性之外,主要的生产挑战是速度,特别是当使用更灵活的基于代理的方案时。ChatGPT和大多数其他助手使用的这种流式功能不是一种随机的cyberpunk风格,而只是一种缩短感知答案生成时间的方法。这就是为什么我看到了一个非常光明的未来,较小的LLM和最近发布的Mixtral和Phi-2是引导我们在这个方向前进。
[1]?https://pub.towardsai.net/advanced-rag-techniques-an-illustrated-overview-04d193d8fec6?source=email-c63e4493b83d-1703617670645-digest.reader-98111c9905da-04d193d8fec6----0-98------------------585a2b75_5c50_49b0_a73f_79ae0baec16e-1