☰
PyMuPDF+Qwen-VL构建图文兼容PDF RAG:解决解析层难题
2026/10/8 10:37:25 网站建设 项目流程

做 PDF RAG 这件事,最难受的从来不是“模型不够聪明”,而是文档进系统的那一刻就已经残废了。大多数解析方案默认 PDF 是纯文本容器,图片、表格、扫描页全被丢掉,用户问“这张流程图第三步是什么”时,检索系统根本找不到任何可用的片段。后来我用 PyMuPDF 做底层解析、Qwen-VL 做视觉理解,重新搭了一套图文兼容的 PDF RAG 管线,算是把这类问题解决得比较彻底。这个方案适合做企业知识库问答、技术手册检索、扫描件归档分析这些场景,尤其适合那些文档里“有图有真相”但传统方案只会抽文字的项目。

1. 为什么 PDF RAG 总在解析层翻车

1.1 RAG 瓶颈不在检索,而在“喂进去的是什么”

很多人一开始迷信检索算法,觉得换个向量模型、调个相似度阈值就能提升效果。我踩了一圈坑之后的结论是:RAG 的上限由解析层决定,检索和生成只是在解析结果里做减法和拼图。

热词里频繁出现“rag瓶颈”,真实瓶颈恰恰在文档进入索引之前的那一步。PDF 本质上是一套坐标系统:文字有坐标、图片有坐标、表格有坐标,但我们常用的解析工具往往只取文本内容,把 coordinate(坐标)信息丢掉。于是一份好好的产品说明书,包含结构图、流程图、数据表,进入知识库之后只剩干巴巴的几段文字。用户问“电气接线图里标号 7 是什么”,系统里根本没有“电气接线图”这个实体,自然召回不到。

更隐蔽的问题是文本块本身也可能被破坏。PDF 的排版引擎按视觉位置排布文字,两栏文档的文字流会被切成左栏一段、右栏一段、又跳回左栏,直接拼起来语义是碎的。如果解析层不做结构重组,后面的分块和向量化全在垃圾上做精装修。

所以我的第一个建议是:在动手写 RAG 之前,先把解析层当成核心项目来做,而不是随便找个库调用一下就完事。

1.2 PyMuPDF + Qwen-VL 的选型逻辑

选 PyMuPDF 不是因为它最流行,而是它在一个库内同时满足了三个刚需:

第一是速度和稳定性。PyMuPDF 解析几百页的 PDF 只要几秒钟,内存占用也远低于 pdfplumber 这类纯 Python 实现,跑批量任务时差距非常明显。

第二是能同时拿到文本和图像的位置信息。page.get_text("dict")返回的 blocks 里有文字块的 bbox(边界框),page.get_image_info()能返回每个图片在页面上的实际显示区域。这意味着我可以把“图在哪里、文在哪里、两者什么关系”全部搞清楚,而不是拿了文字丢图片。

第三是坐标级裁剪能力。page.get_pixmap(clip=...)可以直接按矩形区域导出图像,这在处理“一张大图里嵌着一小块关键配图”的场景下是救命功能。

选 Qwen-VL 是另一套逻辑。PDF 里的图片要进入向量检索,必须先变成可比较的语义向量,好消息是向量化不一定要走图向量,更务实的做法是让视觉模型把图翻译成结构化文本描述,再用文本向量化。Qwen-VL 的中文识别、版面理解、OCR 能力都足够稳,API 调用简单,也开源了可私有化部署的版本(Qwen2-VL、Qwen2.5-VL),无论走云端还是内网都能落地。

为什么不直接让一个多模态大模型端到端读 PDF?我也试过,问题在于:长文档上下文放不下、每页都要过一遍模型成本太高、而且用户问的往往是某个局部细节,端到端模型很难像 RAG 那样精准定位。用 PyMuPDF 做结构化拆分、Qwen-VL 做图片语义化、再走向量检索,才是工程上划算的方案。

2. PDF 解析层:PyMuPDF 把文档拆成“结构化积木”

2.1 文本、图片、表格三类元素的提取方案

PyMuPDF 的解析核心是page.get_text("dict"),返回一个包含 blocks 列表的字典,每个 block 有type字段:0 是文本块,1 是图片块。文本块内部还分层为 lines 和 spans,可以拿到字体、字号、颜色这些版式信息。我第一次用的时候第一反应是“这哪是解析,这是在给我 PDF 的解剖图”。

