原文:towardsdatascience.com/how-to-make-a-rag-system-to-gain-powerful-access-to-your-data-caf4bb9186ea

RAG 系统是一种创新的信息检索方法。它结合了传统的信息检索方法,如向量相似度搜索,以及最先进的大语言模型技术。结合这些技术,它们构成一个强大的系统,可以从简单的提示中访问大量信息。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/933a988d97b8b5621e763f9f651897b1.png

ChatGPT 的 RAG 系统图像。图由 ChatGPT 制作。“你能制作一个 RAG 系统的插图,展示计算机访问知识库的点”提示。ChatGPT,4,OpenAI,2024 年 3 月 17 日。chat.openai.com.

动机

我写这篇文章的动机是在尝试寻找一封旧邮件时的挫败感。我通常对这封邮件有一些信息,比如收件人是谁,或者电子邮件的大致主题是什么。然而,在进行直接单词搜索时,我必须更加具体,这使得找到我想要的特定邮件变得具有挑战性。我希望有一个 RAG 系统能够让我提示我的电子邮件去搜索它们。所以,如果我从 NTNU 大学需要关于某个主题的旧邮件,我可以提示类似“我在 NTNU 第二年时报名了哪门技术课程?”这样的提示的直接单词搜索是具有挑战性的,因为我需要在提示中提供更具体的信息。相反,一个 RAG 系统可以在拥有所有所需数据的情况下找到这封邮件。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/c2dd22157deb0ccdf2ee372aa998c84c.png

本教程遵循一个流程。首先,我将向您展示如何为 RAG 检索一些数据。然后,在实现 RAG 并测试系统之前,您将预处理这些数据。图由作者制作。

目录

· 动机 · 检索数据 · 预处理数据 · 其他选项 · 实现 RAG ∘ 准备数据 ∘ 加载 LLM ∘ 使用 LLM · 测试 ∘ 测试 1 ∘ 测试 2 · 未来工作 · 结论

检索数据

构建 RAG 系统的第一步是找到你想要 RAG 系统使用的数据。在我的例子中,我想搜索电子邮件,尽管我将讨论的方法也适用于任何其他数据源。我下载了我所有的电子邮件,你可以在takeout.google.com找到它们。在那里,你可以只选择下载你的邮件并点击导出。这将发送一个链接到你的邮箱,你可以从中下载所有邮件信息。如果你有其他电子邮件客户端,也有类似的方法可以下载所有你的电子邮件信息。

此外,在这个教程中,你可以使用除了电子邮件之外的其他数据源。一些例子包括:

这些例子中的共同因素是它们都包含文本信息。然而,你也可以将此信息应用于其他类型的信息,如图像或音频。对于 RAG 系统来说,挑战不在于搜索数据,因为你可以轻松地将文本、照片和音频等不同模态的数据矢量化。相反,更大的问题是生成文本、音频或图像,尽管有方法可以做到这一点。然而,本文将仅讨论从文本信息生成文本答案。

预处理数据

在下载完你的数据后,是时候进行预处理了,这是大多数机器学习流程中的关键步骤。我下载了我的电子邮件,因此我的预处理步骤将展示如何预处理下载的电子邮件,尽管你也可以用同样的思路处理其他类型的数据。

从 takeout.google.com 下载数据后,你会在下载文件夹中有一个 zip 文件夹。解压 zip 文件夹,找到名为All mail Including Spam and Trash.mbox的文件,它包含所有必要的邮件信息。将此文件移动到你在工作的编程文件夹中,然后你可以使用以下代码(部分来自StackOverflow)来从文件中读取最关键的信息:

首先,安装并导入包:

# install packages
!pip install beautifulsoup4
!pip install pandas
!pip install tqdm

# import packages
import pandas as pd
import email
from email.policy import default
from tqdm import tqdm
from bs4 import BeautifulSoup #to clean the payload

然后创建一个类来读取 mbox 文件:

# code for class from https://stackoverflow.com/questions/59681461/read-a-big-mbox-file-with-python/59682472#59682472
class MboxReader:
    def __init__(self, filename):
        self.handle = open(filename, 'rb')
        assert self.handle.readline().startswith(b'From ')

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.handle.close()

    def __iter__(self):
        return iter(self.__next__())

    def __next__(self):
        lines = []
        while True:
            line = self.handle.readline()
            if line == b'' or line.startswith(b'From '):
                yield email.message_from_bytes(b''.join(lines), policy=default)
                if line == b'':
                    break
                lines = []
                continue
            lines.append(line)

然后,你可以使用以下命令将邮件信息提取到 pandas 数据框中:

path = r"<PATH TO MBOX FILE>/All mail Including Spam and Trash.mbox"
mbox = MboxReader(path)

MAX_EMAILS = 5
current_mails = 0

all_mail_contents = ""
mail_from_arr, mail_date_arr, mail_body_arr = [],[],[]
for idx,message in tqdm(enumerate(mbox)):
    # print(message.keys())
    mail_from = f"{str(message['From'])}n".replace('"','').replace('n','').strip()
    mail_date = f"{str(message['Date'])}n".replace('"','').replace('n','').strip()
    payload = message.get_payload(decode=True)
    if payload:
        current_mails += 1
        if current_mails > MAX_EMAILS:
            break
        soup = BeautifulSoup(payload, 'html.parser')
        body_text = soup.get_text().replace('"','').replace("n", "").replace("t", "").strip()
        mail_from_arr.append(mail_from)
        mail_date_arr.append(mail_date)
        mail_body_arr.append(body_text)
        all_mail_contents += body_text + " "

其中:

  • 是之前提到的 mbox 文件的根路径

  • MAX_EMAILS 是你想要提取的邮件数量。请注意,我正在提取电子邮件的正文(有效负载),并不是所有电子邮件都包含正文。因此,一些电子邮件将被跳过,因此不会计入你想要提取的最大邮件数量。

此代码将邮件信息保存到数组和一个包含所有邮件内容的字符串中。你可以使用以下命令将其保存到文件中:

df = pd.DataFrame({'From':mail_from_arr, 'Date':mail_date_arr, 'Body':mail_body_arr})
df.to_pickle("df_mail.pkl")

# write all mail contents to txt
with open("all_mail_contents.txt", "w", encoding="utf-8") as f:
 f.write(all_mail_contents)

你需要的所有信息现在都存放在文件中,并准备好供 RAG 系统检索。

其他选项

在我开始实现 RAG 系统之前,我还想提及一些不那么花哨但仍然可靠的 RAG 替代方案。RAG 是信息检索领域的一个搜索系统。信息检索是必不可少的,因为它允许我们访问大量可用数据。例如,信息检索技术被用于任何搜索引擎,如 Google。TF-IDF 在 1972 年被实现并且仍然是一个可靠的信息搜索算法。我自己也实现了一个带有它的搜索引擎系统,你可以在下面的文章中看到,你会对算法仍然表现得如此之好而感到印象深刻。

在 Python 中实现 TF-IDF,索引你的数据,并进行推理!

BM25 是对 TF-IDF 的一种改进,可以通过对 TF-IDF 代码进行轻微修改轻松实现。我提到其他信息搜索选项,如 TF-IDF 和 BM25 的原因是,一个完整的 RAG 系统可能并不总是必要的。如果你只需要直接的信息访问,那么现有的简单选项仍然可以很好地工作。

实现 RAG

现在是时候实现 RAG 系统了。我将使用 Langchain 框架,这是一个快速设置可定制 RAG 系统的优秀工具。我遵循了 Langchain 文档,并针对自定义数据定制了代码以创建这一部分。

首先,你必须安装和加载所有必需的包:

# install packages
!pip install langchain
!pip install gpt4all
!pip install chromadb
!pip install llama-cpp-python
!pip install langchainhub

# import packages
from langchain_community.document_loaders import WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.embeddings import GPT4AllEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_community.llms import LlamaCpp
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain.docstore.document import Document
from langchain import hub
from langchain_core.runnables import RunnablePassthrough, RunnablePick
import pandas as pd

准备数据

然后,加载你在预处理数据步骤中创建的数据集。请注意,在这种情况下,我只使用邮件内容,但你也可以添加额外的信息,如发件人和每封邮件的日期。

# load custom dataset
with open("all_mail_contents.txt", "r", encoding="utf-8") as f:
 all_mail_contents = f.read()

然后,你必须将字符串转换为 Langchain 可以读取的格式。下面的代码首先将所有文本转换为 Langchain 文档格式,并初始化一个文本分割器,该分割器将字符串分割成不同的重叠块。这些块是至关重要的,因为当你向 RAG 系统提问时,最相关的块将被检索并作为上下文提供给 LLM 来回答问题。你还可以在块之间保持一些重叠,这样每个块都可以包含足够的信息来回答给定的问题。分割成块后,每个块都被向量化(转换为数字)。这是一个典型的信息检索步骤,其中你正在搜索的数据被向量化。然后,当你提问时,该问题也被向量化,你通过选择与问题向量距离最近的向量中的数据来选择与问题最相关的数据。

