RAG 系列(二十三):多模态 RAG——图片、表格也能检索

文本 RAG 看不见的东西

上传一份年报 PDF,里面有营收走势图、产品对比表格、架构示意图。传统 RAG 怎么处理?

  1. 用 PDF 解析器提取文本
  2. 对文本分块、Embedding、存入向量库
  3. 用户问"第三季度营收环比增长多少"

问题是:营收走势图是一张图片,PDF 解析器只会把它的 alt text(通常是空的)或者图片文件名提取出来。数字在图里,不在文本里,RAG 永远找不到。

表格情况稍好,但也有问题:解析器可能把表格拉平成一行行文字,原来的行列结构丢失,语义变得混乱。

这是真实的业务痛点。文档里 30%--50% 的信息通常以非纯文本形式存在。


三条处理路线

路线一:提取 + 文本化

最直接、最成熟的方案:把图片和表格转换成文字描述,再走标准的文本 RAG 流程。

图片处理:用视觉语言模型(VLM)生成描述

python 复制代码
from openai import OpenAI
import base64

def describe_image(image_path: str) -> str:
    with open(image_path, "rb") as f:
        image_data = base64.b64encode(f.read()).decode("utf-8")
    
    client = OpenAI()
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{
            "role": "user",
            "content": [
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_data}"}},
                {"type": "text", "text": "详细描述这张图片的内容,包括所有数字、标签、趋势和关键信息。如果是图表,列出所有数据点。"}
            ]
        }]
    )
    return response.choices[0].message.content

表格处理 :用 pdfplumber 保留结构,转成 Markdown

python 复制代码
import pdfplumber

def extract_tables_as_markdown(pdf_path: str) -> list[str]:
    tables_md = []
    with pdfplumber.open(pdf_path) as pdf:
        for page_num, page in enumerate(pdf.pages):
            for table in page.extract_tables():
                if not table:
                    continue
                # 第一行作表头
                header = table[0]
                rows = table[1:]
                md = "| " + " | ".join(str(h or "") for h in header) + " |\n"
                md += "| " + " | ".join("---" for _ in header) + " |\n"
                for row in rows:
                    md += "| " + " | ".join(str(c or "") for c in row) + " |\n"
                tables_md.append(f"[第{page_num+1}页表格]\n{md}")
    return tables_md

整合进 RAG 流程

python 复制代码
from langchain_core.documents import Document

def process_document(pdf_path: str) -> list[Document]:
    docs = []
    
    # 1. 提取普通文本
    text_chunks = extract_text_chunks(pdf_path)
    docs.extend([Document(page_content=t, metadata={"type": "text", "source": pdf_path}) for t in text_chunks])
    
    # 2. 提取图片 → VLM 描述
    images = extract_images_from_pdf(pdf_path)
    for img_path, page_num in images:
        description = describe_image(img_path)
        docs.append(Document(
            page_content=description,
            metadata={"type": "image", "source": pdf_path, "page": page_num, "image_path": img_path}
        ))
    
    # 3. 提取表格 → Markdown
    tables = extract_tables_as_markdown(pdf_path)
    for table_md in tables:
        docs.append(Document(page_content=table_md, metadata={"type": "table", "source": pdf_path}))
    
    return docs

优点 :兼容所有现有的文本 RAG 基础设施,不需要换向量库。
缺点:VLM 描述图片有成本和时间开销;描述质量影响检索质量;OCR 对扫描件质量敏感。


路线二:CLIP 多模态 Embedding

原理 :CLIP(Contrastive Language--Image Pre-training,OpenAI 2021)把文本和图片投影到同一向量空间。"营收走势图" 这段文字的向量,和一张营收走势图的向量,在空间上会靠近。

python 复制代码
from langchain_experimental.open_clip import OpenCLIPEmbeddings
from langchain_community.vectorstores import Chroma