提取代码很直白:

import fitz doc = fitz.open("sample.pdf") for page in doc: blocks = page.get_text("dict")["blocks"] for block in blocks: if block["type"] == 0: text = "".join( span["text"] for line in block["lines"] for span in line["spans"] ) bbox = block["bbox"] # (x0, y0, x1, y1) elif block["type"] == 1: # 图块:bbox 是图像在页面上的显示区域 img_bbox = block["bbox"]

但要注意:get_text("dict")里的图片块不一定包含原始图像字节,它更像是一个“图像占位符”。想拿真实图片数据,要用page.get_images(full=True)配合doc.extract_image(xref),或者用page.get_image_info(xrefs=True)拿图片的显示位置和交叉引用编号(xref)。

图片提取我推荐走get_image_info,因为它的返回结果里直接带了在页面上的显示矩形,这个矩形就是后续“这张图在文档里实际占多大、放在哪里”的判断依据:

for info in page.get_image_info(xrefs=True): bbox = info["bbox"] width = info["width"] # 原始像素宽 height = info["height"] # 原始像素高 xref = info["xref"] # 如果 xref 有效,可以进一步通过 doc.extract_image(xref) 取原始字节

表格则用 PyMuPDF 自带的page.find_tables()。它返回表格区域,可以直接转成 pandas DataFrame 或者 HTML 结构:

tables = page.find_tables() for table in tables: df = table.to_pandas() html = table.to_html() bbox = table.bbox

这一层做完,每一页的信息就变成了三类结构化对象:文本块带坐标、图片块带坐标和原始数据、表格块带坐标和表格结构。这三个坐标系统都在同一个 PDF 页面坐标系里,后面做图文关联时才不会错位。

2.2 图片过滤与区域裁剪

解析层拿到一堆图片之后,第一件事不是急着去理解,而是过滤。PDF 文档里大量图片其实是装饰物:公司 logo、页面底纹、二维码、纯色背景图。这些图片如果全部送给 Qwen-VL 去描述,成本浪费且会增加检索噪音。

我的过滤规则有三条,按优先级执行:

  1. 按 xref 去重。同一个 logo 在每页都会出现,但 xref 是同一个。记录已经处理过的 xref,同一个只处理一次,这能立刻砍掉大量重复调用。
  2. 按显示面积过滤。在 PDF 页面坐标系里,装饰性图片往往面积占比很小或者铺满整页。我一般只保留显示面积在页面总面积 3% 到 90% 之间的图片。低于 3% 的基本是 logo、图标、装饰线;高于 90% 的往往是背景纹理或整页扫描图,后者单独走扫描页逻辑。
  3. 按内容密集度过滤。用 Qwen-VL 之前可以先看图片的像素宽度和高度,小于 200x200 的图直接丢弃,因为放大后细节也救不回来;另外纯色图片可以通过计算色彩直方图来判定,方差极低的通常是花哨背景。

真正需要的是“正文里那张有信息量的配图”。确定保留之后,我通常直接用 PyMuPDF 按 bbox 重新渲染一张高分辨率 PNG,而不是用提取出来的原始图。原因很现实:PDF 里的嵌入图片分辨率经常被压缩过,但矢量图形和文字渲染出来的效果反而更清晰。用page.get_pixmap(clip=bbox, dpi=150)把对应区域重新画成位图,效果比直接extract_image好很多:

clip = fitz.Rect(info["bbox"]) pix = page.get_pixmap(clip=clip, dpi=150) pix.save("output_images/region_001.png")

DPI 我建议选 150。太低了 OCR 和细节识别都费劲,太高了导出的文件大、请求慢,实测 150 到 200 之间平衡性最好。

2.3 扫描页的判定与兜底

PDF 文档里还有一种让解析层头疼的情况:整页就是一张扫描图,没有任何文本层。这类文档在银行回单、历史档案、纸质合同扫描件里大量存在。PyMuPDF 本身不做 OCR,get_text拿不到任何文字,所以必须在管线里单独识别这类页面。

判定逻辑很粗暴但有效:如果某页get_text("dict")返回的文本块为空,且该页存在一张面积占比超过 90% 的图片,基本可以认定是扫描页。

