资讯详情

RAG文档加载解析实战:从PDF解析到PGVector向量库入库的关键步骤

📅 2026/10/17 9:48:28 | 华诺云谱 👁 阅读
RAG文档加载解析实战:从PDF解析到PGVector向量库入库的关键步骤
“文档都没有加载对后面检索质量再调也是白搭。”这句话是我在一堆RAG项目里踩坑之后最想说的。很多朋友搭RAG知识库第一个想到的词是向量库、是Embedding、是Prompt结果真正动手才发现第一道坎根本不是模型选型而是那份PDF里表格被拆得跟乱码一样Word文档页眉页脚满天飞扫描件压根提不出一行字。RAG检索增强生成中文里也叫检索增强生成本质上就是给大模型外挂一个可检索的知识库而文档加载解析就是这座知识库的地基。这篇文章会从文档加载解析这个角度切入把RAG链路里最容易忽视却最要命的一步讲透。我会结合LangChain、Unstructured、PyMuPDF这些常见的开源工具一步步拆解不同格式文档的加载策略、切块参数怎么选、元数据怎么设计、解析后怎么接入PGVector这样的向量库并把实操中经常踩的坑整理给你。适合正在搭企业知识库的工程师、准备做RAG进阶项目的同学以及那些“检索效果总是不好但不知道问题出在哪”的开发者。1. 先把RAG链路看清楚才知道文档加载有多重要1.1 RAG不是“脚本”那么简单好多教程把RAG写成三段式加载文档、向量化、问答。代码一跑效果稀碎然后就开始调Embedding模型、换Prompt模板忙了半天问题还在。我做了几个项目以后发现大部分RAG效果不好根因都出在第一阶段——文档解析。文档加载解析的任务不是把文字从文件里抄出来而是要把人类阅读的文档转造成大模型和向量检索系统都能理解的、结构化的、信息无损的文本单元。这里有个关键概念要拎清楚RAG检索增强生成的目标是“让答案有据可依”那么“据”的质量就取决于原始文档被还原得有多完整还原得越差后面检索和生成的天花板就越低。很多朋友一上来就用最快的方案pip install一个库、十行代码跑通看到控制台输出几段文字就觉得完事了。实际上这种“能跑”和“能用”之间差了十万八千里。文档加载解析要考虑的东西非常具体这个PDF是文字版还是扫描版扫描版要不要OCRWord里表格和段落的顺序是不是乱了HTML页面里的导航和广告有没有混进正文每一类问题都会直接污染后面的向量质量而这些问题在项目初期往往又不会立刻暴露要等到检索结果莫名奇妙时才想起回头查。1.2 文档加载在整个链路里的位置可以把RAG知识库拆成五个环节文档加载、解析清洗、切块与元数据构建、向量化与入库、检索生成。文档加载和解析清洗是前两环很多人误以为它们不创造“智能”实际上却是决定知识库上限的环节。你想想如果切块阶段拿到的文本是残缺的、错位的那么Embedding模型学到的语义就是残缺的向量检索召回的内容自然也是残缺的。LLM再聪明也只能基于残缺的上下文作答。所以这一步不是“体力活”而是真正的技术活它有大量格式兼容、编码处理、结构恢复等细节需要处理。从成本和效率来看文档加载解析也是整个链路里性价比最高的优化点。调Embedding模型、换大模型、写复杂的Prompt效果提升通常都是微调级别的但如果你把一个原本错乱漏字的PDF换成一份干净有序、带好元数据的文本检索效果的提升通常是肉眼可见的甚至可以说是质变。原因很简单信息是零散的还是完整的直接决定了召回内容的上限。做项目要抓主要矛盾文档加载解析是典型的用20%工作量撬动80%效果提升的环节。1.3 一个可以直接落地的架构示例在深入具体格式之前我先给一个我最近常用的架构草图对应现在的热词“基于FastAPILangChainLangGraphRAGPGVector的AI Agentic RAG”。这个架构的特点是用FastAPI提供API层LangChain负责文档加载和切块LangGraph管理多轮对话和Agent状态PGVector存储向量并做相似度检索。这个架构里文档加载解析模块独立成服务不同格式的文件走不同的Loader统一输出带元数据的Document对象再进入切块和向量化。这样的好处是一处解析多处复用后续不管接检索还是接Agent拿到手的永远是一份干净的数据源。选择这个架构还有一个现实原因企业知识库的场景通常不是单一格式的。今天上传一批PDF制度文件明天可能补一批Word合同后天又有什么Markdown格式的技术文档。如果全套流程写死在单个脚本里后期维护成本极高。把文档加载解析独立出来前端只负责上传文件后端按文件类型自动分发到不同解析器输出统一结构这对后续做多轮对话、Agent规划都是很舒服的。后面章节的代码我会围绕这个架构思路展开。2. 文档加载不同格式的解析策略与工具选型2.1 文本类文档别以为纯文本就没有解析问题纯文本.txt和Markdown看起来最没技术含量但恰恰是很多人翻车的地方。TXT文件最常见的坑是编码问题尤其是中文环境下的GBK和UTF-8混用直接导致乱码建议加载时统一尝试UTF-8、GB18030、GBK等编码加一个兜底逻辑。还有一种情况是文件表面上编码正常但中间混入了一些特殊不可见字符比如零宽空格、全角空格这些在人工阅读时看不出来进了向量库却会成为噪音所以字符级清洗在这一步就很重要。Markdown和HTML则是结构丢失的问题Markdown里的标题层级、代码块、列表如果被当作普通文本处理切块时语义边界就会被破坏HTML更是复杂标签、脚本、样式、导航栏这些噪音如果不过滤掉向量库里会混入大量无效内容。我的建议是用LangChain的TextLoader先做基础加载再配合MarkdownHeaderTextSplitter或自定义HTML解析器把文档结构信息保留下来作为后续切块的依据。像HTML这种我通常会先用BeautifulSoup把正文区域提取出来再去掉script、style、header、footer等噪声节点。这一步做得好能直接减少后面30%以上的无效向量。2.2 PDF真实世界的硬骨头PDF是RAG项目里最常见的格式也是坑最多的一种。原因很简单PDF本身不是一种为“提取文本”设计的数据格式它记录的是图形绘制指令文字从PDF解析出来全靠逆推。如果直接用PyPDF2这类库对复杂的PDF做一次性解析结果往往惨不忍睹多栏布局变成一行表格单元格顺序颠倒文字缺字漏字。我目前用得比较多的是三类方案第一类是PyMuPDF也就是fitz解析速度快、对文本做保留位置的提取效果不错适合以文字为主的普通文档。第二类是pdfplumber它对表格和定位信息支持更好适合需要恢复结构化数据的场景。第三类是Unstructured的PDF解析它能做版面分析识别标题、表格、图片块输出的元素本身带类型标签省掉不少二次处理工作。选择标准不是哪个库最流行而是文档类型和你的下游需求。我的经验是先小批量跑几个真实文档看输出文本的质量再决定主线加载器。另外PDF不一定是从头到尾都是正常的。有些PDF前半部分是正常的文字层后半部分却是扫描图片。这类混合型PDF在政府公文、老企业档案里很常见一定要在解析流程里做好页级别的判断哪一页提不出文本就走OCR。否则你会看到前半部分检索效果很好一到后面的扫描页就全员“失明”而且很难定位是数据问题还是模型问题。2.3 Word文档和表格如何处理Word文档在很多企业知识库里比PDF还常见因为文档的源头就是Word。走python-docx可以读取段落和表格读取时有一个容易忽略的点Word里的内容顺序不一定是“段落-段落-表格”的简单排列有些内容嵌在文本框、页眉页脚、批注里直接读正文很可能会漏。用python-docx遍历document.paragraphs只能拿到正文段落表格要额外走document.tables如果文档里表格和段落交错需要按文档的body顺序重新拼装。我在实际项目里会写一个小函数从Word的XML结构里按顺序提取段落和表格表格保留为Markdown风格或有分隔符的纯文本而不是直接拍平成一个长字符串。这样切块时表格语义还在后续大模型也更易读懂。至于Excel类数据常见做法是把每一行变成一条独立记录或者按Sheet拆分切块时的重点是如何保留行与行的关系一般不建议把整张超大Excel表格直接塞进一个切块里。之前做过一个销售合同台账的项目Excel里几万行记录我们最后就是按“客户合同编号”为粒度拆成独立文本块再辅以结构化查询效果比整表向量化好得多。2.4 扫描件与图像型PDF的OCR路径现在很多企业知识库里还有老照片一样的扫描件PDF这类文件没有文本层任何文本提取库都拿不到字符。要让RAG读懂它们必须走OCR流程。我的选择是PaddleOCR它对中文的支持比Tesseract好很多特别是在中英文混排场景下识别率和版面还原能力都更优。实际操作时建议先用PyMuPDF把PDF的每一页转成高分辨率图片DPI大概200到300再做OCR同时保留每页的页码信息。OCR出来的文本很可能顺序凌乱特别是多栏排版需要后处理。这块没有银弹我的经验是按需取舍如果文档是公告、政策文件这类以正文为主的直接按阅读顺序输出文本即可如果是报销单、发票这类强结构文档则要提取关键字段而不是盲目全文识别。另外要提醒一句OCR不是万能的手写体、盖章遮挡、模糊扫描件的识别率都很难保证预算允许的情况下对核心单据做人工校对仍然是必要的。做RAG项目时要提前跟业务方说清楚这个边界否则后期会有一堆“OCR识别错了”的工单找你。3. 解析之后清洗、分块、元数据与去重3.1 文本清洗页眉页脚、噪音与乱码加载和解析完成之后拿到的原始文本通常是脏的。页眉、页脚、页码、目录、水印、参考文献引用块这些“文档本身有效但对检索无益”的内容如果不做清洗会被切块进入向量库检索时经常因为这些噪音召回一堆无关片段。我的清洗流程按优先级分为三步第一步是字符级清洗处理全角半角、不可见字符、乱码替换可以用正则或编码纠错第二步是结构级清洗根据文档类型去掉页眉页脚、目录和网站导航信息这一步最好在加载时用元数据或者预处理正则做第三步是语义级清洗比如去掉“第X页共Y页”这类提示语、去掉无意义的制表符等。清洗并不是越狠越好过度正则会把正文里正常内容误删。曾经有个项目我把所有连续空白字符替换成单个空格结果代码块里的缩进全没了技术文档的源码示例变得不可读。后来调整了清洗规则只在代码块外做空白压缩。我的原则是宁可漏掉一部分噪声也不要误删核心信息清洗规则务必先跑一遍样例再全量应用。可以准备一个“清洗规则清单”每加一条规则就标记适用范围和预期效果防止规则之间互相打架。3.2 分块策略的底层逻辑分块是RAG里最容易被讨论、也最容易过度纠结的环节。MCP和Agentic RAG这类热词不断出现但无论架构多高级最终喂进Embedding模型的还是一个一个文本块块的质量直接决定检索效果。分块的本质是在“单块语义完整”和“单块体积可控”之间找平衡。块太小语义碎片化检索到的是半句话块太大语义信息被稀释Embedding向量趋同检索精度下降并且超过模型最大token限制后会被截断。常见的分块方式有三种固定长度切块、递归字符切块、语义切块。固定长度最容易实现但会硬生生切断句子甚至切断词递归字符切块LangChain的RecursiveCharacterTextSplitter是目前最常用的方案它按段落、句子、字符的优先级逐级切分能在多数情况下保持语义边界语义切块更复杂一般会结合嵌入向量的相似度或者标题结构来动态划定边界。我的建议是大部分项目先用递归字符切块跑通再根据效果决定要不要上语义切块不要一上来就用复杂的方案。关于参数chunk_size和chunk_overlap是两个最核心的调参对象。chunk_size一般按token数计算而不是按字符数不同模型有自己的tokenizer常用值在300到800之间。chunk_overlap建议设为chunk_size的10%到20%它的作用是让跨块上下文在相邻块之间保留一部分减少信息断层。举个例子如果chunk_size是500overlap设为75那么第一块覆盖token 1到500第二块覆盖425到925这样第425到500之间的内容在两个块里都出现检索时可以避免因为切块导致的边界断句。实际操作时建议把每个项目的文档类型固定下来先用不同chunk_size做一个对照实验观察检索命中率再确定默认值。3.3 元数据让检索结果可追溯很多新手做RAG跳过元数据只把文本向量化入库结果回答问题的时候没有任何出处可查。元数据是RAG知识库的“档案”至少要包含来源文件、页码、文档类型、创建时间、所属业务线等。有了元数据才能在下游检索时做过滤比如只查某个业务线的文档、只查最近半年的文档也才能在生成时给出可追溯的引用来源这对企业知识库尤其重要。在建Document对象时我会把source设为文件路径或URLpage_number设为页码或块序号document_type设为pdf、md、html等。切块时要让切出来的每一个块都继承父文档的元数据避免有的块缺source导致检索后无法溯源。这里有一个细节LangChain的RecursiveCharacterTextSplitter默认会保留Document对象的metadata字段但如果你自己写切块函数很容易把metadata丢掉。我见过不止一次向量库里存了一堆内容却不知道来自哪个文件排查起来非常痛苦。元数据的另一个用途是做权限控制。企业知识库会有不同部门、不同密级的文档检索时如果能在向量召回前按元数据过滤比召回后再判断权限高效得多。PGVector支持metadata过滤和向量检索的组合查询这也是我推荐它做企业级RAG的原因之一。3.4 去重与版本管理企业里最容易被忽略的是文档去重。同一份材料可能以PDF、Word、HTML三种形态都存在也可能被多次上传。如果不去重向量库里全是冗余数据检索时同样的内容被召回好几条不仅浪费存储还会干扰大模型的答案。去重的手段建议在入库前用文本哈希粗排再结合相似度判断精确去重。更讲究一点的团队会做文档版本管理新版本入库的同时把旧版本的向量标记为失效或直接删除。这块投入产出比很高尤其是知识库规模上来之后去重带来的收益非常明显。我有一个朋友的团队知识库里十万个文本块去重后发现将近20%是同一份材料的重复上传清理之后检索准确率直接提升了一截也把向量存储成本降了下来。去重逻辑不用很复杂一个基于内容哈希的判重表就能解决大部分问题。4. 实操配置与代码演示4.1 环境准备与依赖下面进入可直接复现的实操环节。我用的是Python 3.10核心依赖有langchain、langchain-community、langchain-text-splitters、langchain-openai、unstructured、pymupdf、pdfplumber、fastapi、pgvector、psycopg2-binary等。安装命令在一开始执行一遍pip install langchain langchain-community langchain-text-splitters langchain-openai pip install unstructured[pdf] pymupdf pdfplumber pip install fastapi uvicorn pgvector psycopg2-binary如果你的服务端已经有一个PostgreSQL数据库建议手动启用pgvector扩展先创建扩展再建表建表时把embedding字段类型设置为vector。不管是用LangChain的PGVector封装还是自己写SQL都绕不开这一步。启用和建表的SQL大致如下CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS documents ( id serial PRIMARY KEY, content text, metadata jsonb, embedding vector(1536) );这里的vector(1536)取决于Embedding模型的输出维度如果是OpenAI的text-embedding-3-small就是1536换成BGE或其他开源模型要对应修改。这个维度直接影响表的存储成本和检索性能不要随便填。插一句存储成本在文档量大到百万级时会非常明显1536维向量在PGVector里一张普通表可能要占几个GB设计表结构时就要考虑分表还是分区。4.2 核心代码文档加载解析管道我封装了一个解析管道入口接收文件路径和文件类型输出成列表形式的Document对象。核心逻辑是先按文件类型分发到对应的加载器再做清洗最后分块。这里用一个简化版的代码示例来解释流程from langchain_community.document_loaders import PyMuPDFLoader, UnstructuredWordDocumentLoader, TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.documents import Document def load_document(file_path: str, file_type: str) - list[Document]: if file_type pdf: loader PyMuPDFLoader(file_path) docs loader.load() elif file_type docx: loader UnstructuredWordDocumentLoader(file_path) docs loader.load() else: loader TextLoader(file_path, encodingutf-8) docs loader.load() return docs def clean_document(docs: list[Document]) - list[Document]: cleaned [] for doc in docs: text doc.page_content text text.replace(\u3000, ).replace(\xa0, ) text \n.join([line.strip() for line in text.splitlines()]) doc.page_content text cleaned.append(doc) return cleaned def split_documents(docs: list[Document], chunk_size500, chunk_overlap75): splitter RecursiveCharacterTextSplitter( chunk_sizechunk_size, chunk_overlapchunk_overlap, separators[\n\n, \n, 。, , , ], length_functionlen, ) return splitter.split_documents(docs)这里有一个细节separators的顺序很重要。它决定了切块时优先按什么粒度断开我把“。 ”放进去是为了让中文文本尽可能在句子边界断开。如果直接用默认分隔符中文文本容易被切成残句。这个经验是我经过不少项目试出来的很多人代码跑通了但效果差就是因为切块时句子被拦腰截断。另外用len做length_function是按字符数计算长度如果Embedding模型计费或限制按token走可以换成一个tokenizer来统计比如tiktoken的encode函数。字符数和token数在中文场景下差异很大一般一个中文汉字算一个token左右但英文单词和标点就不好说了最好按客户端的模型来选。4.3 入库到PGVector解析、清洗、切块完成后下一步就是把文本块向量化并写入PGVector。这里可以使用LangChain的PGVector封装from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import PGVector embeddings OpenAIEmbeddings(modeltext-embedding-3-small) vector_store PGVector.from_documents( documentssplit_documents, embeddingembeddings, collection_nameknowledge_base, connection_stringpostgresql://user:passwordlocalhost:5432/ragdb, )如果你的场景对隐私要求比较高不想用云端Embedding API也可以换成开源的Embedding模型比如BGE系列或者通过vLLM、Ollama这类工具本地部署。核心流程都是一样的先是文档加载解析再是切块然后向量化入库。关于“RAG必须用API吗”这个疑问答案是不一定。云端API只是方便本地模型完全可行关键看你的数据隐私和性能要求。真正上线时我不建议每来一批文档就从头重新入库而是要做增量更新。PGVector封装里提供了add_documents方法新文档解析后直接追加即可删除时可以通过metadata里的source或文档ID定位删除。增量更新的好处是知识库可以持续扩展不用每次全量重建。全量重建在数据量小的时候很爽但数据量上来以后一次全量可能要几小时业务上根本无法接受。4.4 参数选择与验证接入实际业务之前我建议先做一个小规模的参数验证实验。步骤是挑选5到10份真实文档作为样本分别用不同chunk_size比如300、500、800切块对同样的测试问题做检索统计召回结果的相关性。不用太复杂肉眼就能判断哪个参数下召回内容更完整。要重点观察两点一是切出来的块有没有断句二是检索问题时命中的块是不是包含答案的关键信息。我用这个方法调过很多项目基本一轮就能确定合适的默认参数。之后可以将这套参数固化到配置中心避免每次都要手改代码。测试问题怎么设计也有讲究不能只挑一个“最好的问题”自我感动要覆盖常见问答、模糊问答、跨文档问答三类。常见问答看基础命中率模糊问答看语义泛化能力跨文档问答看元数据和切块的配合。很多项目调参时只看了一两个例子上线后一测就崩就是因为测试集太片面。5. 常见问题与排查技巧实录5.1 PDF乱码与提取内容不完整PDF解析最容易遇到的就是乱码。先判断是编码问题还是字体问题如果是常见中文乱码尝试换PyMuPDF或pdfplumber看是否复现如果内容是以符号、方框形式出现多半是字体子集或自定义映射导致这时候可以借助OCR做辅助识别。我的经验是不要盲目相信某一个加载器一个文档解析失败换一个工具再试往往就好了。实在不行就回归到“转图片OCR”这条保底路径虽然慢但结果相对可控。还有一个常见坑是PDF页面里嵌入了图片型文字比如某些扫描件虽然能选中文字但选中后复制出来是乱的这种其实是隐藏在图片后面的OCR文字层。判断方法很简单把PDF页面放大到400%看文字是否清晰如果文字边缘是锯齿状、实际上是图片渲染的那就要按扫描件处理。5.2 表格结构错乱表格错乱是RAG文档解析的重灾区。PDF里的表格文本提取库往往把单元格内容按位置或顺序拼成一串行和列的对应关系基本丢失。一种处理方式是改用能够识别表格结构的工具比如pdfplumber提取表格原始结构或者用Camelot这类专门处理表格的库另一种方式是把表格转为Markdown格式让单元格结构以明确语法保留下来这样切块时表格能被当作一个整体语义块。项目里如果表格不多我甚至建议把关键表格直接单独存成结构化数据文件再在RAG链路里单独处理查询时走精确匹配或结构化检索效果会好很多。比如一个“产品参数表”与其让RAG去猜“某型号的功率是多少”不如把参数表抽出来放到关系型数据库里先做精确查询再做生成。这也是所谓“RAG和结构化数据结合”的典型实践。5.3 超长文档的加载与切块超长文档比如几百页的规章制度常见问题是加载过程内存占用过高、切块后重复内容过多或者文档中不同章节被切块时标题错位。我的建议是加载前先用工具读取目录或章节结构按章节切成大段再对大段做二次切块而不是一次把整个文本丢给切块器。同时在切块前打上章节标题的元数据这样检索时能直接定位到某个章节召回的内容会更有条理。这里有个实操技巧先把文档按空行或章节标题预切一次得到一组“预块”再对每个预块做固定大小切块。这样既避免了整篇文档的切块时标题和正文错位又保留了章节的上下文语义。很多开源框架里已经有类似的递归切分方案但默认参数不一定适合中文长文档需要自己改。5.4 检索效果不好是解析的锅还是切块的锅这是排查时最常问的问题。我的排查顺序是先人工查看被检索到的原始文本块确认内容是否完整正确。如果文本本身就缺字、错位那就是解析环节的问题如果文本内容完整、但检索命中的块包含答案却不相关、或者相关内容被切成了两半那是切块参数的问题如果块没问题、检索也命中了相关片段但生成答案质量差那才轮到Embedding模型、Prompt和生成模型去背锅。按这个顺序逐层排查能省下大量无效调参时间。我见过一个团队调了一个星期的Embedding模型和Prompt最后发现原因是PDF表格解析时把列顺序搞反了检索到的“正确文档”内容本身就是错的。这种问题靠调模型永远解决不了只能回头修解析。所以我在每个项目的索引流程里都会做一个“抽样审计”步骤随机挑几十个文本块人工看一眼确认解析结果可信后再大量建索引。5.5 和Agentic RAG、MCP的关系最近总有人问RAG和MCP的区别以及Agentic RAG是怎么回事。这里简单说一句MCP解决的是“模型如何标准化调用外部工具和数据源”的问题RAG解决的是“模型如何从知识库中检索相关信息”的问题两者定位不同可以配合使用。Agentic RAG则是把RAG从一次性的“检索-生成”变成带决策和规划的Agent流程。但无论架构名词怎么变文档加载解析都是它们的基础。模型调用工具、Agent规划执行步骤最终还是要落到“知识库里的数据是否干净、是否结构化、是否可检索”上面。地基不牢上层越复杂问题反而越难排查。做RAG项目的体会是文档加载解析考验的不是掌握了多少高级框架而是是否愿意把脏活、碎活干细致。很多团队一上来就想着切多高级的块、用多新的模型最后卡住的却是一份PDF里的表格。我现在接手一个新项目第一件事永远是先看一批真实文档的加载结果人工翻一翻解析出来的文本长什么样再决定后面的方案。这套方法看着笨实则最快。如果你也在搭RAG知识库建议先把第一份文档从加载到入库的全流程走通再谈优化很多问题都会在第一步暴露出来。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