ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

多模态RAG实战:基于WeMM-Embedding构建图文混合检索系统

多模态RAG实战:基于WeMM-Embedding构建图文混合检索系统 说实话半年前接到这个项目时我差点被一张架构图劝退。业务方丢给我一批产品手册和培训PPT要做一个能回答问题的内部知识库。我一开始想得很简单不就是RAG嘛解析文档、切块、向量化、召回老套路了。结果把资料库翻开一看整个人有点麻——手册里大量关键信息都以截图、流程图、参数表格的形式存在很多页面甚至没有几个字。用传统纯文本RAG跑了一轮效果惨不忍睹。搜“设备电源接口在哪个位置”召回的是一堆含“电源”字样的不相干段落真正在图片里标注答案的内容根本进不了候选。没办法只能认真搞多模态RAG把图文混合检索这条链路完整搭起来。做了一圈调研选了日调用量达10亿次级别的WeMM-Embedding作为主力向量模型。这篇文章就是把这套方案的完整落地过程、踩坑记录和可直接复用的代码都整理出来给正在被图文资料库虐的同行们一个参考。1. 图文检索为什么难纯文本RAG在真实资料库里的翻车现场1.1 真实资料库的构成核心信息大量以图像形式存在我这次要处理的是一个产品知识库里面大概有3000多页资料构成很典型产品说明书、售后FAQ、销售培训PPT、活动海报截图。粗略统计下来超过40%的关键信息是以图片形式承载的。比如设备接口说明经常是一张产品背板图每个接口旁边标着序号底部一个表格逐行解释比如故障排查流程基本就是一张决策树截图很少有纯文字版本。这意味着什么如果你用纯文本RAG第一步就会丢掉一半信息。传统的做法是OCR抽文字可OCR只能抽出来“图片里有哪些字”抽不出“这些字的位置关系”“这个标注指的是哪块区域”。对于流程图、结构图、对比表格这类强依赖版式的内容OCR后的纯文本完全变形检索质量自然崩。1.2 把图和文字切进同一个chunk问题变得更复杂纯文本RAG还有一个经典操作文档切块然后把每个块向量化。但图文混排资料一旦切块新的麻烦就来了。最直接的方案是PDF解析时把图片也当成“一个元素”按阅读顺序跟文本一起切。可这样会导致图文被硬生生拆开一张产品结构图在chunk 5图里的关键部件文字解释在chunk 8检索时很难同时命中。如果硬把图和它周围所有文本塞进同一个chunk一个chunk可能几千token向量化时语义被噪声稀释召回精度反而下降。我实操中遇到一个特别典型的翻车案例用户问“这款型号支持POE供电吗”资料里答案其实是一张电源规格截图图里清清楚楚标注了“POE 802.3af”。但纯文本链路把这张图OCR成了几段乱序文本向量化后这条内容淹没在几十个无关块里最终LLM答的是“未在产品资料中找到明确信息”。这种体验基本没法用。1.3 为什么不能指望多模态大模型“硬看”全部资料有人会问既然图片是重点干嘛不直接把所有资料喂给多模态大模型让它看完整库再回答问题这个思路听起来直接但落地时会撞墙。3000页PDF、上万张图就算塞进VLM的上下文窗口成本、延迟、幻觉率都不可控。而且业务知识库是持续更新的每更新一版资料就要全量重读不现实。所以多模态RAG的核心价值仍然在“召回的准确性”先把大海捞针的问题变成小范围精读把最相关的图文片段找出来再交给LLM或VLM生成答案。这也是我最终选择“多模态Embedding 向量检索”而不是“无检索硬答”的根本原因。2. WeMM-Embedding 到底能干什么模型能力边界与选型分析2.1 多模态统一向量空间图像和文字能相互检索了传统RAG里文本和图像用的是两套完全独立的表示方式文本检索和图像检索各跑各的最后强行合并结果跨模态匹配的效果很差。WeMM-Embedding这类多模态Embedding模型解决的核心问题是把文本和图像映射到同一个向量空间。也就是说在同一个空间里“POE供电支持图”的图像向量和“如何判断是否支持POE供电”的文本向量距离是近的。这样你就能做两件事用文字直接检索图片以及用图片检索相关的文字描述。WeMM-Embedding覆盖了80多种语言对中文场景尤其友好不需要单独做英文翻译再走一遍。它还有配套的Rerank模型可以在第一轮向量召回后做精细重排这套组合基本覆盖了RAG从召回到底排的完整链路。2.2 “日调用10亿次”在工程上意味着什么选型时我被“日调用10亿次”这个数字吸引说实话这个量级的在线服务一定经过了极大规模的流量打磨和长尾数据检验。在工程上它至少意味着接口的稳定性、各种极端输入模糊截图、低分辨率图、中英混排文本的鲁棒性、以及多语言场景下的覆盖面都经过了一线验证。但我也提醒自己冷静大厂内部跑得好不代表自己项目里直接套就行。我们自己的数据分布、查询意图、图文比例可能跟它的线上场景差别很大。所以不能因为“背景硬”就不做适配测试该有的小样本评测、阈值校准、badcase分析一个都不能少。2.3 选型判断什么场景真的需要多模态Embedding不是所有RAG项目都需要上多模态。如果知识库以纯文本为主图像只是点缀那多模态Embedding引入的解析复杂度反而会拖慢开发进度纯文本Embedding模型足够。但如果出现以下特征基本可以确定需要走多模态路线相当比例的资料是截图、海报、扫描件、流程图。用户查询中有大量“图上的信息”比如“第一张图里标红的接口是什么”。图文强关联单独看文字无法理解完整语义例如培训PPT里一页图一句标题。这里给个简单的决策表场景特征纯文本Embedding多模态Embedding资料几乎全是Markdown/Word纯文本推荐简单高效没必要成本更高PDF里图像占比高图表是核心信息检索效果差基本不可用推荐能图文互检需要跨语言检索依赖翻译链路效果一般原生多语言支持更适合实时性要求极高数据量巨大单模态向量化速度快图像向量化稍慢需要异步处理我这次项目显然属于第二类所以直接锁定多模态路线。3. 接入前必须想清楚的架构问题走API还是本地部署向量库怎么选3.1 API与本地部署的取舍WeMM-Embedding同时提供了云端API和开源权重这个选择会直接决定后续的系统复杂度和运维成本。我做了个简单对比维度云端API本地部署开源权重上线速度有Key就能跑几分钟接入需要准备GPU、拉模型、起服务成本结构按量付费量越大成本越高一次性硬件投入长期边际成本低数据隐私文档内容会经过云端数据完全不出内网推理延迟网络往返一般100ms级别看显卡通常更快运维负担几乎为零模型更新、并发扩容都要自己管我当时的建议是第一版先用API跑通全流程把产品逻辑验证清楚再根据数据量和隐私要求决定是否迁移到本地部署。原因很简单项目早期最大的风险是“链路是否可靠”不是“GPU利用率”。用API可以把向量化和接入的复杂度压缩到最小让你集中精力调检索效果。3.2 向量库选型pgvector先行Milvus兜底向量库这个环节我见过太多项目一上来就上重型组件最后发现数据量万把条运维成本比检索成本还高。这次我选了pgvector理由很朴素项目初始数据量就是几万chunkpgvector完全扛得住。团队对PostgreSQL已经很熟不需要额外引入Milvus/Weaviate/ES这类分布式系统。结构化过滤按文档类型、页码、业务线过滤可以直接用SQL跟向量检索写在一起非常方便。但如果你预判数据量会快速冲到百万级、查询QPS很高那还是要上Milvus或专门的向量数据库。它支持HNSW这类ANN索引的分布式部署、多副本、动态扩容运维上有更成熟的方案。小步快跑永远比一步到位更稳后面真的遇到瓶颈再加中间件也不迟。3.3 整体数据流设计我把整个链路画成了下面这条线用文字描述更直观文档解析抽文本块和图像块→ 切块文本按语义切图像按信息密度切→ 多模态Embedding向量化 → 统一写入pgvector → 查询时文本Query向量化 → 双路召回文本向量表图像向量表→ Rerank精排 → 拼装上下文 → 交给LLM生成回答。这里的关键点是文本Query只做一次向量化但它可以同时去检索文本向量表和图像向量表因为WeMM-Embedding把图文放在同一空间完全支持文字直接查图片。这就是“跨模态检索”的落地点。4. 从零搭起图文混合检索链路文档解析、切块策略与入库逻辑4.1 环境准备与模型调用如果你用云端API先把依赖装上。我习惯用OpenAI SDK来调兼容接口因为这样切换供应商时改动最小。import os from openai import OpenAI client OpenAI( api_keyos.getenv(WEMM_API_KEY), base_urlos.getenv(WEMM_BASE_URL, https://dashscope.aliyuncs.com/compatible-mode/v1), ) def embed_texts(texts): resp client.embeddings.create( modelwemm-embedding, input[{text: t} for t in texts], ) return [item[embedding] for item in resp.data] def embed_images(image_urls): resp client.embeddings.create( modelwemm-embedding, input[{image: url} for url in image_urls], ) return [item[embedding] for item in resp.data]如果你倾向本地部署可以用transformers直接加载开源权重from transformers import AutoModel, AutoTokenizer model AutoModel.from_pretrained(你的本地模型目录/WeMM-Embedding, trust_remote_codeTrue) tokenizer AutoTokenizer.from_pretrained(你的本地模型目录/WeMM-Embedding) def embed_local(textsNone, imagesNone, devicecuda): inputs tokenizer(texttexts, imagesimages, return_tensorspt, paddingTrue).to(device) with torch.no_grad(): embeddings model.encode(**inputs) return embeddings.cpu().tolist()建议正式开发前先跑一次冒烟测试拿一张清晰的产品图和一句对应的中文描述算一下相似度确认调用链路没问题再往下走。4.2 PDF图文解析怎么把图从文档里“抠”出来做多模态RAG文档解析是最容易被低估的一步。我这边主要处理PDF用的是PyMuPDF一个非常好用的库。核心逻辑是按页遍历提取文本块同时把所有图片区域单独抠出来。import fitz # PyMuPDF def parse_pdf_with_images(pdf_path): doc fitz.open(pdf_path) elements [] # 每个元素要么是text要么是image for page_idx, page in enumerate(doc): # 提取文本 page_text page.get_text(text) if page_text.strip(): elements.append({ type: text, content: page_text.strip(), page: page_idx 1, }) # 提取图像 image_list page.get_images(fullTrue) for img_index, img in enumerate(image_list): xref img[0] base_image doc.extract_image(xref) image_bytes base_image[image] ext base_image[ext] image_path fextract/page{page_idx1}_img{img_index}.{ext} with open(image_path, wb) as f: f.write(image_bytes) elements.append({ type: image, content: image_path, page: page_idx 1, }) return elements这里有个工程细节PDF里的图片不只会以独立“对象”形式存在有的其实是页面背景或装饰图。我建议对抠出来的图像做一个基础过滤小于某个面积比例例如页面面积的8%的图直接跳过。否则你可能会给一张边框装饰线都做向量化浪费存储和额度还拉低检索精度。4.3 切块策略文字按语义拆图像按“信息密度”拆文本切块相对常规我按512字符一个chunk重叠80字符按标题层级做边界修正。但图像切块一定要讲策略。以我的实践来看图像有两种典型情况需要区别处理弱信息密度图比如一张整体的产品外观渲染图信息都在“整体观感”上这种图应该整张向量化切成小块反而丢失全局语义。强信息密度图比如一张软件界面截图里面密密麻麻全是文字如果整张图向量化模型容易眼花,细碎信息被压缩丢失。这种情况建议先OCR抽一层文字把文字作为文本chunk单独入库图像本身也保留一份向量两条检索通道互相补充。再比如流程图建议整张切分而不是按节点拆分因为流程的核心价值在于结构关系。多模态Embedding在做这类图时会把整体拓扑关系编码进向量拆碎了反而毁了语义。表格截图则相反如果一张表格特别大建议按行区间切成多个小图否则一行重点信息会被其他行淹没。每块入库时metadata里记录page、来源文件名、chunk类型、原始图片路径后面调试badcase时能直接定位到原始资料。4.4 入库逻辑统一表还是分表我最终选了“一张统一向量表 chunk_type字段区分”的方案。理由很现实查询时用一条SQL就能同时召回文本和图像按类型重新排序分表虽然结构上更干净但跨类型合并结果时多一层逻辑对项目早期来说是多余的复杂度。建表SQL示意CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE doc_chunks ( id BIGSERIAL PRIMARY KEY, chunk_type TEXT NOT NULL, -- text 或 image content TEXT, -- 文本内容或图片的本地路径/URL embedding VECTOR(1024), -- 维度跟模型输出对齐 page_number INT, source_file TEXT, metadata JSONB, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX ON doc_chunks USING hnsw (embedding vector_cosine_ops);插入代码import psycopg2 from psycopg2.extras import Json def insert_chunk(cursor, chunk_type, content, embedding, page, source, metadataNone): cursor.execute( INSERT INTO doc_chunks (chunk_type, content, embedding, page_number, source_file, metadata) VALUES (%s, %s, %s, %s, %s, %s) , (chunk_type, content, embedding, page, source, Json(metadata or {})))入库前建议做一次批量向量化不要一张图一次请求而是攒一批比如64个一起调吞吐会好很多。5. 检索阶段的核心实现双路召回、跨模态匹配与阈值调优5.1 查询链路完整代码检索逻辑的核心是把用户query做一次文本向量化然后同时去查文本chunk和图像chunk各自取topN再做合并和过滤。说的更直白点用户输入一句话它能查到相关的文本段落也能查到“最可能包含答案的那张图”。def search(query, top_k10): query_vec embed_texts([query])[0] cursor.execute( SELECT id, chunk_type, content, page_number, source_file, 1 - (embedding %s::vector) AS score FROM doc_chunks WHERE content IS NOT NULL ORDER BY embedding %s::vector LIMIT %s , (query_vec, query_vec, top_k)) rows cursor.fetchall() # 可以按 chunk_type 统计召回分布方便观察 return rows def search_by_type(query, chunk_type, top_k10): query_vec embed_texts([query])[0] cursor.execute( SELECT id, content, page_number, source_file, 1 - (embedding %s::vector) AS score FROM doc_chunks WHERE chunk_type %s ORDER BY embedding %s::vector LIMIT %s , (query_vec, chunk_type, query_vec, top_k)) return cursor.fetchall()实际项目中我建议先走统一检索看返回结果里图像chunk和文本chunk的比例。如果图像chunk一个都没出现说明要么图像解析入库环节有bug要么query的语义离图太远这时候再分别用search_by_type单独查一次快速定位问题。5.2 相似度阈值怎么定别拍脑袋要数据向量检索的相似度阈值最容易犯的错误是拍脑袋定一个0.8、0.9然后发现召回超少或噪音超多。我这里用的距离是余弦距离1 - cosine_similarity是最终得分。实际操作中我先抽取了20条真实业务问题对每条问题查询后把所有候选的得分捞出来画出分布。你会发现一个规律真正相关的chunk得分普遍在0.65以上弱相关或无关chunk大多在0.4以下。中间0.4到0.65这段属于模糊地带建议不要直接丢弃而是保留下来交给Rerank去判断。我把初筛阈值定在0.35这样能尽量不丢答案把“是否相关”的精细生杀大权交给Rerank。这里还有一个经验不同文档类型、不同图片清晰度得分分布差异很大。扫描件或低分辨率截图出来的向量得分整体会偏低如果统一用一个阈值很容易误杀。所以阈值最好按“来源文件”或“资料类型”配置而不是全局写死。5.3 文中图、图中文的输入限制问题多模态Embedding模型对图像输入是有尺寸和token限制的。我踩过一个坑把一张超大架构图分辨率6000x4000直接丢进去向量化模型把大量细节压缩检索时死活召不回图里的某个模块名。后来我加了一个图像预处理步骤读取图片尺寸如果长边超过模型建议值比如2048px先把图切分成2x2或3x3的网格每个子图单独向量化入库同时保留整图的向量。查询时如果命中了某个子图可以回溯到整图把整图作为上下文喂给LLM。这样既不会丢细节又能保留全局结构。类似地用户上传的查询图片也要做同样处理不然检索端“图搜图”时精度会明显下降。6. 从跑通到好用Rerank、缓存与搜索架构的长尾问题6.1 第一轮召回不准交叉编码器Rerank来兜底向量召回本质上是一道“粗筛题”它足够快但精度有限。多模态RAG落地时第一轮top10里可能只有两三个真正相关。这时候如果直接把top10拼给LLM不仅浪费上下文窗口还容易因为噪声信息干扰导致幻觉。我的做法是加一个Rerank层用交叉编码器CrossEncoder对召回结果逐条精排。交叉编码器能把query和candidate拼在一起做深度交互比双塔式向量检索精度高不少。WeMM-Embedding官方也配套了对应的Rerank模型。简化版代码from sentence_transformers import CrossEncoder reranker CrossEncoder(wemm-rerank-model, max_length512) def rerank(query, candidates): pairs [(query, c[content]) for c in candidates] scores reranker.predict(pairs) for c, s in zip(candidates, scores): c[rerank_score] s candidates.sort(keylambda x: x[rerank_score], reverseTrue) return candidates[:5]加了这个层以后我们内部评测里“答案命中率”正确答案出现在最终上下文中的比例大概提升了18个百分点。代价是每条查询增加了几十到上百毫秒延迟但换来的精度提升非常值。6.2 图片内容的OCR与多模态Embedding的配合很多人以为有了多模态EmbeddingOCR就可以扔了。实际用下来两者是互补关系不是替代关系。对于界面截图、扫描表格这类文字密集的图像OCR抽出的结构化文本往往是“最硬的线索”向量相似度反而会因为图像压缩损失精度而排得不够靠前。我在项目里的策略是双轨并行图片既做图像向量化也做OCR文字提取OCR文本作为一个独立chunk入库。检索时如果query偏文字匹配比如“登录按钮在哪”OCR文本chunk大概率命中如果query偏视觉语义比如“这张图整体在讲什么流程”图像向量chunk效果更好。两条结果进Rerank层统一排序最后取最优。6.3 缓存与限流别把日调用10亿次的接口当成无限量虽然WeMM-Embedding本身日调用量是10亿级别但那是整个平台的总量你一个项目拿到的账号有QPS限制。如果索引阶段一次性喂几千张图很容易触发限流报错。我踩过的坑是批量向量化没做并发控制跑了一半接口开始报错还得写重跑脚本。后面我加了两层保护本地缓存相同图片或相同文本块的向量结果直接读缓存不重复请求。指数退避重试请求失败时等1秒、2秒、4秒重试最多5次。import time def embed_with_retry(textsNone, imagesNone, max_retries5): for attempt in range(max_retries): try: return embed_texts(texts) if texts else embed_images(images) except Exception as e: wait 2 ** attempt print(fembedding failed: {e}, retry in {wait}s) time.sleep(wait) raise RuntimeError(embedding retry exhausted)如果是一次性全量入库建议把任务拆成多个批次用消息队列或简单的生产者消费者模型慢慢消费别让短期内流量太集中。6.4 从单次查询到Agentic RAG多跳检索怎么接项目跑顺之后业务方自然会提更多需求比如“连续问好几轮”“需要根据一个问题去查多个来源再总结”。这时候单次检索就不够用了需要在外面套一层Agent逻辑。我目前的做法是用FastAPI把检索服务包成一个独立微服务上层再用LangGraph编排多轮检索流程pgvector继续当向量存储。多轮场景里有个小心得在每轮检索前先用LLM把对话历史压缩成一个独立的、自包含的搜索query再进检索链路。否则问题里的代词“它”“这个设备”会让向量检索完全失焦。这些属于进阶方向第一版先把“单轮图文检索”做扎实多跳Agent可以在基础稳定的情况下逐步叠加。搜索架构这东西最怕的就是基础没打牢就堆花活。回到我自己的项目感受。多模态RAG对比传统纯文本RAG最大的进步不是“多了一个模态”而是让检索真正理解了图文之间的关系。文字和图像落进同一个高维空间后很多以前根本搜不到的答案现在能被捞出来了。但这套链路也绝不是装个模型就万事大吉文档解析的精细度、切块策略、图像预处理、阈值校准、Rerank组合每一步都在影响最终体验。最后分享一个比较实用的习惯无论换什么模型、调什么参数先在真实数据集上攒一批badcase批量跑一轮按“召回缺失、重排错误、上下文截断、生成幻觉”分类记录。发现问题先改链路再考虑换模型。这个习惯帮我少走了很多弯路。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表