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代码实现,敬请下回分解!

相关推荐
Eward-an1 分钟前
高效构建长度为 n 的开心字符串中第 k 小的字符串
python·leetcode
Bert.Cai10 分钟前
Python time.sleep函数作用
开发语言·python
workflower10 分钟前
OpenClaw 是什么
人工智能·chatgpt·机器人·测试用例·集成测试·ai编程
彭于晏Yan12 分钟前
Springboot实现微服务监控
spring boot·后端·微服务
shughui14 分钟前
Miniconda下载、安装、关联配置 PyCharm(2026最新图文教程)
ide·python·pycharm·miniconda
小江的记录本26 分钟前
【Spring Boot—— .yml(YAML)】Spring Boot中.yml文件的基础语法、高级特性、实践技巧
xml·java·spring boot·后端·spring·spring cloud·架构
左耳咚27 分钟前
Claude Code 技术全景概览
前端·ai编程
爱敲代码的小黄38 分钟前
Agent 能力模块化:Skill 设计与执行机制解析
人工智能·后端·面试
掘金者阿豪44 分钟前
告别SQL性能焦虑:金仓数据库“连接条件下推”的性能魔法
后端
rgb2gray1 小时前
论文详解 | TWScan:基于收紧窗口的增强扫描统计,实现不规则形状空间热点精准检测
网络·人工智能·python·pandas·交通安全·出租车