RAG:如何与您的数据对话

ChatGPT 进行主题建模,我们的任务是分析客户对不同连锁酒店的评论,并确定每家酒店提到的主要主题。通过这种主题建模,我们知道每个客户评论的主题,并且可以轻松地过滤它们并进行更深入的研究。然而,在现实生活中,不可能有如此详尽的主题集来涵盖所有可能的用例。

例如,以下是我们之前从客户反馈中确定的主题列表。

这些主题可以帮助我们全面了解客户反馈并进行初步预过滤。但假设我们想了解顾客对健身房或早餐饮料的看法。在这种情况下,我们需要自己从“酒店设施”和“早餐”主题中查看相当多的客户反馈。

幸运的是,LLM可以帮助我们进行这种分析,并节省大量时间来浏览客户的评论(尽管亲自聆听客户的声音可能仍然会有所帮助)。在本文中,我们将讨论此类方法。

我们将继续使用LangChain(LLM应用程序最流行的框架之一)。

1.原始的方法

获取与特定主题相关的评论的最直接方法就是在文本中查找某些特定单词,例如“gym”或“drink”。在不存在 ChatGPT时,我已经多次使用这种方法来做。

这种方法的问题非常明显:

l您可能会收到很多关于附近的健身房或酒店餐厅的酒精饮料的不相关评论。此类过滤器不够具体,无法考虑上下文,因此会出现很多误报。

l另一方面,您可能也没有足够好的覆盖范围。人们倾向于对相同的事物使用略有不同的词语(例如饮料、茶点、饮料、果汁等)。可能有错别字。如果您的客户使用不同的语言,这项任务可能会变得更加复杂。

因此,这种方法在精确度和召回率上都存在问题。它会让你对问题有一个粗略的理解,但它的能力是有限的。

另一个潜在的解决方案是使用与主题建模相同的方法:将所有客户评论发送给 LLM 并要求模型定义它们是否与我们感兴趣的主题(早餐或健身房的饮料)相关。我们甚至可以要求模型总结所有客户反馈并提供结论。

这种方法可能会很有效。然而,它也有其局限性:每次您想深入研究某个特定主题时,您都需要将所有文件发送给LLM。即使根据我们定义的主题进行高级过滤,传递给 LLM 的数据也可能相当多,而且成本相当高。

幸运的是,还有另一种方法可以解决此任务,它称为 RAG。

2.检索增强生成

我们有一组文档(客户评论),我们想提出与这些文档内容相关的问题(例如,“客户喜欢早餐的哪些方面?”)。正如我们之前讨论的,我们不想将所有客户评论发送给 LLM,因此我们需要有一种方法来仅定义最相关的评论。然后,任务将非常简单:将用户问题和这些文档作为上下文传递给 LLM,仅此而已。

这种方法称为检索增强生成或 RAG。

RAG 的管道由以下阶段组成:

  1. 从我们拥有的数据源加载文档。
  2. 将文档分割成易于进一步使用的块。
  3. 存储:向量存储通常用于此用例以有效地处理数据。
  4. 检索与问题相关的文档。
  5. Generation是将问题和相关文件传递给LLM并得到最终答案

您可能听说过 OpenAI本周推出了Assistant API ,它可以为您完成所有这些步骤。然而,我相信有必要经历整个过程来了解它的工作原理和特点。

那么,让我们逐步完成所有这些阶段。

2.1.加载文件

第一步是加载我们的文档。LangChain支持不同的文档类型,例如CSVJSON

您可能想知道对于这样的基本数据类型使用 LangChain 有什么好处。不用说,您可以使用标准 Python 库解析 CSV 或 JSON 文件。但是,我建议使用 LangChain 数据加载器 API,因为它返回包含内容和元数据的 Document 对象。以后您使用LangChain Documents会更加方便。

让我们看一些更复杂的数据类型示例。

我们经常有分析网页内容的任务,所以我们必须使用HTML。即使您已经掌握了BeautifulSoup库,您也可能会发现BSHTMLLoader很有帮助。

与 LLM 应用相关的 HTML 的有趣之处在于,您很可能需要对其进行大量预处理。如果您使用浏览器检查器查看任何网站,您会发现文本比您在网站上看到的多得多。它用于指定布局、格式、样式等。

