一个 RAG 框架处理 PDF 里的图表和公式------RAG-Anything 架构拆解与踩坑实录
传统 RAG 系统遇到 PDF 里的柱状图、LaTeX 公式、嵌套表格时,基本上就废了。OCR 强转文字丢掉视觉语义,表格结构全乱,公式变成一堆乱码。我最近在项目里需要处理一批混合了大量图表的金融研报,试了三四个 RAG 方案,最后停在了 RAG-Anything 上。
这是港大黄超团队开源的框架,GitHub 上 16k+ 星。它基于 LightRAG,核心卖点是把文本、图片、表格、公式统一扔进一个知识图谱里做检索。听起来很理想,实际用下来有惊喜也有坑。这篇文章拆一下它的架构,贴实际能跑的代码,然后说说我踩过的问题。
传统 RAG 碰到多模态文档会怎样
先说问题有多严重。拿一份典型的券商研报举例:30 页 PDF,里面有 15 个数据表格、8 张趋势图、若干段内嵌公式。传统 RAG 的处理流程是:
- PyPDF2 提取文字 → 表格变成一行一行的碎片文本,列对不上
- 图片直接跳过或者 OCR 提取图上的文字 → "2024" "营收" "增长" 这些散碎词,完全丢失了"2024 年营收同比增长 23%"这个语义
- 公式用 OCR 识别 →
\frac{P}{E}变成 "P E" 或者乱码
问个简单问题:"这份报告里哪个季度毛利率最高?"传统 RAG 大概率答不上来,因为毛利率数据在表格里,表格被切碎了。
RAG-Anything 的三阶段管线
RAG-Anything 用三个阶段解决这个问题:文档解析 → 跨模态知识构建 → 检索生成。
第一阶段:文档解析(MinerU 干的活)
RAG-Anything 没有自己造解析轮子,它用的是 MinerU(上海 AI Lab 开源的文档解析引擎)。MinerU 做的事情是把 PDF 拆成结构化的 block:
- 文本块:正文段落、标题、脚注
- 图片块:截图 + 位置坐标
- 表格块:识别出行列结构,输出 HTML 或 Markdown 表格
- 公式块:LaTeX 格式输出
这一步很关键。MinerU 不只是 OCR,它有布局分析模型(用的 PaddleOCR),能区分"这一块是表格"和"这一块是正文"。
python
from raganything import RAGAnything
rag = RAGAnything(
working_dir="./my_rag_storage",
llm_model_func=your_llm_func,
vision_model_func=your_vlm_func,
embedding_func=your_embed_func,
)
# 一行代码触发整个管线
await rag.insert_document(
file_path="research_report.pdf",
lang="ch", # 中文文档用 "ch"
device="cuda:0", # MinerU 推理设备
)
insert_document 内部调用 MinerU 解析 PDF,拿到结构化输出后进入第二阶段。
第二阶段:跨模态内容理解与知识图谱构建
解析完拿到不同类型的 block 后,RAG-Anything 按类型走不同的处理路径。
文本块最简单,直接送进 LLM 做实体提取和关系抽取,生成知识图谱的节点和边。表格块稍复杂一些,先用表格专用处理器把行列结构转成自然语言------比如一个收入表格会被转成"2024年Q1营收为85.3亿元,同比增长12.7%;Q2营收为91.6亿元...",然后再做实体提取。
图片块是最费钱的环节。RAG-Anything 把截图发给 VLM(视觉语言模型),让它用文字描述图片内容。一张柱状图会被描述成"图表显示2022年至2024年的营收趋势,2022年为310亿元,2023年为356亿元,2024年为412亿元"。公式块走 LaTeX 解析器,把 \text{ROE} = \frac{\text{净利润}}{\text{净资产}} 这类表达式转成文字。
这四条路径的输出最后都汇入同一个 LightRAG 知识图谱。图表里的数据点和正文里的分析结论,通过实体关系连在一起。
来看实际的知识图谱构建代码:
python
# RAG-Anything 内部的多模态路由逻辑(简化版)
async def process_block(block):
if block.type == "text":
# 直接走 LightRAG 的文本处理
entities, relations = await extract_from_text(block.content)
elif block.type == "table":
# 表格先转自然语言
table_desc = await table_processor.describe(block.html_content)
entities, relations = await extract_from_text(table_desc)
elif block.type == "image":
# 图片送 VLM
image_desc = await vlm.describe(block.screenshot_path)
entities, relations = await extract_from_text(image_desc)
elif block.type == "equation":
# 公式转文本描述
eq_desc = latex_parser.to_text(block.latex)
entities, relations = await extract_from_text(eq_desc)
# 统一写入知识图谱
await knowledge_graph.insert(entities, relations)
第三阶段:检索与生成
查询时 RAG-Anything 组合两种检索方式:向量检索走 embedding 相似度匹配,适合语义模糊的问题;图检索沿着知识图谱的边做跳转,适合需要跨文档关联的问题。
python
# 三种检索模式
result = await rag.aquery(
"2024年Q3的毛利率是多少?",
param=QueryParam(mode="hybrid") # naive / local / global / hybrid
)
print(result)
hybrid 模式同时跑向量检索和图检索,合并结果后送进 LLM 生成回答。对于"毛利率"这种问题,图检索能沿着"Q3 → 毛利润 → 营收"的路径找到表格里的具体数字,比纯向量检索准确不少。
实际部署踩坑记录
坑 1:MinerU 3.0 的 API 变更
今年 3 月底 MinerU 升级到 3.0.0,改成了服务化架构。以前是直接调二进制文件解析,现在变成 mineru-api 服务模式。如果你不传 --api-url,它会自动启动一个本地临时服务。
问题在于:旧版教程(包括很多博客文章)里的代码是按 2.x 写的,直接跑会报错。要么锁定 MinerU 2.x 版本,要么改成服务调用方式:
bash
# 方法1:锁版本
pip install magic-pdf==0.9.3
# 方法2:用 3.0 的服务模式
mineru-api serve --port 8765
# 然后在 RAG-Anything 配置里指定 api_url
我选的是方法 1,因为项目里不想多维护一个服务。
坑 2:embedding 双重包装 bug
RAG-Anything 的 examples 目录里有个已知 bug:embedding 函数被多包了一层,导致推理时报维度不匹配。官方已经在 GitHub commit 里修了,但如果你 clone 的时间不对,可能还会碰到。
修复方式很直接:
python
# 错误写法(旧 example 里的)
async def embedding_func(texts):
return await openai_embed(texts) # 外面又包了一层
# 正确写法
embedding_func = openai_embed.func # 直接用内部函数
坑 3:VLM 调用的成本控制
RAG-Anything 默认对每个图片块都调 VLM。一份 50 页的 PDF 可能有几十张图,每张图一次 VLM 调用。如果用的是 GPT-4o,成本会很可观。
我的做法是加了一个过滤层:先对图片做简单分类(用一个轻量的 CLIP 模型),只对信息密度高的图片(数据图表、流程图)调 VLM,纯装饰性的图片直接跳过。
python
import clip
import torch
from PIL import Image
model, preprocess = clip.load("ViT-B/32")
def should_process_image(image_path):
"""判断图片是否值得送 VLM 分析"""
image = preprocess(Image.open(image_path)).unsqueeze(0)
text = clip.tokenize(["data chart", "table", "diagram", "decorative image", "logo"])
with torch.no_grad():
image_features = model.encode_image(image)
text_features = model.encode_text(text)
similarity = (image_features @ text_features.T).softmax(dim=-1)
# 前三类(图表/表格/流程图)相似度高就处理
info_score = similarity[0][:3].sum().item()
return info_score > 0.5
加上这个过滤后,VLM 调用量降了大约 60%,成本和速度都改善明显。
坑 4:中文文档的 PaddleOCR 依赖链
MinerU 依赖 PaddleOCR 做 OCR。PaddleOCR 有 C++ 组件,在 macOS 上编译偶尔会出问题(特别是 M 系列芯片)。如果遇到安装失败,可以试:
bash
# 先装 paddlepaddle
pip install paddlepaddle==3.0.0
# 再装 paddleocr
pip install paddleocr==2.9.1
# 如果还不行,用 conda 环境
conda install -c conda-forge paddlepaddle
中文文档解析时记得设 lang="ch",不然 OCR 模型加载的是英文的,中文识别率会很差。
和其他方案的对比
用同一份 30 页中文研报测试,问 10 个涉及图表和表格数据的问题:
- 纯 LangChain + PyPDF:10 个里答对 2 个,基本只能回答纯文本段落的问题
- Unstructured.io + Chroma:答对 5 个,表格处理比纯 PyPDF 好,但图表还是不行
- RAG-Anything:答对 8 个,图表和表格数据都能检索到,错的 2 个是公式相关的(LaTeX 解析偶尔会丢符号)
速度方面有代价。RAG-Anything 建索引时要跑 MinerU 解析加 VLM 调用,单文档 3-5 分钟(GPU 环境),比纯文本方案慢好几倍。查询速度差不多,都是在知识图谱和向量库上走。
适合什么场景
RAG-Anything 适合图文混合的专业文档:金融研报、学术论文、技术手册。如果你的文档主要是纯文本,LightRAG 就够了。
有个许可证的问题值得提一下。MinerU 用 AGPL-3.0,商业闭源项目用它需要评估合规风险,这在实际落地时可能是个阻碍。
代码在 github.com/HKUDS/RAG-Anything,pip install raganything 装。版本用 1.2.10 以上,之前的接口不太稳定。