扫描页的处理有两条路,我根据文档质量动态选择:

  • 质量一般的扫描件:直接调用 Qwen-VL 整页理解,让它输出“这是银行回单,收款方为 XX,金额 5000 元,日期 2024-03-15”这样的结构化描述,然后作为该页的文本索引。
  • 质量很差(歪斜、模糊、有手写批注)的扫描件:先做图像预处理(灰度化、二值化、倾斜校正),再走上面的视觉理解流程。

这里有个细节:有些 PDF 是“混合型”的,前面几页是电子版、中间夹了几页扫描附件。所以不能一上来就全局判定“这是扫描版 PDF”,而是逐页判断、逐页路由,文字页走文本解析、扫描页走视觉理解,最后把两种结果统一写进同一个索引体系。

3. Qwen-VL 接入:让图片成为可检索的“文字影子”

3.1 图像描述的 Prompt 设计与多轮策略

图片被筛选出来之后,下一步是让 Qwen-VL 把它描述成一段结构化的文字。这段描述的质量直接决定后续检索能不能命中,而描述质量又取决于 Prompt 怎么设计。

我经过多轮迭代之后,稳定使用的描述模板包含四个维度:图片类型、关键文字、结构关系、数据与细节。只看图不总结、只描述不推断,防止模型把自己脑补的内容写进去污染索引。

请分析这张图片,输出以下四个方面的结构化描述: 1. 图片类型:是流程图、架构图、表格截图、产品照片、界面截图还是其他? 2. 可见文字:完整提取图中所有文字,包括标题、标签、标注。 3. 结构关系:图中各个元素之间的组织关系,如“A模块位于顶部,连接到下方的B和C模块”。 4. 关键数据:图中出现的数字、日期、型号、金额等具体信息。 要求:客观描述,不推测图片以外的内容。

调用方式用 DashScope 的 OpenAI 兼容接口,图片走 base64 编码:

from openai import OpenAI import base64 client = OpenAI( api_key="your_api_key", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" ) def image_to_description(image_path): with open(image_path, "rb") as f: b64_image = base64.b64encode(f.read()).decode() resp = client.chat.completions.create( model="qwen-vl-plus", messages=[{ "role": "user", "content": [ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64_image}"}}, {"type": "text", "text": "请输出图片的图片类型、可见文字、结构关系、关键数据四个方面的结构化描述。"} ] }], temperature=0.1, ) return resp.choices[0].message.content

多图场景里有个容易忽略的问题:大图上截取小区域时,模型只能看到局部,不知道它属于哪个大图。所以我给裁剪图命名时会把上级信息带进去,比如page12_fig03_region01.png,描述 Prompt 里也说清楚“这是第 12 页第 3 张图的一部分,描述时注意与整体布局的关系”。

单张截图如果分辨率很高、细节很多,一次描述往往会遗漏角落里的信息。稳妥的做法是按区域二次裁剪,分块描述,最后拼接。对架构图这类强布局型的图片,左上、右上、左下、右下四块分别描述,再合并成一段完整文档。

3.2 表格走的另一条路

表格不能简单当成图来理解。PyMuPDF 的find_tables()对标准网格线表格的提取效果很好,可以直接得到结构化的行列表格,而 Qwen-VL 面对复杂表格时容易看错行列对齐关系。

所以我的表格处理分两种情况:

  • 简单表格(有规整网格线、内容以短文本为主):直接用find_tables提取成 HTML,保结构、保顺序,转成文本描述后入库。
  • 复杂表格(合并单元格、斜线表头、嵌套表格、扫描件里的表格):转图片交给 Qwen-VL,让它逐行逐列描述。视觉模型对合并单元格的理解能力实际上强于纯规则解析,因为它是按人类的视觉阅读顺序在看。

这里分享一个实测结论:表格转成 HTML 之后,如果直接以 HTML 源码入库,向量化的效果通常不理想。HTML 标签喧宾夺主,语义向量被标签字符带偏。更稳的做法是先df.to_markdown()转成 Markdown 表格,或者直接拼成“第1行:xxx;第2行:xxx”的自然语句,再交给向量模型。

3.3 描述结果的存储与更新

解析一遍 PDF 会产生大量中间产物:裁剪出来的临时图片、Qwen-VL 返回的描述文本、表格提取的 HTML。如果每次重跑都全量解析,成本和耗时都不可接受。

我给每个 PDF 建立一个处理清单,按页、按图建立唯一 ID,并记录处理状态:

{ "pdf_path": "docs/产品手册.pdf", "image_blocks": [ { "id": "p12_fig03", "page": 12, "bbox": [120.0, 300.0, 260.0, 400.0], "xref": 42, "md5": "ab12cd...", "description": "...", "embed_status": "done" } ] }

