乐于分享
好东西不私藏

一份文档如何变成 RAG 知识库?LangChain 完整处理流程

一份文档如何变成 RAG 知识库?LangChain 完整处理流程

LangChain 完整处理RAG流程

搭建 RAG 知识库时,很多人首先会遇到一个问题:

能不能直接把一份 PDF、Word 或 Markdown 文档交给大模型?

通常不建议这样做。

一份文档可能有几十页甚至几百页,不仅容易超过模型的上下文长度,而且用户提出的问题往往只和其中几个段落有关。如果每次都把整份文档交给大模型,不仅成本更高,还容易受到无关内容干扰。

因此,在正式问答之前,需要先把原始文件处理成可以被检索的知识。

LangChain 提供了文档加载、文本切分、向量化、向量存储和检索等组件,可以把整个处理过程串联起来:


一、读取文件:获取原始文档内容

第一步,是使用 LangChain 的 Document Loader 读取文件。

针对不同的数据来源,LangChain 提供了不同的 Loader,例如:

  • • TextLoader:读取 TXT 文件;
  • • PyPDFLoader:读取 PDF 文件;
  • • CSVLoader:读取 CSV 文件;
  • • WebBaseLoader:读取网页内容;
  • • UnstructuredWordDocumentLoader:读取 Word 文件。

例如,使用 PyPDFLoader 读取一份 PDF:

from langchain_community.document_loaders import PyPDFLoaderloader = PyPDFLoader("knowledge.pdf")documents = loader.load()

load() 会读取文件内容,并将结果转换成 LangChain 后续组件能够处理的 Document 对象。

print(len(documents))print(documents[0].page_content)print(documents[0].metadata)

对于普通文本和结构简单的 PDF,可以直接使用 LangChain Loader。

如果 PDF 中包含复杂表格、多栏排版、图片或扫描内容,一般会使用 LangChain 集成的 Docling、Unstructured 等非结构化文档解析工具,尽可能保留原始文档结构,再进入后续处理流程。


二、加载文档:转换为 Document 对象

在 LangChain 中,读取出来的内容不会只用普通字符串表示,而是会被统一封装成 Document 对象。

一个 Document 主要包含两部分:

from langchain_core.documents import Documentdocument = Document(    page_content="LangChain 可以用于构建 RAG 应用。",    metadata={        "source": "knowledge.pdf",        "page": 1,    },)

其中:

  • • page_content:保存正文内容;
  • • metadata:保存文件来源、页码、标题等附加信息。

可以简单理解为:

Document = 文本内容 + 来源信息

为什么要转换为 Document

因为系统检索到一段内容后,不仅需要知道内容是什么,还需要知道它来自哪个文件、位于哪一页,以及回答时应该引用哪个来源。

同时,无论原始内容来自 PDF、Word、网页还是数据库,只要统一转换成 Document,后续就可以使用相同的切分、向量化和检索流程。

在 LangChain 中,读取文件和转换为 Document 通常会同时完成:

documents = loader.load()

返回结果一般是:

list[Document]

三、文本切分:将长文档拆分为 Chunk

文档加载完成后,通常不能直接对整份长文档进行向量化,而是需要先将它拆分成多个较小的文本块,也就是 Chunk。

这样做的目的,是让每个文本块只包含一段相对完整、主题集中的内容,从而提高后续检索的准确性。

LangChain 常见的文本切分方式包括:

  • • 字符切分:按照固定字符数量拆分;
  • • Token 切分:按照模型 Token 数量拆分;
  • • 递归切分:依次按照段落、句子和字符拆分;
  • • 结构化切分:按照 Markdown 标题、HTML 标签或代码结构拆分;
  • • 语义切分:根据文本语义变化确定切分位置。

其中,普通文本最常用的是 RecursiveCharacterTextSplitter

它会优先按照段落和句子切分;如果文本仍然过长,再继续按照字符切分,尽可能保留原有语义。

from langchain_text_splitters import RecursiveCharacterTextSplittersplitter = RecursiveCharacterTextSplitter(    chunk_size=500,    chunk_overlap=80,    separators=[        "\n\n",        "\n",        "。",        "!",        "?",        ";",        ",",        " ",        "",    ],)chunks = splitter.split_documents(documents)

这里有两个核心参数:

  • • chunk_size:每个文本块的最大长度;
  • • chunk_overlap:相邻文本块之间重复保留的内容。

chunk_overlap 可以减少一句话或一个完整概念刚好在切分位置被截断的问题。

文本切分并不是越碎越好。Chunk 太大,检索结果容易包含无关内容;Chunk 太小,又可能破坏语义完整性。

实际项目中,如果处理复杂 PDF、Word 或 PPT,一般会先通过 Docling、Unstructured 等工具提取标题、段落和表格结构,再使用 LangChain 进行二次切分。


