乐于分享
好东西不私藏

LangChain RAG 实战|文档加载、PDF 解析、文本切分与嵌入完整实战

LangChain RAG 实战|文档加载、PDF 解析、文本切分与嵌入完整实战

RAG检索增强生成,是构建私有知识库的主流方案。

背景:大模型存在知识滞后、无法读取内部文档、易产生幻觉等痛点,而RAG就用来解决这类问题。

它预先处理PDF、Word等本地文档,用户提问时检索出相关文档片段,交给大模型依托真实资料生成答案。

本文顺着RAG完整流程,介绍文档加载、PDF解析、文本切分与嵌入全流程。

1.RAG
1.1什么是 RAG?
RAG(Retrieval‑Augmented Generation)检索增强生成。 简单说:给大模型外接私有知识库,解决大模型幻觉、知识老旧、不能读取自有文档的问题。

整体流程分为两大阶段:离线知识库预处理 和 用户在线问答

大致流程:RAG 就是检索增强生成,先对私有文档向量化建库;用户提问先检索相关上下文,再把上下文和问题交给大模型,让大模型依据外部资料生成回答,缓解大模型幻觉问题

下面是RAG工作流程图
RAG 检索增强生成工作流程

阶段 1:知识库预处理(离线构建向量库)

  1. 读取源文件
    非结构化数据加载器读取本地文件(PDF、Word、TXT 等),提取原始文本。
  2. 文本切分
    文本分割器把长文档切割成多个短小的文本块 Text Chunk。
  3. 生成向量
    文本块送入嵌入模型,转化为向量嵌入。
  4. 存入向量库
    将向量做索引并保存到向量数据库。

阶段 2:用户在线问答

  1. 用户发起提问
    用户问题交给嵌入模型,转为问题向量。
  2. 相似度检索
    拿着问题向量在向量数据库做相似度搜索,取出相似度高的向量,还原出对应的原始文本,作为回答的上下文 Context。
  3. 构造提示词
    通过提示词模板,把检索得到的上下文和用户问题拼接,得到完整 Prompt。
  4. 大模型推理
    把 Prompt 交给 LLM 大模型,大模型参考检索到的资料生成回答 Answer。
  5. 返回最终结果展示给用户。

💡核心逻辑:提前把文档变成向量存起来;用户提问后召回相关文档片段,把片段和问题一起交给大模型,让大模型基于真实资料作答。

 ⚠️注意:RAG 只能缓解幻觉,并不能完全消除幻觉。如果检索回来的原始资料本身存在错误,大模型依旧会输出错误内容。 

✅适用业务场景:企业内部知识库、产品手册问答、论文阅读助手、私有文档 AI 问答。

下面是对每一个流程做一个介绍

2.文档加载

想要做 RAG,第一步就是把各式各样的文件读取到程序中。下面以 Markdown、Docx、PDF 三种常见格式举例演示。

2.1加载Markdown

import loggingfrom langchain_community.document_loaders import UnstructuredMarkdownLoader#查看开发日志,ERROR,WARN,INFO,DEBUG四个日志等级是依次降低,如果设置INFO,他会打印INFO前面的包括它自己,# 开发者一般用ERROR,日志内容里面主要看error是什么原因logging.basicConfig(level=logging.DEBUG)def load_markdown():    #1.创建markdown文件的加载器,里面传入文档的位置,里面传入两个参数路径和mode    """    (如何分辨相对和绝对路径?绝对是以盘符开头,相对路径起点是当前代码所在目录的根目录,mode默认值是single)    :return:    """    非结构化的markdown加载器去加载markdown文件    loader = UnstructuredMarkdownLoader(         r"E:\Python\workspace\CozeApi\LangChain_Day03\d_retrieval\assets\sample.md",        mode = "elements"#标题是一个元素,标题下的文字以段落为单位也是一个元素    )    #2.markdown文档的加载器对象去加载文档    #这里是对象.函数名。函数名.load() 是 LangChain 文档加载器的实例方法,已经写好了只需要调用    documents = loader.load()#既然是段落,返回的是元素容器,#loader.load() 返回Document 对象组成的列表,不是字符串列表。    3.对容器里的数据进行遍历    for document in documents:        #4.想要拿到文本,必须访问 .page_content;直接打印对象不会只输出文字。        print(document.page_content,end="\n==========================\n")#每个段落用分隔符去分隔load_markdown()

2.2加载word

Markdown方法一样

只不过把加载器换成UnstructuredWordDocumentLoader

