文本 RAG 看不见的东西
上传一份年报 PDF,里面有营收走势图、产品对比表格、架构示意图。传统 RAG 怎么处理?
- 用 PDF 解析器提取文本
- 对文本分块、Embedding、存入向量库
- 用户问"第三季度营收环比增长多少"
问题是:营收走势图是一张图片,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 理解。目前三条路线:
- 提取 + 文本化:最成熟,工程简单,但依赖 OCR/VLM 质量,适合大多数场景
- CLIP 多模态 Embedding:图文统一向量空间,适合自然图片检索,对专业图表效果有限
- ColPali:页面直接视觉处理,对图表密集型文档效果最好,但需要 GPU 和较高工程成本
表格单独处理往往比图片简单:保留 Markdown 结构 + 表格摘要,标准文本 RAG 就能很好地处理。
下一篇(也是本系列最后一篇):代码 RAG------让 AI 理解你的代码库,包括 AST 分割、代码 Embedding 模型、以及如何用知识图谱表达函数调用关系。