# 初始化 CLIP 嵌入(使用 OpenCLIP 开源实现)
clip_embeddings = OpenCLIPEmbeddings(
    model_name="ViT-H-14",
    checkpoint="laion2b_s32b_b79k"
)

# 嵌入文本
text_embedding = clip_embeddings.embed_query("第三季度营收走势")

# 嵌入图片
image_embedding = clip_embeddings.embed_image(["path/to/chart.png"])

# 两者在同一向量空间,可以直接计算相似度
from numpy import dot
from numpy.linalg import norm
similarity = dot(text_embedding, image_embedding[0]) / (norm(text_embedding) * norm(image_embedding[0]))
print(f"相似度: {similarity:.3f}")  # 图文语义相关时通常 > 0.3

构建图文混合向量库

python 复制代码
import uuid

# 图片和文本都存入同一个向量库
image_ids = []
for img_path in image_paths:
    img_embedding = clip_embeddings.embed_image([img_path])[0]
    doc_id = str(uuid.uuid4())
    vectorstore.add_texts(
        texts=["[IMAGE]"],  # 占位符
        embeddings=[img_embedding],
        metadatas=[{"type": "image", "path": img_path}],
        ids=[doc_id]
    )
    image_ids.append(doc_id)

# 文本用普通文本 Embedding 存入
text_vectorstore = Chroma.from_documents(text_docs, text_embeddings)

查询时的双路检索

python 复制代码
def multimodal_search(query: str, k: int = 5):
    # 文本检索
    text_results = text_vectorstore.similarity_search(query, k=k)
    
    # 图片检索(用 CLIP 的文本 Encoder)
    query_embedding = clip_embeddings.embed_query(query)
    image_results = image_vectorstore.similarity_search_by_vector(query_embedding, k=k)
    
    # 合并结果
    return text_results + image_results

优点 :图片不需要预先文字化,可以检索"视觉内容"本身。
缺点:CLIP 在自然图片上效果好,在专业图表(折线图、财务表格)上效果有限------这类图表的语义需要理解数字关系,不是纯视觉识别。


路线三:ColPali(2024 年的新思路)

背景:传统的文档 RAG 流程是:

复制代码
PDF → 提取文本/图片 → 文字化 → Embedding → 检索

每一步都会丢失信息或引入误差。ColPali(Google Research,2024)换了一个思路:

复制代码
PDF → 每页截图 → 视觉语言模型理解 → 页级 Embedding → 检索

直接把 PDF 页面当图像来理解,绕开文本提取。

核心技术:

  • 骨干模型:PaliGemma 3B(Google 的视觉语言模型)
  • Late Interaction(来自 ColBERT):页面被分成 1030 个 patch,每个 patch 生成独立 embedding;查询也生成 token 级 embedding;检索时做细粒度的 patch × token 相似度匹配,然后聚合打分
  • 结果:能精确定位到页面的哪个区域回答了问题
python 复制代码
# 使用 byaldi 库(ColPali 的 Python 接口)
from byaldi import RAGMultiModalModel

# 加载 ColPali 模型
RAG = RAGMultiModalModel.from_pretrained("vidore/colpali-v1.2")

# 索引一个 PDF 目录(每页截图,生成 patch embeddings)
RAG.index(
    input_path="./financial_reports/",
    index_name="reports_index",
    store_collection_with_index=True,  # 保存原始图片供答案生成时使用
    overwrite=True,
)

# 检索(返回最相关的页面)
results = RAG.search("第三季度营收环比增长", k=3)

for r in results:
    print(f"文件: {r['doc_id']}, 页码: {r['page_num']}, 相关度: {r['score']:.3f}")

检索到页面后,用 VLM 生成答案

python 复制代码
import base64
from openai import OpenAI

