从“脏文档”到“智能问答”:RAG文档清洗、向量化与检索增强生成全解析
写在前面在开发企业级AI知识库问答系统时我踩过最大的坑不是大模型本身而是文档处理。用户上传一份PDF里面有乱七八糟的页眉页脚、表格错位、扫描图片、甚至手写批注——直接扔给大模型回答质量惨不忍睹。后来我花了大量时间打磨文档清洗、智能切分、向量化存储这一整套流水线再配合LangChain FAISS做检索增强生成RAG才真正让系统变得“能用”。本文将完整还原这套流程的每一个环节并附上可直接运行的代码示例和流程图希望对正在做RAG的朋友有所帮助。一、RAG的“第一公里”文档清洗去芜存菁用户上传的原始文档格式五花八门PDF、Word、Markdown、TXT甚至扫描件。如果不做清洗直接切分向量库里会塞满噪声检索时召回一堆垃圾。核心清洗步骤格式统一与文本提取使用PyPDF2/pdfplumber处理PDFpython-docx处理Wordmarkdown库处理MD。对扫描版PDF需要接入OCR如Tesseract或PaddleOCR。噪声过滤移除页眉、页脚、页码通常用正则匹配第X页或Page X去除空行、多余空格、制表符过滤掉长度10字符的“无效行”可能是图片alt或乱码特殊字符标准化将全角符号转半角统一换行符为\n移除控制字符如\x00表格处理将表格转为Markdown格式或JSON保留行列关系。可以先用camelot或tabula提取表格再序列化为文本。下面是一个简单的文档清洗流程图代码示例简化版import pdfplumber import re def clean_pdf(file_path): text with pdfplumber.open(file_path) as pdf: for page in pdf.pages: page_text page.extract_text() # 去除页码假设页码在单独一行 page_text re.sub(r\n\s*\d\s*\n, \n, page_text) # 去除页眉根据你的文档特征定制正则 page_text re.sub(r机密文件.*?\n, , page_text) text page_text \n # 全局清洗 text re.sub(r\n\s*\n, \n\n, text) # 多余空行合并 text re.sub(r[ \t], , text) # 多个空格合并 return text.strip()清洗后的文本才能进入下一环节切分Chunking。二、智能切分决定RAG质量的关键一步切分Chunking的目标是把长文档切成语义完整的短片段每个片段在向量检索时能独立回答问题。切太短上下文丢失切太长检索精度下降且浪费Token。常见切分策略对比我最终选择的方案是“递归字符切分 重叠窗口”使用LangChain的RecursiveCharacterTextSplitter分隔符优先级[\n\n, \n, 。, , , , , ]块大小chunk_size 500字符中文场景约200~250 tokens重叠长度chunk_overlap 50字符保证边界上下文不丢失代码示例from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , , , ], length_functionlen, ) chunks text_splitter.split_text(cleaned_text)下图展示了重叠窗口切分的效果切分完成后每个chunk都会带上元数据源文档ID、页码、标题层级等便于后续溯源。三、向量化把文本变成计算机能理解的“空间位置”向量化Embedding是将文本片段映射到高维向量空间的过程。语义相似的文本在向量空间中距离更近。这是RAG检索的核心。Embedding模型选型代码示例使用LangChain的Embedding接口from langchain_community.embeddings import OllamaEmbeddings embeddings OllamaEmbeddings(modelnomic-embed-text) # 单个文本向量化 vector embeddings.embed_query(什么是RAG) print(len(vector)) # 768 # 批量向量化文档片段 chunk_vectors embeddings.embed_documents(chunks)向量化的输出是一个二维数组[chunk_count, embedding_dim]。接下来就需要把这些向量连同原始文本一起存入向量数据库。四、向量数据库存储FAISS的本地化方案向量数据库负责存储Embedding向量并提供高效的相似度检索。常见选择FAISS本地/内存、Chroma轻量、Pinecone云、Milvus生产级。我的RAG项目初期使用FAISS因为它完全本地化、速度极快、无需额外服务。存储流程为每个chunk生成向量构建FAISS索引默认使用内积或L2距离将向量和对应的元数据文本内容、来源一起保存代码示例from langchain_community.vectorstores import FAISS from langchain.schema import Document # 构造Document对象列表 documents [Document(page_contentchunk, metadata{source: file_name, chunk_id: i}) for i, chunk in enumerate(chunks)] # 创建向量库自动调用Embedding vector_store FAISS.from_documents(documents, embeddings) # 持久化到磁盘 vector_store.save_local(faiss_index)下图展示了向量化存储的全流程加载已有向量库vector_store FAISS.load_local(faiss_index, embeddings, allow_dangerous_deserializationTrue)五、LangChain 向量数据库检索增强生成RAGRAG的核心流程用户提问 → 向量检索相关chunk → 拼接上下文 → 调用大模型生成答案。标准RAG流程LangChain实现简化版from langchain.chains import RetrievalQA from langchain_community.llms import Ollama # 准备Retriever retriever vector_store.as_retriever( search_typesimilarity, # 或 mmr (最大边际相关性) search_kwargs{k: 4} # 召回4个相关片段 ) # 选择大模型本地Ollama llm Ollama(modelqwen2.5:7b, temperature0.1) # 构建QA Chain qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, # 将所有chunk一次性塞进prompt retrieverretriever, return_source_documentsTrue # 返回引用 ) # 执行问答 result qa_chain.invoke({query: 什么是RAG}) print(答案:, result[result]) for doc in result[source_documents]: print(来源:, doc.metadata[source])进阶技巧MMR检索search_typemmr平衡相关性和多样性避免重复内容上下文压缩如果chunks过多先压缩再拼入Prompt用LLMChainExtractorHyDE假设性文档嵌入先让模型生成一个“假设答案”再用这个答案去检索提高检索质量下面是一个完整的RAG流水线总览图六、总结与最佳实践建议清洗是根本脏数据进垃圾答案出。针对不同文档类型财报、法律合同、技术文档定制清洗规则。切分策略要实验没有万能参数。用你自己的测试集对比chunk_size256/512/1024下的检索命中率。Embedding模型选择中文场景优先BAAI/bge系列或m3e兼顾效果与速度。有条件可以用OpenAI的text-embedding-3。检索参数调优Top-K一般在3-5之间。加上相似度阈值score threshold过滤低质量匹配。引用溯源务必保存每个chunk的元数据让用户能点击查看原文这大大提升了信任度。持续迭代定期评估问答质量人工或用LLM-as-Judge根据bad case调整清洗和切分逻辑。RAG系统不是“一锤子买卖”它需要像软件一样持续优化。希望这篇博客能帮你少踩一些坑快速搭建出可用的知识库问答系统。