人工智能学习

一份关于RAG的完整指南

本文主要是介绍一份关于RAG的完整指南,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

如果你还没从冰箱里听到关于RAG的消息,很快你就会听到很多人谈论这项技术(RAG)变得如此流行。让人意外的是,目前却很少有全面的指南来详细考虑所有方面(如相关性评估,对抗幻觉等),而是提供一些零散的信息。基于我们的经验,我整理了一份详尽的指南来全面覆盖这个话题。

图片来自 DALL-E 3 生成

那我们来说说为什么我们需要RAG吧?

你可以使用像ChatGPT这样的LLM模型来生成 h或者oscopes(例如),或者用于更实用的事情(比如工作)。但是,有一个问题:公司通常有大量的文档、规章制度等,而ChatGPT对此一无所知,显然。

能做些什么?

有两种选择:一是用你的数据重新训练模型,二是使用RAG系统。

重新培训既漫长又昂贵,而且极有可能行不通(别担心,这并不是因为你不是一个称职的父母;只是因为能做到的人很少,而且知道怎样做的人更少)。

第二个选项是 检索增强生成(也称为RAG)。基本思路很简单:使用一个优秀的现有模型(如OpenAI的模型),然后附加一个公司信息检索功能。这个模型对公司的情况仍然了解不多,但现在它有了可以查询的资料库。虽然这不如完全了解公司有效,但对于大多数任务来说已经足够用了。

这里是对RAG结构的基本介绍。

RAG结构图,作者绘制的示意图

The Retriever 是信息搜索系统的一部分,用于搜索与您的查询相关的信息(类似于您在自己的维基、公司文档或谷歌上搜索)。通常,会使用像 Qdrant 这样的向量数据库来存储公司所有索引的文档,但实际上可以使用任何东西。

生成器这个部分接收检索器找到的数据,并将这些信息结合、提炼,并提取重要部分,以便向用户提供答案。这一环节通常使用大型语言模型,比如OpenAI,来完成。它只是利用找到的所有或部分内容,进行理解,并给出答案。

这里有一个用Python和LangChain实现的最简单的RAG示例。

    import os  
    import wget  
    from langchain.vectorstores import Qdrant  
    from langchain.embeddings import OpenAIEmbeddings  
    from langchain import OpenAI  
    from langchain_community.document_loaders import BSHTMLLoader  
    from langchain.chains import RetrievalQA  

    #下载《战争与和平》这部作品  
    wget.download("http://az.lib.ru/t/tolstoj_lew_nikolaewich/text_0073.shtml")  

    #加载从html获取的文本  
    loader = BSHTMLLoader("text_0073.shtml", open_encoding='ISO-8859-1')  
    war_and_peace = loader.load()  

    #初始化向量数据库(Vector DB)  
    embeddings = OpenAIEmbeddings()  

    doc_store = Qdrant.from_documents(  
        war_and_peace,   
        embeddings,  
        location=":memory:",   
        collection_name="docs",  
    )  

    llm = OpenAI()  
    #输入问题  

    while True:  
        question = input('你的问题: ')  
        qa = RetrievalQA.from_chain_type(  
            llm=llm,  
            chain_type="stuff",  
            retriever=doc_store.as_retriever(),  
            return_source_documents=False,  
        )  

        result = qa(question)  
        print(f"答案: {result['result']}")

这听起来很简单,但有一个细微之处,

由于知识不是硬编码到模型中的,答案的质量很大程度上取决于Retriever找到的信息及其形式。这并不是一个简单的任务,因为公司在处理文件时通常会遇到混乱。文件和知识通常以松散的结构存储在不同的地方,形式多样,有时以图片、图表、手写笔记等形式存在,信息可能以各种形式出现。信息在一处与另一处可能相互矛盾,需要从这些混乱中找出头绪。

有些信息没有上下文根本无法理解,比如公司的缩写、首字母缩略词以及特定的名称和术语。

咋办?

这里就是各种搜索优化(也就是大家常说的技巧)发挥作用的地方。这些优化手段会在搜索过程中的各个阶段被应用。总的来说,搜索过程大致可以分为:

  • 处理和清理用户的问题
  • 从存储库搜索数据
  • 对从存储库获得的结果进行排序
  • 处理结果并将它们整合成答案
  • 评估回复
  • 调整格式、风格和语气