def answer_with_page_image(question: str, page_image_path: str) -> str:
    with open(page_image_path, "rb") as f:
        img_b64 = base64.b64encode(f.read()).decode("utf-8")
    
    client = OpenAI()
    return client.chat.completions.create(
        model="gpt-4o",
        messages=[{
            "role": "user",
            "content": [
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}"}},
                {"type": "text", "text": f"根据这一页的内容回答:{question}"}
            ]
        }]
    ).choices[0].message.content

ColPali 的完整流程

复制代码
用户问题 → ColPali 检索最相关页面 → 取出页面图片 → 发给 VLM → 生成答案

优点

  • 天然处理图表、公式、混排内容,无需 OCR
  • 页面级理解,不破坏视觉布局
  • 在学术文档、财务报告等视觉密集型文档上效果显著优于传统方法

缺点

  • 模型较重(PaliGemma 3B),检索延迟比向量检索高
  • 依赖 NVIDIA GPU,不适合 CPU-only 部署
  • 索引时间长(每页需要前向传播)

表格的专项处理

表格和图片不同,它有结构化语义,值得专门处理。

方法一:保留 Markdown 结构

python 复制代码
# 提取时保留表格的 Markdown 格式
def table_to_markdown(table: list[list]) -> str:
    if not table or not table[0]:
        return ""
    header = table[0]
    md = "| " + " | ".join(str(h or "-") for h in header) + " |\n"
    md += "| " + " | ".join(":---:" for _ in header) + " |\n"
    for row in table[1:]:
        md += "| " + " | ".join(str(c or "") for c in row) + " |\n"
    return md

好的 LLM 能理解 Markdown 表格并做跨行列推理。

方法二:表格摘要 + 完整表格并存

python 复制代码
def index_table(table_md: str, table_metadata: dict) -> None:
    # 让 LLM 生成表格摘要(用于检索)
    summary = llm.invoke(
        f"用一段话描述这个表格的核心信息(不超过 100 字):\n{table_md}"
    )
    
    # 摘要用于向量检索
    vectorstore.add_texts(
        [summary.content],
        metadatas=[{**table_metadata, "full_table": table_md}]
    )

检索时用摘要找到相关表格,答案生成时把完整表格 Markdown 发给 LLM。

方法三:结构化提取 → 自然语言化

对高价值表格(财务数据、产品规格),用结构化方式提取后转成自然语言描述:

python 复制代码
# 表格 → JSON 结构
table_json = {
    "columns": ["季度", "营收(亿)", "环比增长"],
    "rows": [
        {"季度": "Q1", "营收(亿)": 12.3, "环比增长": "+5.2%"},
        {"季度": "Q2", "营收(亿)": 14.1, "环比增长": "+14.6%"},
        {"季度": "Q3", "营收(亿)": 13.8, "环比增长": "-2.1%"},
    ]
}

# JSON → 自然语言(对检索更友好)
nl_description = (
    "2024 年各季度营收数据:Q1 营收 12.3 亿,Q2 营收 14.1 亿(环比增长 14.6%),"
    "Q3 营收 13.8 亿(环比下降 2.1%)。"
)

自然语言格式对语义检索更友好,也适合被 LLM 直接引用在答案里。


三条路线怎么选

提取 + 文本化 CLIP 多模态 Embedding ColPali
文档类型 所有类型 图片内容主导 视觉密集型(财报、学术 PDF)
基础设施 标准文本 RAG 需要 CLIP 支持 需要 GPU,重模型
图表理解 依赖 VLM 描述质量 弱(图表不是自然图片) 强(页面级理解)
更新成本 高(重新索引代价大)
工程复杂度
成本 VLM 描述有费用 模型推理费用

大多数场景的实用建议

css 复制代码
场景                          推荐方案
─────────────────────────────────────────────────────
普通企业文档(图片少)          文本 RAG,忽略图片或 OCR
产品文档(有示意图)           提取 + GPT-4V 描述图片
财务报告/研究报告(图表密集)   ColPali
电商图片检索                  CLIP
知识库快速搭建(不想复杂化)    提取 + 文本化(最简单)

多模态 RAG 的完整 Pipeline

