如今,检索增强生成 (RAG) 技术已成为构建超越静态、预训练知识的 LLM 驱动系统的关键。本文将深入探讨 RAG 管道中一个经常被忽视但至关重要的环节:文档摄取。我们将一起构建一个轻量级、可扩展的 Markdown 知识库摄取管道,利用 Python 和 LangChain,为下游 RAG 系统(如向量搜索或语义检索)准备数据。

文档摄取的重要性

典型的 RAG 系统 包含三个核心组件:语言模型(例如 GPT-4、Mistral)、用于提取相关上下文的检索器,以及嵌入并索引以供检索的知识库。在进行任何检索之前,您的文档需要经过规范化、元数据丰富和拆分为可管理的数据块等处理。文档摄取 阶段直接影响 检索器 的性能。优质的 文档摄取 方案,能够显著提升检索的准确性和效率,最终影响整个 RAG 系统的效果。

想象一下,您有一个庞大的公司内部知识库,其中包含各种政策、指南和技术文档。如果这些文档未经妥善处理,LLM 将难以从中找到所需的信息。通过精心设计的 文档摄取 流程,我们可以将这些信息转化为 LLM 能够理解和利用的结构化数据。

知识库的构建与组织

为了更好地演示,我们使用一个结构化的 Markdown 文件目录,模拟一个内部知识库:

knowledge-base/
├── company-handbook/
├── developer-guides/
├── team-policies/

每个文件夹都包含按主题分组的 .md 文件。为了加载和处理这些数据,我们使用 LangChain 的 DirectoryLoaderTextLoader

import glob
import os
from langchain.document_loaders import DirectoryLoader, TextLoader
from langchain.text_splitter import CharacterTextSplitter

folders = glob.glob("knowledge-base/*")

def add_metadata(doc, doc_type):
    doc.metadata["doc_type"] = doc_type
    return doc

documents = []
for folder in folders:
    doc_type = os.path.basename(folder)
    loader = DirectoryLoader(
        folder,
        glob="**/*.md",
        loader_cls=TextLoader,
        loader_kwargs={"encoding": "utf-8"}
    )
    folder_docs = loader.load()
    documents.extend([add_metadata(doc, doc_type) for doc in folder_docs])

这种方法保留了知识库的结构,通过基于父文件夹为每个文档添加 doc_type 标签。此元数据稍后有助于在检索期间过滤或对文档进行评分。例如,您可以优先检索“developer-guides”文件夹中的文档,以便为开发人员提供更准确的信息。

实际案例:

假设您正在构建一个内部技术支持机器人。通过将文档按产品类型(如“产品 A”、“产品 B”)分类,并添加相应的 doc_type 元数据,您可以确保机器人能够优先检索与用户查询的产品相关的文档,从而提供更具针对性的帮助。

分割为数据块 (Chunking)

接下来,我们使用 LangChain 的 CharacterTextSplitter 将文档分割为重叠的文本块:

text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = text_splitter.split_documents(documents)

重叠的上下文可确保重要信息不会在段之间丢失,这在使用具有固定上下文窗口的基于 Transformer 的模型时尤其有用。

为什么要进行 Chunking 以及元数据的重要性?

Chunking 和元数据直接影响检索质量。以下是从实验中得出的一些经验教训:

  • 较小的 Chunk 提高检索精度,但过多的 Chunk 会增加计算成本。 例如,在处理法律文件时,较小的 Chunk 可能更合适,因为需要精确匹配关键词和短语。
  • 重叠的 Chunk 保持语义连续性,从而减少幻觉。 想象一下,一个句子被分割成两个 Chunk,重要的上下文信息丢失了。重叠的 Chunk 可以避免这种情况。
  • 添加像 doc_type 这样的元数据允许更智能的过滤,例如,将答案限制为“developer-guides”部分中的文档。 这有助于缩小检索范围,并提高结果的准确性。

以下是如何快速验证摄取的质量:

print(f"Total number of chunks: {len(chunks)}")
print(f"Document types found: {set(doc.metadata['doc_type'] for doc in documents)}")

数据示例:

假设您的知识库包含 1000 个 Markdown 文件,总共被分割成 5000 个 Chunk。通过分析 Chunk 的数量和 doc_type 的分布,您可以快速了解 文档摄取 流程是否正常工作,并确保所有文档都被正确处理。

从理论到实践:构建对话式 RAG

在设置 摄取管道 并准备好分块的文档之后,下一步是将所有内容连接到能够进行智能检索和对话的语言模型。这就是 LangChain 的 ConversationalRetrievalChain 的用武之地。

以下是将向量存储、检索器和 LLM 连接起来以创建可用于生产环境的 RAG 管道的核心逻辑:

from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationalRetrievalChain
from langchain.vectorstores import FAISS  # 示例向量存储,您可以选择其他向量存储

# 假设您已经创建了一个名为 'vectorstore' 的向量存储
# 例如: vectorstore = FAISS.from_documents(chunks, embeddings)

