利用LangChain实现RAG
RAG介绍
定义
大型语言模型(LLM)相较于传统的语言模型具有更强大的能力,然而在某些情况下,它们仍可能无法提供准确的答案。为了解决大型语言模型在生成文本时面临的一系列挑战,提高模型的性能和输出质量,研究人员提出了一种新的模型架构:检索增强生成(RAG, Retrieval-Augmented Generation)。该架构巧妙地整合了从庞大知识库中检索到的相关信息,并以此为基础,指导大型语言模型生成更为精准的答案,从而显著提升了回答的准确性与深度。
目前 LLM 面临的主要问题有:
- 信息偏差/幻觉: LLM 有时会产生与客观事实不符的信息,导致用户接收到的信息不准确。RAG 通过检索数据源,辅助模型生成过程,确保输出内容的精确性和可信度,减少信息偏差。
- 知识更新滞后性: LLM 基于静态的数据集训练,这可能导致模型的知识更新滞后,无法及时反映最新的信息动态。RAG 通过实时检索最新数据,保持内容的时效性,确保信息的持续更新和准确性。
- 内容不可追溯: LLM 生成的内容往往缺乏明确的信息来源,影响内容的可信度。RAG 将生成内容与检索到的原始资料建立链接,增强了内容的可追溯性,从而提升了用户对生成内容的信任度。
- 领域专业知识能力欠缺: LLM 在处理特定领域的专业知识时,效果可能不太理想,这可能会影响到其在相关领域的回答质量。RAG 通过检索特定领域的相关文档,为模型提供丰富的上下文信息,从而提升了在专业领域内的问题回答质量和深度。
- 推理能力限制: 面对复杂问题时,LLM 可能缺乏必要的推理能力,这影响了其对问题的理解和回答。RAG 结合检索到的信息和模型的生成能力,通过提供额外的背景知识和数据支持,增强了模型的推理和理解能力。
- 应用场景适应性受限: LLM 需在多样化的应用场景中保持高效和准确,但单一模型可能难以全面适应所有场景。RAG 使得 LLM 能够通过检索对应应用场景数据的方式,灵活适应问答系统、推荐系统等多种应用场景。
- 长文本处理能力较弱: LLM 在理解和生成长篇内容时受限于有限的上下文窗口,且必须按顺序处理内容,输入越长,速度越慢。RAG 通过检索和整合长文本信息,强化了模型对长上下文的理解和生成,有效突破了输入长度的限制,同时降低了调用成本,并提升了整体的处理效率。
工作流程
RAG 是一个完整的系统,其工作流程可以简单地分为数据处理、检索、增强和生成四个阶段:
- 数据处理阶段
- 对原始数据进行清洗和处理。
- 将处理后的数据转化为检索模型可以使用的格式。
- 将处理后的数据存储在对应的数据库中。
- 检索阶段
- 将用户的问题输入到检索系统中,从数据库中检索相关信息。
- 增强阶段
- 对检索到的信息进行处理和增强,以便生成模型可以更好地理解和使用。
- 生成阶段
- 将增强后的信息输入到生成模型中,生成模型根据这些信息生成答案。
词向量
RAG技术是基于向量
概念的。
词向量
在机器学习和自然语言处理(NLP)中,词向量(Embeddings)是一种将非结构化数据,如单词、句子或者整个文档,转化为实数向量的技术。这些实数向量可以被计算机更好地理解和处理。
嵌入背后的主要想法是,相似或相关的对象在嵌入空间中的距离应该很近。

