news 2026/10/24 17:14:20

图文兼容PDF知识库RAG方案:PyMuPDF+Qwen-VL实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
图文兼容PDF知识库RAG方案:PyMuPDF+Qwen-VL实践

PDF 大概是做 RAG 知识库最容易翻车的输入格式,没有之一。它不像 Markdown 那样自带标题结构,也不像 HTML 那样有 DOM 可循,更麻烦的是正经文档几乎不可能只有纯文字——架构图、柱状图、流程图、扫描件随时混在里面。我最近在几个内部知识库项目里反复打磨了一套方案:用 PyMuPDF 把 PDF 的版面解剖成文本块和图片块,再让 Qwen-VL 把图片真正"看懂",最后把图文信号一起送进向量库参与检索,实现真正意义上的图文兼容 PDF RAG。这篇文章我把从选型到落地踩过的坑完整复盘一遍,非常适合正在做文档问答、知识库搭建、合同或技术手册处理的朋友。

先说明一下这篇实战分享的定位。它不是一篇教科书式的原理科普,也不会只丢一个"PDF 转 Markdown"的玩具代码。我要解决的场景是:一个几十上百页的 PDF 文档里,既有大段正文文字,又有内嵌的架构图、数据图表、表格截图,用户提问时既可能问"文档第 12 页提到的阈值是多少"这类纯文本问题,也可能问"那张系统架构图里 API 网关连接了哪几个服务"这种纯图表问题,更可能问"结合架构图和前文描述,这个系统的容错机制是怎么设计的"这种图文混合推理问题。纯文本解析方案在第二、三类问题上基本束手无策,而这篇方案恰好能把这些场景补齐。

1. 处理带图 PDF 时,纯文本 RAG 的翻车现场

1.1 一个真实的检索失败案例

先讲我实际遇到的一个案例。我处理过一份约 60 页的技术白皮书,里面有一页是系统的整体架构图,图上画着服务分层、组件名称、数据流向箭头,这些内容全部以矢量图形方式嵌在 PDF 里。用常规 pdfplumber 或者 pdfminer 提取文本时,这页能拿到的文字只有图下方的几行说明,架构图内部的服务名一个都提取不出来。

当时用这套纯文本方案上线后,有同事提问:"这个系统的消息队列用的是 Kafka 还是 RabbitMQ?"这个问题其实架构图里画得清清楚楚,但检索系统压根没有把图上内容索引进来,最后给了一个完全错误的回答,说"文档中未找到相关信息"。更尴尬的是,用户把 PDF 翻到那一页截图发过来,图上明明写着 Kafka——这种错误对知识库系统的信任度打击是致命的。

这并不是个例。我后来统计过手头的几份电子版 PDF(非扫描版),纯文本提取平均只能拿到页面真实信息的 70% 左右,剩下的 30% 全藏在图片、矢量图、嵌套表格和特殊排版里。这些藏起来的信息恰好是用户最爱问的:架构图、流程图、数据图表、看板截图。

1.2 三种"PDF":文本型、图片型、图文混合型

做了这么多文档后,我习惯把 PDF 分成三类来对待:

第一类是纯文本型。典型代表是学术论文的预印本、政府公文、纯文字合同。这类 PDF 自带文本层,用 PyMuPDF 的page.get_text()就能把内容完整抽出来,准确率几乎是 100%,完全不需要上视觉模型。

第二类是图片型,也就是扫描件。整页都是一张大图,文本层空空如也。这类文档必须走 OCR 或者整页视觉理解,PaddleOCR、Qwen-VL 都干得动,但那个成本就完全不一样了。

第三类是图文混合型——这也是现实世界中最常见、也最棘手的一类。电子排版生成的技术手册、产品方案、财报、售前材料几乎全是这种:有真实文本层,但同时又嵌入了大量图片、图表、矢量图。这类文档不能一刀切,你得先把版面拆开,把文本信号和图像信号分别抽出来,再重新组合成结构化的知识元。

我这一整套方案的精髓,就是针对第三类文档做"信号分离",而不是无脑转图片。

1.3 为什么不能只靠"渲染整页给视觉模型"一条路

有人可能会问:现在多模态大模型这么强,直接把 PDF 每一页渲染成 PNG,整页丢给 Qwen-VL 让它描述,不也能解决问题吗?

