RAG 做不好?可能是你的 PDF 在"捣乱" 😅

RAG 做不好?可能是你的 PDF 在"捣乱" 😅

------一次把 PDF 解析从"能用"优化到"好用"的实战记录

很多人做 RAG 时都会关注:向量模型、分块策略、Top-K 参数。 但真正影响效果的,往往是最容易被忽视的一步------PDF 解析

如果解析结构错了,后面再强的模型,也只是在"错误文本"上做优化。


一、RAG 的隐形地基:PDF 解析

RAG 的流程:

  1. 解析文档
  2. 切分 chunk
  3. 向量化
  4. 检索
  5. 拼接上下文给 LLM

问题出在第一步。

当 PDF 被错误解析时,常见问题包括:

  • 表格被拆成乱序文本
  • 多栏内容拼接混乱
  • 标题层级丢失
  • 页眉页脚混入正文
  • 图片中的文字被忽略

这些问题会直接导致检索命中不准、上下文错位。


二、PDF 为什么难?

PDF 本质不是"文本文件",而是"绘图指令"。

它只记录:

  • 在某个坐标画一段文字
  • 在某个位置画一张图片
  • 在某个区域画线条

它没有"段落"、"标题"、"表格"概念。

解析 PDF,其实是在做:

结构重建。

这就是难点。


三、RAG 场景中的真实痛点

1. 标题结构丢失

原文:

  1. 项目背景
    1.1 技术路线

错误解析后变成一整段文本,导致 chunk 边界失效。


2. 表格被拍扁

原本结构化表格被识别成:

产品 价格 数量 A 100 2 B 200 5

Embedding 失去列语义。 在合同、标书、财务文档场景里,这会严重影响检索质量。


3. 多栏排版顺序错乱

两栏论文如果按扫描顺序拼接,会导致语义错位。

看起来"有点相关",但始终不精准。


四、工程痛点:显存爆炸

大 PDF(200+ 页)直接解析时常见问题:

  • GPU OOM
  • 模型重复加载
  • 未使用 no_grad
  • 中间张量未清理

解决方案:

  • 模型只加载一次
  • 分段解析 PDF
  • 使用 no_grad
  • 主动清理显存

示例:

python 复制代码
with paddle.no_grad():
    result = model(image)
python 复制代码
del obj
gc.collect()
paddle.device.cuda.empty_cache()

五、为什么选择 PaddleOCR

优势包括:

  • 支持文本识别 + 表格识别 + 版面分析
  • 可输出结构化表格
  • 支持本地部署
  • 可控性强,适合工程优化
  • 在开源的OCR中,准确率高

在企业级 RAG 场景下,这些能力非常关键。


六、工程优化过程

PaddleOCR-VL 是一款先进、高效的文档解析模型,专为文档中的元素识别设计。其核心组件为 PaddleOCR-VL-0.9B,这是一种紧凑而强大的视觉语言模型(VLM),它由 NaViT 风格的动态分辨率视觉编码器与 ERNIE-4.5-0.3B 语言模型组成,能够实现精准的元素识别。

使用比较简单

bash 复制代码
#生成md文件
paddleocr doc_parser \
  -i keep.pdf \
  --save_path ./output1

但是直接使用有很多问题,生成的md文档结构无法达到要求,特别是针对表格的生成。

  • 表格里面的内容,继续被识别成文本与标题,造成内容重复;

    通过解析出来的图片既能看到问题的原因,即被识别到表格中,又识别成text

  • 由于分页造成一行的信息,被分割成多行。特别对标书这种文件,表格里面某一列的内容特别的多,就被分割成多行;

  • 当一个pdf很大,超过150页时,直接卡死;

优化思路

原始做法(容易出问题):

复制代码
大 PDF → PaddleOCR → 直接输出 Markdown

优化后改为:

javascript 复制代码
大 PDF
  ↓
① 切分(10页一段)
  ↓
② PaddleOCR 输出结构化 JSON
  ↓
③ 自己解析 JSON → 生成 Markdown

这一步是工程思维的转变

分段解析大 PDF

每 5~10 页切一段,分段后优点:

  • 可控
  • 可恢复
  • 可并行
  • 不容易 OOM

源码示例:

python 复制代码
def split_pdf(pdf_path, output_dir, chunk_size):
    """将大 PDF 按页切分为多段。

    参数:
        pdf_path (str): 输入 PDF 文件路径。
        output_dir (str): 切分后的临时输出目录。
        chunk_size (int): 每个分段包含的页数。

    返回:
        list[str]: 切分得到的分段 PDF 路径列表。
    """
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    doc = fitz.open(pdf_path)
    total_pages = len(doc)
    logger.info(f"📦 [文件读取] 总页数: {total_pages}")
    split_files = []
    for i in range(0, total_pages, chunk_size):
        start = i
        end = min(i + chunk_size, total_pages)
        chunk_name = f"chunk_{start+1}_{end}.pdf"
        chunk_path = os.path.join(output_dir, chunk_name)
        new_doc = fitz.open()
        new_doc.insert_pdf(doc, from_page=start, to_page=end-1)
        new_doc.save(chunk_path)
        new_doc.close()
        split_files.append(chunk_path)
    doc.close()
    return split_files

生成pdf结构json

