首页 / 文章

从零开始使用LangChain实现RAG:加载器、分块组件、嵌入模型、评估方法。

逐步构建文档问答系统:文件加载、分块处理、嵌入模型、索引与检索阶段,以及如何对检索结果和答案进行评分。

发布者:REACTAPP.TOP阅读约 8 分钟1939 词

来源: Building a RAG System with LangChain

如今,利用大语言模型构建能够读取文档并回答相关问题的系统是极具实用价值的应用之一。这种技术模式被称为RAG,即检索增强生成。下面将详细介绍其各个组成部分、功能用途以及如何判断所构建的系统质量如何。

LangChain是什么

LangChain既不是模型,也不是数据库。它是一个框架,更类似于通用适配器。目前有数十家大语言模型提供商提供各自的API,还有数十种向量数据库和文档加载工具。如果没有这样的中间层,今天使用GPT-4,明天又要改用Claude,那就意味着需要重写一半的代码。LangChain则为所有这些工具提供了统一的接口。

这一名称源于它的第二项功能。真正的大语言模型应用很少只是单一的模型调用。你可能需要加载文档、对其进行总结,再针对总结结果输入新的提示语并格式化输出结果。链条式结构将这些步骤串联成完整的流程。对于RAG而言,这一链条始于用户的提问,该提问会被嵌入向量,进而用于数据库搜索,搜索到的最佳结果会被填入提示语模板中。随后用该模板调用大语言模型,解析其回复后将结果返回给用户。LangChain为每个步骤提供了相应的构建模块,因此无需手动拼接所有组件。

加载文档

首先,文档需要被导入,且不能以原始字节的形式出现。无论数据是PDF、网页、CSV、Word文件还是Markdown文件夹,加载器都会将其转换为标准的Document对象,该对象包含两个字段:page_content用于存储文本,metadata则用于记录文档的来源(页面、URL以及其他有用信息)。元数据在后续非常重要,因为它能帮助你向用户显示答案的来源。

from langchain.document_loaders import PyPDFLoader

loader = PyPDFLoader("handbook.pdf")
documents = loader.load()

这样,PDF的每一页都会生成一个Document对象,每个对象都包含相应的文本及页码。这是一个应当尽早养成的习惯:始终将文本的来源信息附在其上,因为从数据分块到在最终答案中引用来源的每一个后续步骤都依赖于这些信息。即便更换为其他文件类型,输出格式依然保持不变。

数据分块

一份文档可能是一份有200页的PDF。即便上下文窗口很大,也将整本书的内容都放入每个提示词中也是浪费资源、成本高昂的,而且往往不如向模型提供经过筛选的片段有效。因此,过长的文档会被分割成几百个标记长度的片段,只有最相关的部分会在有人提问时传递给大语言模型。

有两个设置最为重要。分块大小指的是每个分块的长度:过小会导致上下文丢失,过大则会包含无关文本。500个标记左右是一个合理的起点。重叠比例则是指相邻分块之间的共享程度。如果没有重叠,跨两个分块的句子就会失去意义;而设置适当的重叠比例后,两个分块都能理解句子的连贯性。

RecursiveCharacterTextSplitter是常用的选择。它会优先考虑自然的分界点,先尝试按段落分割,再按句子,最后按单词,只有在不得已的情况下才会在中途单词处进行分割。此外还有专门的选项:MarkdownHeaderTextSplitter适用于包含标题的文档,而TokenTextSplitter则用于需要统计实际标记数而非字符数的情况。

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50
)
chunks = splitter.split_documents(documents)

分块看似简单,却对质量有着重大影响。这也是人们最晚调整的设置,尽管糟糕的分块会悄悄限制后续所有流程的效果。如果分块有误,再多的检索技巧也无法挽救系统。

嵌入模型

计算机无法直接询问“哪个分块与我的问题相似?”。嵌入模型,也称为编码器,可将文本转换为向量,即一组数字,其核心特性是含义相似的文本会得到相似的向量。

可以把每段文本想象成存在于数百或数千维空间中的地址。一只狂吠的狗和一只叫个不停的幼犬会处于相邻的地址,而股市崩盘则位于遥远的某个位置。比较含义就变成了几何问题:两个点之间的距离有多近?

有两点需要澄清。首先,生成向量的是编码器,而非向量数据库。Chroma、Pinecone、Weaviate和Qdrant仅负责存储向量并实现快速检索,它们本身并不理解语言。人们常常将这两者混淆。其次,向量的维度由编码器决定。OpenAI的text-embedding-3-small模型生成的向量维度为1536,而像all-MiniLM-L6-v2这样的开源模型则生成384维的向量。更高的维度能捕捉更多细节,但代价是更大的存储需求和更慢的检索速度。

真实的嵌入向量难以直观想象,因此人们通常将其投影到2D或3D空间。将大量词语进行嵌入并作投影后,就会出现不同的聚类:动物聚集在一起,水果在另一处,车辆则在另一个角落。其实并没有人向模型告知这些分类规则,它是通过大量文本学习到“狗”和“猫”常出现在相似的上下文中的。

这就是RAG能够发挥作用的原因。如果用户询问公司对于育儿假的规定,该问题的向量会落在与“家庭休假福利”或“新父母休假”相关的文本片段附近,尽管这些表述并不完全相同。

处理流程