我早期真这么干过,效果一言难尽。先说第一个问题:整页图片会被严重压缩。一个 A4 页面渲染成 1600x2200 像素的 PNG,再缩放进视觉模型的输入分辨率,页面上的小字号、表格里的数字、架构图里的组件名,基本糊成一片。Qwen-VL 能把页面大概主题说出来,但细节完全靠猜。

第二个问题是图文交叉排版时容易"看漏"。视觉模型对整页长图的理解并不是逐像素精读,而是类似人眼的注意力扫描。当一页上有多个独立图块、图注、脚注混在一起时,模型经常只盯着最大的那张图,其他内容就漏掉了。

第三个问题最致命:整页渲染方案丢失了"锚点"。文本 RAG 有一个宝贵特性——检索结果可以精确到某一段文字,回答时可以引用"见第几页第几段"。而整页视觉方案只能做到"答案在这一页的图上",至于在哪张图、哪个区域,完全无法定位。这意味着用户无法验证答案,系统也不敢保证回答的准确性。

所以我的结论是:不要整页渲染扔给视觉模型,而是先用 PyMuPDF 把页面拆成文本块和图片块,图片单独裁剪出来再喂给 Qwen-VL。这就是"图文双通道"的核心思路。

2. 选型思路:PyMuPDF 和 Qwen-VL 各自解决什么

2.1 PyMuPDF 在版面解析上的独特优势

先说 PyMuPDF 为什么在这个场景里不可替代。不是说 pdfplumber、pdfminer 这些库不行,而是在"版面解析"这件事上,PyMuPDF 是少数能同时提供四件事的库:

  • 文本提取:page.get_text("blocks")直接返回带坐标的文本块,每一块的左上角/右下角坐标、文本内容、块类型都齐了。
  • 图片定位:page.get_image_info()能拿到页面里每一张图片的显示位置(bbox),还能拿到图片对象的 xref 编号。
  • 矢量图读取:page.get_drawings()能读出页面里的矢量绘图元素,比如架构图里的矩形、线条、文本框轮廓。
  • 页面渲染:page.get_pixmap()可以在百毫秒级把任意区域渲染成高分辨率图像,这个能力用来把图片区域截出来非常方便。

我做了个简单的对比表,方便你判断自己的场景该选谁:

库文本提取图片坐标矢量图元素区域渲染综合性能
pdfminer.six好,但慢一般无不提供中等
pdfplumber好,表格提取方便能取到基础位置弱不提供中等
PyMuPDF快,块级带坐标完整 xref+bbox完整 drawings原生支持,性能极高强

表格只是参考,真正让我定下 PyMuPDF 的是它的数据模型:文本块、图片、矢量图都以 bbox(一个简单的矩形坐标)来表述,这对我来说是金矿。因为后续做图文关联、caption 归属、窗口合并,全都在跟坐标打交道,PyMuPDF 提供的坐标统一性省了我大量数据对齐的功夫。

2.2 Qwen-VL 的接入位置与模型选择逻辑

选 Qwen-VL,准确说是 Qwen2-VL 系列,我从三个角度讲为什么。

第一是私有化部署能力。知识库应用场景里文档内容往往敏感,不太可能把 PDF 页面传到云端闭源模型接口去处理。Qwen-VL 是开源权重模型,7B 和 72B 都有,可以完全私有化部署。7B 模型在 24GB 显存上就能跑起来,处理批量文档完全够用。

第二是中文图表理解能力。测试过几个开源视觉模型后,Qwen-VL 对中文图表的理解是最稳的。中文柱状图、折线图的坐标轴文字、图例、数据标签,它基本都能读对。这跟它训练数据里中文图文语料占比有关,也是我这种以中文文档为主的场景最刚需的能力。

第三是输入分辨率可控。Qwen2-VL 的处理器支持我自定义最大像素数,这意味着我可以把裁剪出来的大图适当放大后送进去,而不是被强制压缩到固定分辨率。对读取图表里的小字来说,这个特性太关键了。

接入位置刚才讲了,不是整页渲染后丢给它,而是 PyMuPDF 先把版面拆开,把每个独立的图片块裁剪成小图,再让 Qwen-VL 做定向描述。就好比一个精读小组:PyMuPDF 是图书管理员,负责把书拆开、标出每一页的"正文区域"和"插图区域";Qwen-VL 是视觉研究员,只负责盯着被裁出来的插图细看,然后写下解读附注。两个人各管一段,效率比一个人把整本书拍照再逐页扫描要高得多,准确度也完全不在一个量级。