把以上方案组合成一个统一的 pipeline:

python 复制代码
from enum import Enum

class DocElement(Enum):
    TEXT = "text"
    IMAGE = "image"
    TABLE = "table"

class MultimodalRAGPipeline:
    def __init__(self, text_embeddings, clip_embeddings, llm):
        self.text_emb = text_embeddings
        self.clip_emb = clip_embeddings
        self.llm = llm
        self.vectorstore = Chroma(embedding_function=text_embeddings)
    
    def index(self, pdf_path: str) -> None:
        elements = extract_all_elements(pdf_path)  # 文本/图片/表格
        docs = []
        for elem in elements:
            if elem.type == DocElement.TEXT:
                docs.append(Document(page_content=elem.content, metadata={"type": "text"}))
            elif elem.type == DocElement.IMAGE:
                caption = self._generate_caption(elem.image_path)
                docs.append(Document(
                    page_content=caption,
                    metadata={"type": "image", "path": elem.image_path}
                ))
            elif elem.type == DocElement.TABLE:
                docs.append(Document(
                    page_content=table_to_markdown(elem.content),
                    metadata={"type": "table"}
                ))
        self.vectorstore.add_documents(docs)
    
    def _generate_caption(self, image_path: str) -> str:
        return describe_image(image_path)  # 调用 GPT-4V
    
    def query(self, question: str) -> dict:
        results = self.vectorstore.similarity_search(question, k=5)
        # 构建包含图片和表格的上下文
        context_parts = []
        images_to_show = []
        for r in results:
            if r.metadata["type"] == "image":
                context_parts.append(f"[图片描述] {r.page_content}")
                images_to_show.append(r.metadata["path"])
            else:
                context_parts.append(r.page_content)
        
        answer = self.llm.invoke(
            f"基于以下内容回答问题:\n\n{'---'.join(context_parts)}\n\n问题:{question}"
        )
        return {"answer": answer.content, "images": images_to_show}

小结

多模态 RAG 的本质是把非文本信息转化为可检索的形式,然后在检索时取回原始内容供 LLM 理解。目前三条路线:

  1. 提取 + 文本化:最成熟,工程简单,但依赖 OCR/VLM 质量,适合大多数场景
  2. CLIP 多模态 Embedding:图文统一向量空间,适合自然图片检索,对专业图表效果有限
  3. ColPali:页面直接视觉处理,对图表密集型文档效果最好,但需要 GPU 和较高工程成本

表格单独处理往往比图片简单:保留 Markdown 结构 + 表格摘要,标准文本 RAG 就能很好地处理。

下一篇(也是本系列最后一篇):代码 RAG------让 AI 理解你的代码库,包括 AST 分割、代码 Embedding 模型、以及如何用知识图谱表达函数调用关系。


参考资料

相关推荐
淘矿人1 分钟前
Claude辅助DevOps实践
java·大数据·运维·人工智能·算法·bug·devops
Cosolar9 分钟前
万字详解:RAG 向量索引算法与向量数据库架构及实战
数据库·人工智能·算法·数据库架构·milvus
星浩AI14 分钟前
OpenHuman 对比 OpenClaw、Hermes Agent
人工智能·后端·agent
SeaTunnel25 分钟前
AI 让 SeaTunnel 读源码和调试过时了吗?
大数据·数据库·人工智能·apache·seatunnel·数据同步
搬砖的小码农_Sky1 小时前
AI Agent:WebMCP介绍和具体实现方案
人工智能·ai·人机交互·agi
t_hj1 小时前
大模型微调
人工智能·python·深度学习
冬奇Lab1 小时前
一天一个开源项目(第106篇):Claude Plugins Official - Anthropic 官方 Claude Code 插件生态全解析
人工智能·开源·资讯
落羽的落羽2 小时前
【算法札记】练习 | Week4
linux·服务器·数据结构·c++·人工智能·算法·动态规划