多模态RAG-处理图片表格和混合文档
> **[进阶选读]** 本篇适合需要处理含有大量图表、产品图片或技术图纸的企业文档的读者。纯文本 RAG 无法处理图片里的信息,多模态 RAG 用视觉模型弥补这个盲区。
多模态 RAG:处理图片、表格和混合文档
[进阶选读] 本篇适合需要处理含有大量图表、产品图片或技术图纸的企业文档的读者。纯文本 RAG 无法处理图片里的信息,多模态 RAG 用视觉模型弥补这个盲区。
1.1 文字之外的信息盲区
多模态 RAG 架构——文本/图片/表格各自编码后映射到统一向量空间,支持跨模态检索
一家上市公司的年度财务报告,通常长达 200 页。其中 30% 的关键信息以图表形式呈现:收入增长折线图、各业务线占比饼图、成本结构瀑布图。HR 的岗位说明文档里有组织架构图。产品手册里有系统架构图和界面截图。
如果用标准 RAG 处理这些文档,只能提取出文字部分,图表中的信息完全丢失。当用户问"Q3 收入同比增长了多少?"时,RAG 系统无法从年报的折线图里读取数字——因为那是一张图片,不是文字。
这就是多模态 RAG 要解决的问题:让知识库能"看懂"图片、表格和混合文档中的信息。
多模态(Multimodal,多种模态,指同时处理文字、图片、音频、视频等不同类型信息的能力)RAG 特指能检索和利用文字之外信息的 RAG 系统。
1.2 企业文档中的三类"视觉信息"
类型一:图表(Charts & Diagrams)
- 财务图表:折线图、柱状图、饼图,包含数值趋势和比例关系
- 架构图:系统组件图、网络拓扑图,包含技术结构信息
- 流程图:业务流程、审批链路,包含逻辑关系
类型二:表格(Tables)
- 数据表格:价格表、规格对比表、财务明细表
- 特点:结构化信息,行列关系重要,纯文本提取后结构往往被破坏
类型三:嵌入图片(Embedded Images)
- 产品图、界面截图、证件照片
- 内容信息分散,需要描述性理解
1.3 两种主流解决方案
目前业界处理多模态 RAG 主要有两种技术路线,各有取舍。
1.3.1 方案一:多模态 Embedding(图文统一向量化)
核心思想: 用一个特殊的多模态 Embedding 模型,把图片和文字都映射到同一个向量空间。文字"季度收入增长图"和包含收入增长信息的折线图,在这个向量空间里距离很近,因此能互相检索到。
代表模型: OpenAI 的 CLIP(Contrastive Language-Image Pre-training,对比语言-图像预训练模型,通过大量图文配对数据训练,使描述同一事物的图片和文字在向量空间里紧密靠近)。
实现流程: 文档中的图片提取出来 → 用 CLIP 等模型转成向量 → 存入向量数据库(同时存储图片路径)→ 检索时图文统一处理 → 返回相关图片路径给前端展示。
以下为代码示例,非程序员可跳过代码,重点看文字说明。
from PIL import Image
import torch
from transformers import CLIPProcessor, CLIPModel
import numpy as np
# 加载 CLIP 模型
# CLIP 能把图片和文字都转成 512 维向量
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
def embed_image(image_path: str) -> np.ndarray:
"""将图片转成向量"""
image = Image.open(image_path)
inputs = processor(images=image, return_tensors="pt")
with torch.no_grad():
image_features = model.get_image_features(**inputs)
# 归一化,方便后续余弦相似度计算
return image_features.numpy()[0] / np.linalg.norm(image_features.numpy()[0])
def embed_text(text: str) -> np.ndarray:
"""将文字转成向量(与图片向量在同一空间)"""
inputs = processor(text=[text], return_tensors="pt", padding=True)
with torch.no_grad():
text_features = model.get_text_features(**inputs)
return text_features.numpy()[0] / np.linalg.norm(text_features.numpy()[0])
# 建立图片索引(离线阶段)
image_index = {} # 实际应存入向量数据库
for image_path in ["report_chart1.png", "report_chart2.png", "arch_diagram.png"]:
image_index[image_path] = embed_image(image_path)
# 用文字查询图片(在线阶段)
query = "季度收入增长趋势"
query_vector = embed_text(query)
# 计算余弦相似度,找最相关的图片
similarities = {
path: float(np.dot(query_vector, vec))
for path, vec in image_index.items()
}
best_match = max(similarities, key=similarities.get)
print(f"最相关图片:{best_match},相似度:{similarities[best_match]:.3f}")
方案一的适用场景: 图片本身是主要内容(如电商产品图、设计稿),需要跨模态检索(用文字找图片,或用图片找相似图片)。
1.3.2 方案二:图片转文字(先理解再检索)
核心思想: 不直接处理图片,而是用 Vision LLM(具备视觉理解能力的大语言模型,能接收图片输入并用文字描述图片内容,如 GPT-4o、Claude Opus 4 系列)先把图片"翻译"成文字描述,再按普通文本 RAG 的方式处理这些描述。
Vision LLM 可以理解为"会看图说话的 AI"——输入一张图片,输出对图片内容的文字描述。
实现流程: 文档中提取图片 → Vision LLM 生成图片的文字描述 → 将描述文本存入向量数据库(附带原始图片路径)→ 检索时按文本匹配 → 返回描述文本和图片路径。
以下为代码示例,非程序员可跳过代码,重点看文字说明。
import base64
from pathlib import Path
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_core.messages import HumanMessage
vision_llm = ChatOpenAI(model="gpt-4o", max_tokens=1000)
embeddings = OpenAIEmbeddings()
def image_to_base64(image_path: str) -> str:
"""将图片转成 base64 编码(base64 是一种将二进制数据转成纯文本字符串的编码方式,方便通过 HTTP 传输图片)"""
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
def describe_image(image_path: str, context: str = "") -> str:
"""
用 Vision LLM 描述图片内容
context:图片所在的文档背景(如"这是2024年Q3财务报告第12页的图表")
"""
img_b64 = image_to_base64(image_path)
prompt = f"""请详细描述这张图片的内容,重点提取其中的数据和关键信息。
要求:
1. 如果是图表,提取具体数值、趋势、最大/最小值
2. 如果是架构图,描述各组件及其关系
3. 如果是流程图,描述各步骤及其顺序
4. 用中文描述,确保描述包含图中所有重要数字和名称
背景信息:{context}"""
response = vision_llm.invoke([
HumanMessage(content=[
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}"}}
])
])
return response.content
def index_document_with_images(pdf_path: str, vectorstore: Chroma):
"""
处理包含图片的 PDF 文档
实际项目中需要先用 PDF 解析库提取图片,这里用伪代码说明流程
"""
# 步骤1:解析 PDF,提取文字段落和图片
# (实际使用 pdfplumber、pymupdf 等库,详见第13章)
text_chunks = extract_text_chunks(pdf_path) # 提取文字块
images = extract_images(pdf_path) # 提取图片及其页码
documents = []
# 步骤2:文字块直接转为文档
for chunk in text_chunks:
documents.append(Document(
page_content=chunk["text"],
metadata={
"source": pdf_path,
"page": chunk["page"],
"type": "text"
}
))
# 步骤3:图片先用 Vision LLM 生成描述,再转为文档
for img_info in images:
context = f"来源:{pdf_path},第{img_info['page']}页"
description = describe_image(img_info["path"], context)
documents.append(Document(
page_content=description,
metadata={
"source": pdf_path,
"page": img_info["page"],
"type": "image",
"image_path": img_info["path"], # 保存原始图片路径,供前端展示
"image_description": description
}
))
# 步骤4:统一存入向量数据库
vectorstore.add_documents(documents)
print(f"已索引 {len(text_chunks)} 个文字块,{len(images)} 张图片")
方案二的适用场景: 图片内容需要深度理解(如财务图表、技术架构图),已有成熟的文本 RAG 基础设施,不想引入新的多模态 Embedding 模型。
1.4 两种方案对比
| 维度 | 方案一:多模态 Embedding | 方案二:图片转文字 |
|---|---|---|
| 实现原理 | 图文映射到同一向量空间 | Vision LLM 理解后转文本 |
| 检索质量 | 中等(依赖 CLIP 理解深度) | 较高(LLM 深度理解) |
| 图表数值提取 | 弱(CLIP 不善于读数字) | 强(LLM 能准确提取数值) |
| 计算成本 | 低(Embedding 便宜) | 高(Vision LLM 很贵) |
| 建库速度 | 快(批量 Embedding) | 慢(每张图需 LLM 调用) |
| 跨模态检索 | 支持(图搜图、文搜图) | 不支持(只有文搜文) |
| 前端展示 | 需返回图片路径 | 需返回图片路径 + 描述 |
| 基础设施要求 | 需要多模态 Embedding 模型 | 只需 Vision LLM API |
| 推荐场景 | 产品图、设计图为主 | 财务报告、技术文档为主 |
对于大多数企业场景(处理财报、产品手册、技术文档),方案二更实用,因为这类场景对信息提取准确率要求高,而方案一在数字提取上的弱点往往是致命的。
1.5 表格的特殊处理策略
表格是企业文档中最常见、也最棘手的元素。表格数据的价值在于行列的对应关系,但 PDF 提取文字时经常把表格破坏成一行行杂乱的文字。
1.5.1 三种表格处理策略
策略一:转为 Markdown 格式(保留结构)
将表格转换为 Markdown 的 | 分隔格式,保留行列结构。向量化时结构清晰,LLM 生成答案时也能理解表格的行列关系。
| 产品名称 | Q1销售额 | Q2销售额 | 同比增长 |
|----------|----------|----------|----------|
| 产品A | 120万 | 156万 | +30% |
| 产品B | 80万 | 72万 | -10% |
策略二:转为自然语言描述(更易检索)
用 LLM 把表格内容改写成句子,更接近用户提问的表述方式,提升语义检索的相关性。
公司产品销售对比:产品A第一季度销售额120万元,第二季度增长至156万元,
同比增长30%;产品B第一季度80万元,第二季度下降至72万元,同比下滑10%。
策略三:混合存储(推荐)
同时存储 Markdown 格式和自然语言描述,索引时生成两条记录。Markdown 版本用于精确数据提取,自然语言版本用于语义检索。两者都附带相同的元数据(源文件、页码),确保溯源。
以下为代码示例,非程序员可跳过代码,重点看文字说明。
def process_table(table_data: list, source_info: dict, vectorstore: Chroma):
"""
处理单个表格,采用混合存储策略
table_data: 二维列表,如 [["产品", "Q1", "Q2"], ["产品A", "120", "156"]]
"""
# 方式1:转为 Markdown
headers = table_data[0]
rows = table_data[1:]
markdown_lines = ["| " + " | ".join(headers) + " |"]
markdown_lines.append("|" + "---|" * len(headers))
for row in rows:
markdown_lines.append("| " + " | ".join(row) + " |")
markdown_text = "\n".join(markdown_lines)
# 方式2:用 LLM 生成自然语言描述
llm = ChatOpenAI(model="gpt-4o-mini") # 用小模型降低成本
response = llm.invoke(
f"将以下表格转成流畅的自然语言描述,保留所有数据:\n{markdown_text}"
)
nl_description = response.content
base_metadata = {
"source": source_info["file"],
"page": source_info["page"],
"type": "table"
}
# 同时存入两种格式
vectorstore.add_documents([
Document(page_content=markdown_text,
metadata={**base_metadata, "format": "markdown"}),
Document(page_content=nl_description,
metadata={**base_metadata, "format": "natural_language"})
])
1.6 多模态 RAG 完整处理流程
1.7 实战:处理包含图表的 PDF 年报
以下展示一个接近生产的完整示例,处理包含财务图表的 PDF 文件。
以下为代码示例,非程序员可跳过代码,重点看文字说明。
import fitz # PyMuPDF,用于 PDF 解析
import io
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.documents import Document
def process_financial_report(pdf_path: str) -> Chroma:
"""
完整处理一份财务年报
返回已建好索引的向量数据库
"""
pdf_doc = fitz.open(pdf_path)
vectorstore = Chroma(
collection_name="annual_report",
embedding_function=OpenAIEmbeddings()
)
vision_llm = ChatOpenAI(model="gpt-4o")
text_llm = ChatOpenAI(model="gpt-4o-mini")
all_documents = []
for page_num in range(len(pdf_doc)):
page = pdf_doc[page_num]
# ---- 处理文字 ----
text = page.get_text()
if text.strip():
# 简单按段落分块(实际项目参考第02章的分块策略)
paragraphs = [p.strip() for p in text.split("\n\n") if len(p.strip()) > 50]
for para in paragraphs:
all_documents.append(Document(
page_content=para,
metadata={"source": pdf_path, "page": page_num + 1, "type": "text"}
))
# ---- 处理图片 ----
image_list = page.get_images(full=True)
for img_index, img in enumerate(image_list):
xref = img[0]
base_image = pdf_doc.extract_image(xref)
img_bytes = base_image["image"]
# 过滤太小的图片(通常是 icon 或装饰图,不含信息)
if len(img_bytes) < 10000: # 小于 10KB 的图片跳过
continue
# 将图片字节转为 base64
img_b64 = base64.b64encode(img_bytes).decode("utf-8")
img_ext = base_image["ext"]
# 调用 Vision LLM 描述图表
response = vision_llm.invoke([
HumanMessage(content=[
{"type": "text", "text": f"""这是{pdf_path}第{page_num+1}页的图表。
请详细描述:1)图表类型 2)横纵轴含义 3)主要数据点和数值 4)趋势和结论。
用中文,确保所有数字准确。"""},
{"type": "image_url",
"image_url": {"url": f"data:image/{img_ext};base64,{img_b64}"}}
])
])
# 保存图片到本地(用于前端展示)
img_save_path = f"./extracted_images/page{page_num+1}_img{img_index}.{img_ext}"
with open(img_save_path, "wb") as f:
f.write(img_bytes)
all_documents.append(Document(
page_content=response.content,
metadata={
"source": pdf_path,
"page": page_num + 1,
"type": "image",
"image_path": img_save_path # 前端可用此路径展示原图
}
))
# 批量存入向量数据库
vectorstore.add_documents(all_documents)
print(f"年报处理完成:共 {len(all_documents)} 条记录")
return vectorstore
def query_with_image_support(question: str, vectorstore: Chroma) -> dict:
"""
支持图片引用的查询函数
返回文字答案,以及相关图片的路径列表(供前端展示)
"""
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
docs = retriever.invoke(question)
# 分离文字内容和图片引用
text_contents = []
image_paths = []
for doc in docs:
text_contents.append(doc.page_content)
if doc.metadata.get("type") == "image" and doc.metadata.get("image_path"):
image_paths.append(doc.metadata["image_path"])
# 生成答案
context = "\n---\n".join(text_contents)
llm = ChatOpenAI(model="gpt-4o")
response = llm.invoke([
SystemMessage(content="你是财务分析助手,根据年报内容回答问题,数据要精确。"),
HumanMessage(content=f"参考资料:\n{context}\n\n问题:{question}")
])
return {
"answer": response.content,
"referenced_images": image_paths # 前端据此展示原始图表
}
# 使用示例
vectorstore = process_financial_report("annual_report_2024.pdf")
result = query_with_image_support("2024年各季度营收趋势如何?", vectorstore)
print(result["answer"])
print(f"相关图表:{result['referenced_images']}")
代码关键设计:
图片大小过滤(len(img_bytes) < 10000)能排除大量无意义的小图标,避免浪费 Vision LLM 的 token。
保存 image_path 到元数据,让前端能在显示文字答案的同时展示原始图表,给用户直观的视觉参考。
1.8 局限与注意事项
Token 成本问题: Vision LLM 处理图片的 token 消耗远高于文字。GPT-4o 处理一张普通分辨率图片约消耗 800-1500 个 token,一份 100 页包含 50 张图表的年报,仅图片描述就需要约 50,000-75,000 个 token,成本可能在 0.3-0.5 美元之间。建议对图片进行压缩和尺寸限制,在质量和成本间取得平衡。
图片理解准确率: Vision LLM 在理解清晰的统计图表时准确率较高,但对于低分辨率图片、复杂技术图、手写内容等,识别准确率会显著下降。重要数据需要设置人工审核环节。
存储成本: 多模态 RAG 需要同时保存原始图片文件和文字描述,存储开销是纯文本 RAG 的 3-5 倍。建议将图片存储在对象存储(如 OSS——阿里云对象存储服务、S3——亚马逊云的对象存储服务,两者都是专门存储大量文件的云存储服务)而非本地磁盘,降低成本并提升可扩展性。
处理速度: 因为需要逐张调用 Vision LLM,建库速度远低于纯文本。一份 100 页的 PDF 建库时间可能从 30 秒增加到 10 分钟。建议采用异步批量处理,离线完成后再提供检索服务。
两种方案的选择很简单:企业场景处理财报、技术文档这类要求精确提取数字的内容,用图片转文字方案;产品图、设计稿这种需要跨模态检索的,用多模态 Embedding。
成本是真实的压力:Vision LLM 处理图片贵,批量建库时要做好图片过滤和尺寸压缩,别把小图标都送去 GPT-4o 识别。