1. 为什么PDF里的图片会成为RAG的"盲区"1.1 RAG的基础链路默认忽略图片RAG(检索增强生成)的核心逻辑其实不复杂:大模型记不住你本地的几十份甚至几百份PDF文档,所以先把文档切成文本块,转成向量存进索引库,用户提问时先检索出最相关的几个文本块,再把它们拼进上下文让大模型作答。这个过程能不能答对,前提只有一个——检索端必须覆盖到用户想知道的内容。如果答案是某段正文,那一切顺畅;可如果答案藏在一张图里,而文档解析阶段根本没把图片当回事,那后面的嵌入、检索、生成做得再好,也是白搭。我在做某知识库项目初期就犯过这个错误。当时导出的PDF里文本量很大,我理所当然认为图片属于"锦上添花",最多后面补个OCR就行。结果开头测试就翻车:文档里有一张带箭头指向的部署架构图,用户问"整个链路里哪一步负责数据清洗",系统完全答不上来。明明图里的箭头和标注写得很清楚,但因为检索空间里压根没有这张图的语义信息,大模型只能靠猜测蒙一个答案。这种问题不是模型能力不够,是管道设计漏掉了一整类信息载体。图片在RAG链路里的特殊性在于:它既是高频信息载体,又是最容易被解析阶段丢弃的对象。技术类PDF里的架构图、流程图、数据图表、界面截图,往往浓缩了正文里最关键的结论。如果把图片直接跳过,等于把一个信息宝库锁在门外,检索再多文本也是片面检索。所以对PDF图片的处理,从来不是可选项,而是决定知识库问答质量的核心环节。1.2 图片被忽略之后的真实代价这里举一个典型场景:某份材料里有张季度营收柱状图,图上每个柱子都标注了具体数字,旁边还有一个文字段落描述整体趋势。用户提问"哪个季度增长最明显",如果只做文本检索,系统大概率会召回描述趋势的那句话,但拿不到具体季度和数值。如果图里还有异常点标注,那丢失的信息就更致命了。生成阶段的大模型面对不完整的上下文,只能给出含糊回答,用户一追问具体数字,系统就说不出话了。图片被忽略会导致三个层面的问题:检索缺失:图片内容没有进入索引,检索空间天然存在盲区,任何基于向量的相似度匹配都找不到这部分信息。语义缺失:即使把图片文件本身存进数据库,大模型也读不懂像素,无法把图片内容转换成可引用的知识点。结构缺失:就算用OCR抽出图片里的文字,如果丢掉了视觉布局、箭头方向、层级关系,检索能召回片言只语,但生成答案时依然连不起来。这三个问题层层递进,也是我在后续实践中反复踩过的地方。要解决它们,靠的不是某一个"魔法模型",而是把图片纳入一条完整的、有顺序的处理链路。1.3 处理图片的正确姿势:语义化转写我最终确定的思路是:不把图片直接塞给多模态模型,而是把图片转写成高质量的语义文本,再走回常规文本RAG管道。这样做有几个现实好处:兼容性最强。已有的文本切块、向量化、检索逻辑全部可以复用,不需要为图片单独建一套多模态检索基础设施;可调试性最好。图片转成文本后,你可以直接看检索命中的内容是什么,一条条检查,不会像端到端多模态系统那样难定位问题;成本更可控。文本向量化的资源消耗远低于直接做多模态向量化,也更容易水平扩展。但这套方案真正的难点在于"转写质量的保障"和"步骤顺序的安排"。转写不能是简单一句话"这是一张流程图",那样信息密度太低,等于把图片降级成了噪声文本;步骤顺序更不能乱,比如先OCR再定位,会导致图片描述无法准确关联到它的上下文段落。所以接下来我会按照一条完整可复现的流程,把每个环节拆开细讲。2. 图片处理完整流程拆解2.1 总体步骤顺序与核心决策点我建议的标准化处理顺序是:PDF解析与图片定位,记录页码、坐标、尺寸、唯一ID;图片去重、过滤、预处理,去噪并筛查低质量内容;图片分类决策,判断走OCR、视觉模型描述,还是两者结合;生成结构化描述文本,并保存带元数据的中间结果;上下文拼接,把图片描述挂靠到邻近正文段落;切块、向量化、写入索引;检索召回与生成增强,最后带回引用信息。这个顺序不是随意定的,每一步的输出都是下一步的输入。尤其是第1步的坐标记录,很多人容易忽略。没有坐标,后面就无法把图片描述准确放回它所在的上下文位置,也无法在问答时给出"见图3、第5页右上角"这类精确引用。所以我整条流程里一直强调:图片ID和坐标元数据要像身份证一样全程携带,从解析阶段一路带到检索阶段。为了更直观,我把各阶段的关键产物整理成一个速查表:阶段输入关键操作产物PDF解析PDF文件遍历页面图片对象,记录xref与坐标图片文件+元数据图片预处理原始图片过滤小图、去重、转底色、合并碎块干净图片集内容理解干净图片OCR/视觉模型分类处理文字或结构化描述上下文对齐描述+原文档按坐标匹配邻近文本图片文本块切块索引图片文本块切块、向量化、写入向量库可检索索引检索生成用户问题混合检索、拼装提示词、调用生成带引用的答案这里最关键的决策点在第3步的"图片分类"。分类方向错了,后续处理全歪。比如把文字截图丢给视觉模型做泛泛描述,容易漏掉细节;把流程图丢给纯OCR,又会丢失箭头和层级关系。所以分类必须有一套标准化依据。2.2 PDF解析:图片发现、去重与坐标记录PDF解析阶段的目标,不只是把图存到硬盘上,而是要搞清楚三件事:图在哪些页面出现过、图在页面上的具体区域、图本身是位图还是矢量图。先说位图。PDF内部的图片对象可以通过解析库的图片列表接口拿到,每个图片对象有一个唯一的xref引用号。但要注意:同一个xref可能在多页反复出现,比如公司Logo、页眉装饰图,它们在每一页都有相同的引用号。如果你见一次存一次,索引里会出现大量重复图片块。我的习惯是按文件内容哈希去重,把同一张图对应的物理文件只保存一份,但把"出现位置"全部记录到元数据里。再说坐标。每个图片对象在页面上的显示区域,一般可以通过解析库的矩形查询接口拿到。这里要区分:同一个xref可能在一页上被渲染多次,也可能被裁剪成多个可见区域,所以要用返回的矩形列表逐一登记。坐标系的原点在页面左上角,单位是"点",1点约等于0.3528毫米。记得把矩形坐标存成[x0, y0, x1, y1]四元组,后面做上下文匹配和局部渲染都需要它。最后是矢量图。很多PDF图表其实不是位图,而是由线条、曲线和色块组成的矢量对象。如果只用图片列表扫描,这类内容不会被发现。正确做法是按坐标区域渲染页面,把指定区域输出成位图,再交给下游的OCR或视觉模型处理。这也是为什么坐标记录如此重要——没有坐标,你连"把哪块区域渲染出来"都不知道。2.3 图片预处理:清洗、合并与质量把关预处理阶段看起来不起眼,但恰恰是决定下游效果的分水岭。我通常会做四件事:过滤过小图片。宽或高小于100像素的装饰性元素,比如小箭头、分割线、圆点图标,直接跳过。这些内容信息量极低,进入索引只会污染检索结果。透明背景转白底。很多PDF里的图片是PNG透明底,OCR和视觉模型处理时容易把透明区域识别成黑块或噪声。统一填充成白色背景会稳定很多。碎块合并。有些出版工具会把一张完整的表或图拆成多个图片对象插入页面。判断方法很朴素:如果两个图片矩形的水平范围接近、垂直方向紧邻,且宽度差不多,就很有可能原本是一张图。这类碎块最好先合并成一个矩形区域,再整体渲染和识别。旋转和清晰度修正。扫描件常见的歪斜可以通过角度检测和旋转校正;分辨率太低的图,不要强行放大,放大只会增加噪声,不如在元数据里标记"低清晰度",让下游处理时降低期望。这些预处理步骤都不复杂,但如果你跳过它们,后面OCR会多出一堆"神秘字符",视觉模型也会把装饰性图标描述成有意义的内容,最终污染整个索引。我在图形处理上吃过不少亏,现在每次跑管道前都会先加一层过滤规则。2.4 内容理解:OCR与视觉模型的分工图片分类之后的处理策略,我总结成一张决策表:图片特征首选方案补充方案文字截图、代码截图OCR视觉模型整理排版表格截图OCR提取单元格内容视觉模型整理为Markdown表格柱状图、折线图、饼图视觉模型描述趋势与数值OCR补充坐标轴标签架构图、流程图、拓扑图视觉模型提取节点与关系OCR核对节点文字装饰性图标、Logo直接忽略不进入索引OCR的核心价值在于"字准"。纯文字截图、代码块、表格里的数值,用OCR比视觉模型更可靠,因为视觉模型在生成表格时偶尔会凭空补一行数据。不过OCR也有局限,它只能输出文字序列,不理解文字之间的视觉关系。股价曲线里"上升"这个信息,OCR是抓不到的。视觉模型(VLM)的价值在于"懂关系"。它能把图中的箭头方向、层级结构、空间布局、数据趋势描述出来。但VLM最大的风险是幻觉——它可能在描述里编造图里根本不存在的节点或数字。所以我的建议是:对于信息密集的复杂图,先跑OCR拿到文字标签,再把OCR结果和原图一起喂给视觉模型,让它结合文字和视觉布局做结构化描述。这样既能拿到准确文字,又能保住结构关系,幻觉概率也会低很多。2.5 文本化表达:结构化描述与上下文拼接视觉模型输出的原始描述,不能直接丢进索引。我会要求模型按模板输出结构化描述,比如包含图片类型、主要元素、元素关系、图内文字、关键数据结论。这么做不是为了炫技,而是为了让后面的检索阶段能更精准地命中。实际操作中,图片文本块我建议拼成这样一个格式:【图片:p3_x12 页码:3 区域:左上角】 图片类型: 流程图 内容说明: 这是一个四步数据管道... 图内文字: 采集 -> 清洗 -> 训练 -> 发布 关键关系: 采集与清洗呈顺序依赖关系这样拼接之后,检索时不管是问"数据管道分几步",还是直接问"清洗在哪个环节后",都能从不同措辞命中同一图片块。提示词也要多花心思,我会在生成描述时明确要求:"请用简洁客观的语言描述这张图片。包括:图片类型;主要元素;元素之间的关联;图中出现的所有文字;如果包含数据图表,请给出最高值、最低值、异常点等关键信息。不要编造图中不存在的内容。"上下文拼接也很关键。图片不是孤立存在的,它通常紧跟着一段正文说明。我会用图片的坐标矩形,从页面文本里找出距离它最近的段落,把这段文字作为"邻近上下文"加进图片块。这样图文信息就形成了互补,检索时既能靠图内文字命中,也能靠正文描述上下文命中。2.6 切块向量化与混合检索图片块的文本特征和普通文本不太一样:它信息密度高、关键词集中、结构性强。所以切块参数我一般会放宽一点,单块允许到500到1000字,重叠部分保持50到100字。太长会稀释向量语义,太短又会把关系描述切断。向量化阶段,普通中文文本嵌入模型就够用了,关键是把"图片类型"和"页码"这些元数据拼进文本里。为什么?因为用户提问时经常会说"图2"、"架构图"、"流程图"这类词,如果你只在文本里存描述,检索不到这些标签;把元数据拼进去,相当于给图片块加了一层可检索的定位锚点。不过,单靠向量检索在图片场景有个短板。图片描述经常由短词和专有名词组成,比如"Kafka"、"Nginx"、"模型训练",向量相似度对这类精确词不够友好,有时候会被同义改写带偏。所以我更推荐向量检索加全文关键词检索的混合方案。向量负责语义相似,关键词负责精确匹配,两边召回后合并排序,效果会比单路检索稳定很多。2.7 生成增强与溯源引用检索到图片块之后,生成环节的增强方式是"原文回填"。图片块本身就是文本,把它直接拼进提示词,模型就能基于图片的语义内容作答。更关键的是,图片块里携带了页码、图片ID、坐标信息,生成阶段可以要求模型"在回答末尾标注信息来源,比如'根据第3页图2的描述'"。溯源看起来是个小功能,但对知识库系统来说价值极大。用户能核实答案的真伪,管理员能检查哪张图的转写质量有问题,后续优化也能精准定位到具体图片,而不是大海捞针。这也是我把图片处理称为"工程"而不是"调模型"的原因——每一环都要可回查、可追溯。3. 实操代码:从PDF到可问答的图片知识3.1 环境准备与依赖安装实操环节,我用Python生态里最常用的几个库来演示。先安装依赖:pip install pymupdf paddleocr paddlepaddlePyMuPDF负责PDF解析和图片提取,PaddleOCR负责图片文字识别。视觉模型部分,你可以选择任何支持图片加文字输入的开源模型,部署成本一般都在可控范围。如果本机资源有限,也可以重点先用OCR把文字类图片处理掉,视觉模型覆盖最关键的几张复杂图,效果提升依然显著。向量检索我用FAISS做示例,它是目前上手最快的相似度索引库。切块工具可以用通用的文本切分器,也可以自己写一个简单的按分隔符切割函数。3.2 图片提取与去重图片提取的核心逻辑是:遍历所有页面,拿到每个图片对象,保存图片文件,同时记录坐标和引用信息。代码里我会额外加一个MD5哈希缓存,自动过滤重复图片。import fitz import hashlib import os def extract_pdf_images(pdf_path, out_dir): os.makedirs(out_dir, exist_ok=True) doc = fitz.open(pdf_path) file_cache = {} records = [] for page_no in range(len(doc)): page = doc[page_no] for img in page.get_images(full=True): xref = img[0] pix = fitz.Pixmap(doc, xref) # PDF中部分图片是CMYK或多通道格式,统一转成RGB if pix.n - pix.alpha >= 4: pix = fitz.Pixmap(fitz.csRGB, pix) raw = pix.tobytes("png") digest = hashlib.md5(raw).hexdigest() # 同一张图只保存一次,但每次出现都记录 if digest not in file_cache: fname = os.path.join(out_dir, f"img_{digest[:8]}.png") pix.save(fname) file_cache[digest] = fname for rect in page.get_image_rects(xref): records.append({ "image_id": f"p{page_no+1}_x{xref}", "file": file_cache[digest], "page": page_no + 1, "bbox": [rect.x0, rect.y0, rect.x1, rect.y1], }) doc.close() return records注意一个细节:page.get_images(full=True)返回的元组里第一个元素就是xref,但同一张图可能在页面多处渲染,所以要用get_image_rects拿矩形列表逐一遍历。这样每一条渲染记录都能保留独立的页码和坐标。如果遇到矢量图,这段代码的get_images是找不到对象的。解决办法是直接用坐标区域渲染页面:page = doc[page_no - 1] rect = fitz.Rect(bbox) pix = page.get_pixmap(clip=rect, dpi=200) pix.save("region_render.png")dpi=200是我常用的渲染参数,太低看不清细节,太高文件太大,200是比较均衡的档位。3.3 OCR文字提取实现OCR阶段,我推荐直接用带方向分类的中文OCR库,对扫描歪斜的图片有自动矫正能力。from paddleocr import PaddleOCR import json ocr = PaddleOCR(use_angle_cls=True, lang="ch") def ocr_image_to_lines(image_path): result = ocr.ocr(image_path, cls=True) lines = [] if not result: return lines for page_res in result: if not page_res: continue for box in page_res: coords = box[0] text_and_score = box[1] text = text_and_score[0] score = text_and_score[1] if score and score >= 0.6: lines.append({ "text": text, "bbox": coords, "score": float(score) }) return lines置信度阈值0.6是个经验值。阈值太高会漏掉模糊文字,太低会涌入大量错误字符。不同文档扫描质量差异很大,我一般先用一小批图测试,再决定调到0.5还是0.65。OCR输出的结果不要直接拼成一长串,最好保留每一行文字和对应坐标。后面和视觉模型描述合并时,这些坐标能帮你判断文字在图中的位置关系,也可以用来校验视觉模型是否漏读了标签。3.4 视觉模型描述生成视觉模型部分,我假设你已经有一个本地部署好的模型服务,代码里用HTTP接口调用的方式演示。读图转base64,再连同提示词一起提交:import base64 import requests VLM_ENDPOINT = "http://127.0.0.1:8000/vlm" # 换成你的本地服务入口 def vlm_describe(image_path, prompt): with open(image_path, "rb") as f: image_b64 = base64.b64encode(f.read()).decode("utf-8") payload = { "image_base64": image_b64, "prompt": prompt, } resp = requests.post(VLM_ENDPOINT, json=payload, timeout=180) resp.raise_for_status() return resp.json()["text"]这里有两个忠告。第一,提示词一定要写清楚"不要编造图中不存在的内容"。第二,对包含大量文字的复杂图,把OCR结果作为额外提示文本一起传入,可以有效减少模型幻觉。比如这样组织提示词:prompt = """ 请结合OCR识别文本,描述这张图片的结构。 要求: 1. 图片类型,例如流程图、柱状图、架构图 2. 说明主要元素及其位置关系 3. 图中如果存在箭头,说明从哪个元素指向哪个元素 4. 不要编造OCR文本中没有的内容 OCR识别文本: {ocr_text} """.format(ocr_text=ocr_text)3.5 图片上下文拼接与索引构建拿到OCR文本和视觉模型描述后,下一步是把它们拼成标准文本块。我用一个函数来组装:def build_image_text_block(record, vlm_desc, ocr_text, nearby_text): return ( f"【图片:{record['image_id']} 页码:{record['page']} 区域:{record['bbox']}】\n" f"图片说明:{vlm_desc}\n" f"图内文字:{ocr_text}\n" f"上下文:{nearby_text}" )nearby_text怎么取?我在第2章提过,用图片坐标从页面文本里找到最近的段落。简单实现是直接采集页面中所有文本块,取垂直中心点与图片中心点最近的文本块:def get_nearby_text(page, bbox): blocks = page.get_text("blocks") img_cy = (bbox[1] + bbox[3]) / 2 best_block = None best_dist = float("inf") for b in blocks: x0, y0, x1, y1, text, *_ = b block_cy = (y0 + y1) / 2 dist = abs(block_cy - img_cy) if dist < best_dist: best_dist = dist best_block = text.strip() return best_block[:300]拼接完成后,我把所有文本块重新切分,再做向量化:import numpy as np def get_embedding(text): # 这里接入你选用的文本向量模型,返回一个列表 return [] splitter = RecursiveCharacterTextSplitter( chunk_size=800, chunk_overlap=100, separators=["\n\n", "\n", "。", ";", ",", " ", ""] ) final_chunks = [] for block in all_image_blocks: final_chunks.extend(splitter.split_text(block)) embedding_matrix = np.array([get_embedding(c) for c in final_chunks]) embedding_matrix = embedding_matrix.astype("float32") # 归一化,方便直接使用内积计算余弦相似度 embedding_matrix /= np.linalg.norm(embedding_matrix, axis=1, keepdims=True)最后用FAISS建索引:import faiss dim = embedding_matrix.shape[1] index = faiss.IndexFlatIP(dim) index.add(embedding_matrix)3.6 检索问答链路实现查询阶段的核心逻辑是:把用户问题向量化,在向量库中召回TopK,再拼接到提示词里交给生成模型。def search(question, top_k=5): q = np.array([get_embedding(question)], dtype="float32") q /= np.linalg.norm(q) scores, ids = index.search(q, top_k) return ids[0], scores[0] def answer(question, llm_generate): ids, scores = search(question) context = "\n\n".join(final_chunks[i] for i in ids) prompt = f""" 请基于以下资料回答问题。回答中如果引用了图片内容,请注明图片编号和页码。 资料内容: ===== {context} ===== 问题:{question} """ return llm_generate(prompt)这里有个实用细节:如果启用了关键词召回,可以把向量召回的TopK和关键词召回的TopK做合并,再用RRF(倒数排名融合)排序。RRF实现非常简单,就是对同一文档在多个结果列表中的名次取倒数求和,排名越靠前权重越大。混合召回能显著提升图片块的命中率,尤其是用户问题里带专有名词的时候。4. 高频问题与避坑技巧实录4.1 表格图片被OCR拆碎怎么办表格类图片是OCR翻车重灾区。OCR按读序输出,多行多列表格一旦有合并单元格,输出就会彻底乱掉,表头和数值对应关系全丢。我一开始遇到这种问题,傻乎乎地在输出结果后补正则去抓"看起来像数字的内容",结果越补越乱。后来改成双轨方案:表格图片先跑OCR拿到每个单元格的原始文字,然后让视觉模型结合OCR结果把整张表转成Markdown格式。提示词可以这么写:"请把这张图片中的表格完整转换为Markdown表格,保留所有行和列,不要遗漏单元格,不要改动数据。"对于纯OCR输出,我的经验是不要强求它复原表格结构,只把它当作"文字食材"。结构整理交给视觉模型更合适,因为它看得见行列位置关系,能从视觉上判断某一列属于哪个表头。4.2 重复图片和页眉页脚污染索引PDF里最常见的重复图片就是页眉Logo、页脚图标、背景水印。如果不做去重,几千页PDF能索引出几百份一模一样的Logo图片块。用户问"这个Logo是什么"时还好,怕的是这些重复块在检索时频繁命中,把真正有用的图片块挤出了TopK。处理办法在代码里已经体现了:按图片字节内容的MD5哈希去重,同一内容只保存一份。但这还不够,页眉Logo在每页都会出现,虽然文件只有一份,但记录里会有几百条"出现位置"。我建议在进入索引前加一道过滤:如果同一图片ID在文档中出现次数超过阈值,比如10次,大概率是装饰性元素,直接在索引构建阶段剔除。水印文字也要留意。有些PDF加了斜向水印文字,OCR会把水印混进正文。处理办法是让OCR忽略整页统一背景,或者在水印文字出现频次过高时降低对应文本的权重。4.3 视觉模型描述出现幻觉数据视觉模型幻觉是图片转写里最隐蔽的问题。它不像OCR那样有明显的识别错误,而是会一本正经地编造出图里不存在的节点、数字或流程分支。比如一张三步流程图,模型可能描述出第四步"数据验证",因为它在别处见过相似模板。降低幻觉的办法,我总结为三条:提示词硬约束:明确写"只描述图片中可见的内容,不确定的信息用【不确定】标注";交叉验证:图片里出现数字时,让OCR先抽取文字,再让视觉模型只描述关系,不改数字;人工抽检:随机抽取10%的图片块,由人来核对描述和原图是否一致。别小看这个笨办法,它能让你快速发现哪些提示词设计有问题。如果视觉模型输出的描述里混进了OCR文本里没有的实体词,我基本会视为幻觉,删除该实体再向比,或者要求模型重新生成一次。4.4 图片块太长挤占上下文窗口图片描述动辄几百字,如果一页里有多张图,生成的文本块拼接起来很容易超过生成模型的上下文限制。检索时TopK如果是5,可能一半都是图片块,正文内容反而被挤掉了。我的处理思路是分级压缩:如果同一页出现多个图片块,先做一个"图片清单页"式的聚合块,每张图只用一两句话概括,然后完整链路里只把检索命中的那一两张图的详细描述放进去。另外,TopK在图片场景不要设太大,我一般设为3到5,召回内容够用且不容易爆上下文。如果实在要保存长描述,也可以把描述分成"摘要"和"细节"两个字段。检索时用摘要做匹配,生成时把对应的细节块一并带入。这样既能保住关联性,又不会每个命中块都带着冗长细节。4.5 扫描版PDF别当普通PDF处理扫描版PDF里每一页本质上就是一张大图,如果用"提取页面内嵌图片"的逻辑去处理,你会发现整页图片能提取出来,但拆成十几个小碎片,OCR结果惨不忍睹。处理方法完全不同:遇到扫描版PDF,应该走"整页OCR"路线。先把整页渲染成清晰位图,然后对整页做OCR,输出带坐标的完整文本。得到文本后再走正常切块、向量化流程。判断标准很直接:如果页面上最大的图片矩形覆盖面积超过整页面积的80%,就当作扫描页处理。page_pix = page.get_pixmap(dpi=200) # 对整页做OCR扫描版PDF一定要在预处理阶段就识别出来,否则后面所有针对"内嵌图片"的处理都会变成无用功。4.6 图片召回率上不去的补救方法如果图片块建好了索引,但检索时总是召不回,问题多半出在"措辞不一致"上。图片描述里写"圆柱形容器",用户问的是"存储引擎",两者语义相关但不完全一致,向量模型可能算不出高相似度。补救方法有三个:在图片文本块里加类型前缀。比如"【图表:柱状图|收入趋势】",这种明确的标签词检索时命中率极高;给描述补同义词。把"模型训练"补成"模型训练(Model Training)",把"存储引擎"补成"存储引擎(数据库/仓库)",不要过度膨胀,但关键术语的同义词扩展很有用;混合检索兜底。向量召回和关键词召回合并后,图片块的精确词能以关键词方式补进来,即使向量相似度不够高也有一席之地。4.7 常见问题速查表现象可能原因解决方向表格图片OCR乱序表格结构复杂,OCR读序不保留行列关系视觉模型转Markdown同一图片反复命中页眉水印未去重按哈希去重,过滤高频装饰图描述里出现不存在的数字视觉模型幻觉提示词硬约束,OCR交叉校验上下文长度超限图片块过多过长压缩为摘要块,缩小TopK扫描PDF图片支离破碎把扫描页当内嵌图处理整页OCR方案图片召不回描述和提问措辞不一致加类型前缀、同义词、混合检索5. 实战心得与个人建议整套流程落地之后,我最大的体会是:处理PDF图片的难点不在于模型选型,而在于流程里那些一不留神就漏掉的元数据工程。图片ID、页码、坐标、出现频次、文字置信度、描述来源,这些字段如果记录得不完整,后续排查问题时就会被卡在"这条内容到底是OCR还是视觉模型生成的?"这种问题上,进退两难。我现在每次跑图片管道,都会把中间产物完整存下来:提取出的图片文件、OCR的JSON输出、视觉模型的原始描述、最终拼接形成的文本块。这些中间产物既是检查质量的抓手,也是调试问题时的关键线索。你别嫌多这一步麻烦,等你在线上知识库里发现某个答案明显错误时,能快速定位到具体环节,这个价值远大于存储那点开销。另一个心得是要给图片块设立质量阈值。OCR置信度过低、视觉模型描述过短、图片尺寸过小,这些情况都直接标记为低质量块,不进入正式索引,最多放在调试集里。宁缺毋滥,图片块的质量污染对RAG问答的伤害,比缺失还严重,因为它会让系统给出信心满满的错误答案。最后分享一个我一直在用的回归测试法。每次改完图片处理链路,我没空把整个知识库重新问一遍,就固定准备三到五个"必问题",专门覆盖图片场景,比如"图3里的流程分几步""这张架构图里哪两个模块之间有调用关系""图表里哪个数据点最低"。跑完这几个问题,再对比一下答案和引用是否准确,基本就能判断这次改动是变好了还是改坏了。这个习惯帮我防住了好几次"看似升级、实则退化"的改版。
1. 为什么PDF里的图片会成为RAG的"盲区"1.1 RAG的基础链路默认忽略图片RAG(检索增强生成)的核心逻辑其实不复杂:大模型记不住你本地的几十份甚至几百份PDF文档,所以先把文档切成文本块,转成向量存进索引库,用户提问时先检索出最相关的几个文本块,再把它们拼进上下文让大模型作答。这个过程能不能答对,前提只有一个——检索端必须覆盖到用户想知道的内容。如果答案是某段正文,那一切顺畅;可如果答案藏在一张图里,而文档解析阶段根本没把图片当回事,那后面的嵌入、检索、生成做得再好,也是白搭。我在做某知识库项目初期就犯过这个错误。当时导出的PDF里文本量很大,我理所当然认为图片属于"锦上添花",最多后面补个OCR就行。结果开头测试就翻车:文档里有一张带箭头指向的部署架构图,用户问"整个链路里哪一步负责数据清洗",系统完全答不上来。明明图里的箭头和标注写得很清楚,但因为检索空间里压根没有这张图的语义信息,大模型只能靠猜测蒙一个答案。这种问题不是模型能力不够,是管道设计漏掉了一整类信息载体。图片在RAG链路里的特殊性在于:它既是高频信息载体,又是最容易被解析阶段丢弃的对象。技术类PDF里的架构图、流程图、数据图表、界面截图,往往浓缩了正文里最关键的结论。如果把图片直接跳过,等于把一个信息宝库锁在门外,检索再多文本也是片面检索。所以对PDF图片的处理,从来不是可选项,而是决定知识库问答质量的核心环节。1.2 图片被忽略之后的真实代价这里举一个典型场景:某份材料里有张季度营收柱状图,图上每个柱子都标注了具体数字,旁边还有一个文字段落描述整体趋势。用户提问"哪个季度增长最明显",如果只做文本检索,系统大概率会召回描述趋势的那句话,但拿不到具体季度和数值。如果图里还有异常点标注,那丢失的信息就更致命了。生成阶段的大模型面对不完整的上下文,只能给出含糊回答,用户一追问具体数字,系统就说不出话了。图片被忽略会导致三个层面的问题:检索缺失:图片内容没有进入索引,检索空间天然存在盲区,任何基于向量的相似度匹配都找不到这部分信息。语义缺失:即使把图片文件本身存进数据库,大模型也读不懂像素,无法把图片内容转换成可引用的知识点。结构缺失:就算用OCR抽出图片里的文字,如果丢掉了视觉布局、箭头方向、层级关系,检索能召回片言只语,但生成答案时依然连不起来。这三个问题层层递进,也是我在后续实践中反复踩过的地方。要解决它们,靠的不是某一个"魔法模型",而是把图片纳入一条完整的、有顺序的处理链路。1.3 处理图片的正确姿势:语义化转写我最终确定的思路是:不把图片直接塞给多模态模型,而是把图片转写成高质量的语义文本,再走回常规文本RAG管道。这样做有几个现实好处:兼容性最强。已有的文本切块、向量化、检索逻辑全部可以复用,不需要为图片单独建一套多模态检索基础设施;可调试性最好。图片转成文本后,你可以直接看检索命中的内容是什么,一条条检查,不会像端到端多模态系统那样难定位问题;成本更可控。文本向量化的资源消耗远低于直接做多模态向量化,也更容易水平扩展。但这套方案真正的难点在于"转写质量的保障"和"步骤顺序的安排"。转写不能是简单一句话"这是一张流程图",那样信息密度太低,等于把图片降级成了噪声文本;步骤顺序更不能乱,比如先OCR再定位,会导致图片描述无法准确关联到它的上下文段落。所以接下来我会按照一条完整可复现的流程,把每个环节拆开细讲。2. 图片处理完整流程拆解2.1 总体步骤顺序与核心决策点我建议的标准化处理顺序是:PDF解析与图片定位,记录页码、坐标、尺寸、唯一ID;图片去重、过滤、预处理,去噪并筛查低质量内容;图片分类决策,判断走OCR、视觉模型描述,还是两者结合;生成结构化描述文本,并保存带元数据的中间结果;上下文拼接,把图片描述挂靠到邻近正文段落;切块、向量化、写入索引;检索召回与生成增强,最后带回引用信息。这个顺序不是随意定的,每一步的输出都是下一步的输入。尤其是第1步的坐标记录,很多人容易忽略。没有坐标,后面就无法把图片描述准确放回它所在的上下文位置,也无法在问答时给出"见图3、第5页右上角"这类精确引用。所以我整条流程里一直强调:图片ID和坐标元数据要像身份证一样全程携带,从解析阶段一路带到检索阶段。为了更直观,我把各阶段的关键产物整理成一个速查表:阶段输入关键操作产物PDF解析PDF文件遍历页面图片对象,记录xref与坐标图片文件+元数据图片预处理原始图片过滤小图、去重、转底色、合并碎块干净图片集内容理解干净图片OCR/视觉模型分类处理文字或结构化描述上下文对齐描述+原文档按坐标匹配邻近文本图片文本块切块索引图片文本块切块、向量化、写入向量库可检索索引检索生成用户问题混合检索、拼装提示词、调用生成带引用的答案这里最关键的决策点在第3步的"图片分类"。分类方向错了,后续处理全歪。比如把文字截图丢给视觉模型做泛泛描述,容易漏掉细节;把流程图丢给纯OCR,又会丢失箭头和层级关系。所以分类必须有一套标准化依据。2.2 PDF解析:图片发现、去重与坐标记录PDF解析阶段的目标,不只是把图存到硬盘上,而是要搞清楚三件事:图在哪些页面出现过、图在页面上的具体区域、图本身是位图还是矢量图。先说位图。PDF内部的图片对象可以通过解析库的图片列表接口拿到,每个图片对象有一个唯一的xref引用号。但要注意:同一个xref可能在多页反复出现,比如公司Logo、页眉装饰图,它们在每一页都有相同的引用号。如果你见一次存一次,索引里会出现大量重复图片块。我的习惯是按文件内容哈希去重,把同一张图对应的物理文件只保存一份,但把"出现位置"全部记录到元数据里。再说坐标。每个图片对象在页面上的显示区域,一般可以通过解析库的矩形查询接口拿到。这里要区分:同一个xref可能在一页上被渲染多次,也可能被裁剪成多个可见区域,所以要用返回的矩形列表逐一登记。坐标系的原点在页面左上角,单位是"点",1点约等于0.3528毫米。记得把矩形坐标存成[x0, y0, x1, y1]四元组,后面做上下文匹配和局部渲染都需要它。最后是矢量图。很多PDF图表其实不是位图,而是由线条、曲线和色块组成的矢量对象。如果只用图片列表扫描,这类内容不会被发现。正确做法是按坐标区域渲染页面,把指定区域输出成位图,再交给下游的OCR或视觉模型处理。这也是为什么坐标记录如此重要——没有坐标,你连"把哪块区域渲染出来"都不知道。2.3 图片预处理:清洗、合并与质量把关预处理阶段看起来不起眼,但恰恰是决定下游效果的分水岭。我通常会做四件事:过滤过小图片。宽或高小于100像素的装饰性元素,比如小箭头、分割线、圆点图标,直接跳过。这些内容信息量极低,进入索引只会污染检索结果。透明背景转白底。很多PDF里的图片是PNG透明底,OCR和视觉模型处理时容易把透明区域识别成黑块或噪声。统一填充成白色背景会稳定很多。碎块合并。有些出版工具会把一张完整的表或图拆成多个图片对象插入页面。判断方法很朴素:如果两个图片矩形的水平范围接近、垂直方向紧邻,且宽度差不多,就很有可能原本是一张图。这类碎块最好先合并成一个矩形区域,再整体渲染和识别。旋转和清晰度修正。扫描件常见的歪斜可以通过角度检测和旋转校正;分辨率太低的图,不要强行放大,放大只会增加噪声,不如在元数据里标记"低清晰度",让下游处理时降低期望。这些预处理步骤都不复杂,但如果你跳过它们,后面OCR会多出一堆"神秘字符",视觉模型也会把装饰性图标描述成有意义的内容,最终污染整个索引。我在图形处理上吃过不少亏,现在每次跑管道前都会先加一层过滤规则。2.4 内容理解:OCR与视觉模型的分工图片分类之后的处理策略,我总结成一张决策表:图片特征首选方案补充方案文字截图、代码截图OCR视觉模型整理排版表格截图OCR提取单元格内容视觉模型整理为Markdown表格柱状图、折线图、饼图视觉模型描述趋势与数值OCR补充坐标轴标签架构图、流程图、拓扑图视觉模型提取节点与关系OCR核对节点文字装饰性图标、Logo直接忽略不进入索引OCR的核心价值在于"字准"。纯文字截图、代码块、表格里的数值,用OCR比视觉模型更可靠,因为视觉模型在生成表格时偶尔会凭空补一行数据。不过OCR也有局限,它只能输出文字序列,不理解文字之间的视觉关系。股价曲线里"上升"这个信息,OCR是抓不到的。视觉模型(VLM)的价值在于"懂关系"。它能把图中的箭头方向、层级结构、空间布局、数据趋势描述出来。但VLM最大的风险是幻觉——它可能在描述里编造图里根本不存在的节点或数字。所以我的建议是:对于信息密集的复杂图,先跑OCR拿到文字标签,再把OCR结果和原图一起喂给视觉模型,让它结合文字和视觉布局做结构化描述。这样既能拿到准确文字,又能保住结构关系,幻觉概率也会低很多。2.5 文本化表达:结构化描述与上下文拼接视觉模型输出的原始描述,不能直接丢进索引。我会要求模型按模板输出结构化描述,比如包含图片类型、主要元素、元素关系、图内文字、关键数据结论。这么做不是为了炫技,而是为了让后面的检索阶段能更精准地命中。实际操作中,图片文本块我建议拼成这样一个格式:【图片:p3_x12 页码:3 区域:左上角】 图片类型: 流程图 内容说明: 这是一个四步数据管道... 图内文字: 采集 -> 清洗 -> 训练 -> 发布 关键关系: 采集与清洗呈顺序依赖关系这样拼接之后,检索时不管是问"数据管道分几步",还是直接问"清洗在哪个环节后",都能从不同措辞命中同一图片块。提示词也要多花心思,我会在生成描述时明确要求:"请用简洁客观的语言描述这张图片。包括:图片类型;主要元素;元素之间的关联;图中出现的所有文字;如果包含数据图表,请给出最高值、最低值、异常点等关键信息。不要编造图中不存在的内容。"上下文拼接也很关键。图片不是孤立存在的,它通常紧跟着一段正文说明。我会用图片的坐标矩形,从页面文本里找出距离它最近的段落,把这段文字作为"邻近上下文"加进图片块。这样图文信息就形成了互补,检索时既能靠图内文字命中,也能靠正文描述上下文命中。2.6 切块向量化与混合检索图片块的文本特征和普通文本不太一样:它信息密度高、关键词集中、结构性强。所以切块参数我一般会放宽一点,单块允许到500到1000字,重叠部分保持50到100字。太长会稀释向量语义,太短又会把关系描述切断。向量化阶段,普通中文文本嵌入模型就够用了,关键是把"图片类型"和"页码"这些元数据拼进文本里。为什么?因为用户提问时经常会说"图2"、"架构图"、"流程图"这类词,如果你只在文本里存描述,检索不到这些标签;把元数据拼进去,相当于给图片块加了一层可检索的定位锚点。不过,单靠向量检索在图片场景有个短板。图片描述经常由短词和专有名词组成,比如"Kafka"、"Nginx"、"模型训练",向量相似度对这类精确词不够友好,有时候会被同义改写带偏。所以我更推荐向量检索加全文关键词检索的混合方案。向量负责语义相似,关键词负责精确匹配,两边召回后合并排序,效果会比单路检索稳定很多。2.7 生成增强与溯源引用检索到图片块之后,生成环节的增强方式是"原文回填"。图片块本身就是文本,把它直接拼进提示词,模型就能基于图片的语义内容作答。更关键的是,图片块里携带了页码、图片ID、坐标信息,生成阶段可以要求模型"在回答末尾标注信息来源,比如'根据第3页图2的描述'"。溯源看起来是个小功能,但对知识库系统来说价值极大。用户能核实答案的真伪,管理员能检查哪张图的转写质量有问题,后续优化也能精准定位到具体图片,而不是大海捞针。这也是我把图片处理称为"工程"而不是"调模型"的原因——每一环都要可回查、可追溯。3. 实操代码:从PDF到可问答的图片知识3.1 环境准备与依赖安装实操环节,我用Python生态里最常用的几个库来演示。先安装依赖:pip install pymupdf paddleocr paddlepaddlePyMuPDF负责PDF解析和图片提取,PaddleOCR负责图片文字识别。视觉模型部分,你可以选择任何支持图片加文字输入的开源模型,部署成本一般都在可控范围。如果本机资源有限,也可以重点先用OCR把文字类图片处理掉,视觉模型覆盖最关键的几张复杂图,效果提升依然显著。向量检索我用FAISS做示例,它是目前上手最快的相似度索引库。切块工具可以用通用的文本切分器,也可以自己写一个简单的按分隔符切割函数。3.2 图片提取与去重图片提取的核心逻辑是:遍历所有页面,拿到每个图片对象,保存图片文件,同时记录坐标和引用信息。代码里我会额外加一个MD5哈希缓存,自动过滤重复图片。import fitz import hashlib import os def extract_pdf_images(pdf_path, out_dir): os.makedirs(out_dir, exist_ok=True) doc = fitz.open(pdf_path) file_cache = {} records = [] for page_no in range(len(doc)): page = doc[page_no] for img in page.get_images(full=True): xref = img[0] pix = fitz.Pixmap(doc, xref) # PDF中部分图片是CMYK或多通道格式,统一转成RGB if pix.n - pix.alpha >= 4: pix = fitz.Pixmap(fitz.csRGB, pix) raw = pix.tobytes("png") digest = hashlib.md5(raw).hexdigest() # 同一张图只保存一次,但每次出现都记录 if digest not in file_cache: fname = os.path.join(out_dir, f"img_{digest[:8]}.png") pix.save(fname) file_cache[digest] = fname for rect in page.get_image_rects(xref): records.append({ "image_id": f"p{page_no+1}_x{xref}", "file": file_cache[digest], "page": page_no + 1, "bbox": [rect.x0, rect.y0, rect.x1, rect.y1], }) doc.close() return records注意一个细节:page.get_images(full=True)返回的元组里第一个元素就是xref,但同一张图可能在页面多处渲染,所以要用get_image_rects拿矩形列表逐一遍历。这样每一条渲染记录都能保留独立的页码和坐标。如果遇到矢量图,这段代码的get_images是找不到对象的。解决办法是直接用坐标区域渲染页面:page = doc[page_no - 1] rect = fitz.Rect(bbox) pix = page.get_pixmap(clip=rect, dpi=200) pix.save("region_render.png")dpi=200是我常用的渲染参数,太低看不清细节,太高文件太大,200是比较均衡的档位。3.3 OCR文字提取实现OCR阶段,我推荐直接用带方向分类的中文OCR库,对扫描歪斜的图片有自动矫正能力。from paddleocr import PaddleOCR import json ocr = PaddleOCR(use_angle_cls=True, lang="ch") def ocr_image_to_lines(image_path): result = ocr.ocr(image_path, cls=True) lines = [] if not result: return lines for page_res in result: if not page_res: continue for box in page_res: coords = box[0] text_and_score = box[1] text = text_and_score[0] score = text_and_score[1] if score and score >= 0.6: lines.append({ "text": text, "bbox": coords, "score": float(score) }) return lines置信度阈值0.6是个经验值。阈值太高会漏掉模糊文字,太低会涌入大量错误字符。不同文档扫描质量差异很大,我一般先用一小批图测试,再决定调到0.5还是0.65。OCR输出的结果不要直接拼成一长串,最好保留每一行文字和对应坐标。后面和视觉模型描述合并时,这些坐标能帮你判断文字在图中的位置关系,也可以用来校验视觉模型是否漏读了标签。3.4 视觉模型描述生成视觉模型部分,我假设你已经有一个本地部署好的模型服务,代码里用HTTP接口调用的方式演示。读图转base64,再连同提示词一起提交:import base64 import requests VLM_ENDPOINT = "http://127.0.0.1:8000/vlm" # 换成你的本地服务入口 def vlm_describe(image_path, prompt): with open(image_path, "rb") as f: image_b64 = base64.b64encode(f.read()).decode("utf-8") payload = { "image_base64": image_b64, "prompt": prompt, } resp = requests.post(VLM_ENDPOINT, json=payload, timeout=180) resp.raise_for_status() return resp.json()["text"]这里有两个忠告。第一,提示词一定要写清楚"不要编造图中不存在的内容"。第二,对包含大量文字的复杂图,把OCR结果作为额外提示文本一起传入,可以有效减少模型幻觉。比如这样组织提示词:prompt = """ 请结合OCR识别文本,描述这张图片的结构。 要求: 1. 图片类型,例如流程图、柱状图、架构图 2. 说明主要元素及其位置关系 3. 图中如果存在箭头,说明从哪个元素指向哪个元素 4. 不要编造OCR文本中没有的内容 OCR识别文本: {ocr_text} """.format(ocr_text=ocr_text)3.5 图片上下文拼接与索引构建拿到OCR文本和视觉模型描述后,下一步是把它们拼成标准文本块。我用一个函数来组装:def build_image_text_block(record, vlm_desc, ocr_text, nearby_text): return ( f"【图片:{record['image_id']} 页码:{record['page']} 区域:{record['bbox']}】\n" f"图片说明:{vlm_desc}\n" f"图内文字:{ocr_text}\n" f"上下文:{nearby_text}" )nearby_text怎么取?我在第2章提过,用图片坐标从页面文本里找到最近的段落。简单实现是直接采集页面中所有文本块,取垂直中心点与图片中心点最近的文本块:def get_nearby_text(page, bbox): blocks = page.get_text("blocks") img_cy = (bbox[1] + bbox[3]) / 2 best_block = None best_dist = float("inf") for b in blocks: x0, y0, x1, y1, text, *_ = b block_cy = (y0 + y1) / 2 dist = abs(block_cy - img_cy) if dist < best_dist: best_dist = dist best_block = text.strip() return best_block[:300]拼接完成后,我把所有文本块重新切分,再做向量化:import numpy as np def get_embedding(text): # 这里接入你选用的文本向量模型,返回一个列表 return [] splitter = RecursiveCharacterTextSplitter( chunk_size=800, chunk_overlap=100, separators=["\n\n", "\n", "。", ";", ",", " ", ""] ) final_chunks = [] for block in all_image_blocks: final_chunks.extend(splitter.split_text(block)) embedding_matrix = np.array([get_embedding(c) for c in final_chunks]) embedding_matrix = embedding_matrix.astype("float32") # 归一化,方便直接使用内积计算余弦相似度 embedding_matrix /= np.linalg.norm(embedding_matrix, axis=1, keepdims=True)最后用FAISS建索引:import faiss dim = embedding_matrix.shape[1] index = faiss.IndexFlatIP(dim) index.add(embedding_matrix)3.6 检索问答链路实现查询阶段的核心逻辑是:把用户问题向量化,在向量库中召回TopK,再拼接到提示词里交给生成模型。def search(question, top_k=5): q = np.array([get_embedding(question)], dtype="float32") q /= np.linalg.norm(q) scores, ids = index.search(q, top_k) return ids[0], scores[0] def answer(question, llm_generate): ids, scores = search(question) context = "\n\n".join(final_chunks[i] for i in ids) prompt = f""" 请基于以下资料回答问题。回答中如果引用了图片内容,请注明图片编号和页码。 资料内容: ===== {context} ===== 问题:{question} """ return llm_generate(prompt)这里有个实用细节:如果启用了关键词召回,可以把向量召回的TopK和关键词召回的TopK做合并,再用RRF(倒数排名融合)排序。RRF实现非常简单,就是对同一文档在多个结果列表中的名次取倒数求和,排名越靠前权重越大。混合召回能显著提升图片块的命中率,尤其是用户问题里带专有名词的时候。4. 高频问题与避坑技巧实录4.1 表格图片被OCR拆碎怎么办表格类图片是OCR翻车重灾区。OCR按读序输出,多行多列表格一旦有合并单元格,输出就会彻底乱掉,表头和数值对应关系全丢。我一开始遇到这种问题,傻乎乎地在输出结果后补正则去抓"看起来像数字的内容",结果越补越乱。后来改成双轨方案:表格图片先跑OCR拿到每个单元格的原始文字,然后让视觉模型结合OCR结果把整张表转成Markdown格式。提示词可以这么写:"请把这张图片中的表格完整转换为Markdown表格,保留所有行和列,不要遗漏单元格,不要改动数据。"对于纯OCR输出,我的经验是不要强求它复原表格结构,只把它当作"文字食材"。结构整理交给视觉模型更合适,因为它看得见行列位置关系,能从视觉上判断某一列属于哪个表头。4.2 重复图片和页眉页脚污染索引PDF里最常见的重复图片就是页眉Logo、页脚图标、背景水印。如果不做去重,几千页PDF能索引出几百份一模一样的Logo图片块。用户问"这个Logo是什么"时还好,怕的是这些重复块在检索时频繁命中,把真正有用的图片块挤出了TopK。处理办法在代码里已经体现了:按图片字节内容的MD5哈希去重,同一内容只保存一份。但这还不够,页眉Logo在每页都会出现,虽然文件只有一份,但记录里会有几百条"出现位置"。我建议在进入索引前加一道过滤:如果同一图片ID在文档中出现次数超过阈值,比如10次,大概率是装饰性元素,直接在索引构建阶段剔除。水印文字也要留意。有些PDF加了斜向水印文字,OCR会把水印混进正文。处理办法是让OCR忽略整页统一背景,或者在水印文字出现频次过高时降低对应文本的权重。4.3 视觉模型描述出现幻觉数据视觉模型幻觉是图片转写里最隐蔽的问题。它不像OCR那样有明显的识别错误,而是会一本正经地编造出图里不存在的节点、数字或流程分支。比如一张三步流程图,模型可能描述出第四步"数据验证",因为它在别处见过相似模板。降低幻觉的办法,我总结为三条:提示词硬约束:明确写"只描述图片中可见的内容,不确定的信息用【不确定】标注";交叉验证:图片里出现数字时,让OCR先抽取文字,再让视觉模型只描述关系,不改数字;人工抽检:随机抽取10%的图片块,由人来核对描述和原图是否一致。别小看这个笨办法,它能让你快速发现哪些提示词设计有问题。如果视觉模型输出的描述里混进了OCR文本里没有的实体词,我基本会视为幻觉,删除该实体再向比,或者要求模型重新生成一次。4.4 图片块太长挤占上下文窗口图片描述动辄几百字,如果一页里有多张图,生成的文本块拼接起来很容易超过生成模型的上下文限制。检索时TopK如果是5,可能一半都是图片块,正文内容反而被挤掉了。我的处理思路是分级压缩:如果同一页出现多个图片块,先做一个"图片清单页"式的聚合块,每张图只用一两句话概括,然后完整链路里只把检索命中的那一两张图的详细描述放进去。另外,TopK在图片场景不要设太大,我一般设为3到5,召回内容够用且不容易爆上下文。如果实在要保存长描述,也可以把描述分成"摘要"和"细节"两个字段。检索时用摘要做匹配,生成时把对应的细节块一并带入。这样既能保住关联性,又不会每个命中块都带着冗长细节。4.5 扫描版PDF别当普通PDF处理扫描版PDF里每一页本质上就是一张大图,如果用"提取页面内嵌图片"的逻辑去处理,你会发现整页图片能提取出来,但拆成十几个小碎片,OCR结果惨不忍睹。处理方法完全不同:遇到扫描版PDF,应该走"整页OCR"路线。先把整页渲染成清晰位图,然后对整页做OCR,输出带坐标的完整文本。得到文本后再走正常切块、向量化流程。判断标准很直接:如果页面上最大的图片矩形覆盖面积超过整页面积的80%,就当作扫描页处理。page_pix = page.get_pixmap(dpi=200) # 对整页做OCR扫描版PDF一定要在预处理阶段就识别出来,否则后面所有针对"内嵌图片"的处理都会变成无用功。4.6 图片召回率上不去的补救方法如果图片块建好了索引,但检索时总是召不回,问题多半出在"措辞不一致"上。图片描述里写"圆柱形容器",用户问的是"存储引擎",两者语义相关但不完全一致,向量模型可能算不出高相似度。补救方法有三个:在图片文本块里加类型前缀。比如"【图表:柱状图|收入趋势】",这种明确的标签词检索时命中率极高;给描述补同义词。把"模型训练"补成"模型训练(Model Training)",把"存储引擎"补成"存储引擎(数据库/仓库)",不要过度膨胀,但关键术语的同义词扩展很有用;混合检索兜底。向量召回和关键词召回合并后,图片块的精确词能以关键词方式补进来,即使向量相似度不够高也有一席之地。4.7 常见问题速查表现象可能原因解决方向表格图片OCR乱序表格结构复杂,OCR读序不保留行列关系视觉模型转Markdown同一图片反复命中页眉水印未去重按哈希去重,过滤高频装饰图描述里出现不存在的数字视觉模型幻觉提示词硬约束,OCR交叉校验上下文长度超限图片块过多过长压缩为摘要块,缩小TopK扫描PDF图片支离破碎把扫描页当内嵌图处理整页OCR方案图片召不回描述和提问措辞不一致加类型前缀、同义词、混合检索5. 实战心得与个人建议整套流程落地之后,我最大的体会是:处理PDF图片的难点不在于模型选型,而在于流程里那些一不留神就漏掉的元数据工程。图片ID、页码、坐标、出现频次、文字置信度、描述来源,这些字段如果记录得不完整,后续排查问题时就会被卡在"这条内容到底是OCR还是视觉模型生成的?"这种问题上,进退两难。我现在每次跑图片管道,都会把中间产物完整存下来:提取出的图片文件、OCR的JSON输出、视觉模型的原始描述、最终拼接形成的文本块。这些中间产物既是检查质量的抓手,也是调试问题时的关键线索。你别嫌多这一步麻烦,等你在线上知识库里发现某个答案明显错误时,能快速定位到具体环节,这个价值远大于存储那点开销。另一个心得是要给图片块设立质量阈值。OCR置信度过低、视觉模型描述过短、图片尺寸过小,这些情况都直接标记为低质量块,不进入正式索引,最多放在调试集里。宁缺毋滥,图片块的质量污染对RAG问答的伤害,比缺失还严重,因为它会让系统给出信心满满的错误答案。最后分享一个我一直在用的回归测试法。每次改完图片处理链路,我没空把整个知识库重新问一遍,就固定准备三到五个"必问题",专门覆盖图片场景,比如"图3里的流程分几步""这张架构图里哪两个模块之间有调用关系""图表里哪个数据点最低"。跑完这几个问题,再对比一下答案和引用是否准确,基本就能判断这次改动是变好了还是改坏了。这个习惯帮我防住了好几次"看似升级、实则退化"的改版。