# convert to langchain document format
doc =  Document(page_content=all_mail_contents, metadata={"source": "local"})
#split up
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=20)
all_splits = text_splitter.split_documents([doc])
vectorstore = Chroma.from_documents(documents=all_splits, embedding=GPT4AllEmbeddings())

注意:我在 Python3.8 上运行上面最后一行代码时遇到了问题,但在 Python3.11 上它可以直接使用。这个问题是由于 Python 3.8 中预安装的 sqllite3(Python 的一个预安装包)的版本引起的。

向量化

向量化过程的工作原理如下。这里我以随机选择的数字为例。首先,你需要将每个文档向量化:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/afc87561e586bb078a3547beb32de36b.png

展示文档向量化的图像。图由作者提供。

然后你将问题向量化:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/4d5966b0345bb40de5aebcb783b43138.png

展示问题向量化的图像。图由作者提供

然后你找到问题与每个文档之间的相似性;例如,使用余弦相似性,这将输出一个介于 0 和 1 之间的单个数字数组,如下所示:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/7e927b786ae8c683eade956737463ddb.png

展示问题和文档之间相似性的图像。图由作者提供。

然后你选择与问题最相似的文档。如果你想选择两个文档,你应该选择文档 1 和 3。

加载 LLM

在准备数据后,你必须加载你的大型语言模型。有很多语言模型可供选择,但我会专注于免费模型。Langchain 是一个包装器,它使得使用 LLM 变得容易,但你仍然需要自己下载 LLM。然而,这并不一定是一个复杂的过程,我将展示两种方法。

第一个选项,也是我的首选,是 Llama2,你可以在下面的教程中学习如何下载和使用:

在 Windows 上下载和运行 Llama2

你必须下载模型并将其转换为.gguf 格式,Langchain 可以轻松使用。然后你可以使用以下方式使用模型:

n_gpu_layers = 1 
n_batch = 512 

# Make sure the model path is correct for your system!
llm = LlamaCpp(
    model_path=r"<path to model>ggml-model-f16_q4_1.gguf",
    n_gpu_layers=n_gpu_layers,
    n_batch=n_batch,
    n_ctx=1024,
    f16_kv=True,  # MUST set to True, otherwise you will run into problem after a couple of calls
    verbose=True,
)

其中模型路径是你模型的路径,而 n_ctx 是你想要使用的上下文大小。较大的上下文大小会更好,但也需要更多的计算,这是你必须考虑的权衡。

第二个,更易于访问的选项是使用 GPT4All,这是Langchain 教程中提到的。尽管名字暗示了这一点,GPT4All 有开源模型你可以使用。然后你可以下载一个 Tiny LLama HuggingFace(_tinyllama-1.1b-chat-v1.0.Q5_KM.gguf模型)。如果你想提高性能,你也可以寻找更大的 Llama 模型,尽管使用较小的模型你仍然可以得到相当好的结果。

在你下载了模型之后,将其移动到你的本地文件夹。然后你可以使用以下方式加载模型:

from langchain_community.llms import GPT4All

llm = GPT4All(
    model=r"<root path to your model>tinyllama-1.1b-chat-v1.0.Q5_K_M.gguf",
    max_tokens=2048,
)

你现在就可以使用你的 LLM 了!

使用 LLM

如果你想要使用你的语言模型,你可以这样调用它:

question = "What is Medium"
llm.invoke(question) 

在其中我的 Llama2 模型给出了以下响应:

Medium is a platform that allows writers to share their thoughts, ideas, and stories with the world. It was founded by Evan Williams, one of the co-founders of Twitter, and has become a popular place for writers to publish long-form content, including articles, essays, personal stories, and more. Here are some things you can do on Medium:n1\. Publish your writing: The most obvious thing you can do on Medium is to publish your writing. Whether you want to write about a personal experience, share your expertise in a particular field, or tell a story, Medium provides a platform for you to do so.n2\. Follow other writers: Medium has a large community of writers, and you can follow other writers whose work interests you. This way, you'll see their new publications in your feed and can discover new voices and perspectives.n3\. Read and engage with publications: Medium publishes a wide variety of content, including articles, essays, personal stories, and more. You can read and engage with these publications by commenting, liking, or sharing them with others.

另一个你可以使用的激动人心的功能是相似性搜索。相似性搜索就像一个标准的搜索算法,返回与你的问题最相似的数据点。这是通过将问题和数据点向量化,并选择与问题距离最小的那些来实现的。你可以使用以下方式执行相似性搜索:

#this code can be used to see if the correct documents are retrieved. The documents retrieved should be regarding your questions, and is the data the LLM uses to answer the questions
question = "What is Google"
docs = vectorstore.similarity_search(question)

