首页 / 资讯中心 / 文章详情

从零搭建本地RAG知识库:PDF解析到向量检索全流程实战

从零搭建本地RAG知识库:PDF解析到向量检索全流程实战 ★ FEATURED ARTICLE
纯粹从零开始接触这个概念的人往往第一反应是“我是不是得先学会写大模型”其实完全不用。RAG这套东西站在使用者的角度核心就是“把你自己手里的资料变成AI能看懂、能检索、能引用的知识库”。这篇文章不讲虚的直接沿着一条零基础能走通的路从PDF解析讲到向量检索把每一步的原理、工具、踩坑点都摊开说清楚。适合刚接触知识库搭建、想用本地文档做问答系统、或者被各种术语绕晕了的读者看完能自己动手跑通一个最小可用的系统。1. RAG 概念拆解为什么需要把 PDF 塞给 AI1.1 大模型的“记忆力”缺陷大模型训练完之后它的知识就固定在一个时间点上新的内部资料、行业文档、个人笔记它一概不知道。就算你硬把一段很长的PDF内容贴进对话窗口模型也会被上下文长度限制住超过几万字就“记忆错乱”前面说的后面就忘了。更麻烦的是模型天生有“一本正经胡说八道”的倾向对于它没见过的具体数据它宁愿编一个看起来合理的答案也不会承认自己不知道。这就是所谓的幻觉问题。RAG检索增强生成的思路其实非常朴素既然模型记不住那就别让它硬记。每次提问的时候先从你的知识库里把相关的段落捞出来再把这些段落连同问题一起交给模型让它“看着资料回答问题”。这样模型不需要记住你的PDF内容它只需要做好一个“阅读理解”的角色。整个过程就像开卷考试答案都在资料里模型负责组织语言。1.2 RAG 系统的四个核心环节一个标准的RAG流水线由四个环节组成文档加载与解析、文本切块、向量化存储、检索与生成。文档加载是把PDF、Word、网页等原始格式转成纯文本这是最脏最累的活文本切块是把长文档切成适当大小的片段切得太碎丢失上下文切得太大检索不精准向量化是把文本片段转成一串数字向量让计算机能算“语义相似度”检索与生成则是根据用户问题找出最相关的几个片段拼进Prompt里让大模型回答。第一次接触的人容易把RAG当成一个“模型”其实它不是模型而是一套架构。你完全可以用本地开源的Embedding模型做向量化用开源的大模型做生成整个链路跑在自己的电脑上数据不出门。这也解释了为什么近半年“本地知识库”这么火因为企业私密文档、个人笔记这些东西谁都不想随便传到第三方API里。1.3 判断要不要自建知识库开始之前先想清楚需求不是所有场景都适合自建RAG。如果你的文档总量不超过几十页而且只是偶尔查一两个问题直接用大模型的长上下文模式把全文塞进去硬问可能更省事。但一旦文档量上来了比如几十本手册、几百篇行业报告、持续更新的内部WikiRAG的优势就体现出来了它不需要把所有内容都塞进上下文每次只取最相关的一小部分成本低、响应快、答案还能溯源到具体页码。另外要区分“知识库”和“全文检索”。传统的关键词搜索比如在PDF阅读器里CtrlF只能做字面匹配你搜“怎么开发票”就搜不到内容里写的是“开票流程”。RAG用的是向量语义检索它能理解“开发票”和“开票流程”是一回事。这是本质区别也是RAG最有价值的点。2. PDF 解析整个流程里最容易被低估的关卡2.1 文本型 PDF 与扫描型 PDF 的分水岭PDF这种格式天生就不是为文本提取设计的它更像是“把排版固定下来的一张纸”。所以解析PDF是整个RAG流程里翻车率最高的一步。拿到一份PDF第一件事不是急着写代码而是确认它是文本型PDF还是扫描型PDF。文本型PDF里真的有文字层可以直接复制扫描型PDF本质上是图片里面是拍照或扫描出来的页面图像你复制出来全是乱码。判断方法很简单用PDF阅读器打开试着用鼠标选中一段文字。如果能正常选中复制就是文本型如果选中是一个整块矩形或者根本选不中文字大概率是扫描版。还有一种偷懒的办法用pdfplumber库直接抽取第一页文本抽出来有内容就是文本型抽出来是空的就是扫描型。2.2 工具选型pdfplumber、PyMuPDF 与 OCR对于文本型PDF我常用的工具就是pdfplumber和PyMuPDF (fitz)。pdfplumber的强项是提取表格和精细的文本位置信息适合处理带复杂排版的文档PyMuPDF的速度更快处理几百页的大文件时优势明显而且它对文本块的分组更符合阅读顺序。实际项目里我通常先用PyMuPDF抽全文遇到表格多的页面再单独用pdfplumber补一次。import fitz # PyMuPDF doc fitz.open(用户手册.pdf) full_text [] for page in doc: full_text.append(page.get_text()) doc.close() text \n.join(full_text) print(text[:500]) # 先看看前500字确认提取质量这段代码能把文本型PDF的每一页文字按阅读顺序拼出来。需要提醒的是get_text()默认输出可能带着奇怪的换行和空格这是PDF排版导致的不是代码写错了。后处理阶段可以用正则把这些多余的空白清理掉但要注意别把所有换行都删光否则段落结构就丢了。扫描型PDF就得走OCR路线业界常用的方案是PaddleOCR或Tesseract。PaddleOCR对中文的支持明显更好识别准确率高但依赖较重首次安装会拉下不少底层库。Tesseract轻量但中文识别率一般需要额外下载中文语言包。还有个折中方案先用ocrmypdf把扫描版PDF直接转成带文字层的PDF转完之后再用正常方式抽取这样以后每次处理都会省力。2.3 表格处理的隐藏陷阱PDF里的表格是解析时的重灾区尤其是带有合并单元格、跨页表格、复杂表头的文档。pdfplumber提供了extract_table()方法但它要求表格结构相对规整遇到跨页表格还得自己拼接。我的经验是如果PDF里的表格是为了展示数据优先尝试用pdfplumber提取成Markdown格式再把Markdown交给大模型它对Markdown表格的理解力远强于对原始坐标的推断。import pdfplumber with pdfplumber.open(数据报告.pdf) as pdf: table_text [] for page in pdf.pages: tables page.extract_tables() for table in tables: for row in table: # 过滤掉 None 值转换为字符串 table_text.append( | .join(cell if cell else for cell in row)) result \n.join(table_text)这里有个容易踩的坑extract_tables()返回的单元格可能是None拼接时一定要做空值处理不然字符串拼接直接报错。表格提取出来之后建议人工扫一眼结果因为PDF里单元格的内容可能是换行嵌套的提取出来之后贴在一行里语义会变。2.4 文档专项预处理的三板斧抽取完文本之后还有几道预处理工序直接关系到后面RAG的效果。第一是编码清洗PDF转出来的文本经常混入各种不可见字符、全角半角混乱、不规范的换行符建议用正则统一处理。第二是元数据补充记录每段文本来自哪个文件、哪个章节、哪一页这样后续回答问题时能溯源。第三是去噪去重封面页、版权页、目录页这些对知识问答毫无价值反而会在检索时干扰结果最好提前过滤掉。import re def clean_text(raw): # 去掉页面底部的页码和页眉 raw re.sub(r\n\d{1,3}\n, \n, raw) # 合并断行的英文单词如 recor-\nd - record raw re.sub(r-\n, , raw) # 将多个空白行压缩为单个 raw re.sub(r\n{3,}, \n\n, raw) return raw.strip()这一套清洗逻辑很基础但能解决大部分PDF的“脏乱差”问题。如果你处理的PDF里面有很多分栏排版还需要额外处理阅读顺序。分栏文档的文本抽取结果往往是左栏读完了跳右栏颠三倒四目前没有万能解法比较实用的招是用PyMuPDF按坐标排序或者干脆人工确认后手动调整。3. 文本切块决定检索精度的隐形工程3.1 为什么切块比向量化更影响效果很多人第一次搭RAG把精力全放在选Embedding模型上结果效果不好其实是切块策略没做对。切块粒度直接影响检索的命中率块太大比如把整个章节塞成一个块向量化之后语义被稀释了问一个细节问题匹配出来的相似度不高块太小比如一句话一块检索倒是精准了但上下文信息丢了大模型拿到的材料太碎回答起来没有前因后果。这里可以类比查字典如果词典按整页做索引你查一个字它能告诉你在哪一页但没法告诉你在页面的哪个位置如果按每个词条做索引查“苹果”这个词条很精准但如果你问“苹果的营养价值”就得同时查到好几个词条拼起来看。切块就是在“检索精度”和“上下文完整性”之间找平衡。3.2 块大小与重叠参数的实战经验LangChain社区的默认参数是块大小(chunk size) 1000字符、块重叠(chunk overlap) 200字符这是一条比较稳妥的起跑线。但实际项目里需要按文档类型调整我的经验值如下文档类型块大小块重叠理由技术手册、规范类800-1000字符100-200字符保留完整技术上下文细节查询靠重叠弥补文章、公众号内容500-800字符50-100字符段落独立性强块太大跨主题语义干扰表格密集型报表每表格一块0-50字符拆分表格反而撕裂数据优先保完整代码文档按代码块切20-50字符函数定义和调用之间不能断开切块有个很重要的原则尽量按语义边界切而不是按字符硬切。LangChain的RecursiveCharacterTextSplitter会把长文本先按段落分隔符\n\n切再按\n切再按句号切这样切出来的块尽可能落在自然语义边界上。相比之下CharacterTextSplitter按固定字符数硬切经常把一句话拦腰截断我不太推荐。from langchain_text_splitters import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size800, chunk_overlap150, separators[\n\n, \n, 。, , , , , , ], length_functionlen, ) chunks splitter.split_text(cleaned_text) print(f切分得到 {len(chunks)} 个块)最后那个空字符串是兜底选项前面的分隔符都切不动时就直接按字符切。这个参数组合我用了很久语义保留效果不错。在中文场景里把。这些标点加进分隔符列表很有必要因为默认的英文逗号空格对中文断句不起作用会导致一个超大段落被硬切。3.3 切块的边界问题与进阶技巧切块还会遇到一个经典问题同一个主题的内容分布在两个相邻的块里检索时只命中了一个块导致信息不完整。增加块重叠能缓解这个问题但重叠太多会浪费向量存储空间、增加检索噪音。我个人习惯是重叠比例控制在10%到25%之间太低效果不佳太高性价比下降。进阶一点的玩法是按Markdown结构切块。如果你的PDF转出来的文本自带标题层级比如# 一级标题、## 二级标题可以用MarkdownHeaderTextSplitter按标题结构切块子标题的内容会继承父标题的上下文。这种结构化切块对技术文档特别管用检索到子标题下面的内容时能顺带把上级标题作为上下文一并带上。比如问“网络运维有哪些日常巡检项”如果块里自带## 日常巡检这个标题大模型的回答会精准很多。4. Embedding 与向量库选型本地化落地怎么选4.1 Embedding 模型选择的三个考虑点Embedding模型就是把文本变成一串数字向量。选择时主要看三点维度、中文效果、部署成本。维度越高理论上表达语义的能力越强但存储占用和计算量也越大。目前主流的开源Embedding模型输出维度一般是768维或1024维像BAAI/bge-large-zh-v1.5是1024维sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2是384维后者轻量但中文语义捕捉能力明显弱一些。中文场景里我首推BGE系列北京智源开源它在中文语义检索榜单上长期靠前而且对长文本的支持做得不错。其次是text2vec-base-chinese模型更小跑CPU也能忍受。如果电脑配置实在太差还有一条路用硅基流动、智谱等国内平台的API做Embedding质量稳定但数据要过网络传输私密性场景慎用。4.2 向量数据库的选型逻辑向量数据库的作用是存储向量、做相似度检索。早期RAG项目都在用Chroma和FAISS两者口碑都不错但定位不同。Chroma是“零配置”的启动快、API简单适合学习和原型验证FAISS是Meta开源的库检索速度更快但需要自己管理索引的保存和加载工程化程度要求高一些。数据量在百万级以下时这俩都够用选哪个更多取决于个人代码习惯。数据量上来之后尤其是超过百万向量、需要多人并发查询时就得考虑Milvus或Elasticsearch了。Milvus是专为向量检索设计的分布式数据库支持复杂过滤条件Elasticsearch本身是全文检索引擎新版本加入了向量检索能力优势是能同时做关键词检索和向量检索的混合召回。个人项目没必要上这么重的组件先用Chroma把链路跑通才是正事。from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings # 初始化本地 embedding 模型 embedding_model HuggingFaceEmbeddings( model_nameBAAI/bge-large-zh-v1.5, encode_kwargs{normalize_embeddings: True}, ) # 创建向量库浦; persist_directory 用于持久化存储 vector_store Chroma.from_documents( documentschunks_doc, embeddingembedding_model, persist_directory./kb_index, )注意normalize_embeddings这个参数归一化之后的向量做内积相似度等价于余弦相似度检索效果更稳定。我见过不少人在这个参数上翻车忘了归一化结果检索出来的东西语义八竿子打不着。4.3 检索参数与元数据过滤向量检索不是把TopK设得越大越好。TopK代表每次取回多少个候选片段设大了喂给大模型的材料太多容易冲淡核心信息也费Token设小了可能漏答案。我的经验是一般问答场景TopK取4到6如果要让模型做全面的总结分析可以提到8到10。配合相似度分数阈值使用相似度低于0.5的结果直接丢弃宁缺毋滥。元数据过滤是很多人忽略的功能。每个文本块存入向量库时都能附带元数据比如“来源文件名”“章节”“页码”“日期”。检索时可以先按元数据过滤掉不相关的范围再在范围内做向量相似度计算。比如你有一个包含“产品A手册”和“产品B手册”的知识库提问前先定位用户问的是哪个产品把范围过滤到对应手册检索准确率会有肉眼可见的提升。5. 完整 RAG 链路搭建从切块到问答5.1 用 LangChain 串起完整流程LangChain是目前把RAG链路串起来最方便的工具它把文档加载、切块、向量化、检索、问答封装成了标准接口零基础也能照着模板跑通。核心流程就四步加载文档、切块、存向量库、创建检索问答链。from langchain_community.document_loaders import PyMuPDFLoader from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.llms import Ollama # 1. 加载 PDF loader PyMuPDFLoader(产品手册.pdf) documents loader.load() # 2. 切块 splitter RecursiveCharacterTextSplitter(chunk_size800, chunk_overlap150) chunks splitter.split_documents(documents) # 3. 向量化并存储 embedding HuggingFaceEmbeddings( model_nameBAAI/bge-large-zh-v1.5, encode_kwargs{normalize_embeddings: True}, ) vector_store Chroma.from_documents(chunks, embedding, persist_directory./kb) # 4. 检索 生成 llm Ollama(modelqwen2.5:7b, temperature0.3) retriever vector_store.as_retriever(search_kwargs{k: 5})到这里检索器已经建好了。如果直接用langchain.chains.RetrievalQA可以快速测通效果但这个封装太黑盒不方便调试。我建议手写一遍检索和生成的过程这样出了问题能精确知道是检索环节错了还是生成环节错了。# 手动执行检索 生成便于理解每一步 from langchain.prompts import PromptTemplate question 产品的最大工作温度是多少 docs retriever.invoke(question) # 把检索到的内容拼成上下文 context \n\n.join([doc.page_content for doc in docs]) prompt PromptTemplate.from_template( 你是知识库问答助手请严格根据以下资料回答问题。\n 资料\n{context}\n\n 问题{question}\n\n 如果资料中没有答案请直接回答不知道不要编造。 ) full_prompt prompt.format(contextcontext, questionquestion) answer llm.invoke(full_prompt) print(answer)这段代码把RAG的本质完整暴露出来了没有任何魔法就是“检索资料、拼Prompt、问模型”三步。新手把这个手动流程跑通对RAG的理解会比用一百遍封装API都深。5.2 Prompt 设计与答案溯源同样一组检索结果Prompt写法不同回答质量天差地别。最基础的Prompt要包含三部分角色设定、参考资料、问题。角色设定告诉模型“你是知识库助手”参考资料给它事实依据问题就是用户的真实提问。记住要加一句“资料中没有答案就直说不知道”这句话能显著降低幻觉率。进阶一点的写法是要求模型引用来源让回答以“根据《产品手册》第3章设备的最大工作温度是...的格式输出。这既让用户信任答案也方便你排查是哪一块被检索出来了。实践中我会要求模型在回答末尾附上“参考片段编号”对应到检索结果的页码。from langchain.prompts import PromptTemplate prompt PromptTemplate.from_template( 你是一名严谨的技术文档助手。\n 请根据【参考资料】回答问题回答时必须标注信息出处文件名页码。\n 如果【参考资料】中没有相关信息请直接回答“资料中未找到相关内容”。\n\n 【参考资料】\n{context}\n\n 【问题】\n{question}\n )5.3 本地模型的部署选择Ollama 是最省事的一条路如果你要把整个流程跑在本地大模型生成环节我推荐用Ollama。它是一个本地模型运行工具一条命令就能把开源模型拉下来运行不需要折腾CUDA、Python依赖这些环境问题。安装好Ollama之后执行ollama pull qwen2.5:7b然后直接在代码里调用即可LangChain对Ollama有原生支持。模型参数上7B模型在CPU上跑会有点慢但回答简单知识库问题完全可行如果有独立显卡可以上14B甚至32B的模型回答质量会明显提升。值得提醒的是大模型的参数选temperature0或0.1因为知识库问答需要的是忠实于资料不需要模型发挥想象力。温度太高模型就开始“自由创作”了这是幻觉的主要来源。6. 效果优化实操命中率、答案质量与检索诊断6.1 用评估指标量化知识库好不好用知识库搭完之后最怕的就是“感觉能用但不知道好不好”。没有量化指标优化无从下手。RAG领域有三个关键指标命中率Hit Rate、生成准确率、答案相关性。命中率指的是检索环节能不能把包含正确答案的片段捞回来这是RAG的根基检索都漏了生成再强也没用。生成准确率只能靠人工标注来评估拉上同事一块抽几十条问题逐个给模型回答打分。Hit Rate的计算需要一个“测试集”每一对问题正确答案所在的文档块。跑一遍检索流程看每个问题对应的正确块是否出现在TopK结果里。用Python脚本批量跑比手工点快得多。我自己的测试集一般是50到100对问答对从文档里挑高频问题配上正确答案所在页码和段落。这个测试集建好之后后续每次调整切块参数、换Embedding模型都能快速对比效果。6.2 检索不到答案时先查哪一个环节RAG效果不好的时候先别急着怀疑模型90%的问题出在检索环节。常规排查顺序是先看检索TopK结果里有没有正确答案有的话问题出在生成环节说明Prompt或模型没用好这些材料没有的话问题出在检索环节需要检查切块粒度、Embedding模型、相似度阈值。一个很实用的调试技巧把检索到的TopK片段直接打印出来人工读一遍。很多时候一眼就能看出来比如“块切太大导致语义被稀释了”“这一块内容跟问题完全无关是元数据过滤没生效”“分页把一句话切断了”。这个调试手段虽然原始但信息量比任何指标都大。6.3 常见陷阱向量检索不是万能的向量检索擅长“语义相近”但有两个明显短板。第一是精确匹配问题你问“产品型号ABC-123的保修期是多长”如果文档里写的是“ABC123”这串型号经过向量化之后语义很相近但不完全精确模型生成时容易把型号搞混。这时候需要在检索结果上叠加一次关键词强制匹配或者把型号类信息单独提取成结构化字段。第二是否定和逻辑问题向量相似度处理不了“哪些设备不支持无线连接”这类问题检索回来的片段可能都在讲“哪些设备支持”堆给模型之后它只能猜。此类问题暂时没有完美解法结构化知识图谱是方向但对零基础项目太复杂了。另外中文文档里的同义词问题也很棘手。比如“价格”和“费用”语义相近向量检索能兜住但“保修”和“三包”在某些语境下含义有差异向量检索可能误召回不相关内容。解决办法是丰富测试集多拿实际用户的问法去跑检索别用自己“精心组织”的语言去测。7. 常见问题与排查技巧实录7.1 PDF解析乱码都是编码和字体惹的祸症状抽取出来的文本是满屏的乱码符号或者大段空白。原因分两种一是PDF本身用了非标准编码二是文档里用了特殊字体比如某些方正字体PyMuPDF抽不出来。排查方法先用Chrome打开PDF如果能正常复制文字说明文字层存在问题在抽取工具如果Chrome也复制不出来那就是字体嵌入的问题。解决方案是换OCR路线直接用PaddleOCR整页识别。一页页调试太慢的话先用前5页测试确认方案可行再批量处理。7.2 检索结果总是不相关先看 chunk 是不是太长了症状提问“有哪些注意事项”检索回来的片段里全是前言和背景介绍关键注意事项反而没召回。这是典型的chunk太长的症状。800字符的块在技术文档里可能跨越三个主题向量化之后每个主题的语义都被稀释了。尝试把chunk_size降到500、300重跑测试集看命中率。另外检查一下重叠参数重叠区域里的关键信息如果太短容易被当成噪音过滤掉。这类问题调参见效很快。7.3 生成答案总是“车轱辘话”Prompt 的设计问题症状模型老是重复说“根据资料显示这个问题可能涉及多个方面”但说不出来到底哪些方面。这是Prompt里没做限制导致的。建议在Prompt里加上“回答需包含具体数字、型号、操作步骤等细节严禁泛泛而谈”这会让模型收敛到资料里的具体内容上。还有一种情况是检索回来的片段确实太泛比如TopK返回的全是大段的介绍性文字那还得回头优化切块。7.4 本地推理速度慢到没法用症状问一个问题要等两三分钟。原因基本是模型太大、配置太低。7B模型在纯CPU上跑生成速度一般是每秒几个Token回答几百字就要一分钟起步这不是代码能优化的。解决方案有三条换更小的模型比如 3B/4B在可接受效果的前提下大幅提速换API方案把生成环节放到云端或者做缓存高频问题直接存答案命中缓存就秒回。8. 学习路线总结与一点个人体会从零开始学AI知识库搭建最忌讳一上来就啃框架源码和论文。我的建议是按“最小闭环”思路走先用现成工具把一个10页的PDF跑通检索问答感受一下全流程再逐步替换每个环节的组件比如把默认Embedding换成中文模型、把默认切块换成结构化切块最后再研究指标和优化。每一步都能看到“之前什么样、之后什么样”的对比进步最扎实。我踩过几次坑之后最大的体会是RAG系统就像一条水管漏水的地方通常不在龙头而在接口处。文档解析、切块、检索、生成任何一个环节出问题最终都表现为“回答不对”但不懂拆解的人往往只会觉得“AI太笨了”。把链路每一环独立出来调试比盲目调参有用一百倍。先跑通再优化这条路零基础完全走得通动手做起来比看十篇教程都强。
阅读完成 · 觉得有帮助?
咨询建站