2.3 为什么不走全页大模型 OCR 路线

这条路我得专门说一下,因为踩坑率太高了。很多团队一看到 PDF 里有图片,第一反应是"上 OCR",比如 PaddleOCR。但 OCR 是针对扫描件的,一套完整 OCR 流程:版面分析、文字检测、识别、坐标还原,跑一页的时间是 PyMuPDF 提取文本的几十倍。如果你面对的是带文本层的电子版 PDF,用 OCR 去处理原本能直接读取的文字,纯属杀鸡用牛刀,还容易把排版信息搞丢。

我的实践原则很简单:能用文本层解决的绝不上 OCR,只有确认某个区域没有文本层时才动用视觉模型。PyMuPDF 把版面拆开之后,哪些块有文本、哪些块是纯图片一目了然,这样就能精准控制视觉模型的消耗范围。一个上百页的文档,可能真正需要 Qwen-VL 出马的只有二三十张图,成本完全可控。

3. pipeline 总览:图文双通道的 RAG 架构

3.1 整体流程与核心设计

直接说方案的整体流程,我把它拆成五个阶段:

1. 版面解剖:PyMuPDF 解析每一页,得到文本块列表和图片信息列表 2. 文本通道:文本块清洗、拼接、加上 page_id 和坐标信息 3. 视觉通道:图片块裁剪 → Qwen-VL 看图 → 输出结构化 JSON 描述 4. 统一入库:文本块和图片描述块分别做 embedding,存入向量库 5. 查询链路:向量检索 + 图文窗口合并 + 多模态模型组装答案

这个 pipeline 最核心的设计是"双通道统一进了同一个向量库"。文本块走常规文本 embedding,图片块走的是 Qwen-VL 生成的文本描述再 embedding。为什么要这样设计?因为最终用户的查询是文本,我们要保证查询向量和图片向量的语义空间一致。如果你用 CLIP 那类独立的图像编码器做图片向量,文本 embedding 和图像 embedding 根本不在一个语义空间里,检索效果会很飘。

我自己测过实验:用户问"系统支持哪几种部署方式",如果图片块向量是 CLIP 生成的,文本查询向量检索出来的图片块经常答非所问;但换成 Qwen-VL 描述文本再送 bge-m3 embedding,图片块的语义和文本查询能对齐得非常好。这个细节决定了整个方案的检索质量上限。

3.2 text 通道与 vision 通道的分工

两个通道的分工用一句话总结:凡是能从文本层读到的,交给 text 通道;凡是必须"看"才行的,交给 vision 通道。

text 通道处理的是:正文段落、标题、列表项、表格里的文字单元格。这些内容提取出来后,我还会做小的清洗——去页眉页脚、合并断裂的行、过滤重复块——然后带上页码和坐标信息构成一个文档块。

vision 通道处理的是:内嵌的架构图、数据图表、产品截图、矢量流程图,以及在 PyMuPDF 里检测到但没有任何文本关联的图片区域。这些块会先被裁剪成独立 PNG,再交给 Qwen-VL 生成一个结构化描述,描述里包括图片类型、标题、图中关键文字、核心数据、结论等字段。

两个通道最后都变成统一的"知识块"进向量库,知识块的 payload 里有一个type字段标记是text还是image。这样检索阶段才能区别对待,也才能在最终回答时把图片路径作为多模态上下文传给模型。

3.3 图文块的关联策略(caption 归属问题)

这里有个一开始很容易忽略的坑:图片和它的图注/上下文文字是分离的。比如一页上有一张柱状图,下面是"图 3-2 季度营收对比",这张图的标题和结论可能就藏在这行 caption 里。如果我们把图和 caption 拆成两个独立块,用户问"图 3-2 展示的是什么",检索命中 caption 的概率很低,命中图片描述块的概率也不稳。

我的解决方案是"坐标窗口关联"。在完成文本块和图片块的坐标提取后,对每个图片块做一次邻域搜索:找出同一页上,垂直方向距离图片 bbox 最近的上方和下方的 1-2 个文本块,把它们作为这张图的"关联上下文"拼进图片描述里,一并入块。这样图片的语义就包含了图注的说明,检索命中率能提升一大截。

