资讯详情

图文兼容的PDF RAG实战:PyMuPDF+Qwen-VL构建高质量知识库问答

📅 2026/10/12 9:25:40 | 华诺云谱 👁 阅读
图文兼容的PDF RAG实战:PyMuPDF+Qwen-VL构建高质量知识库问答
1. 为什么纯文本的 PDF RAG 不够用先从一个实际场景说起。我最近在给某个内部团队搭知识库问答时遇到了一个非常典型的问题文档目录看起来内容齐全但把 PDF 转成纯文本丢进向量库之后用户无论怎么问都得不到满意答案。排查之后才发现这份文档里有将近三分之一的页面包含重要配图包括业务流程图、权限关系图、系统架构图甚至还有不少“纯截图式”的操作教学页。传统方式只提取文字最终拿到的是零散的按钮名和段落标题真正表达“异常情况走 A 分支还是 B 分支”的信息全都在图像里。这就是图文兼容 PDF RAG 要解决的痛点。单纯的文本 RAG 在面对这类文档时检索到的上下文本身就是残缺的后面的生成环节再强也无济于事。常见的补全路线大概有三种我整理了一张对比表方案能识别图能理解图表语义实现成本典型问题纯文本提取否否低图区信息完全丢失OCR 文字识别部分弱中只能拿到文字无法理解布局与逻辑视觉语言模型是较强中高需要设计调用链路与成本控制OCR 只能解决“图里的文字”但解决不了“这张图想表达什么逻辑”。比如一张包含“判断条件 多个处理分支”的流程图OCR 能提取出几个判断词和节点文案却无法整理出节点之间的先后关系和分支走向。而视觉语言模型可以直接把整张图作为输入输出结构化的描述这就让下游检索和问答质量有了质的提升。所以我最终选了 PyMuPDF 负责解析图纸Qwen-VL 负责读图理解并围绕它们搭了一整套索引和检索链路。这套方案适合谁如果你的 PDF 里有不少图表、架构图、截图或者你正在做企业内部文档问答、产品说明书问答、论文阅读助手这类项目那么这一篇实战内容可以直接参考。下面我会从方案架构、代码实现、检索链路搭建、问题排查几个维度完整展开。2. 方案架构与工具选型2.1 整体流程是怎样设计的整套方案可以拆成四个阶段解析、理解、索引、检索。先用 PyMuPDF 把 PDF 页面拆成“文本块”和“图像区域”两类元素然后对图像区域调用 Qwen-VL 生成结构化描述接着把纯文本块和图像描述块统一做向量化写入向量库最后在问答时检索 TopK 相关块拼装成上下文交给大模型生成回答。这里有个关键设计点图像块并不是“原图直接进向量库”而是用 Qwen-VL 的描述文本代替。原因很简单向量检索模型擅长处理文本语义跨模态检索虽然也可以做但工程链路复杂很多。我们只需要把图像“翻译”成高质量的语义描述文本就能复用一套成熟且稳定的文本检索体系。检索结果出来之后如果命中图像描述块再在最终展示时把原图路径一并带到前端用户仍然能看到原始截图。2.2 为什么是 PyMuPDF Qwen-VLPyMuPDF 在内地开发者圈子里可能更常见的名字是 fitz。我选它而不是 pdfplumber 或 pdfminer主要看中三点原生支持把页面按 block 输出每个 block 自带坐标、类型、内容处理起来非常顺手。内置渲染能力可以按指定矩形区域直接输出图片做裁剪非常方便。解析速度快几千页的大文档也不会慢到让人抓狂。Qwen-VL 这边我看重的是它对中文文档和常见图表的理解能力。比起通用 API 或者自建 OCR 管线它能直接响应结构化指令。比如我对一张架构图调用 Qwen-VL它不仅能告诉我图里有“网关”“服务层”“数据库”还能描述层与层之间的调用关系这正是 RAG 后续做语义检索最需要的东西。而且它的开放接口兼容 OpenAI 的调用风格接代码成本很低。2.3 环境准备与依赖我的环境是 Python 3.10Windows 和 Linux 都跑过依赖如下pip install pymupdf pip install openai pip install faiss-cpu pip install requests pip install Pillow如果你对向量检索有更高的并发需求可以把 faiss-cpu 换成 qdrant-client后面我会单独讲差异。OpenAI 库在这里只是作为调用兼容接口的客户端工具不是使用外部服务的含义你完全可以换成 requests 直接 POST示例代码我也会给出两种写法。3. 用 PyMuPDF 把 PDF 变成“块 图”3.1 先看页面结构而不是直接抽字符串很多人用 PyMuPDF 第一步就是page.get_text()这当然能拿到全文但拿不到排版结构。想要做图文兼容第一步应该用get_text(dict)拿到页面上的块结构。import fitz doc fitz.open(demo.pdf) page doc[0] blocks page.get_text(dict, sortFalse)[blocks] for block in blocks: print(block[type], block[bbox])block 的 type 只有两种0 代表文本块1 代表图像块。这里有个很容易踩的坑很多 PDF 里的“示意图”并不是真正的位图而是由矢量绘图指令绘制的这种图在 block 列表里可能根本不会出现 type1 的块。所以光靠 block 分类还是不够还需要配合页面绘图对象检测。看是否包含矢量绘图对象可以使用page.get_drawings()如果返回结果比较多说明页面上存在大量矢量图形。典型场景是数据报表和架构图这时的处理方案是先渲染整页再根据坐标区域把目标区域裁剪为位图。3.2 图像块的裁剪与原图提取对于真正的位图块PyMuPDF 提供了两种思路。第一种是直接从 PDF 里面把嵌入的原始图片提取出来page doc[page_no] image_list page.get_images(fullTrue) for img in image_list: xref img[0] pix fitz.Pixmap(doc, xref) if pix.n - pix.alpha 3: # 转 RGB pix fitz.Pixmap(fitz.csRGB, pix) pix.save(fimg_{xref}.png)但这种方法有个问题提取出来的原图尺寸和它在页面上显示的大小不一定一致而且如果同一个 xref 被多个页面引用你还需要额外记录它出现的位置。更稳妥的做法是直接按显示区域裁剪渲染clip_rect fitz.Rect(block[bbox]) pix page.get_pixmap(matrixfitz.Matrix(2, 2), clipclip_rect) pix.save(region.png)这里我用了Matrix(2, 2)也就是把裁剪区域以 2 倍分辨率渲染出来主要目的是让图片中的小字号文字更清晰后续给视觉模型理解时成功率会高很多。如果是复杂的整页截图我甚至会用到 3 倍分辨率但同时要考虑接口对图片尺寸的限制后面会讲到。3.3 文本块的合并策略文本块如果直接按 block 切粒度往往太碎。比如一页里有三个小段落每个段落可能被切成七八个 block。我的做法是给每个文本块记录 bbox 坐标然后按“行”粒度做合并保证一个语义段落尽量完整。page_dict page.get_text(dict) blocks page_dict[blocks] chunks [] for b in blocks: if b[type] ! 0: continue text for line in b[lines]: for span in line[spans]: text span[text] if len(text.strip()) 5: continue # 过短内容直接跳过 chunks.append({ page: page_no, bbox: b[bbox], text: text.strip() })注意文本长度这个阈值我试过很多次5 个字符以下的块大概率是页码、页眉、装饰性符号直接过滤掉可以减少向量库里的大量噪声。如果过滤太粗暴导致表格文字丢失可以在进入向量库前再补一层规则而不是在这里放开阈值。3.4 页面分割时要记录关键元数据无论文本块还是图像区域最终入库的时候都要带上页码和 bbox。这一步看似多此一举实际检索阶段的价值巨大。当检索结果命中某块时我可以轻松定位它在 PDF 中的位置让前端展示原文。更重要的是用户问“第二页那张流程图里的异常条件是什么”时页码和 bbox 都能成为辅助排序的因子。我习惯把所有块记录成统一结构{ id: fitz_0021_03, page: 21, bbox: [110.0, 72.0, 540.0, 380.0], type: image, image_path: cache/0021_03.png, text: Qwen-VL 生成的描述文本 }这个结构贯穿整个流程后续所有模块都围绕它工作。4. 让 Qwen-VL 帮你“读图”4.1 调用准备与提示词设计图像区域准备完成后接下来就是调用视觉语言模型。我用的是 Qwen-VL 的开放接口兼容 OpenAI 风格直接构造 messages 传入图片的 base64 内容即可。import base64 import requests with open(region.png, rb) as f: b64_img base64.b64encode(f.read()).decode() prompt 你是一位文档结构分析专家。请仔细查看这张图片然后输出一个 JSON 对象包含以下字段 - title: 图片的标题或主题如果图中没有标题就根据内容概括 - type: 内容类型取值只能是 flowchart / architecture / table / screenshot / illustration / photo 之一 - description: 用 3 到 5 句话描述图片的核心内容包括图中出现的对象和它们之间的关系 - keywords: 适合用于检索的关键词数组建议 5 到 10 个 - text_content: 图片中出现的所有文字按原样保留如果没有则返回空字符串 请以严格 JSON 格式输出不要夹带任何其他说明。 resp requests.post( https://your-qwen-vl-endpoint/v1/chat/completions, headers{Authorization: Bearer YOUR_API_KEY}, json{ model: qwen-vl-max, messages: [{ role: user, content: [ {type: image_url, image_url: {url: fdata:image/png;base64,{b64_img}}}, {type: text, text: prompt} ] }] } ) ans resp.json()[choices][0][message][content]提示词设计是整个流程里最值得反复调优的地方。最开始我用的是“请描述这张图片的内容”这种宽泛指令结果输出五花八门有的给一大段散文有的只给一两个词。后来改成“结构化 JSON 输出”并且在 type 字段里明确枚举值输出质量立刻稳定了很多。我的经验是让视觉模型做分类题比做描述题容易得多所以必须把限定条件写清楚。4.2 将描述结果入库调用完成后我会把 JSON 解析出来并把它拼接成一个更连贯的文本块import json data json.loads(ans) visual_text ( f图片标题{data[title]}\n f内容类型{data[type]}\n f内容描述{data[description]}\n f图中文字{data[text_content]}\n f检索关键词{.join(data[keywords])} )要注意一点如果 Qwen-VL 返回的 JSON 解析失败不要直接抛弃这是那种在“检索不到但明明图里就有答案”的情况下最可惜的一种浪费。我的做法是先正则提取大括号部分再做 JSON 解析。如果仍然失败就把原始返回内容降级当作描述文本存起来至少不会比完全丢失更差。4.3 成本控制与并发优化Qwen-VL 按调用次数计费所以无脑对每一张图都调用会很心疼。我的控制策略有几条只对面积足够大的图像区域调用。如果某个图像块的宽度或高度小于 100 像素大概率是 logo、图标不值得花一次调用。先做一轮“标题重复检测”。同一份 PDF 里经常会出现重复的架构图或截图用图像哈希简单比对命中缓存就不再调用。对已经处理过的 PDF 建立缓存目录缓存 key 用pdf文件名 页码 bbox拼接第二次解析直接读取缓存。并发控制在 4 到 8 个线程避免触发接口限流。这些优化做完之后我那个 300 页带大量截图的项目真正发生视觉调用的图像只有不到 200 张整体成本完全可控。5. 构建检索链路并跑通问答5.1 向量化与索引选择文本块和视觉描述块准备好之后需要统一向量化。嵌入模型我用了 bge-m3它对中文长文本的支持一直很稳定。如果团队里有人习惯用接口调用也可以换成 text-embedding-v3只要保证入库和查询用的是同一个模型就行。向量库的选择上我的建议很简单单机小项目用 FAISS多服务并发场景用 Qdrant。FAISS 不需要额外部署适合文章里这种几百上千块的规模Qdrant 的优势是有过滤能力可以按page、doc_id过滤适合后面接入多文档场景时做权限隔离。FAISS 建库的代码非常简单import faiss import numpy as np embeddings [embedding_func(chunk[text]) for chunk in all_chunks] dim len(embeddings[0]) index faiss.IndexFlatIP(dim) # 用内积做相似度配合归一化等价于余弦相似度 emb_arr np.array(embeddings).astype(float32) faiss.normalize_L2(emb_arr) index.add(emb_arr)记住一个细节用内积之前一定要对向量做 L2 归一化不然检索结果会被向量长度误导表现很不稳定。5.2 检索后如何组装上下文检索阶段我把查询向量送入 FAISS 拿到 TopK然后根据块类型拼接最终上下文。这里的关键点是图像块的描述文本通常比普通文本块短如果只用向量相似度排序可能整页都被文本块占据图像块被挤下去。我的做法是设置一个“图像块保障比例”比如 TopK12 时确保至少 3 个来自图像类型。这个比例在实测中对提升图表类问答的正确率特别明显。组装上下文时我还会把同一页的内容尽量放在一起减少大模型在阅读时来回跳页的负担context_lines [] for chunk in selected_chunks: source_line f[第{chunk[page]}页] {chunk[text]} context_lines.append(source_line) context \n\n.join(context_lines)最后交给生成模型时我会明确告诉它“如果图片描述信息不足请直接承认不知道不要强行猜测”。这一步非常有用能大幅降低幻觉比例。很多人只关注检索的召回率却忽略了生成阶段的约束提示实际上生成提示词写得好不好对最终答案的可用性影响极大。5.3 效果对比纯文本 RAG 与图文 RAG 差异有多大用我那个包含大量截图和架构图的文档做对比我抽取了 30 个问题覆盖“图中某个流程的判断条件是什么”“架构图里哪一层负责鉴权”“某张截图中弹窗提示的完整文案是什么”等类型。纯文本 RAG 的正确率只有 33% 左右大部分问题是因为根本没检索到相关内容加入 Qwen-VL 描述之后正确率提升到 83%。差距最大的恰恰是那些“答案完全藏在图里”的问题纯文本方案连一点上下文都提供不了图文方案却能给出准确的流程分支和弹窗文案。6. 高频问题与排查心得6.1 PyMuPDF 解析相关图像区域裁剪出来是黑块或空白这通常是渲染矩阵与页面旋转没对上。解决办法是不要直接用手写的 bbox 裁剪而是通过get_image_rects(xref)获取官方显示区域再按这个 rect 渲染。明明页面上有图但get_images()只返回很少的结果说明大部分外形是矢量绘图这时候应该走get_drawings()或者直接整页渲染后裁剪。提取出来的文本顺序错乱多栏文档经常出现这种情况解决办法是开启sortTrue让块按阅读顺序排列但仍然要复核PyMuPDF 的排序并非完全智能极端情况建议配合page.find_tables()做表格区域识别。6.2 Qwen-VL 调用相关接口返回图片过大错误部分图片渲染到 3 倍分辨率后可能超过接口限制。我的做法是在调用前用 Pillow 统一将长边压到 2048 像素并转成 JPEG 压缩质量 85基本不影响理解效果。返回内容不是合法 JSON尽管提示词里写了“严格 JSON”偶发情况还是会出现。不要慌先用正则提取 JSON 片段再 fallback 到原始文本。需要对同一张图多次调用这通常是因为提示词写得太泛模型描述不到位。与其重试不如把提示词改得更具体把输出字段分别列出来准确率会明显提升。6.3 检索结果不对怎么排查如果问答效果差先别急着怀疑大模型大概率是检索环节出了问题。我习惯按照下面的顺序排查确认问题里的关键词是否真的命中某个块。如果连关键词都对不上先优化块切分粒度。确认图像描述文本是否包含足够多的业务词汇。我之前发现 Qwen-VL 描述架构图时用了大量“模块”“服务”这类泛词真实组件名反而没写全。后来在提示词里加了“尽可能保留图中出现的专业名词和英文缩写”效果立刻改善。确认 TopK 数量是否合适。太多会引入噪声太少会漏掉关键信息。我的经验是一般取 8 到 16 之间具体看块的平均长度。7. 我在实际落地中最后的几点体会这套方案核心其实不复杂PyMuPDF 负责把 PDF 拆成结构化的块Qwen-VL 负责替图像“开口说话”然后统一走文本检索。但想把它从 demo 变成真正稳定可用的知识库问答系统我最大的体会有三点。第一图像质量是天花板。如果 PDF 里的截图本身分辨率极低或者文字严重发虚再强的视觉模型也没办法。遇到这种文档能补原图就补原图不能补就在裁剪渲染时用更高的 DPI。第二缓存一定要先做。第一次跑全量文档可能耗时很久但有了缓存之后后续任何调参和重试都变得很快。我经常反复调整提示词如果没有缓存每次重新调用 API 的耐心早就耗尽了。第三别迷信单一模型。Qwen-VL 在中文文档理解上确实给了我很大帮助但架构图、表格、手绘图、照片之间差异很大不同视觉模型各有擅长。做生产系统时最好把图像类型的判断前置让不同类型走不同模型或不同提示词模板整体效果会再上一个台阶。最后留一个小技巧给想继续折腾的读者视觉描述文本生成之后除了进检索库它还可以作为离线摘要直接生成文档目录或知识地图。我用这个思路给那份 300 页的项目文档自动生成了一份基于图片内容的导读效果意外地好用。如果你已经在做图文 PDF RAG不妨往这个方向再扩展一层。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