优化点:

  • 模型只加载一次

    python 复制代码
    def create_pipeline(device: Optional[str] = None) -> PaddleOCRVL:
        """创建并返回 `PaddleOCRVL` 管线。
    
        参数:
            device (Optional[str]): 设备标识,如 `gpu:0` 或 `cpu`;为 None 时按默认配置。
    
        返回:
            PaddleOCRVL: OCR/结构化管线实例。
        """
        return PaddleOCRVL(device=device) if device else PaddleOCRVL()
  • 推理不建图

    python 复制代码
    with paddle.no_grad():
  • 主动清理显存

    python 复制代码
    del obj
    gc.collect()
    paddle.device.cuda.empty_cache()

完整示例:

python 复制代码
def parse_one_file(pipeline: PaddleOCRVL, file_path: Path, output_path: Path):
    """解析单个文件并输出结构化结果。

    参数:
        pipeline (PaddleOCRVL): 已初始化的 OCR/结构化管线实例。
        file_path (Path): 待解析的文件路径(PDF 或图片)。
        output_path (Path): 结果输出目录,包含 JSON/图片/Markdown。

    返回:
        None
    """
    logger.info(f"开始解析 {file_path}")
    with paddle.no_grad():
        out = pipeline.predict(input=str(file_path))
    pages_res = list(out)
    structured = pipeline.restructure_pages(pages_res,merge_tables=False,relevel_titles=False)
    for res in structured:
        res.print()
        res.save_to_json(save_path=output_path)
        res.save_to_img(save_path=output_path)
        res.save_to_markdown(save_path=output_path)
    logger.info(f"完成解析 {file_path}")
    try:
        del pages_res
        del structured
    except Exception:
        pass
    gc.collect()
    try:
        if paddle.device.is_compiled_with_cuda():
            paddle.device.cuda.empty_cache()
    except Exception:
        pass

def create_pipeline(device: Optional[str] = None) -> PaddleOCRVL:
    """创建并返回 `PaddleOCRVL` 管线。

    参数:
        device (Optional[str]): 设备标识,如 `gpu:0` 或 `cpu`;为 None 时按默认配置。

    返回:
        PaddleOCRVL: OCR/结构化管线实例。
    """
    return PaddleOCRVL(device=device) if device else PaddleOCRVL()

def run_pdf2md(input_path: str, output_dir: str, pipeline: Optional[PaddleOCRVL] = None):
    """运行解析流程,输入为单文件或目录。

    参数:
        input_path (str): 输入路径,可以是文件或目录。
        output_dir (str): 输出目录,将保存 JSON/图片/Markdown。
        pipeline (Optional[PaddleOCRVL]): 可选外部管线;提供则复用,不提供则本地创建。

    返回:
        None
    """
    setup_logger()
    ip = Path(input_path)
    op = Path(output_dir)
    op.mkdir(parents=True, exist_ok=True)
    local_pipeline = False
    if pipeline is None:
        local_pipeline = True
        dev = os.environ.get("PADDLE_DEVICE")
        pipeline = create_pipeline(dev)
    if ip.is_dir():
        for fp in sorted(ip.rglob("*")):
            if fp.is_file() and fp.suffix.lower() in {".pdf", ".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff"}:
                try:
                    parse_one_file(pipeline, fp, op)
                except Exception as e:
                    logger.warning(f"文件处理失败 {fp}: {e}")
    else:
        try:
            parse_one_file(pipeline, ip, op)
        except Exception as e:
            logger.warning(f"文件处理失败 {ip}: {e}")
    if local_pipeline:
        try:
            del pipeline
        except Exception:
            pass
    gc.collect()
    try:
        if paddle.device.is_compiled_with_cuda():
            paddle.device.cuda.empty_cache()
    except Exception:
        pass

解析结构json

总体目标

  • 将目录或单页的 OCR 结构化结果( *_res.json )合并为有序、干净、可读的 Markdown 表格/段落输出
  • 正确处理文本与表格的关系、同表的连续合并、跨页首尾行续接、去重与清理

具体核心代码逻辑有:

  • 输入与排序
  • 同页文本去重
  • 文本并入表格
  • 表格连续合并
  • 跨页首尾行续接

这边的解析结构通过java代码实现,敬请下回分解!

相关推荐
FL16238631291 小时前
基于yolov11+django+deepseek的脑肿瘤检测系统带登录界面python源码+onnx模型+精美web界面
python·yolo·django
Cache技术分享2 小时前
332. Java Stream API - Java Stream 实战进阶:按年份找出合作最多的作者对
前端·后端
果冻虾仁2 小时前
vllm使用plugin集成外部模型结构
人工智能·后端
Penge6662 小时前
Go—临时对象池 sync.Pool
后端
hash__2 小时前
AI Agent 技术解析:从原理到实战
后端
软希网分享源码2 小时前
AIGC自动化编程实战(Python、Java、JavaScript和VBA) -2.9G课程
python·自动化·aigc
Rhystt2 小时前
furryCTF 题解|Web方向|保姆级详解|固若金汤、DeepSleep
git·python·安全·web安全·网络安全
谁不学习揍谁!2 小时前
基于python大数据机器学习旅游数据分析可视化推荐系统(完整系统+开发文档+部署教程+文档等资料)
大数据·python·算法·机器学习·数据分析·旅游·数据可视化
johnny2332 小时前
《Vibe Coding:AI编程时代的认知重构》笔记
笔记·ai编程