写在前面在之前的系列文章中我们多次提到高质量文档解析 高质量 RAG 召回 企业 AI 落地的地基。很多读者留言问能不能给一份完整的代码示例我想把我的 PDF 文档变成能对话的知识库具体该怎么做今天我们就用一份可复现的实战教程手把手带你走完从 PDF 文档解析 → 向量化存储 → 智能问答的全流程。所有代码均在 GitHub 开源欢迎 Star 和 Fock一、核心链路概览先明确我们要搭建的系统架构用户提问 ↓ 用户问题向量化 ↓ 向量数据库检索Milvus ↓ 检索到最相关的文档片段Chunk ↓ 将 Chunk 作为上下文 用户问题 → 调用大模型 ↓ 大模型生成精准回答 ↓ 返回给用户整个链路中最关键的起点是第一步如何将 PDF 文档高质量地转化为可检索的 Chunk这就是 MinerU 大显身手的地方。二、环境准备与安装2.1 基础环境要求# Python 版本要求3.8 python --version # 推荐使用 conda 创建独立环境 conda create -n mineru_qa python3.10 conda activate mineru_qa2.2 安装 MinerU# 安装 MinerU 文档解析引擎 pip install mineru # 验证安装是否成功 python -c from mineru import MinerU; print(MinerU 安装成功)2.3 安装其他依赖组件# 向量数据库我们使用 Milvus 的轻量版 pip install pymilvus # 大模型 API以 OpenAI 兼容接口为例 pip install openai # 文本嵌入模型 pip install sentence-transformers # 其他工具 pip install tqdm三、核心实战从 PDF 到智能问答3.1 第一步用 MinerU 解析 PDF 文档这是整个系统的基础。MinerU 采用多模型协作架构能够将复杂排版的 PDF 转化为高质量的结构化 Markdown 数据。from mineru import MinerU # 初始化 MinerU 解析器 parser MinerU() # 解析 PDF 文档 pdf_path ./企业年度技术报告.pdf result parser.parse(pdf_path) # 查看解析结果 print(f文档标题: {result.title}) print(f页面数量: {result.total_pages}) print(f解析耗时: {result.parse_time:.2f} 秒) # 获取结构化 Markdown 内容 markdown_content result.to_markdown() # 保存为 Markdown 文件 with open(parsed_output.md, w, encodingutf-8) as f: f.write(markdown_content) print(✅ PDF 解析完成结果已保存为 parsed_output.md)为什么要用 MinerU传统解析工具如 PyPDF2、pdfplumber在处理双栏排版、复杂表格、数学公式时要么版面错乱要么数据丢失。而 MinerU 基于版面分析 OCR 公式识别的多模型协作架构能够✅ 精准还原双栏、多栏文档的正确阅读顺序✅ 无损提取复杂表格转化为结构化 Markdown 表格✅ 将数学公式转化为 LaTeX 代码✅ 识别标题层级H1/H2/H3保留文档逻辑结构3.2 第二步构建语义级文档切片Chunking有了高质量的结构化数据接下来要进行智能切片。传统的做法是按固定字数暴力切割但 MinerU 保留了文档的标题层级和段落边界我们可以基于文档语义结构进行切片。import re def semantic_chunking(markdown_text: str, max_chunk_size: int 1000): 基于文档语义结构进行智能切片 chunks [] current_chunk current_heading lines markdown_text.split(\n) for line in lines: # 检测标题H1/H2/H3 heading_match re.match(r^(#{1,3})\s(.)$, line) if heading_match: # 如果当前区块有内容保存为一个 Chunk if current_chunk.strip(): chunks.append({ heading: current_heading, content: current_chunk.strip(), length: len(current_chunk.strip()) }) # 开始新的区块 level len(heading_match.group(1)) current_heading heading_match.group(2) current_chunk line \n else: current_chunk line \n # 如果当前区块超过最大长度强制分割 if len(current_chunk) max_chunk_size: chunks.append({ heading: current_heading, content: current_chunk.strip(), length: len(current_chunk.strip()) }) current_chunk # 处理最后一个 Chunk if current_chunk.strip(): chunks.append({ heading: current_heading, content: current_chunk.strip(), length: len(current_chunk.strip()) }) return chunks # 对解析结果进行切片 chunks semantic_chunking(markdown_content) print(f✅ 共生成 {len(chunks)} 个语义 Chunk) for i, chunk in enumerate(chunks[:5]): print(f Chunk {i1}: [{chunk[heading]}] {chunk[length]} 字符)语义切片相比暴力切片的优势对比维度暴力切片按字数语义切片MinerU 赋能上下文完整性段落可能被切断上下文断裂标题正文完整保留检索精度容易检索到无意义的片段检索到的是语义完整的知识块大模型理解需要拼凑多个片段容易产生幻觉单一片段即可提供完整上下文3.3 第三步向量化并存入 Milvusfrom sentence_transformers import SentenceTransformer from pymilvus import connections, Collection, CollectionSchema, FieldSchema, DataType, utility # 加载嵌入模型 embedder SentenceTransformer(BAAI/bge-base-zh-v1.5) # 中文场景推荐 # 连接 Milvus connections.connect(hostlocalhost, port19530) # 定义集合名称 collection_name mineru_docs # 如果集合已存在先删除 if utility.has_collection(collection_name): utility.drop_collection(collection_name) # 定义字段 fields [ FieldSchema(nameid, dtypeDataType.INT64, is_primaryTrue, auto_idTrue), FieldSchema(nameheading, dtypeDataType.VARCHAR, max_length500), FieldSchema(namecontent, dtypeDataType.VARCHAR, max_length10000), FieldSchema(nameembedding, dtypeDataType.FLOAT_VECTOR, dim768) # bge-base 维度为768 ] schema CollectionSchema(fields, descriptionMinerU PDF 文档知识库) collection Collection(namecollection_name, schemaschema) # 准备数据 headings [chunk[heading] for chunk in chunks] contents [chunk[content] for chunk in chunks] embeddings embedder.encode(contents) # 插入数据 collection.insert([ headings, contents, embeddings.tolist() ]) # 创建索引加速检索 index_params { metric_type: IP, # 内积相似度 index_type: IVF_FLAT, params: {nlist: 128} } collection.create_index(field_nameembedding, index_paramsindex_params) # 加载集合到内存 collection.load() print(f✅ 已将 {len(chunks)} 个 Chunk 存入 Milvus 向量数据库)3.4 第四步构建检索问答函数from openai import OpenAI # 初始化大模型客户端以兼容 OpenAI API 的服务为例 client OpenAI( api_keyyour-api-key, base_urlhttps://api.openai.com/v1 # 或你使用的其他兼容服务 ) def search_and_answer(query: str, top_k: int 3): 检索最相关的文档片段并用大模型生成回答 # 1. 将用户问题向量化 query_embedding embedder.encode([query]) # 2. 在 Milvus 中检索最相似的 Chunk search_params { metric_type: IP, params: {nprobe: 10} } results collection.search( dataquery_embedding, anns_fieldembedding, paramsearch_params, limittop_k, output_fields[heading, content] ) # 3. 构建上下文 context_parts [] for i, hits in enumerate(results): for hit in hits: context_parts.append(f[{hit.entity.get(heading)}]\n{hit.entity.get(content)}) context \n\n---\n\n.join(context_parts) # 4. 调用大模型生成回答 system_prompt 你是一个专业的文档问答助手。请根据提供的文档内容回答用户问题。 要求 1. 如果文档内容中有明确答案请直接回答并引用原文 2. 如果文档内容不足以回答问题请如实告知不要编造 3. 回答要简洁、准确、有条理 user_prompt f以下是相关文档内容 {context} --- 请根据以上文档内容回答以下问题 {query} response client.chat.completions.create( modelgpt-4o-mini, # 或您使用的其他模型 messages[ {role: system, content: system_prompt}, {role: user, content: user_prompt} ], temperature0.3, # 低温度确保回答更精确 max_tokens1000 ) answer response.choices[0].message.content return { question: query, answer: answer, references: [hit.entity.get(heading) for hits in results for hit in hits] }3.5 第五步体验智能问答效果# 测试问答 test_questions [ 我们公司去年的营收增长率是多少, 研发部门在 AI 技术方面有哪些主要成果, 报告中提到的未来战略方向是什么 ] for q in test_questions: print(f\n{*60}) print(f❓ 问题: {q}) print(f{*60}) result search_and_answer(q) print(f\n 回答:) print(result[answer]) print(f\n 参考来源:) for ref in result[references]: print(f - {ref}) print(f{*60})四、效果验证有 MinerU vs 无 MinerU 的对比为了让大家更直观地感受 MinerU 的价值我们做了一组对照组实验。测试文档一份包含复杂表格、双栏排版和数学公式的技术白皮书PDF32 页对照组传统解析PyPDF2 暴力切片 Milvus GPT-4o问题回答质量问题表 3 中的 Q3 净利润是多少❌ 回答抱歉我找不到相关信息 或给出错误数值表格数据丢失或错位公式 2.1 中的变量含义是什么❌ 回答乱码或答非所问公式被识别为普通文本报告第二章的核心观点是什么❌ 回答的是第三章的内容双栏排版导致阅读顺序错乱实验组MinerU 解析 语义切片 Milvus GPT-4o问题回答质量原因表 3 中的 Q3 净利润是多少✅ 准确回答Q3 净利润为 1,280 万元同比增长 15.3%表格被无损还原为结构化数据公式 2.1 中的变量含义是什么✅ 准确解释每个变量的物理意义公式识别为 LaTeX 代码语义完整保留报告第二章的核心观点是什么✅ 精准回答第二章的完整内容语义切片保留了标题层级和段落边界五、进阶技巧与最佳实践5.1 批量处理大规模文档import os from glob import glob # 批量处理一个目录下所有 PDF pdf_dir ./docs/ pdf_files glob(os.path.join(pdf_dir, *.pdf)) all_chunks [] for pdf_path in pdf_files: print(f正在处理: {pdf_path}) result parser.parse(pdf_path) markdown_content result.to_markdown() chunks semantic_chunking(markdown_content) all_chunks.extend(chunks) print(f 完成生成 {len(chunks)} 个 Chunk) print(f\n✅ 共处理 {len(pdf_files)} 个 PDF 文件生成 {len(all_chunks)} 个 Chunk)5.2 混合检索策略除了向量检索还可以结合全文检索BM25提升召回效果# 在 Milvus 中同时使用向量检索和全文检索 # 或者使用 ElasticSearch Milvus 的混合方案 hybrid_results hybrid_search( query营收增长率, vector_weight0.7, # 向量检索权重 keyword_weight0.3 # 关键词检索权重 )5.3 文档解析质量监控def quality_check(parsed_result): 对解析结果进行质量检查 issues [] # 检查是否存在过多的乱码字符 garbled_ratio sum(1 for c in parsed_result.to_markdown() if ord(c) 127) / len(parsed_result.to_markdown()) if garbled_ratio 0.1: issues.append(f警告乱码字符比例较高 ({garbled_ratio:.2%})) # 检查标题层级是否完整 heading_count parsed_result.to_markdown().count(\n#) if heading_count 3: issues.append(f警告文档标题层级较少 ({heading_count})建议检查版面分析质量) return issues六、常见问题 FAQQ1MinerU 支持哪些格式的输入目前主要支持 PDF包括原生数字 PDF 和扫描件 PDF。对于图片格式如 PNG/JPG建议先转为 PDF 后再处理。Q2解析速度如何对于一份 50 页的普通 PDF无大量 OCR解析时间通常在 10-30 秒。对于扫描件需要 OCR速度会慢一些约 1-3 分钟。Q3中文文档支持好吗MinerU 对中文文档有专门的优化包括中文版面分析模型和中文 OCR 模型效果非常出色。Q4能否识别手写文字目前 MinerU 主要针对印刷体文档优化手写文字的识别效果有限建议结合专门的 handwriting OCR 模型。七、总结与展望通过本文的实战教程我们完整地走通了从 PDF 文档解析 → 语义切片 → 向量化存储 → 智能问答的全流程。核心收获有三点MinerU 是 RAG 系统的数据基石它解决了复杂 PDF 文档的高质量解析问题让垃圾进、垃圾出变为高质量进、精准回答出。语义切片优于暴力切片基于 MinerU 保留的标题层级和段落边界进行切片检索和问答质量有质的飞跃。开源生态无缝集成MinerU 可以轻松接入 Milvus、ElasticSearch 等主流组件构建完整的 RAG 系统。在 AI 时代企业真正的竞争壁垒不在于模型而在于数据——尤其是那些深藏在 PDF 中的独家知识资产。MinerU 正是帮助企业把这些沉睡的知识唤醒的最佳工具。 最后的话如果你正在构建 RAG 系统建议把文档解析环节作为优先优化的方向。很多时候90% 的检索效果提升来自于更好的数据预处理而不是更复杂的模型调优。本文所有代码已开源欢迎在 GitHub 上关注 MinerU 项目。如果你有更复杂的文档解析需求欢迎在评论区留言交流