MD5 的作用是幂等去重:同一张图即使出现在不同页面,或者解析任务跑了两次,只要内容没变,md5一致就直接复用已有的 description,不再调用视觉模型。这个设计在批量处理几百份文档时能省掉 40% 以上的视觉模型调用量。

4. 索引与检索:图文混合的向量策略

4.1 向量化与入库

所有内容统一转成文本之后,就可以进入向量库了。文本块直接切分,图像描述块作为独立 chunk 与文本块放在同一个 collection 里。每一条记录的 metadata 至少包含:

  • page:页码,用于回答时溯源
  • type:text、image、table、scan_page
  • bbox:原始页面坐标,用于精准定位
  • source:来源 PDF 文件名

我用 Chroma 做本地持久化演示,生产环境可以换 Milvus 或 ES:

import chromadb client = chromadb.PersistentClient(path="./pdf_rag_db") collection = client.get_or_create_collection( name="pdf_blocks", metadata={"hnsw:space": "cosine"} ) collection.add( ids=["p12_fig03", "p12_text01", "p13_table01"], embeddings=[embed_text1, embed_text2, embed_text3], documents=[desc1, text1, table_md1], metadatas=[ {"page": 12, "type": "image", "bbox": "[120,300,260,400]", "source": "产品手册.pdf"}, {"page": 12, "type": "text", "bbox": "[30,50,540,100]", "source": "产品手册.pdf"}, {"page": 13, "type": "table", "bbox": "[60,200,520,500]", "source": "产品手册.pdf"}, ] )

文本块和图像描述块放进同一个向量空间,好处是查询时不用区分图文类型,一个问题可能在文本块里命中关键词、又和某张图的描述语义相近,两者能被同时召回。这也正是“图文兼容”在索引层面的落地方式。

文本分块需要单独说一下。PDF 文字块本身已经天然分段,我通常以段落或标题层级为边界做聚合,而不是按固定字符数硬切。固定 500 字切分会导致一个表格文本块被劈成两半,检索时哪一半都不完整。我的做法是:文本块小于 50 字的合并到上下文,超过 300 字的再按段落切,尽量保持每个 chunk 语义完整。

4.2 查询路由与混合召回

到了检索阶段,最怕的问题是用户的问题里同时包含图文信息,比如“看第二页的架构图,再结合后面的部署说明,告诉我服务怎么启动”。这种问题如果只走文本向量检索,架构图相关的那段描述可能因为和问题字面不重叠而被漏掉。

我先实现了一版简单方案:不做任何路由判断,对 Query 向量化后同时检索文本块和图像描述块,两者在同一个向量空间里天然竞争排名,取 Top-K 合并送进生成模型。实测这个方案已经能覆盖 80% 的图文混合问答场景。

后来发现的问题是:文本块数量通常远多于图像描述块,导致召回结果被文本块淹没。我做了两项调整:

  1. 对图像描述块设置一个小的召回加成系数(比如对 image 和 scan_page 类型的距离分数减 0.05,相当于在同等相似度下优先让图被捞出来)。
  2. 对“图”、“架构图”、“流程图”、“截图”、“表格”这类视觉意图词做轻量检测,如果 Query 包含这些词,强制粗召回图像类 chunk,再和语义检索结果合并。

合并排序我用 RRF(Reciprocal Rank Fusion)或者简单的分数归一化加权。代码示意:

def hybrid_score(dense_sim, bm25_sim, alpha=0.6): return alpha * dense_sim + (1 - alpha) * bm25_sim

这里 dense_sim 和 bm25_sim 都需要先做 min-max 归一化,否则两个分布不在一个量级上,加权没意义。这一步是工程里最容易忽略的细节。

5. 完整实现流程与核心代码

5.1 解析主流程

整个管线在代码层面分成三个阶段:解析、索引、问答。解析阶段的核心是把 PDF 变成中间产物,不只是向量化,还要把图片裁出来、把描述写进 JSON。我按文档为单位封装了主流程:

import fitz import json import os from pathlib import Path def process_pdf(pdf_path, output_dir): doc = fitz.open(pdf_path) manifest = {"pdf_path": pdf_path, "images": [], "text_chunks": [], "tables": []} for page_idx, page in enumerate(doc, start=1): blocks = page.get_text("dict")["blocks"] page_text_parts = [] for block in blocks: if block["type"] == 0: text = "".join( span["text"] for line in block["lines"] for span in line["spans"] ).strip() if text: page_text_parts.append(text) elif block["type"] == 1: bbox = block["bbox"] # 过滤小图、空白图 if valid_image_region(bbox, page): img_path = extract_image_region(page, bbox, output_dir, page_idx, block) desc = image_to_description(img_path) manifest["images"].append({ "id": f"p{page_idx}_fig", "page": page_idx, "bbox": bbox, "description": desc, "image_path": str(img_path) }) page_text = "\n".join(page_text_parts) if len(page_text.strip()) < 5: # 疑似扫描页:整页视觉理解 desc = image_to_description(render_full_page(page, output_dir, page_idx)) manifest["text_chunks"].append({ "page": page_idx, "type": "scan_page", "content": desc }) else: manifest["text_chunks"].append({ "page": page_idx, "type": "text", "content": page_text }) with open(os.path.join(output_dir, "manifest.json"), "w", encoding="utf-8") as f: json.dump(manifest, f, ensure_ascii=False, indent=2) return manifest

这个主流程的完整版本在工程里还需要加异常处理:个别 PDF 页面会抛 FITZ 的断言错误,通常是畸形 PDF 导致的,要用 try/catch 包住单页处理,失败后继续下一页,别让一份坏文档拖垮整个批处理任务。

5.2 图像描述与表格解析模块

图像区域提取我用 PyMuPDF 的 clip 参数渲染,分辨率 150 DPI 起步。代码里有两个细节:

  • 区域必须限制在页面范围内。有的 PDF 图片 bbox 超出了页面边界(排版软件留下的残留),直接把 clip 传给 get_pixmap 会报错或产生黑边。先求交页面矩形再裁剪。
  • 文件名不要用自增 id,用“页码编号+x坐标+y坐标”拼成稳定字符串,保证同一区域多次解析时文件名一致,方便做幂等跳过。
def extract_image_region(page, bbox, output_dir, page_idx, block): page_rect = page.rect clip = fitz.Rect(bbox) clip &= page_rect # 与页面区域求交,防止越界 x0, y0, x1, y1 = clip filename = f"p{page_idx}_x{int(x0)}_y{int(y0)}.png" output_path = Path(output_dir) / filename if output_path.exists(): return output_path # 幂等:已存在则直接复用 pix = page.get_pixmap(clip=clip, dpi=150) pix.save(str(output_path)) return output_path

表格模块的取舍前面讲过,核心实现如下:先尝试规则解析,解析结果异常(行数过少但区域过大)就转到视觉模型。

def extract_table(page, table_bbox): tables = page.find_tables(clip=fitz.Rect(table_bbox)) if not tables.tables: return None table = tables.tables[0] df = table.to_pandas() if len(df.columns) < 2 or len(df) < 1: return None # 不像是有效表格,转视觉模型 return df.to_markdown(index=False)

5.3 索引与问答联调

索引阶段读取 manifest,把所有 chunk 拼起来向量化入库。问答阶段就是标准的“检索-组装-生成”三步走。组装 Prompt 时我加的约束比较具体:

请仅根据下面的参考内容回答问题。参考内容里标注了信息来源(页码和类型), 如果参考内容没有足够信息,请直接回复“根据现有资料无法回答”,不要猜测。 参考内容: [1] (第12页 / 图片) 架构图:系统从上到下分为接入层、业务层、数据层…… [2] (第13页 / 正文) 服务启动需要在接入层配置网关地址……

把“来源类型”写进参考内容,能让生成模型在回答“看第二页的图”这类问题时意识到它正在读图的信息,而不是凭空发挥。最终回答再附上引用页码,这在实际知识库产品里是刚需。

6. 实战中的坑与排查方法

6.1 图片重复提取与像素陷阱

第一个坑是 PDF 里同一张嵌入图会因为显示变换被拆成多个 image_info 记录。比如一张矢量图经 PDF 内部机制被切分成多个小 tile 来渲染,get_image_info会返回一堆小矩形,每个看起来都像独立的图。特征是中转区域大小接近、横向或纵向排列规则。遇到这种情况,我的处理是:如果多个 info 的 bbox 间隙小于 5 像素且总面积接近一个完整矩形,就把它们合并成一个区域,再按合并后的 bbox 重新渲染。

