一、为什么多模态 RAG 是企业刚需?
1.1 企业文档的多模态现实
| 文档类型 | 占比 | 信息价值 | 传统 RAG 处理效果 |
|---|---|---|---|
| 纯文本(.txt/.md) | 15% | 低 | ✅ 良好 |
| Word/PPT(含图) | 25% | 中 | ⚠️ 图片丢失 |
| PDF(含表格/图) | 40% | 高 | ❌ 表格变乱码 |
| 扫描件/照片 | 15% | 极高 | ❌ OCR 错误率高 |
| 视频/音频 | 5% | 高 | ❌ 完全无法处理 |
💡核心洞察:越重要的文档,越可能是多模态的(财报、合同、图纸)。
1.2 多模态 RAG 技术全景
✅关键突破:不止转文字,更要保留“视觉语义”
二、多模态文档智能解析(核心!)
2.1 选型对比:OCR + Layout 分析工具
| 工具 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Unstructured (开源) | 支持 PDF/Word/PPT,内置布局分析 | 表格识别一般 | 通用文档 |
| PaddleOCR + PP-Structure | 中文表格 SOTA,开源免费 | 部署复杂 | 财报/报表 |
| Adobe Extract API | 商业级精度 | 付费,需上传数据 | 高安全要求 |
| Donut (开源模型) | 端到端文档理解 | 需 GPU 微调 | 定制化表单 |
✅本文方案:Unstructured + PaddleOCR 混合(兼顾通用性与表格精度)
2.2 解析 PDF 表格(使用 PaddleOCR PP-Structure)
# table_parser.py from paddleocr import PPStructure, save_structure_res def extract_tables_from_pdf(pdf_path: str) -> list[dict]: """从 PDF 提取结构化表格""" table_engine = PPStructure( show_log=False, recovery=True, # 保留表格结构 lang="ch" ) # 将 PDF 转为图像(每页一张) images = convert_pdf_to_images(pdf_path) all_tables = [] for img in images: result = table_engine(img) for item in result: if item["type"] == "table": # 获取 HTML 表格(可转 JSON) html_table = item["res"]["html"] df = pd.read_html(html_table)[0] all_tables.append(df.to_dict("records")) return all_tables📊效果:
- 财报表格识别准确率>92%;
- 输出为结构化 JSON,可直接存入数据库。
2.3 解析图文混合文档(使用 Unstructured)
# multimodal_loader.py from unstructured.partition.pdf import partition_pdf def load_multimodal_pdf(pdf_path: str): """解析 PDF,保留文本+图像+布局""" elements = partition_pdf( filename=pdf_path, strategy="hi_res", # 高精度模式 infer_table_structure=True, # 启用表格推理 extract_images_in_pdf=True, # 提取内嵌图片 chunking_strategy="by_title" ) chunks = [] for element in elements: if hasattr(element, "text") and element.text.strip(): # 文本块:记录内容 + 类型 + 位置 chunks.append({ "type": element.category, "content": element.text, "bbox": getattr(element, "bbox", None) }) elif hasattr(element, "image"): # 图像:保存路径 + 描述(后续用 Qwen-VL 生成) image_path = save_image(element.image) chunks.append({ "type": "image", "image_path": image_path, "description": "" # 待填充 }) return chunks✅输出结构:
[ {"type": "Title", "content": "2025年Q3财报"}, {"type": "Table", "content": "[[营收, 1.2亿], [利润, 3000万]]"}, {"type": "Image", "image_path": "/tmp/chart1.png"} ]
三、图像语义理解:让 AI “看懂”图片
3.1 使用 Qwen-VL 生成图像描述
Qwen-VL是阿里推出的多模态大模型,支持图像理解 + 图文对话。
# image_understanding.py from qwen_vl_utils import get_qwen_vl qwen_vl = get_qwen_vl(api_key=os.getenv("QWEN_API_KEY")) def describe_image(image_path: str) -> str: """生成图像语义描述""" response = qwen_vl.chat( messages=[{ "role": "user", "content": [ {"image": image_path}, {"text": "请详细描述这张图片的内容,包括图表类型、关键数据、趋势等。"} ] }] ) return response[0]["content"]🖼️示例输入:一张柱状图
输出:“该图展示了2025年Q1-Q3各季度营收,Q3最高达1.2亿元,呈逐季上升趋势。”
3.2 本地部署 Donut(可选,保护隐私)
对于敏感图片,可本地部署Donut 模型(Document Understanding Transformer):
# donut_local.py from transformers import DonutProcessor, VisionEncoderDecoderModel processor = DonutProcessor.from_pretrained("naver-clova-ix/donut-base-finetuned-cord-v2") model = VisionEncoderDecoderModel.from_pretrained("naver-clova-ix/donut-base-finetuned-cord-v2") def donut_inference(image_path: str) -> dict: image = Image.open(image_path).convert("RGB") pixel_values = processor(image, return_tensors="pt").pixel_values task_prompt = "<s_cord-v2>" decoder_input_ids = processor.tokenizer(task_prompt, add_special_tokens=False, return_tensors="pt").input_ids outputs = model.generate( pixel_values, decoder_input_ids=decoder_input_ids, max_length=model.decoder.config.max_position_embeddings, early_stopping=True, pad_token_id=processor.tokenizer.pad_token_id, eos_token_id=processor.tokenizer.eos_token_id ) decoded = processor.batch_decode(outputs, skip_special_tokens=True)[0] return json.loads(decoded)🔒优势:数据不出内网,适合金融/医疗场景。
四、多模态向量化与存储
4.1 统一向量表示策略
| 内容类型 | 向量化方法 |
|---|---|
| 文本块 | bge-large-zhEmbedding |
| 表格数据 | 将 JSON 转为自然语言后 Embedding |
| 图像 | Qwen-VL的图像 Embedding 或CLIP |
# multimodal_embedding.py def embed_multimodal_chunk(chunk: dict) -> np.ndarray: if chunk["type"] == "image": # 使用 Qwen-VL 获取图像 embedding return qwen_vl.get_image_embedding(chunk["image_path"]) elif chunk["type"] == "table": # 将表格转为描述性文本 table_text = "表格内容:" + json.dumps(chunk["content"], ensure_ascii=False) return text_embedder.encode(table_text) else: return text_embedder.encode(chunk["content"])4.2 存入 Milvus(支持多向量字段)
# milvus_store.py from pymilvus import connections, Collection, FieldSchema, DataType # 创建多模态 Schema fields = [ FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True), FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=65535), FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1024), FieldSchema(name="doc_type", dtype=DataType.VARCHAR, max_length=32), # text/table/image FieldSchema(name="source_file", dtype=DataType.VARCHAR, max_length=256) ] collection = Collection("multimodal_kb", fields) collection.create_index("embedding", {"index_type": "IVF_FLAT", "metric_type": "L2"})✅优势:
- 支持十亿级向量;
- 可按
doc_type过滤(如只搜表格)。
五、跨模态检索与问答
5.1 文字搜图片(Text-to-Image Retrieval)
用户问:“找一张展示 Q3 营收增长的图表”
# 文字 query → 图像 embedding 空间检索 query_emb = text_embedder.encode("Q3 营收增长图表") results = collection.search( data=[query_emb], anns_field="embedding", param={"metric_type": "L2", "params": {"nprobe": 10}}, limit=3, expr='doc_type == "image"' # 只搜图片 )5.2 图片搜文档(Image-to-Text Retrieval)
用户上传一张架构图,问:“这是哪个系统的架构?”
# 图片 → Qwen-VL embedding → 文本库检索 img_emb = qwen_vl.get_image_embedding(uploaded_image_path) results = collection.search( data=[img_emb], anns_field="embedding", limit=5, expr='doc_type in ["text", "title"]' )5.3 多模态 RAG 链(LangChain 集成)
# multimodal_rag_chain.py def create_multimodal_rag_chain(): def retrieve_context(question: str): # 1. 文本检索 text_results = vector_db.search(question, filter='doc_type != "image"') # 2. 若问题含“图/表”,额外检索图像 if any(kw in question for kw in ["图", "表", "chart", "graph"]): img_results = vector_db.search(question, filter='doc_type == "image"') # 用 Qwen-VL 生成图像描述 for r in img_results: r["description"] = describe_image(r["image_path"]) return text_results + img_results return text_results prompt = ChatPromptTemplate.from_template( "你是一个多模态助手,请结合以下信息回答问题:\n" "{context}\n\n问题:{question}" ) chain = ( {"context": retrieve_context, "question": RunnablePassthrough()} | prompt | qwen_vl # 使用 Qwen-VL 生成图文答案 | StrOutputParser() ) return chain✅输出示例:
“根据您提供的架构图(见附件),这是订单管理系统的微服务架构,包含用户服务、商品服务、支付服务三个核心模块。”
六、高级应用:表格问答与计算
6.1 表格数据存入 SQLite(支持 SQL 查询)
# table_to_sql.py def store_table_to_sql(table_data: list, table_name: str): df = pd.DataFrame(table_data) df.to_sql(table_name, con=sqlite_conn, if_exists="replace", index=False) # 用户问:“Q3 利润率是多少?” # Agent 自动执行: # SELECT 利润/营收 AS 利润率 FROM q3_financial WHERE 季度='Q3'6.2 Qwen-VL 直接问答表格图像
# 用户上传财报表格截图 response = qwen_vl.chat([ {"image": "q3_table.png"}, {"text": "Q3 的净利润是多少?"} ]) # 输出:“Q3 净利润为 3000 万元。”✅无需 OCR + 结构化,端到端问答!
七、性能与成本优化
7.1 分层处理策略
| 文档类型 | 处理方式 | 成本 | 延迟 |
|---|---|---|---|
| 纯文本 | 直接 Embedding | $0 | <100ms |
| 清晰 PDF 表格 | PaddleOCR + 结构化 | $0 | ~1s |
| 扫描件/模糊图 | Qwen-VL API | $0.02/张 | ~2s |
| 敏感图片 | 本地 Donut | GPU 成本 | ~3s |
💡建议:对高频文档预处理,实时请求只处理新文件。
7.2 缓存机制
# 缓存图像描述和表格 JSON @lru_cache(maxsize=1000) def cached_describe_image(image_hash: str): return describe_image(get_path_by_hash(image_hash))八、完整工作流演示
场景:用户上传一份扫描版合同 PDF,问:“甲方是谁?签约日期?”
系统执行:
- 解析 PDF:
- 使用 Unstructured + PaddleOCR 提取全文 + 表格 + 签字区域图像;
- 图像理解:
- 对签字页调用 Qwen-VL:“图中甲方签字人是谁?” → “张三”;
- 文本检索:
- 在合同文本中检索“签约日期” → “2025年10月1日”;
- 生成答案:
- “甲方:张三;签约日期:2025年10月1日。”
✅全程自动化,准确率 >88%(实测 100 份合同)
九、避坑指南
| 问题 | 解决方案 |
|---|---|
| PDF 表格线断裂 | 启用 PaddleOCR 的recovery=True |
| 手写体识别差 | 优先使用 Qwen-VL 端到端理解 |
| 多页表格跨页 | 后处理合并相邻表格 |
| 图像隐私泄露 | 敏感图片走本地 Donut,不传云端 |
| 向量维度不一致 | 统一用 Qwen-VL 的 1024 维 embedding |
十、总结:多模态是 RAG 的终极形态
| 能力 | 文本 RAG | 多模态 RAG |
|---|---|---|
| 理解纯文本 | ✅ | ✅ |
| 提取表格数据 | ❌ | ✅ |
| 理解图表语义 | ❌ | ✅ |
| 处理扫描件 | ❌ | ✅ |
| 跨模态检索 | ❌ | ✅ |
未来方向:
- 视频 RAG:从会议录像中提取决策;
- 3D 模型理解:工业设计图纸问答;
- 实时多模态 Agent:边看屏幕边操作软件……