整个过程分为两个阶段。索引构建在初始化时进行一次:加载数据、将其分割成多个块、嵌入每个块并存储向量。后续新增的文档只需被嵌入并添加即可。查询检索则在每个问题处理时执行:使用相同的编码器对问题进行嵌入,从向量数据库中找到距离最近的块,将这些块与问题一起放入提示词中,再发送给大语言模型,这样模型的回答就会基于你的文档而非猜测。同一个向量数据库同时用于这两个阶段——索引时写入数据,检索时读取数据。这就是整个架构。由于索引构建只需执行一次,那些耗时的操作(如嵌入数千个块)不会影响用户的等待时间,只有单次查询的嵌入、搜索以及向大语言模型的调用才会产生影响。

在运行时,有四个角色参与其中:用户、你的应用程序代码、检索器以及大语言模型。用户向你的代码发送问题,你的代码会请求检索器提供相关内容片段,随后检索器会将问题嵌入并搜索数据库,最终返回最相似的前K个内容片段。你的代码会利用这些片段加上原始问题构建提示词,然后将其发送给大语言模型,该模型会根据提供的上下文生成答案,最后你的代码将这个答案返回。

LangChain通过两种抽象概念涵盖了上述大部分流程。LLM(或ChatModel)用于封装你的模型,而Retriever则负责处理向量存储的查询操作。将它们串联起来,框架便会处理其余的工作。

from langchain.chains import RetrievalQA

qa_chain = RetrievalQA.from_chain_type(
    llm=ChatOpenAI(),
    retriever=vectorstore.as_retriever(),
    return_source_documents=True
)
response = qa_chain.invoke({"query": "What is our parental leave policy?"})

短短几行代码就实现了一个可用的RAG系统,此时很容易想要就此停止。但不要这样。能够回答问题的系统还远不能算是值得信赖的系统。

为何要进行评估

常见的误区是只提出一个问题,得到一个看似合理的答案就认为任务已完成。RAG系统会在隐蔽处出现故障:检索模块可能抓取到错误的文本片段,大语言模型即便拥有良好的上下文也可能会产生幻觉,而文本分块则可能将答案一分为二。通过评估可以确定是哪个环节出了问题:是检索、生成还是两者都有,因为每一种问题的解决方法都不同。此外,评估还能为你提供衡量标准,因为你会不断尝试不同的文本分块大小、编码器和提示词,而没有量化指标的话,每一次改动都只是猜测而已。

检索指标

有四种标准指标,每种都能反映不同的信息:

  • Recall@K:在所有应该出现的文档中,有多少出现在前K个结果中?如果语料库中有4篇相关文档,其中3篇在前5名内,则Recall@5为0.75。较高的召回率意味着几乎没有遗漏重要文档。
  • Precision@K:在返回的K个结果中,有多少是相关的?5个结果中有3个相关,则精度为0.60。较高的精确率意味着错误结果较少。
  • MRR(平均倒数排名):仅考虑第一个正确结果的位置。如果在第2位找到正确答案,得分为0.50;如果在第1位则得分为1.0。该指标旨在奖励能够快速找到正确答案的情况。
  • nDCG(标准化折现累积增益):是最为精细的评估指标。它会综合考虑每篇文档的相关性及其排名位置,因此排名越靠前的相关文档权重越高,同时还会与最佳可能的排序结果进行比较。

在实际应用中,会使用两种评估指标:一种用于衡量完整性(召回率),另一种用于衡量排序质量(MRR或nDCG)。

答案质量

即便检索结果完美,也仍可能产生劣质答案,因此需要单独对答案进行评分。评估基于相同的精选测试集,其中每条记录都会将一个问题与应返回的片段以及参考答案对应起来。上述四种检索指标用于评估前半部分内容;而对于答案本身,则需从其他维度进行评分:

  • 真实性:答案是否基于检索到的上下文,还是凭空编造的?
  • 相关性:答案是否针对该问题?
  • 正确性:答案与真实答案相比如何?
  • 完整性:答案是否涵盖了所有应包含的内容?

常用的方法是让另一个大语言模型充当评判者。将问题、答案以及真实标准提供给它,让其对每个维度进行评分。这种方法虽不完美,但具备可扩展性。

测试集

上述每一个评估步骤都依赖于同一样东西:一组已知正确答案的问题集合。

这是最容易被忽视的部分。没有它,就无法进行任何评估。建议准备50到几百个示例,每个示例都包含实际的问题、包含答案的文本片段以及理想的回答。如果系统已上线,这些示例可来自真实用户查询;否则可使用合成数据:将文档输入大语言模型,让它生成问题与答案对,随后再进行审核和整理。

要妥善保护这个测试集。一旦建立起来,所有的改动都会以此为标准进行评分,从而判断该改动是否有帮助。

更难的部分

简而言之,RAG的流程是:将文档分割成块,对每个块进行向量化处理并存储起来,嵌入用户查询,找到最接近的向量,然后将这些块传递给大语言模型。LangChain为每一步提供了标准组件,评估结果也能显示其效果如何。但实际产品会面临更复杂的挑战:如何确定文档的最佳分割大小、哪种编码器最合适、如何保持索引的新鲜度、何时需要重新排序、是否要采用混合搜索方式。这些决策决定了一个简单的演示程序能否真正发挥作用,而它们都建立在上述基础之上。