在大多数现实生活中,我们不需要将所有这些数据传递给 LLM。站点的整个 HTML 很容易超过 200K 个token(其中只有大约 10-20% 是您作为用户看到的文本),因此将其适应上下文大小将是一项挑战。更重要的是,这些技术信息可能会让模型的工作变得更加困难。

因此,从 HTML 中仅提取文本并将其用于进一步分析是相当标准的。为此,您可以使用以下命令。结果,您将获得一个 Document 对象,其中page_content参数中包含网页文本。

from langchain.document_loaders import BSHTMLLoader
 = BSHTMLLoader( "my_site.html" )
data = loader.load()

另一种常用的数据类型是 PDF。例如,我们可以使用 PyPDF 库解析 PDF。让我们从 DALL-E 3 纸张加载文本。

from langchain.document_loaders import PyPDFLoader          
loader = PyPDFLoader("https://cdn.openai.com/papers/DALL_E_3_System_Card.pdf")          
doc = loader.load()

在输出中,您将获得一组文档 - 每页一个。在元数据中,source和page字段都将被填充。

因此,正如您所看到的,LangChain 允许您处理广泛的不同文档类型。

让我们回到最初的任务。在我们的数据集中,我们有一个单独的 .txt 文件,其中包含每家酒店的客户评论。我们需要解析目录中的所有文件并将它们放在一起。我们可以用DirectoryLoader来加载。

from langchain.document_loaders import TextLoader, DirectoryLoader          
         
text_loader_kwargs={'autodetect_encoding': True}          
loader = DirectoryLoader('./hotels/london', show_progress=True,          
    loader_cls=TextLoader, loader_kwargs=text_loader_kwargs)          
         
docs = loader.load()          
len(docs)          
82

我也使用了’autodetect_encoding’: True设置,因为我们的文本不是用标准 UTF-8 编码的。

结果,我们得到了文档列表——每个文本文件一个文档。我们知道每个文档都包含单独的客户评论。对我们来说,处理较小的块比处理酒店的所有客户评论会更有效。因此,我们需要拆分我们的文档。让我们进入下一阶段,详细讨论文档分割。

2.2.拆分文档

下一步是拆分文档。您可能想知道为什么我们需要这样做。文档通常很长并且涵盖多个主题,例如 Confluence 页面或文档。如果我们将如此冗长的文本传递给LLM,我们可能会面临LLM被不相关信息分散注意力或文本不适合上下文大小的问题。

因此,为了与LLM有效合作,值得从我们的知识库(文档集)中定义最相关的信息,并仅将此信息传递给模型。这就是为什么我们需要将文档分成更小的块。

一般文本最常用的技术是按字符递归拆分。在LangChain中,它是在RecursiveCharacterTextSplitter类中实现的。

让我们尝试了解它是如何工作的。首先,为拆分器定义一个按优先级排列的字符列表(默认情况下为["\n\n", "\n", " ", ""])。然后,拆分器遍历该列表并尝试按字符将文档一个接一个地拆分,直到获得足够小的块。这意味着这种方法试图将语义上接近的部分(段落、句子、单词)保持在一起,直到我们需要将它们拆分以达到所需的块大小。

让我们使用Zen of Python来看看它是如何工作的。本文共824字、139字、21段。

如果你执行import this的话,你就能看到Zen of Python

zen = ''' Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one -- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those! ''' print('Number of characters: %d' % len(zen)) print('Number of words: %d' % len(zen.replace('\n', ' ').split(' '))) print('Number of paragraphs: %d' % len(zen.split('\n'))) # Number of characters: 825 # Number of words: 140 # Number of paragraphs: 21

让我们使用RecursiveCharacterTextSplitter并从一个相对较大的块大小(等于 300)并开始。

from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size = 300, chunk_overlap = 0, length_function = len, is_separator_regex = False, ) text_splitter.split_text(zen)

我们将得到三个块:264、293 和 263 个字符。我们可以看到所有句子都连接在一起。

以下所有图片均由作者制作。

您可能会注意到一个chunk_overlap参数可以允许您通过重叠进行分割。这很重要,因为我们将向 LLM 传递一些带有问题的块,并且拥有足够的上下文来仅根据每个块中提供的信息做出决策至关重要。

让我们尝试添加chunk_overlap.

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 300 ,
    chunk_overlap = 100 ,
    length_function = len ,
    is_separator_regex = False ,
)
text_splitter.split_text(zen)

现在,我们有四个分区,分别有 264、232、297 和 263 个字符,我们可以看到我们的块重叠。