我们来仔细看看每一个阶段:

初步处理用户的问题

你绝对猜不到用户会提出什么样的问题。别说他们提问会有多合理了,可能以要求、陈述、投诉、威胁、一个字,甚至像《战争与和平》一样长的文章的形式出现。比如说:

作者供图

啥意思的“和”?

or

只有某些愤怒的 Medium 读者在评论中才可能达到这种程度。图片来自作者

输入需要被处理,转换成可以用来搜索信息的查询。为解决这个问题,我们需要一个从用户语言到通用语言的翻译器。谁能做到这一点呢?当然是一个LLM。大致上,它会是这样的:

猜得不错 😂 图片由作者提供

最简单的选项是——让LLM重新表述用户的请求。不过,这样可能对您的受众来说还不够。

然后稍微复杂一点的技术登场了——RAG融合技术

zh: RAG(融合)

想法是让大语言模型生成用户的几个问题版本,基于这些版本进行搜索,再将结果进行排名和结合。如Cross-Encoder这样的巧妙算法可以用于此过程。它运行较慢,但可以提供更相关的结果,因此不太适合用于信息检索,但对于对找到的结果列表进行排序来说,是非常合适的。

关于跨编码器和双编码器的备注

向量数据库使用双编码器模型来计算两个概念在向量空间中的相似度。这些模型经过训练可以将数据表示为向量形式,因此,在搜索期间,用户的查询也会被转化为向量,返回最接近查询的向量。然而,这种接近并不一定意味着找到了最佳答案。

  • BERT,即双向编码器表示来自变压器,这是一种基于变压器的模型,将文本编码成向量(嵌入)。

Cross-Encoder 的工作方式不同,。它接受两个对象(如文本或图像),并返回它们之间的相关性(相似度)。其准确性通常 要高于 Bi-Encoder。通常,会从向量数据库中返回比实际需要更多的结果(比如 30 个),然后使用 Cross-Encoder 或类似技术对它们进行排序,最终返回前 3 个结果。

作者的图像:Cross-Encoder

用户请求的预处理还包括请求的分类。例如,请求可以细分为问题、投诉、请求类等。请求还可以进一步分类为紧急、非紧急、垃圾信息或欺诈请求。还可以按部门(如会计、生产、人事)等分类。所有这些都有助于更快更高质量地响应。

对于分类,可以使用大型语言模型(LLM)或专门用来分类的神经网络。

在数据仓库里的数据搜索

所谓的检索器模块(RAG中的R,)负责检索。

通常,向量数据库充当来自各种来源的公司数据的索引库。不过,这并非强制性的,也可以使用其他系统,比如Elasticsearch,甚至可以用Google搜索。

在这里,我不会讨论非基于向量的搜索,因为这些搜索的原理都是一样的。

关于向量数据库的一点讨论

一种向量数据库(或向量存储,我交替使用这些术语,尽管在技术上它们并不完全相同)是一种优化了存储和处理向量(本质上是数字数组)的数据存储类型。这些向量用于表示复杂的对象,如图像、文本或声音,在向量空间中表示为向量,以用于机器学习和数据分析任务。在向量空间中,更准确地说,语义上相似的概念彼此靠近,无论它们的表示形式如何。例如,“狗”和“斗牛犬”会很接近,而“锁”(门锁)和“锁”(城堡)相距较远。因此,向量数据库非常适合进行语义数据搜索。

目前最流行的向量数据库:

  • QDrant — 开源数据库
  • Pinecone — 云原生数据库(即他们会向你收取很多费用)
  • Chroma — 另一个开源数据库(Apache-2.0 许可证)
  • Weaviate — 采用 BSD-3-Clause 许可证
  • Milvus — 采用 Apache-2.0 许可证
  • FAISS — 独特的一个,不是一个数据库而是一个框架,由 Meta 提供

此外,一些流行的非矢量数据库也开始支持矢量功能。

  • Pgvector 适用于 PostgreSQL:
  • Atlas 适用于 MongoDB:

