初学者指南:为您的RAG应用程序实现网站分块与嵌入技术

引言
大型语言模型(LLMs)的进步开启了广泛的复杂自然语言应用,从文本生成到提供高度上下文化答案的个性化聊天机器人。
然而,像ChatGPT、LLAMA和Mistral这样的流行LLMs的一个限制是,它们的知识仅限于它们训练过的数据。例如,ChatGPT的训练数据截止到2023年4月,因此,它可能无法为比其训练数据更新的信息提供准确的答案。
解决这个问题的一种方法是通过检索增强生成(RAG)。简而言之,通过RAG,我们为LLMs提供额外的上下文信息以及查询,使它们能够提供高度相关的答案。除了书籍或内部文件等其他来源,网站信息是为LLMs添加上下文的流行选择。
RAG如何缓解LLM幻觉.png
RAG如何缓解LLM幻觉
在这篇文章中,我们将解释如何从网站提取内容并将其用作RAG应用中的LLMs的上下文。然而,在这样做之前,我们需要理解网站的基本原理。
网站架构的基础知识
在互联网上浏览各种网站时,你会遇到各种不同的布局、字体和信息结构。这种多样性源于网站通常基于特定架构构建的事实。网站架构规定了网站的组织方式、与用户的互动方式以及与其他系统的集成方式。
网站由三个主要组成部分组成:HTML、CSS和JavaScript。
HTML(超文本标记语言)定义了网页的结构和内容,为网站的布局和组织奠定了基础。
CSS(层叠样式表)控制网页的视觉样式和布局,通过颜色、字体和其他视觉元素增强用户体验。
JavaScript为网页添加交互性,使用户能够与网站互动并执行任务,如表单提交和数据操作。
为了创建一个用户友好的网站并提取其内容,我们还需要理解文档对象模型(DOM)的概念。
DOM将HTML元素表示为由浏览器在解析网页的HTML代码时生成的类似树形的数据结构。它动态地反映网页,并在用户与之互动时实时更新。
DOM由几个关键组成部分组成:
元素是DOM的构建块,代表单个HTML元素,如标题、段落和图像。
属性为元素添加信息,例如图像的src属性或元素的id属性。
文本指的是HTML元素内的文本内容。这些文本节点没有属性或子节点,被认为是DOM树中的叶节点。
理解这些元素对于有效的网页抓取至关重要,使我们能够提取网站内容,正如我们将在接下来的部分中看到的。
网页抓取及其挑战的基础
对于我们的RAG应用,我们可以使用各种网站作为信息来源,包括维基百科、Common Crawl、Google BigQuery公共数据集和Wayback Machine。本质上,网页抓取涉及从这些网站提取内容。
从网站抓取内容的最简单方法之一是利用它们的API。如果网站提供API,开发人员可以利用它以方便的格式提取内容。然而,并非所有网站都提供API,即使它们提供,数据也可能不是所需的格式。这就是网页抓取工具发挥作用的地方。
有许多在线网页抓取工具,但对于RAG应用,使用开源Python网页抓取库如BeautifulSoup、Selenium和Scrapy是一个不错的选择,因为它们易于与其他框架或库集成。
然而,进行网页抓取可能会面临一些挑战:
多样性:每个网站都有不同且独特的HTML结构。因此,我们通常需要为每个网站编写一个独特的抓取脚本以获取我们想要的特定内容。
稳定性:即使我们已经构建了一个抓取器来从特定网站获取我们想要的内容,那个网站也会在某个时候发生变化。因此,我们必须不断调整我们的抓取脚本以继续无误地使用它。
从网页抓取到向量嵌入的网络内容提取
这篇文章将提供一个逐步指南,介绍如何抓取网站并为RAG应用预处理内容。这个过程涉及网页抓取、分块和为每个块生成嵌入。让我们从网页抓取开始。
通过网页抓取提取网页内容
正如前面提到的,网页抓取涉及从网站提取内容或信息。有多种方法可以抓取网站,而在这篇文章中,我们将使用BeautifulSoup和requests库的组合进行网页抓取。
requests:一个用于发起HTTP请求的Python库。
BeautifulSoup:一个允许网页抓取和HTML解析的Python库,使我们能够有效地从网络提取文本内容。
对于我们的RAG案例,我们将使用维基百科文章作为我们的主要信息来源。接下来的部分将演示如何使用这些库从维基百科文章中检索文本信息。
让我们考虑一个例子,我们的目标是从维基百科上关于数据科学的文章中提取文本信息。我们可以通过使用requests库向文章的URL发送HTTP请求来实现这一点。
)
接下来,我们准备抓取这篇文章的内容。当你检查任何维基百科文章的元素时,你会注意到每篇文章在维基百科上共享类似的文档对象模型(DOM)结构。然而,由于我们想要提取文章的文本信息,我们可以专注于一个具有名为bodyContent的id属性的div元素。这个div包含了任何维基百科文章的所有文本内容,正如你在下图中看到的:
我们可以很容易地使用BeautifulSoup库提取这个div元素内的文本,如下所示:
code block
输出:
来自维基百科,自由的百科全书
跨学科的研究领域,从数据中提取知识和见解
不要与信息科学混淆。
通过分析由空间望远镜,宽场红外勘测探测器获取的天文调查数据,发现了彗星NEOWISE的存在。
数据科学是一个跨学科的学术领域[1],它使用统计学、科学计算、科学方法、流程、算法和系统从潜在的嘈杂、结构化或非结构化数据中提取或推断知识和见解。[2]
数据科学还整合了来自底层应用领域的领域知识(例如,自然科学、信息技术和医学)。[3] 数据科学是多方面的,可以被描述为科学、研究范式、研究方法、学科、工作流程和职业。[4]
这就是我们需要做的全部!一旦我们有了文本内容,我们需要将其转换为向量嵌入,文本数据的语义含义、上下文和相关性的数值表示。
然而,在将这段长文本转换为向量嵌入之前,需要一个中间步骤:分块。
网页分块
分块在处理像维基百科文章这样的大量文本数据时是必要的。直接将整个文本转换为单个向量可能不是最有效的方法,原因有几个。首先,它会导致一个高维向量,可能具有数百万维,需要大量的计算资源进行存储和处理。其次,将整个文档压缩成一个向量可能会丢失细微的语义信息和上下文,影响后续任务的性能。此外,这种方法可能会过度简化文档的内容,引入歧义并忽视关键细节。
相反,我们应该将文本分成块,每个块包含文本部分的细粒度和重要信息。有几种分块方法可用,例如固定大小长度分块和基于内容的分块。
在固定大小长度分块中,每个块的长度需要提前定义,分块过程将根据定义的长度进行。因此,每个块将具有相同的长度。
另一方面,基于内容的分块会在每个块之间产生不同的长度。这种方法根据提前设置的特定分隔符拆分文本。分隔符可以是任何东西,例如句号、逗号、空格、冒号、新行等。
在我们的示例中,我们将使用基于预定义块大小的递归方法来拆分文章。这意味着文本的拆分过程将从段落、新行、空格和字符开始,顺序进行,直到块大小等于预定义值。要递归地将文本分成块,我们可以使用LangChain中的RecursiveharacterTextSplitter,如下所示:
"""
Output: 67 Data science is "a concept to unify statistics, data analysis, informatics, and their related methods" to "understand and analyze actual phenomena" with data.[5] It uses techniques and theories drawn from many fields within the context of mathematics, statistics, computer science, information science, and domain knowledge.[6] However, data science is different from computer science and information science. Turing Award winner Jim Gray imagined data science as a "fourth paradigm" of science (empirical, """
输出:
数据科学是“将统计学、数据分析、信息学及其相关方法统一起来的概念”,以“用数据理解并分析实际现象”。[5] 它使用从数学、统计学、计算机科学、信息科学和领域知识等多个领域中提取的技术和理论。[6] 然而,数据科学与计算机科学和信息科学不同。图灵奖获得者吉姆·格雷将数据科学想象为科学的“第四范式”(经验的,
如你所见,如果我们根据递归方法拆分数据科学维基百科文章的文本内容,我们将得到67个块。
网页块到向量嵌入
现在我们已经将整篇文章的文本转换为块,让我们将每个块转换为向量嵌入。这种方法允许我们提取最相关的块作为我们RAG应用中LLM的上下文。
在现实世界的情况下,通常应用两种类型的向量嵌入:密集和稀疏嵌入。
密集嵌入由深度学习模型产生,如来自OpenAI或Sentence Transformers的模型。这种类型的嵌入以紧凑的格式表示文本,其中大多数元素是非零的,因此得名。
另一方面,稀疏嵌入由类似词袋模型的模型产生,如TF-IDF或BM25。这种类型的嵌入具有比密集嵌入更高的维度,其中大多数元素是零的,因此得名“稀疏”。
然而,我们也可以利用更先进的稀疏嵌入模型如SPLADE作为传统稀疏方法的替代品。SPLADE通过将BERT集成到其架构中,为每个块中的每个词添加上下文信息,产生比传统方法更丰富的稀疏向量。
你可以使用任何你喜欢的嵌入模型将块转换为嵌入;然而,本文将使用Sentence Transformers中的all-MiniLM-L6-v2模型。这个模型产生一个维度为384的密集嵌入来表示每个块。我们将使用这个模型在LangChain的帮助下生成嵌入。
要实例化all-MiniLM-L6-v2模型并生成嵌入,我们只需要调用LangChain的SentenceTransformerEmbeddings类并指定我们打算使用的模型名称。
embeddings = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")
现在,我们可以使用embed_documents方法为每个块生成向量嵌入。
chunk_embedding = embeddings.embed_documents([chunk_text[3]])
print(len(chunk_embedding[0]))
"""
Output:
384
"""
输出:
384
```
就是这样。现在我们有了每个块的向量嵌入。接下来,我们需要将所有的嵌入放入像Milvus这样的向量数据库中,以便在RAG中使用。
**向量数据库和与Milvus的RAG集成**
到目前为止,我们已经看到了如何从维基百科抓取一篇文章并预处理它,直到它变成一个准备好使用的嵌入。现在,让我们将同样的过程应用到五篇维基百科文章上,这些文章将作为我们稍后演示RAG过程的数据。
我们将获取涵盖数据科学、机器学习、《沙丘2》电影、iPhone和伦敦的五篇维基百科文章的URL。代码实现在这个笔记本中。
以下代码将抓取这五篇文章的内容,并将每篇文章分解为块:
```python
# 加载库
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores.milvus import Milvus
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.embeddings import SentenceTransformerEmbeddings
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
import requests
from bs4 import BeautifulSoup
wiki_articles = ["Data_science", "Machine_learning", "Dune:_Part_Two", "IPhone", "London"]
# 提取每篇维基百科文章的文本内容
texts = ""for article in wiki_articles:
response = requests.get(
url=f"https://en.wikipedia.org/wiki/{article}",
)
soup = BeautifulSoup(response.content, 'html.parser')
# 获取所有文本
all_texts = soup.find(id="bodyContent")
texts += all_texts.text
# 将文本分解为块
chunk_text = text_splitter.split_text(texts)
```
接下来,我们将每个块转换为嵌入,并将嵌入存储在Milvus中。
Milvus是一个开源向量数据库,简化了存储向量嵌入和执行各种任务,如向量搜索、语义相似性和问答。
我们可以通过LangChain的帮助将所有嵌入存储在名为“rag_milvus”的Milvus数据库中,以便稍后轻松进行RAG编排。
```python
vector_db = Milvus.from_texts(texts=chunk_text, embedding=embeddings, collection_name="rag_milvus")
```
现在我们已经将所有的嵌入放入名为“rag_milvus”的数据库中,我们准备使用它进行RAG。
接下来,我们需要将我们的数据库转换为检索器,这意味着我们的向量数据库将接受输入查询,使用我们选择的嵌入模型将其转换为嵌入,然后检索数据库中最相似的块作为我们LLM的上下文,以便它可以生成相关答案。
```python
retriever = vector_db.as_retriever()
```
下一步是定义将为我们的查询生成响应的LLM。为此,我们将使用ChatGPT 3.5 Turbo模型,我们可以通过LangChain的OpenAI包装器调用它。要使用此模型,你需要提供你独特的OpenAI API密钥,你可以从你的OpenAI账户中获取。
```python
import getpass
import os
os.environ["OPENAI_API_KEY"] = getpass.getpass()
llm = ChatOpenAI(model="gpt-3.5-turbo-0125")
```
接下来,我们需要定义我们的LLM的提示。提示是一种用于指导LLM响应的技术,根据所需的格式。在RAG应用中,你可以尝试你的提示,以确保模型以所需的格式生成响应。
在这篇文章中,我们将提供一个基本提示的示例,你可以用它来指导LLM的响应。
```python
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.
Use three sentences maximum and keep the answer as concise as possible.
Always say "thanks for asking!" at the end of the answer.
{context}
Question: {question}
Helpful Answer:"""
custom_rag_prompt = PromptTemplate.from_template(template)
```
最后,让我们使用LangChain定义RAG管道。在这一步中,我们将为我们的RAG应用提供必要的信息,例如上下文的来源(在我们的情况下,Milvus向量数据库)、查询的来源、LLM的提示以及要使用的LLM(在我们的情况下,ChatGPT 3.5 Turbo模型)。
通过配置这些元素以根据提供的上下文和查询生成响应,我们可以设置完整的RAG管道。
chu
现在我们准备好进行RAG了。假设我们现在有以下问题:“数据科学家是什么?”我们可以根据我们的向量数据库提供的上下文获得LLM的响应,代码如下:
```python
for chunk in rag_chain.stream("What is a Data Scientist?"):
print(chunk, end="", flush=True)
```
```plaintext
输出:
数据科学家是一种专业人士,他们将编程代码与统计知识结合起来,从数据中创建洞察。他们负责收集、清理、分析数据,选择分析技术,并部署预测模型。数据科学家在数学、计算机科学和领域专业知识的交汇处工作,解决复杂问题并发现隐藏的模式。
```
我们从LLM获得了准确的响应!你可能已经注意到,LLM的响应在最后也包含了“谢谢提问”这句话,与我们的提示相符。
如果你好奇哪些块的部分被用作我们LLM生成响应的上下文,你可以调用invoke方法并提供问题作为参数。
```python
contexts = retriever.invoke("What is a Data Scientist?")
print(contexts)
```
```plaintext
输出
[Document(page_content='A data scientist is a professional who creates programming code and combines it with statistical knowledge to create insights from data.[9]', metadata={'pk': 450048016754672937}), Document(page_content='The professional title of "data scientist" has been attributed to DJ Patil and Jeff Hammerbacher in 2008.[32] Though it was used by the National Science Board in their 2005 report "Long-Lived Digital Data Collections: Enabling Research and Education in the 21st Century", it referred broadly to any key role in managing a digital data collection.[33]', metadata={'pk': 450048016754672955}), Document(page_content='While data analysis focuses on extracting insights from existing data, data science goes beyond that by incorporating the development and implementation of predictive models to make informed decisions. Data scientists are often responsible for collecting and cleaning data, selecting appropriate analytical techniques, and deploying models in real-world scenarios. They work at the intersection of mathematics, computer science, and domain expertise to solve complex problems and uncover hidden patterns in', metadata={'pk': 450048016754672963})]
```
如你所见,我们LLM用来生成响应的第一个块与回答问题高度相关。这有助于LLM产生准确且上下文化的响应,即使查询来自内部文件的信息。
在我们的提示中,我们还指示LLM仅根据提供的上下文提供答案,如果提供的上下文不包含问题的答案,就不要编造任何答案。
为了评估这一步,让我们要求我们的LLM“总结《盗梦空间》的剧情”,尽管我们只有关于《沙丘2》电影的信息。我们将收到的响应应该是这样的:
```python
for chunk in rag_chain.stream("Summarize the plot of Inception"):
print(chunk, end="", flush=True)
```
```plaintext
输出:
我无法根据提供的上下文提供关于《盗梦空间》剧情的信息。
```
```python
contexts = retriever.invoke("Summarize the plot of Inception")
print(contexts)
```
```plaintext
输出
code block
我们的LLM的响应正是我们想要的,因为提供的上下文并没有真正回答这个问题。
结论
在这篇文章中,我们学习了如何逐步使用网站数据作为我们RAG应用中LLM响应的上下文。
首先,我们使用可用的网页抓取工具和Python库如BeautifulSoup抓取网站的内容。然后,我们将提取的内容分成块,以增加内容的粒度并提高我们LLM的响应质量。接下来,我们将每个块转换为向量嵌入。最后,我们将这些嵌入存储在Milvus向量数据库中,并使用数据库作为我们RAG场景的检索器。
当我们传递查询时,我们的向量数据库使用我们选择的嵌入模型将其转换为嵌入,然后检索数据库中最相似的块作为我们LLM的上下文,使其能够生成高度上下文化的响应。
你可以在这本笔记本中找到这篇文章中演示的所有代码。
技术干货
我决定给 ChatGPT 做个缓存层 >>> Hello GPTCache
我们从自己的开源项目 Milvus 和一顿没有任何目的午饭中分别获得了灵感,做出了 OSSChat、GPTCache。在这个过程中,我们也在不断接受「从 0 到 1」的考验。作为茫茫 AI 领域开发者和探索者中的一员,我很愿意与诸位分享这背后的故事、逻辑和设计思考,希望大家能避坑避雷、有所收获。
2023-4-14技术干货
门槛一降再降,易用性大幅提升!Milvus 2.2.12 持续升级中
一句话总结 Milvus 2.2.12 :低门槛、高可用、强性能。
2023-7-27技术干货
LLM 快人一步的秘籍 —— Zilliz Cloud,热门功能详解来啦!
此次我们在进行版本更新的同时,也增加了多项新功能。其中,数据迁移(Migration from Milvus)、数据的备份和恢复(Backup and Restore)得到了很多用户的关注。本文将从操作和设计思路的层面出发,带你逐一拆解 Zilliz Cloud 的【热门功能】。
2023-4-10