让我们把块的大小调小一点。

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 50 ,
    chunk_overlap = 10 ,
    length_function = len ,
    is_separator_regex = False ,
)
text_splitter.split_text(zen)

现在,我们甚至不得不拆分一些较长的句子。这就是递归分割的工作原理:因为按段落分割后("\n"),块仍然不够小,分割器继续进行" "。

您可以进一步自定义分割。例如,您可以指定length_function = lambda x: len(x.split("\n"))使用段落数而不是字符数作为块长度。按标记拆分也很常见,因为LLM根据标记数量限制上下文大小。

另一个潜在的定制是使用其他的separators,使用split by ","代替" " 。让我们尝试用几个句子来使用它。

text_splitter = RecursiveCharacterTextSplitter( chunk_size = 50, chunk_overlap = 0, length_function = len, is_separator_regex = False, separators=["\n\n", "\n", ", ", " ", ""] ) text_splitter.split_text('''\ If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea.''')

它有效,但逗号不在正确的位置。

为了解决这个问题,我们可以使用带有回溯的正则表达式作为分隔符。

text_splitter = RecursiveCharacterTextSplitter( chunk_size = 50, chunk_overlap = 0, length_function = len, is_separator_regex = True, separators=["\n\n", "\n", "(?<=\, )", " ", ""] ) text_splitter.split_text('''\ If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea.''')

现在已经解决了。

此外,LangChain 还提供了用于处理代码的工具,以便您的文本根据特定于编程语言的分隔符进行分割。

然而,在我们的例子中,情况更加简单。我们知道每个文件中都有"\n"分割的单独的独立注释,我们只需按"\n"进行分割即可。不幸的是,LangChain 不支持这样的基本用例,因此我们需要进行一些修改才能使其按我们想要的方式工作。

