视觉优先的多模态RAG实战:面向民用图纸的问答与合规审查系统

发布时间:2026/8/31 17:11:43
视觉优先的多模态RAG实战:面向民用图纸的问答与合规审查系统 在建设工程与民用设计领域标准图纸的合规审查一直是耗时且依赖人工经验的环节。图纸数量大、检查项细、版次更新频繁光是逐张翻阅图册、核对尺寸标注和规范条文就足以让审图人员耗费大量精力。传统 RAG检索增强生成虽然能帮助我们从文档库中快速定位信息并生成回答但它天生偏向纯文本对图纸中的视觉元素——比如尺寸标注、符号标识、管道走向、空间布局——缺乏有效的理解和利用。本文围绕 PlanSightRAG 这一思路展开一个以视觉优先Visual-First为核心的多模态 RAG 系统用于民用标准图纸的自动问答与合规检查。我会先解释多模态 RAG 的核心概念和系统架构再手把手搭建一个简化版原型涵盖图纸解析、向量化、检索、问答生成和合规校验五个关键环节最后给出常见问题排查和工程落地建议。这套内容适合三类读者想入门多模态 RAG 的算法工程师正在做图纸智能审查、文档智能处理相关项目的开发者以及对 RAG 技术感兴趣、想了解 Visual-First 设计思路的技术爱好者。读完本文你能理解 PlanSightRAG 的整体流程并能基于开源组件快速搭建一个可运行的多模态图纸问答原型。1. 为什么民用标准图纸需要 Visual-First 多模态 RAG1.1 民用标准图纸合规检查的业务痛点民用标准图纸通常包含平面布置图、给排水系统图、电气接线图、暖通空调图、消防疏散图等类型。合规检查要核对的内容非常琐碎房间面积是否满足规范下限疏散门宽度、疏散距离是否合规消防设备位置是否与设计说明一致给排水管径标注是否满足设计流量要求电气回路标识是否与配电箱系统图匹配。这些信息散落在图纸的图形、标注、表格和说明文字中。传统做法是审图工程师将图纸与规范逐条对照经验丰富的老工程师一眼能看出问题但新手需要反复翻查规范条文效率很低。更麻烦的是图纸往往是 PDF 或 CAD 导出的矢量文件直接提取文本会丢失大量视觉上下文。比如某处有个疏散指示标志这个结论单纯靠 OCR 识别出的文字片段很难判断标志的位置、方向、是否与出口对应。这就是为什么仅靠文本 RAG 难以解决图纸合规问题。1.2 从传统 RAG 到多模态 RAG传统 RAG 的工作流程是将文档切分为文本块用文本 Embedding 模型做向量化存入向量数据库用户提问时将问题向量化后在库中检索相似文本块再交给大模型生成回答。优点是实现简单、知识可更新、能降低大模型幻觉。缺点也很明显它天然假设知识都以文本形式存在。图纸中的尺寸线、符号、图例、颜色、空间关系一旦被剥离成文字信息就严重损失。多模态 RAG 在此基础上增加了图像、表格、版面等非文本模态的处理能力。它通常需要两个关键能力视觉特征提取用 CLIP 等多模态模型提取图像特征让相似布局的图纸在向量空间中彼此靠近图文联合检索用户提问后同时检索文本块和图像块再通过重排序融合结果。PlanSightRAG 的核心观点是在民用标准图纸场景下视觉信息应当被优先处理。原因在于图纸合规性判断大量依赖空间关系和图形标注文字只是辅助。1.3 PlanSightRAG 是什么PlanSightRAG 并不是某个固定的商业产品而是一种面向民用标准图纸的多模态 RAG 系统设计范式。它强调三个特点Visual-First先理解图纸的图像内容再结合文本信息做推理问答与合规一体既能回答这个房间面积是多少的事实问题也能回答该设计是否符合消防疏散规范的判断问题证据可回溯每个回答都关联到具体图纸区域和规范条款方便审图人员核验。把它落地到工程上可以拆成一套离线索引 在线问答的流水线这也是本文后面实战部分要实现的架构。2. 环境准备与基础依赖在动手写代码之前先说明本文示例的运行环境。版本需要根据你的实际项目情况调整这里重点演示配置思路。2.1 硬件与运行环境操作系统Ubuntu 20.04 / 22.04Windows 10/11 也可运行但路径写法略有差异Python 版本3.10 或 3.11推荐 3.10 以上内存16GB 以上因为需要同时加载视觉模型和向量索引GPU可选。如果有 NVIDIA GPU显存 8GB 以上视觉特征提取和本地大模型推理会更流畅没有 GPU 也能跑通原型只是推理速度慢一些。2.2 核心依赖清单根据功能模块依赖分为四类模块建议库说明图纸解析PyMuPDF、PaddleOCRPyMuPDF 负责 PDF 渲染和文本抽取PaddleOCR 负责识别扫描件文字视觉特征open-clip-torch 或 transformers提取图纸图像的视觉向量文本向量sentence-transformers生成文本 Embedding向量存储chromadb 或 FAISS存储向量并支持相似度检索大模型推理OpenAI SDK 或 llama.cpp Qwen根据问答结果可选在线 API 或本地模型安装命令示例pip install pymupdf paddleocr chromadb sentence-transformers open-clip-torch如果是本地部署大模型可以借助 llama.cpp 加载 Qwen2-7B 这类量化模型。由于 llama.cpp 的编译参数和你本机的硬件环境关系较大建议优先采用官方 Release 的预编译包。2.3 项目目录规划为了代码清晰建议按下面的目录组织项目plansightrag/ ├── data/ │ ├── raw/ # 原始 PDF 图纸 │ └── processed/ # 解析后的图片、文本 ├── src/ │ ├── parse_pdf.py # 图纸解析模块 │ ├── embed.py # 向量化模块 │ ├── index.py # 向量索引构建 │ ├── retrieve.py # 检索器 │ ├── generate.py # 问答生成 │ └── compliance.py # 合规检查 └── config.yaml # 全局配置这个结构把数据、代码和配置分离后续扩展到真实项目时也容易维护。3. 核心原理拆解PlanSightRAG 的系统架构PlanSightRAG 的整体流程可以拆成两条流水线离线索引流水线和在线问答流水线。下面分别说明。3.1 离线索引流水线离线索引解决的是如何把图纸变成机器可检索的知识。流程如下文档解析将 PDF 图纸逐页渲染为 PNG 图片同时提取文本层版面切分按图框区域切分图纸得到图块 图例 说明文字的单元视觉特征提取用 CLIP 类模型把每个图块转换为固定维度的向量文本特征提取对说明文字、图例文本做切分和 Embedding向量入库将视觉向量和文本向量统一存入向量数据库并保留图纸来源、页码、区域坐标等元数据。关键点在于视觉向量和文本向量可以放在同一个向量空间中也可以分库存储后联合查询。PlanSightRAG 的设计里视觉向量是主索引文本向量是辅助索引。为什么视觉优先因为图纸中两个设计完全不同的房间可能在文字描述上很相似都写着办公室但空间布局差异很大。如果只比较文本检索系统会漏掉真正相关的图纸。而视觉向量能捕捉到布局层面的相似性例如同样是靠窗的工位排布同样是走廊尽头有疏散门。3.2 在线问答与合规检查流水线在线问答解决的是用户提问后如何给出答案和证据。流程如下查询理解对用户问题进行改写提取关键实体如疏散宽度和类型事实问答 or 合规检查多路召回根据问题文本检索文本向量根据问题生成的视觉描述检索视觉向量重排序用重排序模型Reranker对召回结果排序过滤掉低相关片段上下文组装将命中的图纸区域图片、文字说明和规范条款拼接为上下文生成回答将上下文送给 LLM要求输出结论并标注证据来源合规判定如果是合规检查类问题使用规则引擎或让 LLM 结合规范条款给出符合/不符合/待确认的判断。这里有一个容易被忽视的点合规检查不能只靠大模型自由发挥。规范条款是确定性知识比如疏散距离不应大于 30 米这类硬性规则应该优先走规则引擎或结构化知识库LLM 只负责抽取图纸中的实际数值再由规则引擎做比较。3.3 Visual-First 的关键设计Visual-First 听起来只是一个口号但落地时有几个具体操作值得说明。第一个设计是图块优先切分。普通文档解析是按段落切分文本而 PlanSightRAG 是按图纸区域切分。一个图块可能包含标题栏、图例、主视图和尺寸标注。切分时要把这些内容作为一个整体保存而不是拆成独立的文本块。第二个设计是视觉描述生成。为了弥补 CLIP 向量不可解释的缺点可以在索引阶段用视觉大模型例如 Qwen-VL为每个图块生成一句自然语言描述比如该区域为走廊左侧有消防栓右侧为办公室入口疏散门宽度约 1.2 米。这些描述文本也参与向量化供后续检索。第三个设计是证据回传。每个检索结果都携带页码、坐标框和来源文件标记。生成回答时要求 LLM 在答案中引用这些信息。这样审图人员看到结论后可以快速定位到原图区域并人工复核。4. 实战构建一个简化版 PlanSightRAG 原型现在进入代码实战部分。我们搭建一个简化版原型流程完整但不追求工程化。示例中使用的库和 API 可能随版本变化请按你的实际环境调整。4.1 图纸解析模块先写 PDF 解析模块。它的作用是把 PDF 图纸渲染成图片同时提取文本层。# 文件路径src/parse_pdf.py import fitz # PyMuPDF import os def parse_pdf(pdf_path: str, output_dir: str, dpi: int 200): 将 PDF 图纸逐页渲染为 PNG 图片并抽取每页文本。 Args: pdf_path: PDF 文件路径 output_dir: 输出目录 dpi: 渲染分辨率200 足够看清标注 os.makedirs(output_dir, exist_okTrue) doc fitz.open(pdf_path) pages [] for page_index in range(len(doc)): page doc[page_index] # 渲染为图片 mat fitz.Matrix(dpi / 72, dpi / 72) pix page.get_pixmap(matrixmat) img_path os.path.join(output_dir, fpage_{page_index 1:04d}.png) pix.save(img_path) # 抽取文本 text page.get_text(text) pages.append( { page_index: page_index, image_path: img_path, text: text, width: page.rect.width, height: page.rect.height, } ) doc.close() return pages if __name__ __main__: pages parse_pdf(data/raw/demo_plan.pdf, data/processed) for p in pages: print(p[page_index], p[image_path], len(p[text]))说明dpi200适合 A3 以下图纸如果图纸幅面很大比如 A0 加长可能需要调整到 300 以上同时注意内存占用。4.2 Embedding 与向量存储接下来是将图片和文本向量化并存入 ChromaDB。为了简单这里对每一页图纸生成一个整体向量实际项目中建议按图块切分。# 文件路径src/embed.py import chromadb from chromadb.utils import embedding_functions from sentence_transformers import SentenceTransformer class MixedEmbeddingFunction: 统一文本和视觉特征的 Embedding 函数。 实际项目中视觉特征来自 CLIP文本特征来自 Sentence-BERT 两者维度可能不同需要做维度对齐或分库存储。 def __init__(self, text_model_name: str BAAI/bge-small-zh-v1.5): self.text_model SentenceTransformer(text_model_name) def embed_text(self, texts: list[str]) - list[list[float]]: return self.text_model.encode(texts, normalize_embeddingsTrue).tolist() def create_collection(db_path: str, collection_name: str plan_rag): client chromadb.PersistentClient(pathdb_path) embedding_fn embedding_functions.SentenceTransformerEmbeddingFunction( model_nameBAAI/bge-small-zh-v1.5 ) collection client.create_collection( namecollection_name, embedding_functionembedding_fn, metadata{hnsw:space: cosine}, ) return collection需要提醒的是ChromaDB 默认的SentenceTransformerEmbeddingFunction只处理文本。如果你想用 CLIP 的视觉特征做检索可以提前把图片向量算好然后用add(embeddings...)手动传入绕开内置的文本 Embedding 函数。下面是 CLIP 视觉特征提取的示例# 文件路径src/visual_feature.py import torch import open_clip from PIL import Image def load_clip_model(model_name: str ViT-B-32, pretrained: str laion2b_s34b_b79k): model, _, preprocess open_clip.create_model_and_transforms( model_name, pretrainedpretrained ) tokenizer open_clip.get_tokenizer(model_name) return model, preprocess, tokenizer def extract_visual_embedding(model, preprocess, image_path: str, device: str cpu): image Image.open(image_path).convert(RGB) inputs preprocess(image).unsqueeze(0).to(device) model model.to(device).eval() with torch.no_grad(): features model.encode_image(inputs) features / features.norm(dim-1, keepdimTrue) return features.squeeze(0).tolist()如果你没有 GPUCLIP 在 CPU 上推理也是可以接受的只是每张图可能需要几十到几百毫秒。4.3 构建索引将解析结果写入向量库。这里需要把图片路径、文本摘要和视觉向量一并存储。# 文件路径src/index.py import json from embed import create_collection def build_index(pages: list[dict], visual_embeddings: list[list[float]]): collection create_collection(./data/vector_db) ids [] documents [] metadatas [] embeddings [] for idx, page in enumerate(pages): text_sample page[text][:1000] if page[text] else No text doc_id fpage_{page[page_index]} ids.append(doc_id) documents.append(text_sample) metadatas.append( { page_index: page[page_index], image_path: page[image_path], source: demo_plan.pdf, } ) # 这里传入外部计算的视觉向量 embeddings.append(visual_embeddings[idx]) collection.add( idsids, documentsdocuments, metadatasmetadatas, embeddingsembeddings, ) return collection有几个细节值得说明ids必须唯一建议用页码 图块号的命名规则documents存的是文本摘要用于后续关键词过滤embeddings传的是视觉向量检索距离会以视觉相似度为主元数据中保存的image_path是证据回传的基础。4.4 多模态检索器检索器负责把用户的问题转换为向量并从向量库中召回最相关的图纸页。由于视觉检索和文本检索在不同的向量空间中多路召回后要合并排序。# 文件路径src/retrieve.py from sentence_transformers import SentenceTransformer class PlanRetriever: def __init__(self, collection, text_model_name: str BAAI/bge-small-zh-v1.5): self.collection collection self.text_model SentenceTransformer(text_model_name) def retrieve(self, query: str, top_k: int 5): 将问题文本向量化在向量库中检索。 简化版直接使用问题向量做语义检索 实际项目中建议结合视觉向量和文本向量做多路召回。 query_embedding self.text_model.encode(query, normalize_embeddingsTrue).tolist() results self.collection.query( query_embeddings[query_embedding], n_resultstop_k, include[documents, metadatas, distances], ) hits [] for i, doc_id in enumerate(results[ids][0]): hits.append( { id: doc_id, document: results[documents][0][i], metadata: results[metadatas][0][i], distance: results[distances][0][i], } ) return hits这里做了一点简化直接用问题文本向量来检索视觉向量库。实际工程中推荐先把问题改写成一个视觉描述比如这个房间里有没有灭火器改写为搜索包含灭火器图例的图纸区域再用改写后的文本去检索。文本 Embedding 模型对灭火器和灭火器图例的理解会比原始提问更准确。4.5 问答生成模块问答生成模块把检索到的上下文组装成 Prompt交给大模型生成回答。# 文件路径src/generate.py from openai import OpenAI from retrieve import PlanRetriever class PlanQA: def __init__(self, retriever: PlanRetriever, base_url: str None, api_key: str None, model: str qwen2.5:7b): self.retriever retriever self.client OpenAI(base_urlbase_url, api_keyapi_key) self.model model def answer(self, question: str, top_k: int 5) - str: hits self.retriever.retrieve(question, top_ktop_k) context_lines [] for hit in hits: meta hit[metadata] line f[图源: {meta[source]} 第{meta[page_index]1}页] {hit[document][:200]} context_lines.append(line) context \n.join(context_lines) prompt f你是一个民用图纸审图助手。请根据下面的图纸上下文回答问题。 如果上下文无法回答请说明信息不足不要编造。 回答时请引用图纸页码作为证据。 上下文 {context} 问题{question} resp self.client.chat.completions.create( modelself.model, messages[ {role: system, content: 你是严谨的图纸审图助手。}, {role: user, content: prompt}, ], temperature0.1, ) return resp.choices[0].message.content本地大模型场景下可以使用 llama.cpp 启动一个兼容 OpenAI 接口的服务然后让base_url指向本地端口。例如llama-server -m qwen2-7b-instruct-q4_k_m.gguf \ --host 127.0.0.1 --port 8080 \ -c 8192然后在代码中设置qa PlanQA( retrieverretriever, base_urlhttp://127.0.0.1:8080/v1, api_keylocal, modelqwen2.5-7b, )如果硬件有限也可以先用 OpenAI 兼容的在线 API 做测试等流程通了再切换到本地模型。4.6 合规检查模块合规检查是 PlanSightRAG 区别于普通问答的关键能力。它不满足于告诉你图纸上写了什么而是要求判断是否符合规范。最稳妥的做法是规则引擎 LLM 结合。假设规范中有这样一条高层民用建筑内疏散走道净宽度不应小于 1.30 米。# 文件路径src/compliance.py import re class ComplianceRule: def __init__(self, rule_id: str, dimension: str, threshold: float, comparison: str ge): Args: rule_id: 规范条款编号 dimension: 检查维度例如 corridor_width threshold: 阈值例如 1.30 comparison: ge 表示大于等于le 表示小于等于 self.rule_id rule_id self.dimension dimension self.threshold threshold self.comparison comparison def check(self, value: float) - bool: if self.comparison ge: return value self.threshold elif self.comparison le: return value self.threshold else: raise ValueError(fUnsupported comparison: {self.comparison}) def extract_number_from_text(text: str) - float | None: 从文本中提取宽度数值例如 净宽 1.2m - 1.2 match re.search(r(\d(?:\.\d)?)\s*(?:m|米), text) if match: return float(match.group(1)) return None def check_corridor_width(text: str) - str: rule ComplianceRule(GB55037-2022-7.1.2, corridor_width, 1.30, ge) value extract_number_from_text(text) if value is None: return 待确认未在图纸文本中识别到走廊宽度请人工查看对应图块。 if rule.check(value): return f符合走廊净宽 {value}m满足规范要求不小于 {rule.threshold}m。 else: return f不符合走廊净宽 {value}m小于规范要求 {rule.threshold}m。使用示例text 走道净宽 1.2m result check_corridor_width(text) print(result) # 输出不符合走廊净宽 1.2m小于规范要求 1.3m。这里刻意没有把判断逻辑完全交给 LLM。原因很简单合规检查要求稳定、可复现同一个问题每次调用得到相同结论。大模型适合做信息抽取和解释但最终的阈值比较应该由确定性代码完成。4.7 运行验证启动前先确认目录结构正确然后在项目根目录写一个入口脚本# 文件路径main.py from src.parse_pdf import parse_pdf from src.visual_feature import load_clip_model, extract_visual_embedding from src.index import build_index from src.retrieve import PlanRetriever from src.generate import PlanQA # 1. 解析 PDF pages parse_pdf(data/raw/demo_plan.pdf, data/processed) print(f解析完成共 {len(pages)} 页) # 2. 提取视觉向量 model, preprocess, tokenizer load_clip_model() visual_embeddings [] for page in pages: emb extract_visual_embedding(model, preprocess, page[image_path]) visual_embeddings.append(emb) print(视觉特征提取完成) # 3. 构建索引 collection build_index(pages, visual_embeddings) print(索引构建完成) # 4. 检索 问答 retriever PlanRetriever(collection) qa PlanQA(retriever, base_url..., api_key...) ans qa.answer(首层走廊疏散宽度是否符合要求) print(ans)预期输出包括检索命中的页码、上下文摘要、以及大模型基于上下文生成的回答。如果大模型未配置也可以先单独调用retriever.retrieve()查看检索结果是否正确返回对应图纸页。5. 常见问题与排查思路在实际运行中你可能会遇到下面几类问题。这里整理成表格方便对照排查。问题现象常见原因解决思路PyMuPDF 提取的文本为空图纸是纯扫描件没有文本层改用 OCR例如 PaddleOCR 识别图片中的文字视觉向量维度与文本向量不一致CLIP 输出 512 维Sentence-BERT 输出 384 维使用 PCA/LSTM 投影对齐维度或者分库存储分别检索检索结果总是同一页向量库中某一页文档特别长或 Embedding 区分度不够按图块切分而非按页切分检查是否需要重排序ChromaDB add 时报维度错误手动传入的 embeddings 维度与集合初始化时不匹配重建 collection统一 embedding 维度LLM 回答编造规范条款Prompt 中没有约束或上下文缺少规范原文在 Prompt 中强调只引用上下文不编造把规范条款也放入向量库本地 llama.cpp 推理速度慢CPU 运行大模型或上下文过长使用量化模型启用 GPU 加速缩短上下文窗口合规检查结论不稳定同一问题多次调用结果不同用规则引擎做阈值判断LLM 只负责信息抽取图纸比例导致宽度数值不准确视觉模型无法直接读尺寸标注优先解析文本标注必要时结合 CAD 数据结构化数据排查时建议遵循先定位到链路哪一段的思路。如果检索不出正确页面优先检查解析和索引如果检索正常但回答错误优先检查 Prompt 和上下文组装。6. 工程落地最佳实践原型能跑通之后距离真正落地还有不少工作。根据我的经验以下几点值得优先关注。6.1 文档解析质量图纸解析是整个系统的地基。如果解析环节丢失了关键标注后续检索和生成再强也没有用。优先保留矢量 PDF 的文本层纯扫描件再走 OCR大尺寸图纸渲染时按实际比例处理避免标注文字缩放后难以识别对图块做切分时保留图块的坐标信息和标题栏便于证据回传同一份图纸可能有多版本版本管理不能只靠文件名要在元数据中记录版本号。6.2 检索效果优化多模态 RAG 的检索效果需要通过评估集持续验证。建议准备一组问题-相关图页码的标准答案每次调整 Embedding 模型或切分策略后用 RecallK 指标评估。文本检索和视觉检索分别建索引做多路召回后统一重排序重排序阶段可以引入 Cross-Encoder 模型但注意推理延迟如果图纸量达到数千张以上考虑先按专业建筑、给排水、电气分区索引再做二级检索使用 RAG 评估工具监控幻觉率避免模型在新图纸上自由发挥。6.3 合规检查的严谨性合规检查结果可能影响工程审批因此对准确率的要求远高于普通问答。所有不符合结论必须附上图纸证据和规范原文设置待确认状态让系统在信息不充分时不要贸然下结论规范条款应结构化存储包含条款编号、强制程度强条/一般条款、适用条件每次规范更新后需要重新索引相关图纸并记录检查版本。6.4 安全与权限边界民用图纸属于设计单位的知识资产搭建系统时必须考虑数据安全。对图纸文件做访问控制给不同用户分配不同项目的访问权限向量数据库和 LLM 服务部署在内网环境避免图纸内容上传到外部在线 API如果必须使用在线模型优先采用私有化部署模型记录所有问答操作日志方便审计涉及生产环境变更时先在测试环境验证再发布。7. 总结与下一步学习路线本文围绕 PlanSightRAG 这个视觉优先的多模态 RAG 设计讲清楚了民用标准图纸场景下问答和合规检查的核心流程。我们从业务痛点出发解释了为什么文本 RAG 不足以处理图纸类文档然后拆解了系统架构离线索引负责解析和向量化在线问答负责检索、生成和合规判定。实战部分给出了一个包含 PDF 解析、CLIP 视觉特征提取、ChromaDB 向量存储、检索器、问答生成和规则引擎合规检查的完整原型。如果你接下来想深入这个方向建议按以下顺序扩展学习先把原型跑通替换成你自己的图纸样本观察检索结果是否需要调整切分粒度学习视觉大模型如 Qwen-VL、InternVL的用法尝试用它们为图块生成更丰富的视觉描述研究 RAG 的重排序策略和 Agentic RAG 架构让系统能自动决定调用哪个检索器、是否需要追问用户建立行业规范知识库将图纸相关规范条款结构化与图纸向量库做联合检索。最后提醒一句合规检查系统不应该替代审图工程师而是作为辅助工具提高效率、降低遗漏。真正落地时务必在测试环境充分验证再逐步推广到生产流程。