反过来也一样,检索到正文文本块时,我会把同一页上与该块 bbox 有交叠或紧邻的图片一并取出来。这样当用户的问题是"结合上面这张图,说说……"时,检索链路能自然地把图文一起呈现给最终模型。

4. PyMuPDF 版面解析实践:提取文本与图片坐标

4.1 文本块提取与清洗

先上最基本的代码,用 PyMuPDF 打开文档并提取文本块:

import fitz # PyMuPDF 的导入名固定是 fitz doc = fitz.open("sample.pdf") page = doc[2] # 拿到第3页 blocks = page.get_text("blocks") # 返回列表,每个元素是元组 # 元组结构: (x0, y0, x1, y1, text, block_no, block_type)

这里的block_type很关键:0代表文本块,1代表图片块。最开始我图省事直接对blocks全量处理,后来发现纯图片页会被当成一个巨大的图片块,导致文本通道漏掉整页内容。正确做法是先按block_type分流,把文本块和图片块分开处理。

文本块拿到后,清洗规则我总结了几条实操经验:

  • 过滤明显是页眉页脚的块:用块坐标(y0)判断,如果文本块靠近页面顶部或底部(小于页面高度的 5% 或大于 95%),且长度较短,基本就是页眉页脚,直接丢掉。
  • 合并同一标题断行的块:PDF 排版经常把长标题断成多行,PyMuPDF 有时也会把一个段落拆成多个块。我的做法是按(x0, y0)坐标排序后,如果相邻两个块在垂直方向间距很小(小于行高的一半),且水平位置相同,就合并成一条。
  • 去重:有些排版会重复输出页脚文字,比如"第 3 页 / 共 20 页"这类,同一文本会在不同页重复出现,入库前要做全文档级去重。

清洗完的文本块我会拼上一个 doc_id 和页码,格式类似:

clean_block = { "page": page_number, "bbox": (x0, y0, x1, y1), "text": cleaned_text, "block_type": "text", }

4.2 图片提取的两种正确姿势

图片提取是我踩坑最多的环节。PyMuPDF 提供了两种完全不同的取图方式,用错一种都会出事。

第一种是按 xref 抽取原始图像对象。适合 PDF 内部显式内嵌的位图图片:

images = page.get_image_info(xrefs=True) for img in images: xref = img["xref"] bbox = img["bbox"] if xref <= 0: continue pix = fitz.Pixmap(doc, xref) if pix.n > 3: # 处理 CMYK 等色彩空间 pix = fitz.Pixmap(fitz.csRGB, pix) pix.save(f"img_{page_number}_{xref}.png")

这种方式拿到的是图片的原始像素数据,质量最高。但有一个前提陷阱:get_image_info(xrefs=True)只在图片是显式嵌入时才能拿到 xref。很多 PDF 里图片被裁剪、遮罩、甚至用 DCTDecode 多次引证时,xref 虽然能拿到,但实际显示区域和原始图像不完全一致,直接存原始图会把不需要的空白区域也存进去。

第二种是按 bbox 渲染页面局部区域。这种方式更通用,也更能保证"所见即所得":

zoom = 2.0 # 2 倍缩放,提高渲染分辨率 mat = fitz.Matrix(zoom, zoom) clip = fitz.Rect(bbox) # 从 get_image_info() 拿到的展示区域 pix = page.get_pixmap(matrix=mat, clip=clip) pix.save(f"clip_{page_number}_{xref or 'vector'}.png")

我实际项目里的推荐策略是:优先用 bbox 渲染方式,因为它是直接从页面渲染出来的,和用户看到的效果完全一致,不会出现原始图像被裁剪导致的变形问题。只有当你明确知道这个 PDF 里的图片质量很高、没有被裁剪时,才用 xref 方式保像素原真度。

这里还必须提一个装饰性小图的过滤问题。PDF 里常有一些 logo、小图标、分隔线,这些图片没有信息量,送进 Qwen-VL 纯属浪费。我的过滤规则很简单:bbox 面积小于一页面积 2% 的图片,直接跳过。

4.3 坐标对齐与旋转页面处理

这个坑我必须记一下,因为它排查了我整整一个下午。问题表现是:某些 PDF 页面提取出来的图片 bbox 和实际渲染出来的位置明显不符,图片往左偏了一大截。后来才发现是页面旋转属性在作怪。