第二个坑是低分辨率图硬放大。PDF 里有些图片本身只有 100x80 像素,PyMuPDF 按 150 DPI 渲染出来还是糊的,Qwen-VL 看这种图时经常把“6”识别成“8”。排查下来发现关键不在渲染 DPI,而在原图有效像素。我加的检查是:渲染前先读原图的info["width"]和高度,如果原始像素小于 150x150,直接标记为 low_quality 并跳过图像描述流程,最多把图片占位信息(“此处在原文档中有一张较低分辨率的插图”)写入索引,避免错误内容污染索引。

6.2 表格跨页断裂与文本错位

PDF 表格跨页是个高频问题。一个 5 页的报表,每个季度一张表,PyMuPDF 的find_tables()会分别识别成两段甚至更多段独立的表格,表头在每一段里还都会重新出现。如果直接把分段表格各自入库,检索到第二段时缺少表头上下文,模型根本不知道那些数字是销售额还是成本。

我后来加了一个后处理:计算同一页序列里多个表格的 bbox 高度、列宽占比、以及表头文本的相似度,如果高度接近、列结构一致、表头重复,就按“同一张跨页表”合并。合并后再做一次df.to_markdown(),这样一来检索到的是完整表格。这个逻辑对年报、财务对账单这类文档特别有用。

文本错位的问题则大多来自多栏布局。两栏 PDF 逐行提取时,左边栏一行、右边栏一行交替出现。解决办法是先按 block 的 x 坐标聚类分栏:把 block 中心 x 坐标排序,间距差异明显的分两组,视为两个栏,然后逐栏内部按 y 坐标排序。排序组合后再做文本切块,语义连贯性一下子好很多。

6.3 成本控制与性能调优

Qwen-VL 是这套方案里唯一有真金白银成本消耗的环节。控制成本的办法我总结出三条:

  1. 缓存一切。图像描述结果按图片 md5 持久化,重跑文档时直接读缓存。同一个 md5 绝不调用第二次视觉模型。
  2. 控制送入模型的图片数量和质量。过滤规则前置,300 页手册里能筛掉一半以上的装饰图;图片在发送前统一压缩到最长边 1024px 内,既能降低延迟又不损失关键细节(Qwen-VL 对输入尺寸有内部缩放,送超大图浪费带宽而已)。
  3. 区分模型档位。简单配图、截图用 qwen-vl-plus 就够;只有架构图、报表扫描件这种信息密度极高的图片才用 qwen-vl-max。实测大部分场景 plus 已经可以满足。

性能方面,PyMuPDF 本身很快,瓶颈基本在视觉模型串行调用上。批量解析时建议用ThreadPoolExecutor或asyncio做图片并发请求,我开 8 个并发线程处理 200 页的手册,解析加描述全程半小时内完成,单线程至少慢三倍。

6.4 常见问题速查表

现象原因排查与解决
某页提取文本为空,但 PDF 打开有字页面是背景图 + 透明文字的编码方式,正常文字是完整图片的一部分按扫描页兜底逻辑处理,整页送视觉模型
同一图片在索引里出现多次一个 xref 被多个页面引用按 md5 或 xref 全局去重,只索引一次
检索图片问题时召回不到图像描述块被文本块淹没给图片类型 chunk 加召回加成,或做视觉意图词的路由
表格数据错误表格被跨页切分,索引了两段不完整表合并同构分段表格后再索引
图片描述包含大量臆测内容Prompt 没限制模型自由发挥增加“客观描述,不推测”约束,temperature 调到 0.1
向量库中文检索效果差用的 embedding 模型对中文不够友好换用中文效果更好的 embedding 模型,并做文档级 title 拼接增强

这套方案我实际跑过的最大一批文档是 400 多页的轨道交通设备检修手册,里面各种架构图、接线表、故障诊断流程图。图文兼容这个能力真正救命的场景是:用户拿着现场照片问“这个指示灯状态对应手册里的哪条故障代码”,系统能从手册的图片描述里召回“指示灯状态表”那张图,再结合旁边的故障代码文本给出完整答案。单靠文本索引是永远做不到这一步的。

最后分享一个小技巧:PyMuPDF 渲染出来的区域图片,在送 Qwen-VL 之前先做一个四周留白 10 像素的扩边,视觉模型对贴近边界的文字识别率会明显提高。这个缩放、留白、压缩的细节,往往比换更贵的模型更管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询