自定义分割器,针对技术文档调整chunk size
我搭建企业知识库混合RAG方案踩坑实录前不久接了一个内部知识库问答系统的项目客户是一家中型制造业企业痛点很明确员工每天要查几十份PDF、Word和Excel文档找资料全靠CtrlF效率低得让人发指。他们希望有一个能“对话”的助手输入自然语言就能直接拿到精准答案。项目规模不大数据量大概在50GB左右涉及数千份文档但要求响应速度必须在2秒以内且准确率不能太低毕竟涉及到生产操作规范答错了是要出安全事故的。说实话一开始我觉得这活儿挺简单不就是调个API加个向量库吗结果试了一圈发现水深得离谱。方案选型与架构决策在动手写代码之前我花了一周时间对比了市面上的主流RAG框架。LangChain生态丰富文档多但太臃肿LlamaIndex在数据处理层做得很深入特别是对于非结构化文本的解析能力很强但对于复杂的业务逻辑编排显得不够灵活。我自研过一套基于LangGraph的轻量级Agent工作流虽然代码量少但维护成本高。当时有方案A和方案B我选了混合方案。方案A是全套使用LangChain方案B是核心检索用自研生成部分调用开源模型。我最终选择了基于LangChain做编排底座但在数据预处理和检索策略上做了深度定制。为什么因为客户的数据格式极其混乱有些扫描件识别后的OCR错误率高达15%普通的Embedding直接扔进去效果极差。我必须介入数据清洗环节而不是盲目依赖框架的默认Loader。我们选用了Qwen 2.5作为基座模型它在中文语境下的表现确实比Llama 3.1更稳尤其是对行业术语的理解。向量数据库则用了Milvus毕竟50GB的数据量单机PGVector虽然部署简单但在并发查询和扩展性上扛不住。有意思的是我们在评估阶段引入了RAGAS工具自动化评估了召回率和忠实度。pythonfrom langchain_community.document_loaders import PyPDFLoaderfrom langchain.text_splitter import RecursiveCharacterTextSplitterfrom langchain_qwen import QwenLLM自定义分割器针对技术文档调整chunk sizesplitter RecursiveCharacterTextSplitter(chunk_size500,chunk_overlap50,length_functionlen,separators[\n\n, \n, 。, ])加载并分割文档loader PyPDFLoader(technical_manual.pdf)documents loader.load()chunks splitter.split_documents(documents)初始化向量存储vectorstore MilvusVectorStore.from_documents(chunks,embedding,collection_nametech_manual)这里有个细节很多人喜欢把chunk设得很大觉得上下文全才有意义。我当时觉得这样就行结果发现召回的片段里包含大量无关噪音导致LLM产生幻觉。后来我把chunk size压到500 tokens虽然检索数量变多了但通过重排序Rerank步骤过滤后质量显著提升。踩坑经历与排查过程真正让我头秃的是“幻觉”问题。有一次测试我问系统“某型号电机的额定电压是多少”它自信满满地回答了一个数字但实际上文档里根本没有这个数据它是瞎编的。查了一下日志发现是因为Embedding模型把“电压”和“电流”的语义空间靠得太近导致检索到了错误的文档片段。坑死了这比代码bug还难修。我排查了整整两天尝试了换Embedding模型从BGE换到M3效果提升有限。最后我想到了“混合检索”。单纯的向量检索对精确匹配很不友好比如查具体的“零件编号”或“错误代码”向量相似度根本抓不到重点。于是我引入了关键词检索BM25并将向量得分和关键词得分进行加权融合。代码如下pythonfrom langchain.retrievers import ContextualCompressionRetrieverfrom langchain.retrievers.document_compressors import FlashRankCompressor混合检索向量 BM25vector_retriever vectorstore.as_retriever(search_typesimilarity, search_kwargs{k: 5})bm25_retriever BM25Retriever.from_documents(documents)使用FlashRank进行重排序提高相关性compressor FlashRankCompressor(model_namems-marco-MiniLM-L-6-v2)compression_retriever ContextualCompressionRetriever(base_compressorcompressor,base_retrievervector_retriever)获取最终文档docs compression_retriever.get_relevant_documents(query)这一步之后幻觉率从15%降到了2%以下。但新的问题来了重排序步骤增加了200ms的延迟。客户要求的2秒响应现在变成了2.2秒。为了优化性能我把FlashRank模型量化了并且将Milvus的索引类型从HNSW改成了IVF_FLAT虽然召回率略有下降但查询速度提升了3倍。另外在处理PDF表格时我发现标准的Markdownify转换器会把跨页表格截断。我不得不写了一个自定义的TableExtractor先用Tabula-py提取表格数据再转为Markdown格式嵌入文档。这部分工作虽然繁琐但却是保证知识库可用性的关键。说实话做RAG系统最难的从来不是调通Demo而是处理那些脏数据和不确定的业务场景。这个项目中我最大的感悟就是不要迷信框架的默认配置每一个组件都要根据实际数据进行微调。混合检索、重排序、以及精细化的数据清洗这三样东西缺一不可。本文基于实际项目经验整理欢迎在评论区交流技术问题。