RAG检索增强生成,是构建私有知识库的主流方案。
背景:大模型存在知识滞后、无法读取内部文档、易产生幻觉等痛点,而RAG就用来解决这类问题。
它预先处理PDF、Word等本地文档,用户提问时检索出相关文档片段,交给大模型依托真实资料生成答案。
本文顺着RAG完整流程,介绍文档加载、PDF解析、文本切分与嵌入全流程。
整体流程分为两大阶段:离线知识库预处理 和 用户在线问答。
大致流程:RAG 就是检索增强生成,先对私有文档向量化建库;用户提问先检索相关上下文,再把上下文和问题交给大模型,让大模型依据外部资料生成回答,缓解大模型幻觉问题
RAG 检索增强生成工作流程阶段 1:知识库预处理(离线构建向量库)
- 读取源文件
非结构化数据加载器读取本地文件(PDF、Word、TXT 等),提取原始文本。 - 文本切分
文本分割器把长文档切割成多个短小的文本块 Text Chunk。 - 生成向量
文本块送入嵌入模型,转化为向量嵌入。 - 存入向量库
将向量做索引并保存到向量数据库。
阶段 2:用户在线问答
用户发起提问 用户问题交给嵌入模型,转为问题向量。 - 相似度检索
拿着问题向量在向量数据库做相似度搜索,取出相似度高的向量,还原出对应的原始文本,作为回答的上下文 Context。 - 构造提示词
通过提示词模板,把检索得到的上下文和用户问题拼接,得到完整 Prompt。 - 大模型推理
把 Prompt 交给 LLM 大模型,大模型参考检索到的资料生成回答 Answer。 返回最终结果展示给用户。
💡核心逻辑:提前把文档变成向量存起来;用户提问后召回相关文档片段,把片段和问题一起交给大模型,让大模型基于真实资料作答。
⚠️注意: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()
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(0, len(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_idelse: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 requestsimport timetoken = "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 链路。
拿到 Document 对象之后,我们不能直接把整篇大文档送入向量库,需要做文本切分,切割为大小合适的文本块 Chunk。
为什么要切分?
如果直接把完整大文档送入 Prompt,会带入大量无关信息,干扰大模型生成效果。 大模型有最大 token 输入限制,超长文档会被截断,丢失关键信息。

3.2流程大概解释
核心思想:优先用高级分隔符切,切完块太大,就换低级分隔符继续切,递归处理,尽量不破坏语义完整。
默认分隔符优先级从高到低:\n\n(段落换行)→\n(换行)→。句号→,逗号→空格→字符。
流程图步骤梳理
1.第一步:拿最高优先级分隔符切原始文本 取分隔符列表第一个符号切文本;分隔符列表去掉已经用过的第一个,剩下次级分隔符备用。意思就是:取分隔符列表第一个符号切文本;分隔符列表去掉已经用过的第一个,剩下次级分隔符备用。
2.遍历切出来的每一小块
✅ 如果这块大小 ≤ chunk_size:属于合格小块,先存起来。
❌ 如果这块超过 chunk_size(太大):
没有分隔符了:直接把当前这块存入结果,继续处理后面片段。
还有分隔符:递归!拿剩下的次级分隔符,对这个超大块重新走一遍整套切分流程,回到流程开头。
先把前面暂存的合格小块合并,清空暂存区;
判断:后面
还有没有剩下的次级分隔符?
循环往复,直到全部文本处理完毕,输出全部 chunk。
“递归” 关键点:遇到切完依然超大的片段,不用暴力硬截断,降级用下一级分隔符再切一遍这个片段,尽可能保证句子、段落语义完整,而不是直接在半个词中间砍断文本
生活例子:先用大刀切猪肉,大刀用完扔掉;切完检查每块肉重量,重量 ≤ chunk_size就存起来;如果肉块太重>chunk_size,还有小刀就拿剩下的小刀,专门反复切割这块超重的肉;小刀全部用完还切不达标,就直接保存这块肉。
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)
[Document(metadata={'source': './assets/sample.docx', 'start_index': 0}, page_content='中华人民共和国民法典\n\n将文档切分成合适的大小之后,就可以使用嵌入模型生成文档的嵌入向量,后续检索时用于与查询的嵌入向量进行相似度计算。
关键点:文档和用户提问,必须使用同一个嵌入模型,否则向量空间不一致,检索会完全失效。
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)#转换后的向量数据
📝知识点快速回顾
- RAG 检索增强生成
分为离线知识库构建、在线问答两大阶段,利用私有文档缓解大模型幻觉问题。 - 文档加载
Markdown、Word 使用 Unstructured 系列加载器;复杂 PDF 优先 MinerU 解析输出 Markdown。 - 递归文本切分 RecursiveCharacterTextSplitter
按照分隔符优先级递归切割,尽可能保留语义完整。 - Embedding 嵌入模型
文本向量化,文档和查询必须使用同一套模型,用于向量库相似度检索。
到这里,我们就完成了 RAG 知识库上游全部预处理流程。下一期将继续讲解向量库存储、检索策略,以及 Milvus 的介绍与部署。
我们下期再见👋。
夜雨聆风