这可以是一个非常有价值的工具,用于调试 RAG 系统是否按预期检索文档。你应该看到与你的问题相关的相关文档。

然后最好有一个辅助函数来格式化文档:

def format_docs(docs):
    return "nn".join(doc.page_content for doc in docs)

并形成一个 Langchain 链来帮助你提问。_ragprompt会在你的问题中添加额外的文本,以确保 LLM 使用检索到的文档来帮助回答问题。

# retrieve relevant docs
rag_prompt = hub.pull("rlm/rag-prompt")
rag_prompt.messages

retriever = vectorstore.as_retriever()
qa_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | rag_prompt
    | llm
    | StrOutputParser()
)

最后,你可以使用以下方式让 LLM 使用检索到的文档回答问题:

qa_chain.invoke(question)

这行代码调用了你创建的链,它检索与你的问题最相关的文档,然后根据检索到的文档提示 LLM 回答你的问题。

测试

定量测试 RAG 系统可能很困难,因为创建标记数据集是劳动密集型的。因此,我将在这篇文章中进行定性测试以检查其性能。请注意,上述实现很简单,你可以期待的不是高级性能。

我通过首先找到一些我想发现的电子邮件,然后查看 RAG 系统能否检索到我所需的信息来测试我的 RAG 系统。需要注意的是,我的许多电子邮件将用挪威语编写,这使得一些任务对 RAG 系统来说更加困难,尽管仍然有可能实现。我不会在这里分享示例,因为我希望保持我的电子邮件私密,但你可以自己尝试。为了进行这些测试,我收集了大约 100 封包含正文的电子邮件,并提出了与这些电子邮件相关的问题。

测试 1

我有一封来自挪威数字邮政服务 Digipost 的电子邮件。我想看看 RAG 系统能否确定我是否收到了这样一封电子邮件。

我提出问题:

question = "Have I gotten a message from Digipost?"

然后 RAG 系统回答:

'Yes, you have received a message from Digipost.'

因此,LLM 通过了测试。这个测试很有趣,因为文档检索成功地检索到了与 Digipost 相关的相关电子邮件,因为 Digipost 是一个不常见的词。然而,关于 Digipost 的电子邮件是用挪威语编写的,这使得这一点更加令人印象深刻。

测试 2

我收到了一封来自微软关于云技能挑战赛(Cloud Skill Challenge)的电子邮件,所以我向 RAG 提问:

question = "What is the topic of my last Microsoft email I have gotten?"

然后,它回答:

'The topic of your last Microsoft email is learning content.'

如你所见,RAG 再次正确,尽管这次答案仍然含糊且不是最优的。

由于我认为答案有些含糊,我尝试让 LLM 更详细地回答:

question = "What is the topic of my last Microsoft email I have gotten? Answer in detail"

在其中它回答:

'nBased on the provided context, the topic of your last Microsoft email is likely related to learning paths and modules in Microsoft Learn, with a mention of trending content and contact information for feedback or help.'

因此,系统工作正常!

未来工作

尽管系统按预期工作,但我希望向这个 RAG 系统添加许多功能。以下是我正在考虑的一些未来工作:

  • 为每封电子邮件添加更多信息,例如发件人和日期。这将给 RAG 系统提供更多可操作的信息

  • 添加一个选项,以便 RAG 可以返回特定的邮件。给定一个像:“你能找到大约 6 月 23 日到达的关于…的邮件吗?”这样的问题,我希望它给我一封(或一系列电子邮件)可能是我正在寻找的邮件

  • 更好的数据分割。目前,我正在将所有邮件的文本合并在一起,但相反,我可以将每封邮件分割成一个段落,并通过这种方式获取信息。

  • 更大的上下文窗口。增加 LLM 的上下文窗口,以便允许后续问题,例如,如果 RAG 模型没有给出足够详细的答案。

结论

在本文中,我讨论了如何使用您自己的数据创建一个 RAG 系统。给定一个提示,这可以成为一个强大的搜索引擎,以检索您所需的信息。为了构建这个系统,您首先需要检索数据,例如,通过从 Gmail 下载您的邮件。然后,您必须预处理数据,使其能够被 RAG 系统访问。之后,所有数据都将转换为 Langchain 格式并加载到 LLM 中。然后,您可以就下载的数据提出问题。我还包括了两个测试,展示了 RAG 系统如何响应查询,并展示了系统按预期工作。

您可以在我的 GitHub 上找到本文中使用的代码:我的 GitHub

本文的第二部分如下:

如何提高您的 RAG 系统以实现更高效的问答

您还可以在我的WordPress上阅读我的文章。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