举个例子,我们可以使用词嵌入(word embeddings)来表示文本数据。在词嵌入中,每个单词被转换为一个向量,这个向量捕获了这个单词的语义信息。例如,”king” 和 “queen” 这两个单词在嵌入空间中的位置将会非常接近,因为它们的含义相似。而 “apple” 和 “orange” 也会很接近,因为它们都是水果。而 “king” 和 “apple” 这两个单词在嵌入空间中的距离就会比较远,因为它们的含义不同。
词向量的优势
在RAG(Retrieval Augmented Generation,检索增强生成)方面词向量的优势主要有两点:
- 词向量比文字更适合检索。当我们在数据库检索时,如果数据库存储的是文字,主要通过检索关键词(词法搜索)等方法找到相对匹配的数据,匹配的程度是取决于关键词的数量或者是否完全匹配查询句的;但是词向量中包含了原文本的语义信息,可以通过计算问题与数据库中数据的点积、余弦距离、欧几里得距离等指标,直接获取问题与数据在语义层面上的相似度;
- 词向量比其它媒介的综合信息能力更强,当传统数据库存储文字、声音、图像、视频等多种媒介时,很难去将上述多种媒介构建起关联与跨模态的查询方法;但是词向量却可以通过多种向量模型将多种数据映射成统一的向量形式。
如何构建词向量?
在搭建 RAG 系统时,我们往往可以通过使用嵌入模型来构建词向量,我们可以选择:
- 使用各个公司的 Embedding API;
- 在本地使用嵌入模型将数据构建为词向量。
向量数据库
什么是向量数据库
向量数据库是用于高效计算和管理大量向量数据的解决方案。向量数据库是一种专门用于存储和检索向量数据(embedding)的数据库系统。它与传统的基于关系模型的数据库不同,它主要关注的是向量数据的特性和相似性。
在向量数据库中,数据被表示为向量形式,每个向量代表一个数据项。这些向量可以是数字、文本、图像或其他类型的数据。向量数据库使用高效的索引和查询算法来加速向量数据的存储和检索过程。
向量数据库的原理及核心优势
向量数据库中的数据以向量作为基本单位,对向量进行存储、处理及检索。向量数据库通过计算与目标向量的余弦距离、点积等获取与目标向量的相似度。当处理大量甚至海量的向量数据时,向量数据库索引和查询算法的效率明显高于传统数据库。因为使用了索引,所以非常快。
主流的向量数据库
- Chroma:是一个轻量级向量数据库,拥有丰富的功能和简单的 API,具有简单、易用、轻量的优点,但功能相对简单且不支持GPU加速,适合初学者使用。
- Weaviate:是一个开源向量数据库。除了支持相似度搜索和最大边际相关性(MMR,Maximal Marginal Relevance)搜索外还可以支持结合多种搜索算法(基于词法搜索、向量搜索)的混合搜索,从而搜索提高结果的相关性和准确性。
- Qdrant:Qdrant使用 Rust 语言开发,有极高的检索效率和RPS(Requests Per Second),支持本地运行、部署在本地服务器及Qdrant云三种部署模式。且可以通过为页面内容和元数据制定不同的键来复用数据。
检索方式
我们主要利用两种方式进行检索:
- 基本语义相似度(Basic semantic similarity)
- 最大边际相关性(Maximum marginal relevance,MMR)
similarity_search主要是依据词向量的余弦相似度来搜索。
MMR则首先用similarity_search找出前n个最相似的文档,但最后返回的是这n个文档中相互最不相似的k个文档,这样我们可以增加搜索结果的丰富度。
文本加载和切割
现在让我们正式开始建立向量数据库的步骤。
文本加载
在这里只展示PDF文档和MD文档的加载。
安装依赖:
1 | !pip install -q pypdf |
加载PDF:
1 | from langchain.document_loaders import PyPDFLoader |
langchain.schema.document.Document
类型包含两个属性。
page_content
:包含该文档页面的内容。meta_data
:为文档页面相关的描述性数据。
1 | print(page.page_content[0:500]) |
加载****MarkDown:
1 | UnstructuredMarkdownLoader(file_path) |
文档切割
一、为什么要进行文档分割
模型大小和内存限制:GPT 模型,特别是大型版本如 GPT-3 或 GPT-4 ,具有数十亿甚至上百亿的参数。为了在一次前向传播中处理这么多的参数,需要大量的计算能力和内存。但是,大多数硬件设备(例如 GPU 或 TPU )有内存限制。文档分割使模型能够在这些限制内工作。
- 计算效率:处理更长的文本序列需要更多的计算资源。通过将长文档分割成更小的块,可以更高效地进行计算。
- 序列长度限制:GPT 模型有一个固定的最大序列长度,例如2048个 token 。这意味着模型一次只能处理这么多 token 。对于超过这个长度的文档,需要进行分割才能被模型处理。
- 更好的泛化:通过在多个文档块上进行训练,模型可以更好地学习和泛化到各种不同的文本样式和结构。
- 数据增强:分割文档可以为训练数据提供更多的样本。例如,一个长文档可以被分割成多个部分,并分别作为单独的训练样本。
需要注意的是,虽然文档分割有其优点,但也可能导致一些上下文信息的丢失,尤其是在分割点附近。因此,如何进行文档分割是一个需要权衡的问题。
二、文档分割方式
Langchain 中文本分割器都根据 chunk_size (块大小)和 chunk_overlap (块与块之间的重叠大小)进行分割。
- chunk_size 指每个块包含的字符或 Token (如单词、句子等)的数量
- chunk_overlap 指两个块之间共享的字符数量,用于保持上下文的连贯性,避免分割丢失上下文信息