from langchain.text_splitter import CharacterTextSplitter text_splitter = CharacterTextSplitter( separator = "\n", chunk_size = 1, chunk_overlap = 0, length_function = lambda x: 1, # hack - usually len is used is_separator_regex = False ) split_docs = text_splitter.split_documents(docs) len(split_docs) 12890

您可以在我之前关于 LangChain 的文章中找到更多关于为什么我们需要 hack 的详细信息。

文档的重要部分是元数据,因为它可以提供有关该块来自何处的更多上下文。在我们的例子中,LangChain 自动填充source元数据参数,以便我们知道每个评论与哪家酒店相关。

还有一些其他方法(即HTMLMarkdown)可以在拆分文档时向元数据添加标题。如果您正在使用此类数据类型,这些方法可能会非常有用。

2.3.矢量存储

现在我们有了评论文本,下一步是学习如何有效地存储它们,以便我们可以获得问题的相关文档。

我们可以将评论存储为字符串,但这不会帮助我们解决此任务 - 我们将无法过滤与问题相关的客户评论。

一个更实用的解决方案是存储文档的嵌入。

嵌入是高维向量。嵌入捕获语义含义以及单词和短语之间的关系,以便语义接近的文本之间的距离较小。

我们将使用OpenAI Embeddings,因为它们非常流行。OpenAI 建议使用该text-embedding-ada-002模型,因为它具有更好的性能、更广泛的上下文和更低的价格。与往常一样,它有其风险和局限性:潜在的社会偏见和对近期事件的了解有限。

让我们尝试在玩具示例上使用嵌入,看看它是如何工作的。

from langchain.embeddings.openai import OpenAIEmbeddings embedding = OpenAIEmbeddings() text1 = 'Our room (standard one) was very clean and large.' text2 = 'Weather in London was wonderful.' text3 = 'The room I had was actually larger than those found in other hotels in the area, and was very well appointed.' emb1 = embedding.embed_query(text1) emb2 = embedding.embed_query(text2) emb3 = embedding.embed_query(text3) print(''' Distance 1 -> 2: %.2f Distance 1 -> 3: %.2f Distance 2-> 3: %.2f ''' % (np.dot(emb1, emb2), np.dot(emb1, emb3), np.dot(emb2, emb3)))

我们可以使用np.dot余弦相似度,因为 OpenAI 嵌入已经标准化。

我们可以看到第一个和第三个向量彼此接近,而第二个向量不同。第一句和第三句语义相似(都是关于房间大小的),而第二句则不太接近,都是在谈论天气。因此,嵌入之间的距离实际上反映了文本之间的语义相似性。

现在,我们知道如何将注释转换为数值向量。下一个问题是我们应该如何存储它以便可以轻松访问这些数据。

让我们考虑一下我们的用例。我们的流程将是:

  1. 提出问题
  2. 计算其嵌入
  3. 找到与该问题相关的最相关的文档块(与该嵌入距离最小的文档块)
  4. 最后,将找到的块与初始问题一起作为上下文传递给 LLM。

数据存储的常规任务是找到 K 个最近的向量(K 个最相关的文档)。因此,我们需要计算问题的嵌入与我们拥有的所有向量之间的距离(在我们的例子中为余弦相似度)。

通用数据库(如 Snowflake 或 Postgres)对于此类任务表现不佳。但也有一些数据库经过优化,特别是针对此用例——矢量数据库。

我们将使用开源嵌入数据库Chroma。Chroma 是一种轻量级内存数据库,因此非常适合原型设计。您可以在这里找到更多矢量存储选项。

首先,我们需要使用 pip 安装 Chroma。

pip install chromadb

我们将使用persist_directory在本地存储数据并从磁盘重新加载。

from langchain.vectorstores import Chroma persist_directory = 'vector_store' vectordb = Chroma.from_documents( documents=split_docs, embedding=embedding, persist_directory=persist_directory )

为了能够在下次需要时从磁盘加载数据,请执行以下命令。

embedding = OpenAIEmbeddings() vectordb = Chroma( persist_directory=persist_directory, embedding_function=embedding )

数据库初始化可能需要几分钟,因为 Chroma 需要加载所有文档并使用 OpenAI API 获取其嵌入。

我们可以看到所有的文档都已经加载完毕。

print(vectordb._collection.count()) 12890

现在,我们可以使用相似性搜索来查找客户对员工礼貌的最重要评论。

query_docs = vectordb.similarity_search('politeness of staff', k=3)

文件看起来与问题非常相关。

我们已经以可访问的方式存储了客户评论,现在是更详细地讨论检索的时候了。

2.4.检索

我们已经习惯于vectordb.similarity_search检索与问题最相关的块。在大多数情况下,这种方法适合您,但可能存在一些细微差别:

l缺乏多样性——模型可能会返回极其接近的文本(甚至重复),这不会为LLM添加太多新信息。

l不考虑元数据——similarity_search不考虑我们拥有的元数据信息。例如,如果我查询问题“Travelodge Farringdon 的早餐”的前 5 条评论,则结果中只有 3 条评论的来源等于uk_england_london_travelodge_london_farringdon

l上下文大小限制——像往常一样,我们的 LLM 上下文大小有限,需要将我们的文档放入其中。

让我们讨论一下可以帮助我们解决这些问题的技术。

2.4.1.解决多样性——MMR(最大边际相关性)

相似性搜索会返回与您的问题最接近的答案。但为了向模型提供完整的信息,您可能不希望关注最相似的文本。例如,对于“Travelodge Farringdon 的早餐”这个问题,排名前五的客户评论可能是关于咖啡的。如果我们只看它们,我们会错过其他提到鸡蛋或员工行为的评论,并且对客户反馈的看法会有些有限。

我们可以使用 MMR(最大边际相关性)方法来增加客户评论的多样性。它的工作原理非常简单:

l首先,我们fetch_k使用similarity_search获得与问题最相似的文档。

l然后,我们挑选了k其中最多样化的。

如果我们想使用MMR,我们应该使用max_marginal_relevance_search而不是similarity_search并指定fetch_k数字。值得保持fetch_k相对较小,这样输出中就不会出现不相关的答案。就是这样。

query_docs = vectordb.max_marginal_relevance_search('politeness of staff', k = 3, fetch_k = 30)

让我们看一下同一查询的示例。这次我们得到了更多样化的反馈。甚至还有负面情绪的评论。

2.4.2.解决特异性——LLM辅助检索

另一个问题是我们在检索文档时没有考虑元数据。为了解决这个问题,我们可以要求LLM将最初的问题分成两部分:

l基于文档文本的语义过滤器,

l根据我们拥有的元数据进行过滤。

这种方法称为“自查询”

首先,我们添加一个手动过滤器,指定一个source参数,该参数的文件名与 Travelodge Farringdon 酒店相关。

query_docs = vectordb.similarity_search('breakfast in Travelodge Farrigdon', k=5, filter = {'source': 'hotels/london/uk_england_london_travelodge_london_farringdon'} )

现在,让我们尝试使用 LLM 自动提出这样的过滤器。我们需要详细描述所有元数据参数,然后使用SelfQueryRetriever.

from langchain.llms import OpenAI from langchain.retrievers.self_query.base import SelfQueryRetriever from langchain.chains.query_constructor.base import AttributeInfo metadata_field_info = [ AttributeInfo( name="source", description="All sources starts with 'hotels/london/uk_england_london_' \ then goes hotel chain, constant 'london_' and location.", type="string", ) ] document_content_description = "Customer reviews for hotels" llm = OpenAI(temperature=0.1) # low temperature to make model more factual # by default 'text-davinci-003' is used retriever = SelfQueryRetriever.from_llm( llm, vectordb, document_content_description, metadata_field_info, verbose=True ) question = "breakfast in Travelodge Farringdon" docs = retriever.get_relevant_documents(question, k = 5)

我们的例子很棘手,因为source元数据中的参数由多个字段组成:国家、城市、连锁酒店和位置。在这种情况下,值得将如此复杂的参数拆分为更细粒度的参数,以便模型可以轻松理解如何使用元数据过滤器。

然而,在详细提示下,它起作用了,只返回了与 Travelodge Farringdon 相关的文档。但我必须承认,我花了好几次迭代才达到这个结果。

让我们打开调试看看它是如何工作的。要进入调试模式,只需执行以下代码即可。

import langchain langchain.debug = True

完整的提示相当长,让我们看看它的主要部分。这是提示的开始,它为模型提供了我们期望的内容和结果的主要标准的概述。

然后,使用少样本提示技术,并为模型提供输入和预期输出两个示例。这是其中一个例子。

我们没有使用像 ChatGPT 这样的聊天模型,而是使用通用的 LLM(未根据说明进行微调)。它经过训练只是为了预测文本的以下标记。Structured output:这就是为什么我们用问题和期望模型提供答案的字符串来完成提示。

结果,我们从模型中得到的初始问题分为两部分:语义一 ( breakfast) 和元数据过滤器 ( source = hotels/london/uk_england_london_travelodge_london_farringdon)

然后,我们使用此逻辑从向量存储中检索文档并仅获取我们需要的文档。

2.4.3.解决大小限制 - 压缩

另一种可能方便的检索技术是压缩。尽管 GPT 4 Turbo 的上下文大小为 128K 令牌,但它仍然受到限制。这就是为什么我们可能想要预处理文档并仅提取相关部分。

主要优点是:

l您将能够在最终提示中加入更多文档和信息,因为它们将被压缩。

l您将获得更好、更集中的结果,因为在预处理过程中将清除不相关的上下文。

这些好处是伴随着成本的——您将有更多的请求来要求 LLM 进行压缩,这意味着更低的速度和更高的价格。

您可以在文档中找到有关此技术的更多信息。

实际上,我们甚至可以在这里结合技术并使用MMR。我们曾经ContextualCompressionRetriever得到过结果。此外,我们指定只需要三个文档作为回报。

from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor llm = OpenAI(temperature=0) compressor = LLMChainExtractor.from_llm(llm) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=vectordb.as_retriever(search_type = "mmr", search_kwargs={"k": 3}) ) question = "breakfast in Travelodge Farringdon" compressed_docs = compression_retriever.get_relevant_documents(question)