import loggingfrom langchain_community.document_loaders import UnstructuredWordDocumentLoaderlogging.basicConfig(level=logging.DEBUG)def load_word():    #第一步创建Word文档的加载器    loader = UnstructuredWordDocumentLoader(        r"E:\Python\workspace\CozeApi\LangChain_Day03\d_retrieval\assets\sample.docx",        mode= "elements"    )    #第二步word文档的加载器对象去加载文档    documents = loader.load()#返回的是元素容器    #第三步遍历元素容器    for document in documents:         print(document.page_content,end="\n======\n")load_word()
2.3加载PDF

PDF 解析存在很多挑战。对于复杂 PDF,需要进行文本提取、布局检测、表格解析、公式识别等处理。

在此处,我们介绍一个开源的,专门用于解析PDF的工具:Mineru。

官网:https://mineru.net/

MinerU 提供了 PDF、Word、PPT、图片等文件的解析,支持图像提取、OCR、公式、表格解析等功能。

总结:我们可以利用MinerU把PDF转成MarkDown

"""这里只需要改两个地方一个token,它是字符串,加双引号一个文件地址file_path最后调用函数batch_id = mineru_upload_file_demo()print(batch_id)打印获得batch_id"""import requestsimport osdef mineru_upload_file_demo():    token = "sk-MtwevmyunWZqn5sir1ergw3JIfyJpRS8EXEXNFEWIML7jBRD"    url = "https://mineru.net/api/v4/file-urls/batch"    header = {        "Content-Type""application/json",        "Authorization"f"Bearer {token}"    }    data = {    "files": [        {"name":"demo.pdf""data_id""abcd"}    ],    "model_version":"vlm"    }    file_path = [r"E:\Python\workspace\CozeApi\LangChain_Day03\d_retrieval\assets\sample.pdf"]    try:        response = requests.post(url,headers=header,json=data)        if response.status_code == 200:            result = response.json()            print('上传成功:{}'.format(result))            batch_id = result['data']['batch_id']            if result["code"] == 0:                batch_id = result["data"]["batch_id"]                urls = result["data"]["file_urls"]                print('batch_id:{},urls:{}'.format(batch_id, urls))                for i in range(0len(urls)):                    with open(file_path[i], 'rb'as f:                        res_upload = requests.put(urls[i], data=f)                        if res_upload.status_code == 200:                            print(f"{urls[i]} 上传成功")                        else:                            print(f"{urls[i]} 上传失败")            else:                print('apply upload url failed,reason:{}'.format(result.msg))            return batch_id        else:            print(f"请求失败,状态码:{response.status_code},响应内容:{response.text}")    except Exception as err:        print(err)batch_id = mineru_upload_file_demo()print(batch_id)
"""改动两处一个token,一个获取后的batch_id"""def mineru_check_result_demo():    import requests    import time    token = "sk-MtwevmyunWZqn5sir1ergw3JIfyJpRS8EXEXNFEWIML7jBRD"    batch_id = "7fd416bb-c2e0-4935-8199-53a3d815f143"    url = f"https://mineru.net/api/v4/extract-results/batch/{batch_id}"    header = {        "Content-Type""application/json",        "Authorization"f"Bearer {token}"    }    res = requests.get(url, headers=header)    while res.json()["data"]['extract_result'][0]['state'] != 'done':        print('当前状态为running,等待3秒后重试')        time.sleep(3)        res = requests.get(url, headers=header)        print(res.status_code)        print(res.json()["data"]['extract_result'][0]['state'],end="\n\n=========\n\n")    print('提取结果为:',res.json()["data"]['extract_result'][0]['full_zip_url'])mineru_check_result_demo()

调用 API 拿到 zip 结果包后,解压得到解析完成的 markdown 文件,之后就可以用`UnstructuredMarkdownLoader`加载解析后的 md 文件,接入后续的切分、向量化流程,打通完整 RAG 链路。

3.文档切分

拿到 Document 对象之后,我们不能直接把整篇大文档送入向量库,需要做文本切分,切割为大小合适的文本块 Chunk。

为什么要切分?

  1. 如果直接把完整大文档送入 Prompt,会带入大量无关信息,干扰大模型生成效果。
  2. 大模型有最大 token 输入限制,超长文档会被截断,丢失关键信息。
3.1切分过程可以通过如下流程图所示:

3.2流程大概解释

核心思想:优先用高级分隔符切,切完块太大,就换低级分隔符继续切,递归处理,尽量不破坏语义完整

默认分隔符优先级从高到低:\n\n(段落换行)→\n(换行)→句号→逗号→空格→字符。

流程图步骤梳理

1.第一步:拿最高优先级分隔符切原始文本 取分隔符列表第一个符号切文本;分隔符列表去掉已经用过的第一个,剩下次级分隔符备用。意思就是:取分隔符列表第一个符号切文本;分隔符列表去掉已经用过的第一个,剩下次级分隔符备用。

2.遍历切出来的每一小块

  • ✅ 如果这块大小 ≤ chunk_size:属于合格小块,先存起来。

  • ❌ 如果这块超过 chunk_size(太大):

    • 没有分隔符了:直接把当前这块存入结果,继续处理后面片段。

    • 还有分隔符:递归!拿剩下的次级分隔符,对这个超大块重新走一遍整套切分流程,回到流程开头。

    1. 先把前面暂存的合格小块合并,清空暂存区;

    2. 判断:后面

      还有没有剩下的次级分隔符?

  1. 循环往复,直到全部文本处理完毕,输出全部 chunk。

“递归” 关键点:遇到切完依然超大的片段,不用暴力硬截断,降级用下一级分隔符再切一遍这个片段,尽可能保证句子、段落语义完整,而不是直接在半个词中间砍断文本

生活例子:先用大刀切猪肉,大刀用完扔掉;切完检查每块肉重量,重量 ≤ chunk_size就存起来;如果肉块太重>chunk_size,还有小刀就拿剩下的小刀,专门反复切割这块超重的肉;小刀全部用完还切不达标,就直接保存这块肉。

3.3代码演示
from langchain_community.document_loaders import UnstructuredWordDocumentLoaderfrom langchain_text_splitters import RecursiveCharacterTextSplitter#1.创建加载器对象并加载文档documents = UnstructuredWordDocumentLoader(    file_path="./assets/sample.docx"#文档路径    mode= "single" #以整体作为文档加载->默认值).load()#2.准备切分器,并切割加载后的文档chunks = RecursiveCharacterTextSplitter(    separators=["\n\n""\n""。""!""?""……"","""],      # 分隔符列表    chunk_size=400,  # 每个块的最大长度    chunk_overlap=50,  # 每个块重叠的长度    length_function=len,  # 可选:计算文本长度的函数,默认为字符串长度,可自定义函数来实现按 token 数切分    add_start_index=True,  # 可选:块的元数据中添加此块起始索引).split_documents(documents)#3.查看切分结果print(chunks)
3.4结果分析
[Document(metadata={'source''./assets/sample.docx''start_index'0}, page_content='中华人民共和国民法典\n\n
结果是一个列表,Document是元素,metadata是对象,'source'是对象的属性以字典形式,start_index是从哪里开始切分, page_content切割后的内容
4.文档嵌入
文档切分成 Chunk 之后,需要使用嵌入模型,将文本转为向量,后续用于相似度检索。

将文档切分成合适的大小之后,就可以使用嵌入模型生成文档的嵌入向量,后续检索时用于与查询的嵌入向量进行相似度计算。

关键点:文档和用户提问,必须使用同一个嵌入模型,否则向量空间不一致,检索会完全失效。

embed_documents():处理知识库文档片段

embed_query():处理用户查询语句

4.1代码演示

"""使用嵌入模型,转换单个文件和多个文件为向量数据"""from langchain_huggingface import HuggingFaceEmbeddings#1.获取嵌入模型对象model = HuggingFaceEmbeddings(    model_name = "./assets/models/bge-base-zh-v1.5",)#2.单文本嵌入,通过嵌入模型,把需要转成向量的数据转成向量query = "你好,世界"result1 = model.embed_query(query)print(len(result1))   #768 ->维度[bge-base-zh-v1.5]print(result1)print("==============================")#3.多文本嵌入documents = ["你好,世界","你好,中国"]result2=model.embed_documents(documents)print(len(result2))   # 这里是2,为什么是2?单个是把所有装着,现在是两个,#有两个向量列表,里面子列表才是768print(result2)#转换后的向量数据

📝知识点快速回顾

  1. RAG 检索增强生成
    分为离线知识库构建、在线问答两大阶段,利用私有文档缓解大模型幻觉问题。
  2. 文档加载
    Markdown、Word 使用 Unstructured 系列加载器;复杂 PDF 优先 MinerU 解析输出 Markdown。
  3. 递归文本切分 RecursiveCharacterTextSplitter
    按照分隔符优先级递归切割,尽可能保留语义完整。
  4. Embedding 嵌入模型
    文本向量化,文档和查询必须使用同一套模型,用于向量库相似度检索。

到这里,我们就完成了 RAG 知识库上游全部预处理流程。下一期将继续讲解向量库存储、检索策略,以及 Milvus 的介绍与部署。

我们下期再见👋。