为了改进结果,我们采用了几种主要的方法:

一组检索器和/或数据源 — 这是一个简单有效的想法,它涉及向多名专家提出同一个问题,然后以某种方式(比如简单取平均)汇总他们的答案——结果通常更好。从某种意义上说,这就像“人多力量大”。

举个例子,比如——使用来自Langchain的各种检索器。当结合比如稀疏检索器(如[BM25](https://python.langchain.com/docs/integrations/retrievers/bm25))和比如密集检索器(比如基于向量相似性工作的)时,这种组合特别有用,因为它们能够很好地互补。

密集检索器 — 通常使用如BERT这样的变压器,将查询和文档编码为多维空间中的向量。查询与文档之间的相似性通过这些向量在空间中的接近程度来衡量,常用余弦相似性来评估它们的接近度。这种技术是向量数据库的基础。这样的模型能更好地理解查询和文档的语义(即有意义的价值),从而产生更准确和相关的结果,特别是对于复杂查询。由于该模型在语义层面(即意义层面)运行,它能够很好地处理同义替换和语义相似的情况。

Sparse Retriever — 使用传统的信息检索方法,如 TF-IDF(词频-逆文档频率)或 BM25。这些方法创建稀疏表示,其中每个维度对应于预定义字典中的特定词项。文档与用户查询的相关性是根据查询中的词项(或者说单词)在文档中的存在和频率来计算的。它对于基于关键词的查询非常有效,当查询中的词项预计会直接出现在相关文档里时也是如此。它们虽然不如密集检索器那样准确,但它们更快,且在搜索和训练过程中需要的资源更少。

The EnsembleRetriever 然后使用诸如 Reciprocal Rank Fusion 等方法对结果进行排序和合并:

示例组合:

    !pip install rank_bm25  
    from langchain.retrievers import BM25Retriever, EnsembleRetriever  
    from langchain.vectorstores import Chroma  

    embedding = OpenAIEmbeddings()  
    doc_list = "/all_tolstoy_novels.txt"  # documents = "/all_tolstoy_novels.txt" commented out as not used  
    bm25_retriever = BM25Retriever.from_texts(doc_list)  
    bm25_retriever.k = 2  

    vectorstore = Chroma.from_texts(doc_list, embedding)  
    vectorstore_retriever = vectorstore.as_retriever(search_kwargs={"k": 2})  

    # 简单来说,这里我们创建了一个集成检索器  
    ensemble_retriever = EnsembleRetriever(retrievers=[bm25_retriever, vectorstore_retriever ], weights=[0.4, 0.6])  
    docs = ensemble_retriever.get_relevant_documents("War and Peace")

如何从这些策略中选择合适的?试试看,或者使用一个框架,比如https://github.com/Marker-Inc-Korea/AutoRAG。

另外,也可以将多个LLM结合起来,这也让结果更好了。参见“More agents is all you need。”

RELP(注:RELP为原文中的特定术语,此处保持原样。)

这是另一种数据检索方法,称为检索增强的语言模型预测(Retrieval Augmented Language Model Prediction,RALMP)。区别在于搜索步骤:在我们从向量存储中找到信息后,包括使用上述提到的技术,我们不是直接用LLM生成答案,而是生成示例答案(通过few-shot prompting)作为LLM的参考。通过这些示例,LLM能够高效地学习,并基于这些简短训练来回答相关问题。这种技术属于动态学习范畴,相比使用标准方法重新训练模型,成本更低。

RELP 流,作者提供

关于少样本提示的说明

有两种相似的大型语言模型提示技术:零样本法和少样本法。零样本法是指在没有提供任何示例的情况下,问你的大型语言模型一个问题。比如:

作者供图

Few-shot 是当你首先给 LLM 几个例子让它学习。这显著增加了得到相关且恰当形式的答案的几率。比如说:

作者供图

你看,不是所有的事情都一目了然,举个例子能帮助理解。

排名顺序、整合以及评价这些所得结果

我们已经部分讨论了这个话题作为RAG融合和检索器集成的一部分。当我们从(向量)存储中提取结果后,在将数据发送给LLM生成答案之前,我们需要对结果进行排序,并可能丢弃不相关的结果。呈现给LLM的搜索结果顺序很重要。LLM首先看到的内容将对最终结果产生更大的影响(详情请参阅此处)。

排名的方法有很多种,最常见的包括:

  1. 使用 Cross-Encoder (如上所述的)对得到的结果进行重新排序并去掉最不相关的(例如,从向量数据库中获取前30个(top k)结果,用Cross-Encoder对它们进行排序,最后选取前10个)。

已经有一些现成的解决方案可供选择,例如来自Cohere。

2. 互反排名融合。 RRF 的主要思想是将较高位置的元素赋予更大的重要性。在 RRF 中,每个元素的得分是根据其在各自搜索结果中的位置计算的。这通常使用公式 1/(k + 排名),其中 “排名” 是元素在特定搜索结果集中的位置,“k” 是一个常数(通常设置为 60 左右)。该公式为排名较高的元素提供了更高的分数。

每个元素在不同结果集中的得分被累加以获得最终分数。然后,这些元素进行排序。最终,形成一个综合结果清单。

RRF 特别有用,因为它不依赖于各个搜索引擎给出的绝对分数,这些分数在规模和分布上有很大不同。RRF 能够有效地整合不同系统的结果,突出显示那些在不同系统中都排名较高的元素。

3. 基于LLM的排名和评估:你可以放松一下,只需让一个LLM来给结果排名和评估就行了 🙂。OpenAI的最新版本在这方面做得很好。不过,这么用它们的成本不低。

对向量存储搜索结果的评估:

假设你做了重排序或者其他更改,怎么判断这些改动有没有好处?这些改动有没有提高相关性呢?总的来说,这个系统表现怎么样?这能帮助衡量你找到的信息质量。它帮助你了解找到的信息的相关性,从而做出进一步改进的决定。

你可以通过以下指标来评估查询结果的相关性:P@K,MAP@K,NDCG@K以及类似的指标。这些指标通常会给出一个介于0到1之间的分数,其中1表示最高准确度。这些指标的含义相似,但细节上有所区别。

P@K 表示 K 元素的精确度,即针对 K 个元素的准确度。假设针对兔子的查询,系统找到了 4 个相关文档

["野生兔子", "任务兔:现代工作网站", "胡萝卜理论", "我的兔子:沃尔特·艾萨克的回忆"]

因为沃尔特·伊萨克的简历或简历平台与兔子没有任何关系,这些职位都算0分,整体的准确率会这样计算。

这张图是作者做的

P@K 在 K=4,或 P@4 = 2 相关 / (2 相关 + 2 不相关) = 1/2 = 0.5。

不过,这并没有考虑到顺序。如果返回的列表是这样的:

["Task Rabbit: 现代任务平台", "我的兔子故事: 瓦尔特·艾萨克的回忆录", "野兔故事", "关于胡萝卜的论文"]

P@K 仍然是 0.5,但正如我们所知,相关和不相关结果的顺序很重要!(无论是对人还是将使用这些结果的大模型来说)。

因此,我们使用 AP@K 或平均精度在 K。想法非常简单:我们需要修改公式,使得顺序被考虑在内,列表末尾的相关结果的排名不应比列表开头的相关结果的排名对总体评分贡献更少。

这张图片由作者提供

或者以上为例:

AP@4 = (0 0 + 0 ½ + 1 ⅓ + 1 + 1 2/4) 乘以 0.2 = (⅓ + 2/4) / 2 = 0,41

这里有几个问题需要考虑:我们是如何评估个别元素的相关度来计算这些指标的?这是一个很好的侦探式问题,确实问得很好。

在RAG的背景下,我们经常让大型语言模型或其他类似的模型来评估。也就是说,我们询问这个大型语言模型,每个元素——我们从向量存储中找到的这份文档——是否和这个查询有关?

现在,第二个问题:仅仅这样问是否足够呢?答案是否定的,不是的。我们需要提出更具体的问题,让LLM根据某些参数来评估相关性。比如,针对上面的例子,问题可能包括:

这份文档是否与兔子这种动物有关?

这份文档里的兔子是真有还是比喻的?

等等之类。可以有很多问题(从两个到数百个),并且它们的数量和类型取决于你如何评估相关性。这就需要进行汇总,这正是。

MAP@K(K时的平均精度)这里指的是所有问题AP@K值的平均。

NDCG@K 就是归一化折损累计增益(在 K 位置的意思),这个我自己也不翻译了 🙂。你自己查查吧。

评估大型语言模型的响应

很多人可能不知道,但你可以让一个大语言模型(包括Llama和OpenAI)不仅返回token(文本),还可以返回token的概率分布。也就是说,你可以看看它对每个token的把握有多准,并查看——模型对自己构造的内容到底有多自信。如果这些概率比较低(什么程度算低取决于任务),这通常意味着模型开始胡扯了,对它的回答没什么把握。这样可以检查回答是否靠谱,如果不行就老实告诉用户“我不知道”。

用格式、风格和调子

最简单的部分 🙂。只需要让LLM按照某种格式来回答,并用特定的说话的语气。最好给模型一个例子,这样它会更好地遵循指令。例如,你可以这样设定语气,比如:

作者拍摄的图片

格式和风格设置可以在请求LLM生成最终答案这一步通过编程设定,比如:

    question = input('你的问题: ')  
    style = '最近用户变得超级嚣张。像贫民窟里的混混一样回答问题'  
    qa = RetrievalQA.from_chain_type(  
      llm=llm,  
      chain_type="stuff",  
      retriever=doc_store.as_retriever()  
    )  

    result = qa(style + question)  
    print(f"回答:{result}")
调整模型

有时你确实可能需要进一步的训练。是的,我之前说过你可能不会成功,但在某些情况下,这种做法也是合理的。如果你的公司使用缩写、名字/姓氏和模型不知道的特定术语,RAG的表现可能会不佳。例如,它在查找俄语姓氏(尤其是它们的形式变化)方面可能会遇到困难。在此情况下,使用LORA进行轻量级微调来训练模型可以帮助模型理解这些特定情况。你可以使用如https://github.com/bclavie/RAGatouille这样的框架。

这样的细微调整超出了这篇文章的范围,不过如果感兴趣的话,我会另写一篇文章来详细说明。

使用RAG这种技术的这样的系统

存在几种更高级的RAG选项。事实上,几乎每天都能看到新的变体涌现,创造者们认为这些变体变得越来越好。基于检索增强生成(RAG)的这些选项在不断进化。

不过,有一种变体格外显眼——FLARE(前瞻主动检索增强生成)。

这个想法非常有趣,这个想法基于一个原则:RAG 不应随意使用,而应该只有在 LLM 自身觉得需要时才使用。如果 LLM 能自信作答而无需使用 RAG,请继续。然而,如果它开始感到不确定,这时就需要查找更多的背景信息。这不应只做一次,而应根据需要多次进行。在响应过程中,当 LLM 感到需要更多数据时,它会执行 RAG 搜索。

在某种程度上,这就像人们平时一样,我们通常不知道自己不知道的东西,直到在寻找的过程中才发现。

这里我不详细讲;那是另一个话题的内容。

这是第一部分。第二部分在这里,关于高级RAG

简介

本文介绍了基于检索的生成技术(RAG)的一份全面指南。

下面是我们旅程的简短回顾:

需求及优势: 我首先讨论了RAG的需求及其重要性,以及它相较于使用自定义数据重新训练模型的优势。

RAG的结构: 然后,我解释了RAG的基本架构,并突出了Retriever和Generator这两个组件的功能。

实现: 我通过一个使用Python和LangChain的示例实现了说明。

处理用户查询: 我深入处理了用户查询,包括RAG融合技术和交叉编码器技术。

数据搜索技术: 然后,我探索了各种数据搜索技术,比如向量数据库技术、集成检索工具等。

排名、整合与评估: 排名、整合和评估检索结果的重要性,从而提高响应质量。

高级方法: 最后,我讨论了优化方法和技术,例如 RELP 和 FLARE,以及微调模型,保持响应的一致性,包括格式、风格和语气。

这篇关于一份关于RAG的完整指南的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!