和往常一样,了解它的幕后工作原理是最令人兴奋的部分。如果我们看一下实际的调用,就会发现对 LLM 的 3 次调用仅从文本中提取相关信息。这是一个例子。

在输出中,我们只得到了与早餐相关的部分句子,因此压缩会有所帮助。

还有许多更有益的检索方法,例如经典 NLP 中的技术:SVMTF-IDF。不同的检索器在不同的情况下可能会有所帮助,因此我建议您比较适合您的任务的不同版本,并选择最适合您的用例的版本。

2.5.生成

最后,我们到达了最后一个阶段:我们将结合所有内容并生成最终答案。

这是一个关于这一切如何运作的方案:

l我们收到用户的一个问题,

l我们使用嵌入从向量存储中检索该问题的相关文档,

l我们将最初的问题连同检索到的文件一起传递给LLM并获得最终答案。

在LangChain中,我们可以使用RetrievalQA链来快速实现这个流程。

from langchain.chains import RetrievalQA from langchain.chat_models import ChatOpenAI llm = ChatOpenAI(model_name='gpt-4', temperature=0.1) qa_chain = RetrievalQA.from_chain_type( llm, retriever=vectordb.as_retriever(search_kwargs={"k": 3}) ) result = qa_chain({"query": "what customers like about staff in the hotel?"})

