课程0基础Agent开发课 / RAG与向量数据库 / 多模态RAG-处理图片表格和混合文档
— 21 min read

多模态RAG-处理图片表格和混合文档

> **[进阶选读]** 本篇适合需要处理含有大量图表、产品图片或技术图纸的企业文档的读者。纯文本 RAG 无法处理图片里的信息,多模态 RAG 用视觉模型弥补这个盲区。

多模态 RAG:处理图片、表格和混合文档

[进阶选读] 本篇适合需要处理含有大量图表、产品图片或技术图纸的企业文档的读者。纯文本 RAG 无法处理图片里的信息,多模态 RAG 用视觉模型弥补这个盲区。


1.1 文字之外的信息盲区

多模态RAG架构图
多模态 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 等模型转成向量 → 存入向量数据库(同时存储图片路径)→ 检索时图文统一处理 → 返回相关图片路径给前端展示。

以下为代码示例,非程序员可跳过代码,重点看文字说明。

python
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 生成图片的文字描述 → 将描述文本存入向量数据库(附带原始图片路径)→ 检索时按文本匹配 → 返回描述文本和图片路径。

以下为代码示例,非程序员可跳过代码,重点看文字说明。

python
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 生成答案时也能理解表格的行列关系。

code
| 产品名称 | Q1销售额 | Q2销售额 | 同比增长 |
|----------|----------|----------|----------|
| 产品A    | 120万    | 156万    | +30%     |
| 产品B    | 80万     | 72万     | -10%     |

策略二:转为自然语言描述(更易检索)

用 LLM 把表格内容改写成句子,更接近用户提问的表述方式,提升语义检索的相关性。

code
公司产品销售对比:产品A第一季度销售额120万元,第二季度增长至156万元,
同比增长30%;产品B第一季度80万元,第二季度下降至72万元,同比下滑10%。

策略三:混合存储(推荐)

同时存储 Markdown 格式和自然语言描述,索引时生成两条记录。Markdown 版本用于精确数据提取,自然语言版本用于语义检索。两者都附带相同的元数据(源文件、页码),确保溯源。

以下为代码示例,非程序员可跳过代码,重点看文字说明。

python
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 完整处理流程

简单图片
产品图

复杂图表
财务/技术图

上传企业文档
PDF/PPT/Word

文档解析
提取文字/图片/表格

文字段落

图片/图表

表格

直接 Chunking
文本分块

图片类型判断

多模态 Embedding
CLIP 向量化

Vision LLM
生成文字描述

转 Markdown 格式

LLM 生成自然语言描述

向量数据库
存储文本向量

用户提问

问题向量化

检索向量数据库
返回Top-K

结果包含图片引用?

加载原始图片
传给 Vision LLM

直接拼装 Prompt

多模态 LLM 生成答案

文本 LLM 生成答案

返回答案
+ 图片附件

返回文字答案


1.7 实战:处理包含图表的 PDF 年报

以下展示一个接近生产的完整示例,处理包含财务图表的 PDF 文件。

以下为代码示例,非程序员可跳过代码,重点看文字说明。

python
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 识别。

本页目录