金色农场种植
46.30M · 2026-02-24
很多人做 RAG 时都会关注:向量模型、分块策略、Top-K 参数。 但真正影响效果的,往往是最容易被忽视的一步------PDF 解析。
如果解析结构错了,后面再强的模型,也只是在"错误文本"上做优化。
RAG 的流程:
问题出在第一步。
当 PDF 被错误解析时,常见问题包括:
这些问题会直接导致检索命中不准、上下文错位。
PDF 本质不是"文本文件",而是"绘图指令"。
它只记录:
它没有"段落"、"标题"、"表格"概念。
解析 PDF,其实是在做:
这就是难点。
原文:
错误解析后变成一整段文本,导致 chunk 边界失效。
原本结构化表格被识别成:
产品 价格 数量 A 100 2 B 200 5
Embedding 失去列语义。 在合同、标书、财务文档场景里,这会严重影响检索质量。
两栏论文如果按扫描顺序拼接,会导致语义错位。
看起来"有点相关",但始终不精准。
大 PDF(200+ 页)直接解析时常见问题:
解决方案:
示例:
with paddle.no_grad():
result = model(image)
del obj
gc.collect()
paddle.device.cuda.empty_cache()
优势包括:
在企业级 RAG 场景下,这些能力非常关键。
PaddleOCR-VL 是一款先进、高效的文档解析模型,专为文档中的元素识别设计。其核心组件为 PaddleOCR-VL-0.9B,这是一种紧凑而强大的视觉语言模型(VLM),它由 NaViT 风格的动态分辨率视觉编码器与 ERNIE-4.5-0.3B 语言模型组成,能够实现精准的元素识别。
使用比较简单
#生成md文件
paddleocr doc_parser
-i keep.pdf
--save_path ./output1
但是直接使用有很多问题,生成的md文档结构无法达到要求,特别是针对表格的生成。
表格里面的内容,继续被识别成文本与标题,造成内容重复;
通过解析出来的图片既能看到问题的原因,即被识别到表格中,又识别成text
由于分页造成一行的信息,被分割成多行。特别对标书这种文件,表格里面某一列的内容特别的多,就被分割成多行;
当一个pdf很大,超过150页时,直接卡死;
原始做法(容易出问题):
大 PDF → PaddleOCR → 直接输出 Markdown
优化后改为:
大 PDF
↓
① 切分(10页一段)
↓
② PaddleOCR 输出结构化 JSON
↓
③ 自己解析 JSON → 生成 Markdown
这一步是工程思维的转变。
每 5~10 页切一段,分段后优点:
源码示例:
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
优化点:
模型只加载一次
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()
推理不建图
with paddle.no_grad():
主动清理显存
del obj
gc.collect()
paddle.device.cuda.empty_cache()
完整示例:
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
总体目标
具体核心代码逻辑有:
这边的解析结构通过java代码实现,敬请下回分解!
一天一个开源项目(第31篇):awesome-openclaw-usecases - OpenClaw 真实用例集合
打造 ML/AI 系统的内部开发者平台(IDP)——设计可靠的机器学习(ML)系统
2026-02-24
2026-02-24