让我们看看对 ChatGPT 的调用。如您所见,我们将检索到的文档与用户查询一起传递。

这是模型的输出。

我们可以调整模型的行为,自定义提示。例如,我们可以要求模型更加简洁。

from langchain.prompts import PromptTemplate template = """ Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. Keep the answer as concise as possible. Use 1 sentence to sum all points up. ______________ {context} Question: {question} Helpful Answer:""" QA_CHAIN_PROMPT = PromptTemplate.from_template(template) qa_chain = RetrievalQA.from_chain_type( llm, retriever=vectordb.as_retriever(), return_source_documents=True, chain_type_kwargs={"prompt": QA_CHAIN_PROMPT} ) result = qa_chain({"query": "what customers like about staff in the hotel?"})

这次我们得到的答案要短得多。另外,由于我们指定了return_source_documents=True,我们得到了一组文档作为回报。它可能有助于调试。

正如我们所见,默认情况下,所有检索到的文档都合并在一个提示中。这种方法非常出色且简单,因为它仅调用一次对 LLM 的调用。唯一的限制是您的文档必须适合上下文大小。如果没有,您需要应用更复杂的技术。

让我们看看不同的链类型,它们可以让我们处理任意数量的文档。第一个是MapReduce。

这种方法类似于经典的MapReduce:我们根据每个检索到的文档生成答案(映射阶段),然后将这些答案组合到最终答案中(减少阶段)。

所有这些方法的局限性在于成本和速度。您需要对每个检索到的文档进行一次调用,而不是对 LLM 进行一次调用。

对于代码,我们只需要指定chain_type="map_reduce"改变行为即可。

qa_chain_mr = RetrievalQA.from_chain_type( llm, retriever=vectordb.as_retriever(), chain_type="map_reduce" ) result = qa_chain_mr({"query": "what customers like about staff in the hotel?"})

结果,我们得到以下输出。

让我们看看它如何使用调试模式工作。由于它是一个 MapReduce,我们首先将每个文档发送给 LLM,并根据这个块得到答案。下面是提示其中一个块的示例。

然后,我们将所有结果结合起来,并要求LLM给出最终答案。

就是这样。

MapReduce 方法还有另一个特定的缺点。该模型单独查看每个文档,并且不会将它们全部放在同一上下文中,这可能会导致更糟糕的结果。

我们可以通过 Refine 链类型克服这个缺点。然后,我们将按顺序查看文档,并允许模型在每次迭代中完善答案。

同样,我们只需要改变chain_type来测试另一种方法。

qa_chain_refine = RetrievalQA.from_chain_type( llm, retriever=vectordb.as_retriever(), chain_type="refine" ) result = qa_chain_refine({"query": "what customers like about staff in the hotel?"})

通过Refine链,我们得到了更加罗嗦和完整的答案。

让我们看看它如何使用调试来工作。对于第一个块,我们从头开始。

然后,我们传递当前答案和一个新块,并让模型有机会完善其答案。

然后,我们对每个剩余的检索文档重复精炼提示并得到最终结果。

这就是我今天想告诉你的全部内容。让我们快速回顾一下。

3.总结

在这篇文章中,我们经历了检索增强生成的整个过程:

l我们研究了不同的数据加载器。

l我们已经讨论了数据分割的可能方法及其潜在的细微差别。

l我们已经了解了嵌入是什么,并设置了向量存储来有效地访问数据。

l我们找到了针对检索问题的不同解决方案,并了解了如何增加多样性、克服上下文大小限制以及使用元数据。

l最后,我们使用RetrievalQA链根据我们的数据生成答案,并比较不同的链类型。

这些知识应该足以开始构建与您的数据类似的东西。

4.数据集

Ganesan, Kavita and Zhai, ChengXiang. (2011). OpinRank Review Dataset.

UCI Machine Learning Repository (CC BY 4.0). https://doi.org/10.24432/C5QW4W

5.参考

本文基于以下课程的信息:

lDeepLearning.AI 和 LangChain 合作的“LangChain for LLM 应用程序开发” ,

lDeepLearning.AI 和 LangChain 的“LangChain:与您的数据聊天” 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值