PDF 里的页面可以有Rotate属性(0、90、180、270),有些扫描排版工具生成 PDF 时会把横版页面标成旋转 90 度。PyMuPDF 的page.get_text("blocks")返回的坐标通常是经过旋转校正后的显示坐标,但get_image_info()在部分版本里返回的 bbox 是原始未校正坐标,这两个坐标系混用,图文关联全乱。

我的处理方式比较暴力但有效:在处理任何一页前,先检查page.rotation:

if page.rotation != 0: # 将图片 bbox 转换到旋转后的坐标系 rect = fitz.Rect(bbox) transformed = rect * page.rotation_matrix bbox = (transformed.x0, transformed.y0, transformed.x1, transformed.y1)

然后再和文本块做关联。如果不想处理坐标变换,也可以用更省事的办法:干脆以page.rect(旋转校正后的页面矩形)为标准,先用page.get_pixmap()渲染整页小图,人工抽查 bbox 是否合理。批量处理前,我强烈建议在每个文档上先这么做一次视觉检查,不然坐标错位的问题会一路传导到检索阶段。

5. Qwen-VL 图像理解接入:从原始图像到结构化描述

5.1 环境与模型部署

Qwen-VL 的接入我用的还是常规 transformers 管线,关键是版本别太老。我当前跑通的依赖环境如下:

pip install transformers>=4.45.0 torch>=2.1.0 Pillow accelerate

加载模型的代码:

import torch from transformers import Qwen2VLForConditionalGeneration, AutoProcessor model = Qwen2VLForConditionalGeneration.from_pretrained( "Qwen/Qwen2-VL-7B-Instruct", torch_dtype=torch.bfloat16, device_map="auto", trust_remote_code=True, ) processor = AutoProcessor.from_pretrained( "Qwen/Qwen2-VL-7B-Instruct", trust_remote_code=True )

显存方面,7B 模型在 bfloat16 下大概需要 16GB 显存左右,24GB 的 4090 可以很舒服地跑。如果你打算部署成服务端批量处理,我建议上 vLLM,把 Qwen-VL 包装成 OpenAI 兼容接口,这样可以用并发满跑,几十张图几秒钟就能处理完。单机串行调用 transform 管线处理大批量 PDF 会慢到怀疑人生——吞吐量能差 5 倍以上。

5.2 提示词设计:让 VL 输出 JSON 字段

给视觉模型的提示词直接决定输出质量。我一开始用的提示词是"请描述这张图片的内容",结果 Qwen-VL 输出一大段散文式描述,关键数据散落各处,很难解析入库。后来我改成了强制 JSON 输出,效果立刻上了一个台阶。

我实际在用的提示词模板如下:

这是一个从PDF中截取的图片区域。请仔细识别图中内容,并严格按JSON格式输出以下字段: { "image_type": "chart/architecture/table/screenshot/decoration/other", "title": "图片标题或图表标题,没有则填空字符串", "texts": ["图中所有可辨认的重要文字,逐条列出"], "data_points": ["图表中的关键数值/指标,如具体数字和单位,没有则空数组"], "conclusion": "用一句话概括这张图表达的核心信息" } 注意事项: 1. 如果图片是记录性内容无法概括,请如实输出字段原文。 2. 如果图片是纯装饰或空白,输出 {"image_type": "decoration"}。 3. 不要添加JSON之外的任何解释。

调用代码如下:

from PIL import Image def describe_image(image_path: str, prompt: str) -> str: image = Image.open(image_path).convert("RGB") messages = [ {"role": "user", "content": [ {"type": "image"}, {"type": "text", "text": prompt}, ]} ] text = processor.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) inputs = processor( text=[text], images=[image], return_tensors="pt", max_pixels=1280 * 28 * 28, # 控制分辨率上限 ).to(model.device) generated_ids = model.generate(**inputs, max_new_tokens=512) output = processor.batch_decode( generated_ids[:, inputs.input_ids.shape[-1]:], skip_special_tokens=True, )[0] return output

这里max_pixels这个参数值得多说一句,它控制了模型接受的最大像素数。值越小推理越快、显存占用越低,但小字识别能力会下降;值越大细节越好,但可能在批量处理大图时爆显存。我用 7B 模型的经验值是设为 1280x28x28 左右,也就是相当于约 90 万像素,在细节和性能之间比较平衡。如果你的图里关键信息是特别小的标注,可以临时调大。

5.3 大图切片与分辨率控制

处理 PDF 里的图片时,我们会遇到另一个矛盾:有些架构图长达一整个 page 的高度,如果直接送进 Qwen-VL,图像会被缩得很小,组件名完全看不清。这里我实践出两个办法:

办法一是按网格切片。把大图按 50% 重叠切成 2x2 或 3x3 的子图,每个子图单独送 Qwen-VL 描述,最后把子图描述合并成对大图的整体描述。这种方法能保留图内每个区域的细节,代价是会增加推理次数。如果一张架构图切成 4 块,就相当于 4 次视觉推理,成本要提前估算好。

办法二是控制渲染 zoom 倍率。在 PyMuPDF 阶段,如果原图分辨率不够,我会提高clip渲染的 zoom,让裁剪出来的 PNG 尺寸足够大。再用max_pixels控制送入模型的上限,这样小字能尽量保持清晰。

切片逻辑不复杂,核心代码是:

from PIL import Image import os def slice_image(image_path, grid=(2, 2), overlap=0.1): img = Image.open(image_path) w, h = img.size slices = [] for i in range(grid[0]): for j in range(grid[1]): left = max(0, (i * w / grid[0]) - overlap * w / grid[0]) upper = max(0, (j * h / grid[1]) - overlap * h / grid[1]) right = min(w, ((i + 1) * w / grid[0]) + overlap * w / grid[0]) lower = min(h, ((j + 1) * h / grid[1]) + overlap * h / grid[1]) crop = img.crop((int(left), int(upper), int(right), int(lower))) slices.append(crop) return slices

切片后的每块图用同样的提示词送 Qwen-VL,得到的分段描述再按坐标拼接。实测里,一张复杂架构图切成 4 块后,组件名的识别完整性从不到 60% 提升到了 90% 以上。

6. 向量化、入库与检索:让图文块协同参与问答

6.1 embedding 与向量库选择

双通道的知识块最终都要向量化入库。embedding 模型我选的是 bge-m3,主要原因是它支持中英文混合语义,且输出 1024 维向量,对短文本块和图注描述的表达能力都很稳。其他模型不是不能用,但要注意:如果你用的是 bge-large-zh-v1.5,对英文图注支持会弱一些;如果用 OpenAI 的 embedding,又会引入外部 API 依赖,知识库场景不太推荐。

向量库我选了 Qdrant。原因很简单:轻量、docker 一条命令启动、Python SDK 友好,中小型项目完全够用。Milvus 我也用过,但那个更适合百万级知识库的大规模场景,部署运维成本明显更高,项目初期没必要。

6.2 入库数据结构

每个知识块入库时我会存这样一组字段:

{ "id": "uuid", "doc_id": "doc-001", "page": 3, "type": "text", "text": "原文文本块或图片描述文本", "bbox": [x0, y0, x1, y1], "image_path": "/data/img/doc-001_p3_xref.png", "related_bbox": [[x0, y0, x1, y1], [...]] }

几个字段的作用说明一下:

  • type:text还是image。检索命中后会根据类型决定如何组装进上下文。
  • image_path:只有type=image时才有。这个字段是为了在最终问答阶段,如果用的是支持多模态输入的大模型,可以直接把图片路径传进去。
  • related_bbox:当前块关联的周围图文块坐标。检索到文本块时,我会用它去拉出附近的图片块;检索到图片块时,同样用它拉出邻近文本块。

入库代码示意:

from qdrant_client import QdrantClient from qdrant_client.models import VectorParams, Distance client = QdrantClient(url="http://localhost:6333") client.create_collection( collection_name="pdf_blocks", vectors_config=VectorParams(size=1024, distance=Distance.COSINE), ) payload = { "doc_id": "doc-001", "page": 3, "type": "image", "text": image_description_text, "bbox": [x0, y0, x1, y1], "image_path": "/data/img/doc-001_p3.png", "related_bbox": [caption_bbox], } client.upsert( collection_name="pdf_blocks", points=[{"id": block_id, "vector": embedding_vector, "payload": payload}], )

6.3 查询链路与多模态推理组装

查询阶段的流程是这样:用户问题先做 embedding,然后在向量库里做 top-K 检索,拿到命中块之后,执行一次"图文窗口合并",最后把合并结果交给最终生成模型(我用 Qwen-VL 做最终问答推理,让它能真正看到图片)。

查询代码示例如下:

query_vec = embedding_model.encode(user_question) hits = client.search( collection_name="pdf_blocks", query_vector=query_vec, limit=20, ) # 图文窗口合并:收集命中的块及其关联块 final_blocks = [] hit_page_map = {} for hit in hits: payload = hit.payload block_key = (payload["page"], tuple(payload["bbox"])) if block_key in hit_page_map: continue hit_page_map[block_key] = True final_blocks.append(payload) # 如果命中的是图片块,把同页关联文本块也拉出来 if payload["type"] == "image": for rb in payload.get("related_bbox", []): related = fetch_block_by_bbox(payload["page"], rb) if related: final_blocks.append(related)

为什么要把窗口合并做得这么复杂?核心原因是用户的问题经常是"看图说话 + 结合上下文"的混合推理。比如"这个架构图里的数据流向是怎么描述的?前面提到的安全策略在图中哪里体现了?"如果只把检索命中的文本块丢给模型,图片信息缺失;如果只给图片,上下文又不够。合并之后,模型既能看到文本上下文,也能看到相关图片,回答才能覆盖全面。

最终组装提示词时,我会把文本块按页码排好序,图片块直接作为多模态输入传给模型:

context_text = "\n".join( f"[第{block['page']}页] {block['text']}" for block in text_blocks ) image_inputs = [Image.open(block["image_path"]) for block in image_blocks]

这样 Qwen-VL 就能在真正"看图"的基础上作答,而不是靠印象造答案。

7. 效果评测与踩坑记录

7.1 三类问题的实测对比

搭建完这套方案后,我在一份约 45 页的混合型技术方案文档上做了抽样测试。文档里有架构图、系统流程图、对比表格截图、若干段正文说明,基本代表了图文混合 PDF 的典型情况。我设计了三类测试问题:

  • 纯文本类:"部署环境的硬件要求是什么?"(答案在正文段落里)
  • 图表类:"架构图里缓存层用了哪个组件?""这张对比表里方案A的并发上限是多少?"
  • 图文混合类:"结合架构图和 3.2 节的描述,该系统的容错机制如何实现?"

抽样结果如下(人工判断回答是否准确,仅供参考,不同文档偏差会很大):

测试类别纯文本 RAG(只提取文本层)本文图文兼容方案
纯文本问题88%91%
图表类问题13%86%
图文混合问题9%79%

图表类和图文混合问题的提升幅度是很夸张的,从几乎不可用变成了可以实际回答问题。纯文本问题两个方案都做得不错,因为这部分本质上是常规文本检索,图文兼容方案并没有因为引入图像通道而降低文本通道的质量。

值得注意的是,图文混合问题的 79% 还有提升空间,主要失分点是最终模型在"图片 + 多个文本块"的长上下文里,偶尔会漏看某些文本块的细节。这可以通过调低 top-K 或者对命中的文本块做重排序来缓解。

7.2 高频踩坑与解决办法

几个高频坑我按出现频率排一下,每个都是真实见过的。

第一个坑:图注归属错误。有些排版把图注放在图片左侧,而不是常见的下方。我最初的"取图片上方/下方最近文本块"策略在这种版面上会抓错文本,把正文段落当成图注并入图片块。解决办法是:对图片周围的文本块做一次方向判断,取四个方向里最近的那个,而不是默认上下。

第二个坑:矢量图无法用get_image_info()捕获。前面提过的架构图如果完全是矢量绘制,get_image_info()返回的列表可能是空的。这时候必须用page.get_drawings()判断该区域是否有大量矢量元素,如果有,直接用 bbox 渲染区域。这个坑我是在第一版上线后才发现,差点漏掉一整类架构图。

第三个坑:旋转页面 bbox 错位。前文说过了,批量处理前务必先人工抽查一页的坐标对齐情况,别等到检索错误再回头查。

第四个坑:Qwen-VL 对小尺寸图标几乎无解。PDF 里有些小 logo、小按钮截图面积太小,送进模型后根本识别不出内容。解决办法是面积阈值过滤,直接放弃这些无信息量的小图,避免脏数据入库。

第五个坑:max_pixels设置太小导致图表数值识别错误。有一版我把 max_pixels 压到 512x512,结果柱状图上的数据标签全被模型"看"成了一团噪点,回答里的数值错得离谱。放大输入分辨率之后,准确率立刻恢复。这个参数一定要结合你实际文档里的字号来调,不要省显存。

第六个坑:批量处理时的并发资源竞争。Qwen-VL 一次性加载到显存后,如果再用多线程往同一个模型实例发送图片,很容易 OOM。稳妥的做法是串行推理,或者用 vLLM 的并发调度,而不是自己开线程。

7.3 后续优化方向

这套方案目前在我的内部项目里已经稳定跑了好几个文档集,但它离"万能"还很远。我自己的下一步优化方向有两个:一是引入版面分析模型做兜底,对 PyMuPDF 无法定位的复杂嵌套表格做二次切分;二是加一层 Rerank 模型,粗排后对命中的图文块再做精排,减少长上下文里无关块对问答的干扰。

最后再分享一条个人体会:图文兼容 RAG 这事儿,最忌讳一上来就追求"大而全",恨不得把版面分析、视觉模型、重排序全都堆上去。理性做法是先盘一盘你的文档类型——如果 80% 的 PDF 都是纯文本型,那做好文本提取就够了,视觉通道按需打开;如果像我一样天天面对图文混合的技术手册,再上 PyMuPDF+Qwen-VL 这套组合也不迟。工具是死的,文档类型分析才是决定方案复杂度该落在哪儿的那个关键判断。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 17:14:19

tracetcp全解析:当ping通但端口不通时,用TCP SYN定位四层故障

简介&#xff1a;tracetcp.zip 是一份基于 C 实现的 TCP 路由追踪工具源码包&#xff0c;面向网络管理员、运维人员、网络专业学生及对底层通信感兴趣的开发者。它通过发送 TCP SYN 包并记录沿途路由节点&#xff0c;可应用于网络故障排查、路由策略验证、延迟与丢包定位等场景…

作者头像 李华
网站建设 2026/10/24 17:14:04

分布式对象存储工程实践:从原理到选型与踩坑全解析

我最早对分布式对象存储产生敬畏&#xff0c;是被一次150TB的归档任务逼的。当时手里全是NFS挂载盘和几套SAN&#xff0c;目录一深、文件一多&#xff0c;ls都要等好几秒&#xff1b;把数据“塞进”对象存储之后&#xff0c;事情突然变得像往桶里扔东西那么简单。这几年&#x…

作者头像 李华
网站建设 2026/10/23 21:18:45

Node.js + Express 从零搭建 API 服务:AI 辅助实战与避坑指南

1. 项目缘起与整体设计思路1.1 为什么选这个题目练手做了几年后端开发&#xff0c;带过不少新人&#xff0c;我发现一个很普遍的现象&#xff1a;很多人学Node.js和Express&#xff0c;看完文档、跟着教程敲完一个Hello World&#xff0c;然后就不知道下一步该干什么了。知识点…

作者头像 李华
网站建设 2026/10/18 3:04:33

Superpowers技能库:为AI编程助手注入Agent Skills工作流

最近在 GitHub 和 AI 编程工具社区里面逛,你应该会频繁撞见一个名字很中二的项目:Superpowers。它不是超级英雄模拟器,而是一套给 AI 编程助手用的技能(skills)增强包。装上之后,像 Claude Code、Codex CLI 这类工具就不再是只会在对话框里蹦代码的应答机,而是能在合适的时机自…

作者头像 李华
网站建设 2026/10/19 13:57:19

软件工程毕设提效攻略:8款AI工具从论文到代码全流程实战

毕设季一到&#xff0c;软件工程专业的学生基本就被两座大山夹着走&#xff1a;一边是代码要能跑、功能要完整&#xff0c;另一边是论文要能过查重、逻辑要自洽。很多人第一反应是“能不能让AI帮我写”&#xff0c;但真正经历过一遍的人会告诉你&#xff0c;AI对软件工程毕业设…

作者头像 李华
网站建设 2026/10/23 15:30:38

JavaWeb校园外卖系统:Servlet+JSP+MySQL全链路实战

简介&#xff1a;这是一套面向Java初学者与高校课程设计学生的校园外卖管理系统实战项目&#xff0c;基于JavaWeb技术栈开发&#xff0c;完整覆盖用户端、商家端与后台管理三大模块&#xff0c;可直接用于Java课程设计、毕业设计或期末大作业。资源包共329个文件&#xff0c;包…

作者头像 李华