四、向量化与存储:构建可检索的知识索引

文本切分完成后,我们已经得到多个相对独立的 Chunk。

但这些 Chunk 仍然是普通文本,计算机无法直接判断两段文字在语义上是否相近。因此,需要使用 Embedding 模型把文本转换成一组数字,也就是文本向量。

例如:

“LangChain 如何切分文档?”              ↓        Embedding 模型              ↓[0.12, -0.38, 0.71, ...]

语义越相近的文本,生成的向量通常也越接近。

例如,“如何拆分长文档?”和“长文本应该怎样切块?”虽然关键词不完全相同,但表达的意思接近,因此可以通过向量相似度找到彼此。

下面使用 LangChain 集成的 Hugging Face Embedding 模型:

from langchain_huggingface import HuggingFaceEmbeddingsembeddings = HuggingFaceEmbeddings(    model_name="BAAI/bge-m3",    encode_kwargs={        "normalize_embeddings": True,    },)

Embedding 模型不负责生成答案,它只负责把文本转换成可以进行相似度计算的向量。

生成向量后,还需要将向量保存到 Vector Store,也就是向量存储中。

下面使用 LangChain 集成的 Chroma:

from langchain_chroma import Chromavector_store = Chroma.from_documents(    documents=chunks,    embedding=embeddings,    collection_name="rag_documents",    persist_directory="./chroma_db",)

这段代码会依次完成:

读取每个 Chunk→ 调用 Embedding 模型生成向量→ 保存向量、原文和 metadata

向量存储中通常包含:

  • • 文本向量;
  • • 原始 Chunk;
  • • metadata
  • • 文档 ID。

检索时,用户问题也会使用同一个 Embedding 模型生成查询向量,然后在 Vector Store 中搜索相近的文档向量。

因此,可以把两者的职责理解为:

  • • Embedding:负责表示文本语义;
  • • Vector Store:负责保存和搜索语义向量。

五、混合检索:同时理解语义和关键词

文档完成向量存储后,就可以创建 Retriever,也就是检索器。

Retriever 可以简单理解为:

接收用户问题,并返回相关 Document 的组件。

单独使用向量检索时,系统能够理解语义相近但表达不同的问题。

但是对于错误码、产品型号、文件编号和专有名词等精确关键词,BM25 检索通常更加有效。

因此,实际 RAG 项目经常把两种检索方式组合起来:

  • • 向量检索:负责理解语义;
  • • BM25:负责匹配关键词。

这种方式称为混合检索,也就是 Hybrid Search。

先创建向量检索器:

vector_retriever = vector_store.as_retriever(    search_type="similarity",    search_kwargs={"k": 4},)

再创建 BM25 检索器:

import jiebafrom langchain_community.retrievers import BM25Retrieverbm25_retriever = BM25Retriever.from_documents(    chunks,    preprocess_func=jieba.lcut,)bm25_retriever.k = 4

对于中文文档,需要先进行中文分词,因此这里使用 jieba.lcut

最后,使用 EnsembleRetriever 融合两种检索结果:

from langchain_classic.retrievers.ensemble import EnsembleRetrieverhybrid_retriever = EnsembleRetriever(    retrievers=[        vector_retriever,        bm25_retriever,    ],    weights=[        0.6,        0.4,    ],)results = hybrid_retriever.invoke(    "E01 故障出现后应该怎样处理?")

在这个问题中:

  • • 向量检索可以理解“故障出现后怎样处理”的语义;
  • • BM25 可以准确匹配“E01”这个关键词;
  • • EnsembleRetriever 再将两种检索结果进行融合和排序。

这样既保留了语义检索能力,也提高了精确关键词的召回效果。

查看检索结果:

for index, doc in enumerate(results, start=1):    print(f"结果 {index}")    print(doc.page_content)    print(doc.metadata)

Retriever 返回的仍然是相关 Document,并不是最终答案。


六、总结

通过 LangChain,我们可以把一份普通文档逐步处理成可检索的知识内容。

其中:

  • • Document Loader 负责读取不同类型的文件;
  • • Document 负责统一保存正文和来源信息;
  • • Text Splitter 负责将长文档拆分成多个 Chunk;
  • • Embedding 模型负责将文本转换为向量;
  • • Vector Store 负责保存向量、原文和元数据;
  • • Retriever 负责根据用户问题检索相关内容。

对于普通问题,可以使用向量检索理解语义;对于错误码、型号和专有名词等内容,可以加入 BM25 关键词检索,再通过 EnsembleRetriever 融合两种检索结果。

到这里,我们已经完成了 RAG 中的文档读取、切分、向量化、存储和检索。

不过,检索返回的仍然只是相关文档片段。完整的 RAG 还需要把这些内容加入 Prompt,再交给大语言模型生成最终答案。