编者按:
相信很多人都对Chatbots背后的技术原理很感兴趣,其实Chatbots并非通过“魔法”与我们交流,而是依靠一种被称为检索增强生成(RAG)的技术。
文章详细梳理了 RAG 技术的具体实现原理。首先,RAG 将用户输入的问题与知识库中的私有数据进行匹配,获取相关知识片段。然后,通过预训练的大语言模型,用提取到的知识片段来增强对问题的回答生成过程。在知识提取步骤,借助词向量的相似度找到与用户提出的问题最匹配的内容。生成回答时,直接向语言模型提供增强知识来指导其产出更符合语境的回答。
RAG 技术看似神奇,但其本质是结合了检索和生成两个子任务的一种系统工程,而每个子任务又都有明确的技术原理支撑。作为 AI 开发者,理解这一工作流程尤为重要。相信本文有助于读者进一步掌握 RAG 的技术原理,从而更好地运用 Chatbots 为用户创造更多价值。
以下是译文,enjoy!
这段代码实现了一个简单的Chatbots:
from langchain.document_loaders import WebBaseLoader
from langchain.indexes import VectorstoreIndexCreator
loader = WebBaseLoader("http://www.paulgraham.com/greatwork.html")
index = VectorstoreIndexCreator().from_loaders([loader])
index.query("What should I work on?")
可以看到这段代码发送了一个查询请求,因为该Chatbots是使用Paul Graham的文章[1]作为专有数据实现的,因此得到输出结果如下:
The work you choose should ideally have three qualities: it should be something you have a natural aptitude for, something you have a deep interest in, and something that offers scope to do great work. If you're unsure, you could start by working on your own projects that seem excitingly ambitious to you. It's also beneficial to be curious, try lots of things, meet lots of people, read lots of books, and ask lots of questions. When in doubt, optimize for interestingness. It's okay to guess and be wrong sometimes, as this can lead to discovering what you're truly good at or interested in.
注意:如果您感兴趣,可以在这里[2]尝试使用Paul Graham的文章构建的Chatbot。
当我第一次运行时,感觉就像是一种魔法。它到底是怎么实现的?
答案是使用一种叫做检索增强生成(Retrieval Augmented Generation,简称RAG)的技术。这是一个非常简单的概念,但其具体实现细节却有着惊人的技术深度。
本篇文章将对 RAG 做一个高层次的概述。我们将从Chatbots的大体工作流程开始,然后放大到所有单独的部分。文章结束时,您应该对这三行神奇的代码是如何工作的,以及创建这些Chatbots所涉及的所有原理有了扎实的了解。
如果您是一名开发人员,并且正在尝试构建像这样的机器人,那么您将学习到哪些参数可以调整以及如何调整它们。如果您是一名非开发人员,并且希望在私有数据集上使用AI工具,那么您将获得帮助您充分利用这些工具的知识。如果您只是对Chatbots十分好奇的人,希望您能学到一两件关于这些能够颠覆我们生活的技术的知识。
接下来让我们深入地进行探讨。
一、什么是检索增强生成(RAG)技术?
检索增强生成是将用户输入的信息补充到大语言模型(LLM)中的过程。然后,LLM 可以使用这些信息来增强其生成的回答或响应。
下图展示了在生产环境中它是如何工作的:
这个流程始于用户提出的问题,例如“我该如何做<某件事>?”
首先进行的是检索步骤。这个过程是根据用户的问题,从知识库中检索可能回答问题的最相关内容。到目前为止,检索步骤是 RAG 链中最重要、最复杂的一个部分。但现在,只需把这个步骤当成一个黑盒子即可,它知道如何提取与用户查询最相关的信息块。
难道我们不能把整个知识库交给 LLM 吗?
您可能会想知道,为什么我们要这样费心进行检索,而不是直接将整个知识库发送给LLM。其中一个原因是大模型预置了每次可读取文本的数量限制(尽管这些限制的上限正在快速增长)。第二个原因是成本 —- 发送大量文本是非常昂贵的。最后,有证据表明,仅发送少量相关信息会产生更好的答案。
一旦我们从知识库中获取到了相关信息,就会将其与用户的问题一起发送给大语言模型(LLM)。LLM会“读取”提供的信息并回答问题。这就是增强生成步骤。
看起来相当简单,对吧?
二、让我们从后往前看:
给LLM额外的知识来回答问题
我们从最后一步开始:生成回答。也就是说,假设我们已经从知识库中提取了我们认为能够回答问题的相关信息。那么我们如何使用这些信息来生成答案呢?
这个过程可能让人感觉像魔法,但其实背后只是一个语言模型。因此,概括地说,答案就是“向LLM提问”。
那么我们如何让LLM做到这一点呢?我们将以ChatGPT为例进行阐释。就像通常的ChatGPT使用一样,这一切都取决于prompts和相关信息。
三、使用system prompt为LLM提供自定义指令
第一个组成部分是system prompt。system prompt为语言模型提供整体指导。对于ChatGPT来说,system prompt类似于这种:“You are a helpful assistant.”。
在这种情况下,我们会希望它做一些更具体的事情。由于它是语言模型,我们可以直接告诉它我们想让它做什么。以下是一个简短的system prompt示例,它为LLM提供了更详细的指令:
You are a Knowledge Bot. You will be given the extracted parts of a knowledge base (labeled with DOCUMENT) and a question. Answer the question using information from the knowledge base.
您是一个Knowledge Bot。接下来将给您一份知识库中提取的文档内容(标记为DOCUMENT)和一个相关的问题。请使用知识库中的信息回答问题。
我们基本上是在说:“嘿,AI,我们将给你一些东西阅读。阅读后回答我们的问题,好吗?谢谢。”由于AI很擅长遵循我们的指示,因此它可以很好地完成任务。
四、为LLM提供特定的知识来源
接下来,我们需要为AI提供阅读材料。还是忍不住再次说明,目前最新的AI非常擅长从文档中直接找出使用者需要的东西。但是,我们仍然可以通过一些固定的结构或格式来提高它的效率。
下面是一个向 LLM 传递文档的格式示例:
------------ DOCUMENT 1 -------------
This document describes the blah blah blah...
------------ DOCUMENT 2 -------------
This document is another example of using x, y and z...
------------ DOCUMENT 3 -------------
[more documents here...]
一定需要所有的这些格式吗?可能不需要,但让内容尽可能明确是件好事。您也可以使用 JSON 或 YAML 等机器可读的格式。或者,如果您感觉这样做让您很烦,您可以将所有内容都写成一个巨大的文本块。但是,对于更高级的使用场景,如,希望LLM在回答问题时引用其来源的场景,格式保持一致十分非常重要。
格式化文件后,我们就可以将其作为普通聊天信息发送给 LLM。请记住,在system prompt中,我们只需要告诉它我们将给它一些文档。
五、将所有内容整合在一起并提出问题
一旦有了system prompt和“文档(documents)”信息,我们就只需将它们与用户的问题一起发送给LLM即可。以下是使用OpenAI ChatCompletion API的Python代码示例:
openai_response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{
"role": "system",
"content": get_system_prompt(), # the system prompt as per above
},
{
"role": "system",
"content": get_sources_prompt(), # the formatted documents as per above
},
{
"role": "user",
"content": user_question, # the question we want to answer
},
],
)
就是这样!一条自定义的system prompt,两条信息,就可以获得针对具体语境的答案!
这只是一个简单的使用案例,可以对其进行进一步地扩展和改进。
我们还没有做的一件事就是告诉人工智能,如果它在信息源中找不到答案该怎么办。我们可以将这些指令添加到system prompt中,通常是告诉它拒绝回答,或者使用它了解的常识,具体取决于您的机器人想要做什么。您还可以让LLM引用其用于回答问题的特定资料来源。我们将在以后的文章中讨论这些策略,但现在,这些就是答案生成的基本要素了。
说完了简单的部分,现在该回到我们跳过的那个黑盒子了......
六、检索步骤:从知识库中获取正确信息
在上文中,我们假设已经有了正确的知识片段可以发送给LLM。但是,我们如何才能从用户的问题中获取这些信息呢?这就是检索步骤,它是任何“与数据聊天(chat with your data)”的系统中的核心基础设施。
检索的核心是搜索操作——我们希望根据用户的输入查找最相关的信息。就像搜索一样,检索有两个主要部分:
索引:将知识库转换为可搜索/查询的内容。
查询:从搜索内容中提取最相关的知识片段。
值得注意的是,任何搜索过程都可以用于检索,任何接受用户输入并返回结果的程序都可以使用。因此,举例来说,你可以尝试找到与用户问题相匹配的文本,然后将其发送给 LLM,或者您可以在Google中搜索问题,并将前几个结果发送过去——顺便说一下,这大致是Bing聊天机器人的工作原理。
也就是说,如今大多数RAG系统都依赖于一种称为语义搜索的技术,它使用了人工智能的另一项核心技术:嵌入(Embedding)。在这里,我们将重点讨论这一使用案例。
那么…什么是嵌入向量呢?
七、什么是嵌入?它们与知识检索有什么关系?
LLM 充满奥妙。它们最神秘的地方之一,就是没有人真正知道它们是如何理解语言的。嵌入是其中的重要一环。
如果你问一个人他们如何将单词转化为自己理解的意义的,他们可能会支支吾吾地说出 "因为我知道它们是什么意思 "这样含糊不清、自说自话的话。在我们大脑深处有一个复杂的结构,它知道 "孩子 "和 "小孩 "基本上是一样的,"红色 "和 "绿色 "都是颜色,而 "高兴"、"快乐 "和 "欣喜 "代表的是同一种情绪,只是程度不同而已。我们无法解释其原理,但我们就是知道。
语言模型对语言也有类似的复杂理解,只不过,因为它们是计算机,所以理解并不在它们的大脑中,而是由数字组成的。在LLM的世界中,任何一段人类语言都可以用数字向量(列表)来表示。这个数字向量就是嵌入。
LLM 技术的一个关键部分是翻译器,它可以从人类文字语言翻译成人工智能数字语言。我们称这种翻译机为 "嵌入机(embedding machine)",尽管在其底层它只是调用了一个应用程序接口。输入人类语言,输出人工智能数字语言。
这些数字意味着什么?没有人知道!它们只对AI来说有“意义”。但是,我们知道的是,相似的单词最终会有相似的一组数字。因为在幕后,AI使用这些数字来“阅读”和“说话”。因此,这些数字在AI语言中具有某种神奇的意义,即使我们无法理解。嵌入机(embedding machine)就是我们的翻译器。
现在,由于我们有了这些神奇的AI数字,我们可以将它们绘制出来。上述例子的简化图可能是这样的,其中的坐标轴仅是人类/AI语言的某种抽象表示:
绘制出这些点之后,我们可以发现,在这个假设的语言空间中,两个点越接近,它们就越相似。例如,“Hello, how are you?” 和 “Hey, how’s it going?” 几乎在同一个位置。另一个问候语“Good morning”也不太远。而“I like cupcakes”则完全与其他内容分开,它们在语言空间中的位置远离其他文本片段。
当然,不可能在一张二维图上表示全部人类语言,但理论是一样的。实际上,嵌入具有更多维的坐标(OpenAI当前使用的模型使用的嵌入向量是由1,536个数字组成的。)。但是你仍然可以通过基本的数学运算来确定两个嵌入——因此两个文本片段之间的接近程度。
这些嵌入和去确定“接近程度”是语义搜索的核心原理,它为检索步骤提供了动力。
八、使用嵌入查找最佳的知识库片段
一旦我们了解了使用嵌入进行搜索的工作原理,我们就能构建出检索步骤的高层次图景。
在索引方面,我们首先要将知识库分解成多个文本块。这个过程本身就是一个完整的优化问题,我们将在下一步进行介绍,但现在只需假设我们已经知道如何做。
完成上述操作后,我们将每个知识片段传递给嵌入机(embedding machine)(实际上是OpenAI API或类似的程序接口)进行处理,然后得到该文本的嵌入表征。然后,我们将该片段与其嵌入一起保存在向量数据库中(这是一种专门用于处理数字向量的数据库,可用于存储和检索嵌入向量。)
现在,我们已经有了一个包含所有内容嵌入的数据库。从理论上来讲,可以将其视为我们整个知识库在“语言”图谱上的绘制:
有了这个图谱之后,在查询方面,我们也要做类似的处理。首先,我们获取用户输入的嵌入:
然后,我们将其绘制在同一向量空间中,并找出最接近的片段(本例中为 1 和 2):
这个神奇的嵌入机(embedding machine)认为这些是与所提出的问题最相关的答案,因此这些就是我们提取出来发送给LLM的知识片段!
实际上,“最接近的点是哪个”这个问题是通过查询向量数据库来解决的。因此,实际过程看起来更像是这样:
知识片段的查询本身涉及一些比较复杂的数学计算,通常使用余弦距离进行计算,当然还有其他计算方法。数学是一个可以深入研究的领域,但不在本篇文章的讨论范围之内,而且从实用的角度来看,它可以大部分转移到函数库或数据库中。
现在回到LangChain代码
在一开始的LangChain代码示例中,已经涵盖了这一行代码所做的所有工作。这个小函数的调用隐藏了背后很复杂的流程!
index.query("What should I work on?")
九、为知识库创建索引
好的,我们马上就要完成了。我们现在已经了解了如何使用嵌入来找到知识库中最相关的内容,然后将所有内容传递给LLM,并获得检索增强后的答案。我们将要介绍的最后一步是从知识库中创建初始索引。换句话说,就是这张图片中的“知识分割机(knowledge splitting machine)”:
也许会令你惊讶,为知识库创建索引通常是整个流程中最难也是最重要的部分。不幸的是,它更像是一门艺术,而不是科学,需要反复试验、不断试错。
从大的方面看,索引过程可以分为两个高层次的步骤:
加载:从通常存储知识库的位置获取其内容。
分割:将知识(knowledge)分割成适合嵌入搜索的片段大小。
区分以下两个技术概念:
从技术角度来看,“加载器(loaders)”和“分割器(splitters)”之间的区别有些粗暴。你可以想象一个组件同时完成所有工作,也可以把加载阶段拆分成多个子组件。
尽管如此,"加载器 "和 "分割器 "在 LangChain 中就是这样实现的, "加载器"和"分割器"提供了一个有用的抽象层,使得底层的概念更易于理解和使用。它们为开发者提供了一种高层次的接口,使得他们可以更方便地处理和操作知识库的内容。
接下来以我自己的使用案例为例。我想构建一个Chatbot来回答关于我的SaaS脚手架产品SaaS Pegasus[3]的相关问题。我想要添加到知识库中的第一个内容就是文档站点。加载器是一种基础设施,它访问我的文档站点,找出可用的页面,然后拉取每个页面。当加载器工作完成后,会输出单个文档,每个站点页面都有一个文档。
在加载器内部执行了很多步骤!需要爬取所有页面,抓取每个页面的内容,然后将HTML格式化为可用的文本。而针对其他内容的加载器,例如针对PDF或Google Drive,都由不同的加载器组成。还需要解决并行化、错误处理等等问题。再一次强调,这是一个几乎无限复杂的话题,但在本文中,我们将大部分内容转移使用函数库。因此,我们再一次假设有一个神奇的盒子,将“知识库”放入其中,然后单个的 "文档 "从这里出来。
从加载器中输出后,我们将得到与文档站点中的每个页面相对应的文档集合。此时,理想情况下,已经删除了额外的内容标记,只保留了底层结构和文本。
现在,我们可以将整个网页传递给嵌入机( embedding machine),并将其用作知识片段。但是,每个页面都可能涵盖了很多内容!而且,页面中的内容越多,该页面的嵌入就越“不具体”。这意味着我们的“closeness”搜索算法可能会失灵。(译者注:“closeness”搜索算法用于确定用户提出的问题与知识库中的文本片段之间的相似度,以便找到最相关的答案。该算法通常基于文本的词频、词向量或其他文本特征来计算相似度。)
更有可能的情况是,用户问题的主题与页面内的某些文本相吻合。这就是分割器(splitting)的作用所在。通过分割器,我们将任何单个文档分割成适合搜索的小块,使其更适合进行嵌入搜索。
此外,将文档分割成小块也是一门艺术,包括平均分割块的大小(如果太大,则无法很好地匹配查询,如果太小,则没有足够的有用上下文来生成答案),如何分割(通常是按标题分割,如果有的话),等等。但是,一些合理的默认设置足以开始处理和优化数据。
获得文档片段后,如上所述,我们将它们保存到向量数据库中,最后就大功告成了!
这就是为知识库创建索引的全部完整过程。
在LangChain中,整个索引过程都封装在这两行代码中。首先,我们初始化网站加载器,并告诉它我们要使用哪些内容:
loader = WebBaseLoader("http://www.paulgraham.com/greatwork.html")
然后,我们从加载器构建整个索引,并将其保存到向量数据库中:
index = VectorstoreIndexCreator().from_loaders([loader])
加载、分割、转化为嵌入和保存都是在幕后进行的。
十、总结整个过程
最后,我们可以完整地展示整个RAG管道。如下所示:
首先,我们需要对知识库进行索引。使用加载器获取知识并将其转换为单个文档,然后使用分割器将其转换为小块或片段。有了这些片段后,我们将它们传递给嵌入机,嵌入机将它们转换为可以用于语义搜索的向量。我们将这些嵌入向量与其文本片段一起保存在向量数据库中。
接下来是检索。该过程始于提出的问题,然后将问题通过相同的嵌入机发送到向量数据库中,以确定最匹配的片段,最后将使用这些片段来回答问题。
最后是检索增强的答案生成。将知识片段与自定义的system prompt和我们提出的问题一起格式化,最终得到针对具体语境的答案。
参考资料
[1]http://www.paulgraham.com/greatwork.html
[2]https://scriv.ai/a/scriv/bots/b/pg-bot/chat/
[3]https://www.saaspegasus.com/
[4]https://python.langchain.com/docs/integrations/document_loaders/
[5]https://python.langchain.com/docs/modules/data_connection/document_transformers/
如果字段的最大可能长度超过255字节,那么长度值可能…
只能说作者太用心了,优秀
感谢详解
一般干个7-8年(即30岁左右),能做到年入40w-50w;有…
230721