# 创建一个新的 Chat with OpenAI
llm = ChatOpenAI(temperature=0.7, model_name="gpt-3.5-turbo") #  你可以选择更强大的模型,如 gpt-4

# Alternative - if you'd like to use Ollama locally, uncomment this line instead
# llm = ChatOpenAI(temperature=0.7, model_name='llama3.2', base_url='http://localhost:11434/v1', api_key='ollama')

# Set up the conversation memory for the chat
memory = ConversationBufferMemory(memory_key='chat_history', return_messages=True)

# The retriever is an abstraction over the VectorStore used during RAG
retriever = vectorstore.as_retriever()

# Combine everything into a Conversational Retrieval Chain
conversation_chain = ConversationalRetrievalChain.from_llm(llm=llm, retriever=retriever, memory=memory)

每个组件的作用:

  • ChatOpenAI 或 Ollama LLM: 初始化用于生成响应的语言模型。您可以根据设置在 OpenAI(基于云)和 Ollama(本地)之间切换。选择合适的 LLM 对于 RAG 系统的性能至关重要。性能更强的 LLM 通常能够更好地理解上下文并生成更准确的响应。
  • ConversationBufferMemory: 存储正在进行的聊天历史记录,允许模型保持对话上下文。这对于后续问题或多轮交互至关重要。如果没有对话记忆,每次用户提问,模型都会像第一次见面一样,无法理解上下文。
  • retriever: 包装在向量存储周围,并处理基于用户的查询获取最相关块的逻辑。这是 RAG 的检索部分。检索器的质量直接影响 RAG 系统的效率。一个好的检索器能够快速准确地找到与用户查询相关的文档,从而减少 LLM 的计算负担。
  • ConversationalRetrievalChain: 将所有内容联系在一起。它使 LLM 能够从向量存储中检索相关知识,维护聊天历史记录,并在上下文中响应。这是 RAG 系统的大脑,负责协调各个组件并生成最终响应。

为什么这很强大?

通过将检索与聊天记忆相结合,您的 LLM 不仅成为静态响应器,而且成为会话知识助手。它可以查找最相关的文档,跟踪过去的查询,并处理后续操作,而无需每次都重新设计提示或上下文。

实际应用:

假设用户问:“公司休假政策是什么?”系统首先会检索与休假政策相关的文档 Chunk,然后 LLM 会根据这些 Chunk 生成回答。如果用户紧接着问:“那么病假呢?”,系统会利用聊天记忆,知道用户仍然在询问休假政策,并检索与病假相关的文档 Chunk,从而生成更准确的回答。

下一步

既然 摄取管道 已经到位,下一步是将块嵌入模型(如 OpenAI、HuggingFace Transformers 或 Ollama),并将它们存储在向量存储中,例如 FAISS 或 Chroma。嵌入模型将文本转换为向量表示,以便向量存储可以有效地搜索语义相似的文档。

这为构建由真实公司或项目文档提供支持的智能检索系统奠定了基础,无需进行微调。

数据说明:

通过将文档嵌入到向量空间中,您可以利用向量的相似度来找到与用户查询相关的文档。例如,如果用户查询“如何部署应用程序?”,系统可以找到与“部署”、“应用程序”等关键词相关的向量,并检索相关的文档 Chunk。

关键经验

  • Markdown 解析与 LangChain 加载器可靠地工作,尽管某些系统可能会出现编码问题。在处理 Markdown 文件时,确保使用正确的编码方式(如 UTF-8)可以避免出现乱码等问题。
  • 基于文件夹的元数据是组织文档上下文的一种简单但有效的策略。通过将文档按文件夹分类,并添加相应的 doc_type 元数据,您可以轻松地过滤和检索特定类型的文档。
  • Chunking 对于不同的用例(例如,聊天机器人与摘要器)并非一刀切;您可能需要不同的尺寸和重叠。对于聊天机器人,可能需要较小的 Chunk 以提高检索精度;对于摘要器,可能需要较大的 Chunk 以保持上下文的完整性。

实际案例:

在构建聊天机器人时,您可以尝试不同的 Chunk 大小和重叠值,并评估机器人的性能。如果机器人经常提供不准确或不相关的答案,您可以尝试减小 Chunk 大小并增加重叠值。如果机器人响应速度慢,您可以尝试增大 Chunk 大小并减少重叠值。

总结

构建强大的 摄取管道 是任何成功的 RAG 系统的基础。这个实验帮助我体会到周到的预处理如何极大地影响检索质量和语言模型的最终输出。

如果您正在处理内部文档或企业知识,那么正确地完成这一步骤将为您节省大量的下游痛苦。请务必重视 文档摄取 的每一个环节,从文档加载、元数据添加、Chunking 到向量存储,每一个环节都至关重要。通过精心设计和优化 文档摄取 流程,您可以构建一个高效、准确、智能的 RAG 系统,从而提高工作效率、改善用户体验并释放知识的价值。