Langchain提供多种文档分割方式,区别在怎么确定块与块之间的边界、块由哪些字符/token组成、以及如何测量块大小。

基于字符分割
如何进行文本分割,往往与我们的任务类型息息相关。当我们拆分代码时,这种相关性变得尤为突出。因此,我们引入了一个语言文本分割器,其中包含各种为 Python、Ruby、C 等不同编程语言设计的分隔符。在对这些文档进行分割时,必须充分考虑各种编程语言之间的差异。
我们将从基于字符的分割开始探索,借助 LangChain
提供的 RecursiveCharacterTextSplitter
和 CharacterTextSplitter
工具来实现此目标。
CharacterTextSplitter
是字符文本分割,分隔符的参数是单个的字符串;RecursiveCharacterTextSplitter
是递归字符文本分割,将按不同的字符递归地分割(按照这个优先级[“\n\n”, “\n”, “ “, “”]),这样就能尽量把所有和语义相关的内容尽可能长时间地保留在同一位置。因此,RecursiveCharacterTextSplitter
比 CharacterTextSplitter
对文档切割得更加碎片化
RecursiveCharacterTextSplitter
需要关注的是如下4个参数:
separators
- 分隔符字符串数组chunk_size
- 每个文档的字符数量限制chunk_overlap
- 两份文档重叠区域的长度length_function
- 长度计算函数
1 | # 导入文本分割器from langchain.text_splitter import RecursiveCharacterTextSplitter, CharacterTextSplitter |
结果:
1 | ['在AI的研究中,由于大模型规模非常大,模', |
基于Token切割
很多 LLM 的上下文窗口长度限制是按照 Token 来计数的。因此,以 LLM 的视角,按照 Token 对文本进行分隔,通常可以得到更好的结果。 通过一个实例理解基于字符分割和基于 Token 分割的区别。
1 | # 使用token分割器进行分割, |
结果:
1 | ['foo', ' bar', ' b', 'az', 'zy', 'foo'] |
可以看出token长度和字符长度不一样,token通常为4个字符。
**其他:**如MarkDown这样的文档有专门的切割工具,可以依照文档的结构进行切割,例如MarkdownHeaderTextSplitter
、NotionDirectoryLoader
等。
示例:搭建并使用向量数据库
清洗数据并非本文重点,请参考一些数据科学的相关文章,在这里仅作简介。对于粗乱数据,可以先清理符号,例如剔除两边空格,剔除双换行等符号。
1 | # 环境安装(首先执行这些命令) |
当然,你也可以使用MMR进行检索:
1 | smalldb_chinese.max_marginal_relevance_search(question_chinese,k=2, fetch_k=3) |