写在前面

在之前的系列文章中,我们多次提到:高质量文档解析 = 高质量 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 python=3.10  
conda activate mineru_qa  

2.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", encoding="utf-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 {i+1}: [{chunk['heading']}] {chunk['length']} 字符")  

语义切片相比暴力切片的优势:

对比维度 暴力切片(按字数) 语义切片(MinerU 赋能)
上下文完整性 段落可能被切断,上下文断裂 标题+正文完整保留
检索精度 容易检索到无意义的片段 检索到的是语义完整的知识块
大模型理解 需要拼凑多个片段,容易产生幻觉 单一片段即可提供完整上下文

3.3 第三步:向量化并存入 Milvus

from sentence_transformers import SentenceTransformer  
from pymilvus import connections, Collection, CollectionSchema, FieldSchema, DataType, utility  

# 加载嵌入模型  
embedder = SentenceTransformer('BAAI/bge-base-zh-v1.5')  # 中文场景推荐  

# 连接 Milvus  
connections.connect(host='localhost', port='19530')  

# 定义集合名称  
collection_name = "mineru_docs"  

# 如果集合已存在,先删除  
if utility.has_collection(collection_name):  
    utility.drop_collection(collection_name)  

# 定义字段  
fields = [  
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),  
    FieldSchema(name="heading", dtype=DataType.VARCHAR, max_length=500),  
    FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=10000),  
    FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768)  # bge-base 维度为768  
]  

schema = CollectionSchema(fields, description="MinerU PDF 文档知识库")  
collection = Collection(name=collection_name, schema=schema)  

# 准备数据  
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_name="embedding", index_params=index_params)  

# 加载集合到内存  
collection.load()  

print(f"✅ 已将 {len(chunks)} 个 Chunk 存入 Milvus 向量数据库")  

3.4 第四步:构建检索问答函数

from openai import OpenAI  

# 初始化大模型客户端(以兼容 OpenAI API 的服务为例)  
client = OpenAI(  
    api_key="your-api-key",  
    base_url="https://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(  
        data=query_embedding,  
        anns_field="embedding",  
        param=search_params,  
        limit=top_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(  
        model="gpt-4o-mini",  # 或您使用的其他模型  
        messages=[  
            {"role": "system", "content": system_prompt},  
            {"role": "user", "content": user_prompt}  
        ],  
        temperature=0.3,  # 低温度,确保回答更精确  
        max_tokens=1000  
    )  
    
    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 的价值,我们做了一组对照组实验。

测试文档:一份包含复杂表格、双栏排版和数学公式的技术白皮书(PDF,32 页)

对照组:传统解析(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_weight=0.7,  # 向量检索权重  
    keyword_weight=0.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  

六、常见问题 FAQ

Q1:MinerU 支持哪些格式的输入?

目前主要支持 PDF,包括原生数字 PDF 和扫描件 PDF。对于图片格式(如 PNG/JPG),建议先转为 PDF 后再处理。

Q2:解析速度如何?

对于一份 50 页的普通 PDF(无大量 OCR),解析时间通常在 10-30 秒。对于扫描件(需要 OCR),速度会慢一些,约 1-3 分钟。

Q3:中文文档支持好吗?

MinerU 对中文文档有专门的优化,包括中文版面分析模型和中文 OCR 模型,效果非常出色。

Q4:能否识别手写文字?

目前 MinerU 主要针对印刷体文档优化,手写文字的识别效果有限,建议结合专门的 handwriting OCR 模型。


七、总结与展望

通过本文的实战教程,我们完整地走通了从 PDF 文档解析 → 语义切片 → 向量化存储 → 智能问答的全流程。

核心收获有三点:

  1. MinerU 是 RAG 系统的数据基石:它解决了复杂 PDF 文档的高质量解析问题,让"垃圾进、垃圾出"变为"高质量进、精准回答出"。
  2. 语义切片优于暴力切片:基于 MinerU 保留的标题层级和段落边界进行切片,检索和问答质量有质的飞跃。
  3. 开源生态无缝集成:MinerU 可以轻松接入 Milvus、ElasticSearch 等主流组件,构建完整的 RAG 系统。

在 AI 时代,企业真正的竞争壁垒不在于模型,而在于数据——尤其是那些深藏在 PDF 中的独家知识资产。MinerU 正是帮助企业把这些"沉睡的知识"唤醒的最佳工具。


💡 最后的话:

如果你正在构建 RAG 系统,建议把文档解析环节作为优先优化的方向。很多时候,90% 的检索效果提升来自于更好的数据预处理,而不是更复杂的模型调优。


本文所有代码已开源,欢迎在 GitHub 上关注 MinerU 项目。如果你有更复杂的文档解析需求,欢迎在评论区留言交流!

 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