多模态RAG实战:从CAD图纸到智能问答

前言:多模态RAG的技术价值与应用前景

  随着人工智能技术的快速发展,传统的文本检索增强生成(RAG)已经无法满足现代企业对多媒体信息处理的需求。多模态RAG作为下一代智能信息系统的核心技术,能够同时理解和处理文本、图像、音频等多种数据类型,为企业数字化转型提供了强大的技术支撑。

  本文将通过一个完整的实战项目,深入探索如何构建一个基于多模态RAG的CAD图纸智能问答系统。在工业制造、建筑设计、房地产等领域,技术图纸是核心资产,但传统的管理方式存在诸多痛点:

  • 检索效率低:需要人工逐一查看图纸,耗时费力
  • 信息孤岛:图纸中的结构化信息难以提取和利用
  • 专业门槛高:非专业人员难以快速理解图纸内容
  • 知识传承难:专家经验难以标准化和传承

  我们将从零开始,逐步构建一个能够"读懂"CAD图纸、自动提取关键信息、并智能回答用户问题的系统,来实现了从图像理解到知识检索的全链路智能化。

系统架构概览

  本系统采用模块化设计,核心技术栈包括:

    第一步:接入VLM模型
                ↓
    第二步:解析本地CAD图片
                ↓
    第三步:提取结构化元数据
                ↓
    第四步:存入向量数据库
                ↓
    第五步:智能问答(直接问答 + 图像检索)
  • 处理图片示例

核心功能一:支持在线上传并自动解析多模态PDF及CAD、工程图纸和复杂架构原型图;

核心功能二:通过自然语言问答,直接检索图片原型及文档原件,并支持溯源和在线预览,实现“以文搜图”、“以图搜图”;

核心功能三:支持实时上传多模态PDF及CAD、工程图纸和复杂架构原型图,并直接对文件内容进行提问,实现“以文搜文”;

  这套系统可以直接应用于:

  • 房地产销售:快速回答客户关于户型的问题(“有几个卧室?”、“主卧面积多大?”)
  • 室内设计:分析户型优缺点,提供设计建议
  • 智能选房:根据用户需求(如"3室2厅,面积100平以上")自动筛选户型
  • 户型对比:智能对比多个户型的优劣

  同时,只要针对性的修改提示词,即可快速迁移到其他的图像分析场景。

5.1 环境准备与依赖安装

  首先,我们需要安装必要的Python包。这个系统的核心依赖包括:

  • openai:用于调用多模态大模型API
  • chromadb:向量数据库,用于存储和检索
  • langchain:RAG框架的核心组件
  • Pillow:图像处理库
# 安装依赖(如果需要)
# !pip install openai chromadb langchain langchain-community langchain-openai Pillow python-dotenv -q

# 导入必要的库
import os
import json
import base64
import io
from pathlib import Path
from typing import Dict, Any, List
from PIL import Image
from dataclasses import dataclass
import sys
# 添加项目路径(使用绝对路径)
project_root = Path(__file__).parent if '__file__' in globals() else Path.cwd()
backend_path = project_root / "backend"
if str(backend_path) not in sys.path:
    sys.path.insert(0, str(backend_path))

# OpenAI SDK
from openai import OpenAI

# LangChain 组件
from langchain.text_splitter import RecursiveCharacterTextSplitter
from qwen_embeddings import QwenEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.docstore.document import Document
from langchain_openai import ChatOpenAI

  上述代码中,导入了构建多模态RAG系统所需的所有核心库:

  • openai:提供了与 gpt-4o 等视觉语言模型交互的接口
  • chromadb 相关:通过 langchain_community.vectorstores.Chroma 实现向量存储
  • HuggingFaceEmbeddings:用于将文本转换为向量表示
  • PIL.Image:处理图像文件的加载和转换

  至此,我们的环境已经准备完毕,接下来就开始真正的系统构建。

5.2 接入视觉语言模型(VLM)

  多模态RAG的核心能力来自于视觉语言模型(Vision-Language Model, VLM)。这类模型能够同时理解图像和文本,对于CAD图纸这种技术图像来说,VLM可以识别其中的结构、尺寸标注、技术参数等关键信息。

  本课程中,我们使用 gpt-4o 作为VLM模型。首先,我们需要配置API密钥和模型接入点。需要配置三个关键参数:

  • API_KEY:你的OpenAI API密钥(或兼容的API服务密钥)
  • BASE_URL:API服务的基础URL
  • MODEL_NAME:使用的模型名称(这里是 gpt-4o

提示:如果你使用的是OpenAI官方API,BASE_URL 设置为 https://api.openai.com/v1 即可。

from dotenv import load_dotenv

# ========== VLM 模型配置 ==========
MODEL_NAME = "gpt-4o"

load_dotenv(override=True)

# 初始化 OpenAI 客户端
vlm_client = OpenAI(
    api_key=os.getenv("OPENAI_API_KEY"),
    base_url=os.getenv("OPENAI_BASE_URL")
)

  这段代码创建了一个 OpenAI 客户端实例,这是我们与视觉语言模型交互的桥梁。通过这个客户端,我们后续可以发送图像和问题给模型,并接收模型的分析结果。

  接下来,我们需要构建一个CAD图纸分析器,它能够将图像转换为模型可以理解的格式,并调用VLM进行智能分析。

5.3 构建CAD图纸分析器

  多模态RAG的核心能力来自于视觉语言模型(VLM)。对于室内平面图来说,VLM需要能够识别:

  • 房间布局和功能区划分
  • 尺寸标注和面积计算
  • 门窗位置和开启方向
  • 家具布置和空间利用
  • 动线设计和连通关系

  在实际应用中,针对不同类型的图纸(CAD、平面图、架构图等),我们需要设计不同的提示词模板,以获得最佳的分析效果。

@dataclass
class AnalysisResult:
    """图像分析结果数据类"""
    answer: str  # VLM的回答
    extracted_info: Dict[str, Any]  # 提取的结构化信息
    raw_response: str  # 原始响应内容


class FloorPlanAnalyzer:
    """室内平面图分析器"""
    
    # 平面图专业提示词模板
    FLOOR_PLAN_PROMPT = """你是一位专业的建筑/室内平面图分析专家。请仔细分析这张室内平面布置图。

**用户问题:**
{question}

**重要说明:**
- 这是一张室内平面布置图,包含房间、尺寸标注、家具布置、动线等信息
- 图中尺寸单位通常为毫米(mm)或米(m),请根据数值大小推断
- 请仔细识别所有可见的房间、标注、符号和空间关系

**分析维度(根据用户问题选择性回答):**

1. **房间/功能区识别**:
   - 识别所有房间名称(客厅、卧室、厨房、卫生间、阳台等)
   - 标注每个房间的位置(左上/右下/中央等方位)
   - 识别特殊功能区(储藏室、玄关、衣帽间等)

2. **尺寸与面积**:
   - 提取图中所有可见尺寸标注
   - 推断单位并统一换算为米(m)
   - 计算房间的长、宽、面积
   - 标注整体平面外墙尺寸

3. **符号与标注**:
   - 解释符号含义(虚线、箭头、红点/红线、轴线等)
   - 识别文字标注(房间编号、面积、备注)
   - 说明墙体类型、门窗位置和开启方向

4. **家具布局**:
   - 列出所有可见家具及其位置
   - 判断空间利用率(拥挤/适中/空旷)
   - 识别家具尺寸

5. **动线与连通性**:
   - 标出主入口、次入口位置
   - 描述主要动线路径(如:"入口→玄关→客厅→...")
   - 列出房间连通关系(哪些房间相连)
   - 判断布局类型(开放式/分隔式)

6. **设计评估**(如果问题涉及):
   - 动线合理性、是否有绕行或死角
   - 采光/朝向分析
   - 空间优化建议

**回答方式:**
- 首先直接、简洁地回答用户的具体问题
- 然后提供相关的详细信息(如果用户问某个房间,重点描述该房间)
- 如果用户问整体布局,提供全局分析
- 如果涉及尺寸计算,请说明推理过程(如:"标注22720mm = 22.72m")

**输出格式(JSON):**
{{
    "answer": "直接回答用户问题的核心内容(简洁明了)",
    "extracted_info": {{
        "total_dimensions": {{
            "length": 22.72,
            "width": 12.5,
            "unit": "m",
            "total_area": 284.0
        }},
        "rooms": [
            {{
                "name": "客厅",
                "position": "中央偏右",
                "dimensions": {{"length": 5.79, "width": 4.2, "area": 24.3, "unit": "m"}},
                "furniture": ["沙发", "茶几"],
                "connected_to": ["餐厅", "卧室1"],
                "windows": 2,
                "doors": 1
            }}
        ],
        "annotations": [
            {{"type": "dimension", "value": "22720", "parsed_value": 22.72, "unit": "m", "description": "外墙总长"}}
        ],
        "symbols": [
            {{"type": "door", "count": 5, "positions": ["客厅-餐厅", "卧室1入口"]}}
        ],
        "circulation": {{
            "main_entrance": "底部中央",
            "main_path": "主入口 → 玄关 → 客厅 → 餐厅",
            "layout_type": "开放式客餐厅"
        }},
        "design_notes": ["主卧带独立卫生间", "动线流畅"]
    }}
}}

**注意事项:**
- 如果标注不清晰,标注为"不可读"或给出估算值并说明
- 优先回答用户的具体问题,不要罗列所有信息
- 如果用户问"有几个卧室",就重点回答卧室数量和位置
- 如果用户问"客厅面积",就重点回答客厅的尺寸和面积
- 保持答案简洁、针对性强"""
    
    def __init__(self, client: OpenAI, model_name: str):
        """初始化分析器"""
        self.client = client
        self.model_name = model_name
    
    def load_image(self, image_path: str) -> Image.Image:
        """加载本地图片"""
        image_path = Path(image_path)
        if not image_path.exists():
            raise FileNotFoundError(f"图片文件不存在: {image_path}")
        
        image = Image.open(image_path)
        print(f"图片加载成功: {image.size}")
        return image
    
    def image_to_base64(self, image: Image.Image, max_size: int = 2000) -> str:
        """将PIL Image转换为base64字符串"""
        # 如果图片过大,进行压缩
        if image.width > max_size or image.height > max_size:
            image = image.copy()
            image.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
            print(f"图片已压缩到: {image.size}")
        
        # 转换为JPEG格式的base64
        buffer = io.BytesIO()
        if image.mode == 'RGBA':
            image = image.convert('RGB')
        image.save(buffer, format='JPEG', quality=85)
        buffer.seek(0)
        
        base64_str = base64.b64encode(buffer.read()).decode('utf-8')
        print(f"图片转换为base64: {len(base64_str) / 1024:.1f} KB")
        return base64_str
    
    def analyze(self, image_path: str, question: str) -> AnalysisResult:
        """
        分析平面图
        
        Args:
            image_path: 图片路径
            question: 用户问题
            
        Returns:
            AnalysisResult对象
        """
        
        # 1. 加载图片
        image = self.load_image(image_path)
        
        # 2. 转换为base64
        image_base64 = self.image_to_base64(image)
        
        # 3. 构建提示词
        prompt = self.FLOOR_PLAN_PROMPT.format(question=question)
        
        # 4. 调用VLM API
        print("正在调用VLM模型...")
        messages = [
            {
                "role": "system",
                "content": "你是一位专业的建筑平面图分析专家。请仔细分析图像并按照要求的JSON格式返回结果。"
            },
            {
                "role": "user",
                "content": [
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/jpeg;base64,{image_base64}"
                        }
                    },
                    {
                        "type": "text",
                        "text": prompt
                    }
                ]
            }
        ]
        
        response = self.client.chat.completions.create(
            model=self.model_name,
            messages=messages,
            max_tokens=4096,
            temperature=0.1,
            response_format={"type": "json_object"}
        )
        
        # 5. 解析响应
        content = response.choices[0].message.content
        parsed = self._parse_json_response(content)
        
        print(f"分析完成!Token使用: {response.usage.total_tokens}")
        print("="*60 + "\n")
        
        return AnalysisResult(
            answer=parsed.get('answer', ''),
            extracted_info=parsed.get('extracted_info', {}),
            raw_response=content
        )
    
    def _parse_json_response(self, content: str) -> Dict[str, Any]:
        """解析JSON响应"""
        try:
            # 清理可能的markdown代码块标记
            content = content.strip()
            if content.startswith('```json'):
                content = content[7:]
            elif content.startswith('```'):
                content = content[3:]
            if content.endswith('```'):
                content = content[:-3]
            content = content.strip()
            
            return json.loads(content)
        except json.JSONDecodeError as e:
            print(f"JSON解析失败: {e}")
            return {
                'answer': content,
                'extracted_info': {}
            }

# 创建平面图分析器
analyzer = FloorPlanAnalyzer(vlm_client, MODEL_NAME)

  这段代码是平面图分析系统的核心。让我们理解几个关键点:

  1. 提示词针对平面图优化

  平面图的提示词更加注重:

  • 房间识别:客厅、卧室、厨房等功能区
  • 尺寸推断:自动判断单位是mm还是m(如22720mm→22.72m)
  • 动线分析:入口→玄关→客厅的流动路径
  • 空间关系:房间之间的连通性和位置关系

  2. 结构化输出的重要性

  输出的JSON结构包含了完整的户型元数据

  • total_dimensions:整体尺寸和总面积
  • rooms:每个房间的详细信息(名称、位置、面积、家具)
  • circulation:动线设计和布局类型
  • design_notes:设计特点和建议

  这些结构化信息将成为后续智能问答的核心数据源!

5.4 测试平面图分析

  接下来让我们测试分析器的功能。

提示:请准备一张平面图(户型图),替换下面的路径后运行。

# ========== 测试平面图分析 ==========

# 指定平面图路径(请替换为你的平面图路径)
FLOOR_PLAN_PATH = "./test_data/house1.png"  # 示例路径

# 用户问题
USER_QUESTION = "请详细分析这张平面图,包括房间布局、尺寸面积、动线设计等信息。"

# 执行分析
result = analyzer.analyze(FLOOR_PLAN_PATH, USER_QUESTION)

# 显示分析结果
print(result.answer)
print("\n【提取的结构化元数据】")
print(json.dumps(result.extracted_info, ensure_ascii=False, indent=2))
✓图片加载成功: (900, 781)
图片转换为base64: 130.5 KB
正在调用VLM模型...
分析完成!Token使用: 2492
============================================================

这张平面图展示了一套三居室的住宅布局,包含客厅、主卧、次卧、儿童房、厨房、主卫、次卫、餐厅和入户玄关等功能区。整体布局紧凑,动线合理,适合家庭居住。

【提取的结构化元数据】
{
  "total_dimensions": {
    "length": 12.949,
    "width": 9.192,
    "unit": "m",
    "total_area": 119.0
  },
  "rooms": [
    {
      "name": "客厅",
      "position": "中央偏右",
      "dimensions": {
        "length": 5.198,
        "width": 3.188,
        "area": 16.57,
        "unit": "m"
      },
      "furniture": [
        "沙发",
        "茶几",
        "电视柜"
      ],
      "connected_to": [
        "餐厅",
        "入户"
      ],
      "windows": 1,
      "doors": 1
    },
    {
      "name": "主卧",
      "position": "左下",
      "dimensions": {
        "length": 4.113,
        "width": 3.617,
        "area": 14.88,
        "unit": "m"
      },
      "furniture": [
        "床",
        "衣柜"
      ],
      "connected_to": [
        "主卫"
      ],
      "windows": 1,
      "doors": 1
    },
    {
      "name": "次卧",
      "position": "右上",
      "dimensions": {
        "length": 3.409,
        "width": 3.198,
        "area": 10.89,
        "unit": "m"
      },
      "furniture": [
        "床",
        "衣柜"
      ],
      "connected_to": [
        "次卫"
      ],
      "windows": 1,
      "doors": 1
    },
    {
      "name": "儿童房",
      "position": "左上",
      "dimensions": {
        "length": 3.327,
        "width": 3.611,
        "area": 12.0,
        "unit": "m"
      },
      "furniture": [
        "床",
        "衣柜"
      ],
      "connected_to": [
        "厨房"
      ],
      "windows": 1,
      "doors": 1
    },
    {
      "name": "厨房",
      "position": "上方中央",
      "dimensions": {
        "length": 2.733,
        "width": 2.49,
        "area": 6.8,
        "unit": "m"
      },
      "furniture": [
        "橱柜",
        "灶台"
      ],
      "connected_to": [
        "儿童房",
        "次卫"
      ],
      "windows": 1,
      "doors": 1
    }
  ],
  "annotations": [
    {
      "type": "dimension",
      "value": "12949",
      "parsed_value": 12.949,
      "unit": "m",
      "description": "外墙总长"
    },
    {
      "type": "dimension",
      "value": "9192",
      "parsed_value": 9.192,
      "unit": "m",
      "description": "外墙总宽"
    }
  ],
  "symbols": [
    {
      "type": "door",
      "count": 6,
      "positions": [
        "客厅-餐厅",
        "主卧-主卫",
        "次卧-次卫",
        "儿童房-厨房"
      ]
    }
  ],
  "circulation": {
    "main_entrance": "右下",
    "main_path": "入户 → 客厅 → 餐厅 → 各房间",
    "layout_type": "分隔式"
  },
  "design_notes": [
    "主卧带独立卫生间",
    "动线流畅",
    "采光良好"
  ]
}

  接下来,我们需要将这些分析结果存储到向量数据库中,以支持高效的检索和问答。

5.5 构建向量数据库存储系统

  在多模态RAG系统中,向量数据库扮演着至关重要的角色。它不仅存储了图纸的文本描述,还保存了提取的结构化元数据,使得我们可以:

  1. 语义检索:根据用户问题的语义,而非关键词匹配,找到相关图纸
  2. 元数据过滤:基于结构化信息(如尺寸、材料等)进行精确筛选
  3. 高效索引:即使有成千上万张图纸,也能毫秒级返回结果

  向量数据库的核心思想是将文本转换为高维向量(Embedding),相似的文本在向量空间中距离较近。当用户提问时,问题也被转换为向量,然后通过计算距离找到最相关的文档。

    文本内容 → Embedding模型 → 向量表示 → 存储到ChromaDB
    用户问题 → Embedding模型 → 问题向量 → 相似度检索 → 返回Top-K结果

  接下来,我们就来实现这个向量存储系统。

class VectorStoreManager:
    """向量数据库管理器 - 基于ChromaDB"""
    
    def __init__(self, persist_directory: str = "./chroma_db_floor_plan"):
        """初始化向量数据库"""
        self.persist_directory = persist_directory
        os.makedirs(persist_directory, exist_ok=True)
        
        # 初始化 Embedding 模型
        print("正在初始化 Qwen Embedding 模型...")
        self.embeddings = QwenEmbeddings(
            model="text-embedding-v4",
            api_key=os.getenv("DASHSCOPE_API_KEY")  # 从环境变量读取
        )
        
        # 初始化 ChromaDB
        self.vector_store = Chroma(
            persist_directory=persist_directory,
            embedding_function=self.embeddings,
            collection_name="floor_plans"
        )
        
        # 文本分割器
        self.text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=500,
            chunk_overlap=100,
            separators=["\n\n", "\n", "。", ".", " ", ""]
        )
        
        print(f"向量数据库初始化完成")
    
    def add_document(
        self,
        file_id: str,
        file_name: str,
        content: str,
        extracted_info: Dict[str, Any]
    ) -> int:
        """添加户型文档到向量库"""
        print(f"\n添加文档到向量库: {file_name}")
        
        # 1. 分割文本
        chunks = self.text_splitter.split_text(content)
        print(f"  文本分割为 {len(chunks)} 个块")
        
        # 2. 创建Document对象
        documents = []
        for i, chunk in enumerate(chunks):
            metadata = {
                "file_id": file_id,
                "file_name": file_name,
                "chunk_id": i,
                "total_chunks": len(chunks),
                "extracted_info_json": json.dumps(extracted_info, ensure_ascii=False)
            }
            
            # 提取关键字段到元数据顶层(便于过滤和问答)
            if "total_dimensions" in extracted_info:
                dims = extracted_info["total_dimensions"]
                metadata["total_area"] = float(dims.get("total_area", 0))
                metadata["total_length"] = float(dims.get("length", 0))
                metadata["total_width"] = float(dims.get("width", 0))
            
            if "rooms" in extracted_info:
                rooms = extracted_info["rooms"]
                metadata["room_count"] = len(rooms)
                # 统计卧室数量
                bedrooms = [r for r in rooms if "卧" in r.get("name", "")]
                metadata["bedroom_count"] = len(bedrooms)
            
            if "circulation" in extracted_info:
                circ = extracted_info["circulation"]
                metadata["layout_type"] = circ.get("layout_type", "")
            
            documents.append(Document(
                page_content=chunk,
                metadata=metadata
            ))
        
        # 3. 添加到向量库
        ids = [f"{file_id}_chunk_{i}" for i in range(len(documents))]
        self.vector_store.add_documents(documents, ids=ids)
        
        print(f"文档已添加,共 {len(documents)} 个文本块")
        return len(documents)
    
    def search(
        self,
        query: str,
        top_k: int = 5
    ) -> List[Dict[str, Any]]:
        """向量检索"""
        print(f"\n执行向量检索: {query[:50]}...")
        
        # 执行相似度检索
        results = self.vector_store.similarity_search_with_score(
            query,
            k=top_k
        )
        
        # 格式化结果
        formatted_results = []
        for doc, score in results:
            formatted_results.append({
                "content": doc.page_content,
                "metadata": doc.metadata,
                "similarity": float(1 - score)
            })
        
        print(f"✓ 找到 {len(formatted_results)} 个相关结果")
        return formatted_results


# 创建向量数据库管理器
vector_manager = VectorStoreManager()
print("✓ 向量数据库管理器已就绪!")
正在初始化 Qwen Embedding 模型...
✓ 初始化通义千问 Embedding
  模型: text-embedding-v4
  维度: 1024


Failed to send telemetry event ClientStartEvent: capture() takes 1 positional argument but 3 were given
Failed to send telemetry event ClientCreateCollectionEvent: capture() takes 1 positional argument but 3 were given


向量数据库初始化完成
✓ 向量数据库管理器已就绪!

5.6 智能问答功能

  这里我们将实现两种问答模式:

  1. 直接问答:从元数据直接提取答案(如"有几个卧室?"、“客厅多大?”)
  2. 图像检索:返回相关户型列表(如"找3室2厅的户型")
class IntelligentQA:
    """智能问答系统 - LLM驱动版本"""
    
    def __init__(self, vector_manager: VectorStoreManager, llm_client: OpenAI, model_name: str):
        self.vector_manager = vector_manager
        self.llm_client = llm_client
        self.model_name = model_name
    
    def direct_answer(self, question: str, top_k: int = 3) -> Dict[str, Any]:
        """使用LLM基于元数据生成答案"""
        print(f"\n{'='*60}")
        print("LLM智能问答模式")
        print(f"   问题: {question}")
        print("="*60)
        
        # 1. 向量检索
        results = self.vector_manager.search(question, top_k=top_k)
        
        if not results:
            return {
                "answer": "抱歉,没有找到相关户型信息。",
                "sources": [],
                "mode": "direct_answer"
            }
        
        # 2. 收集所有相关的元数据
        context_parts = []
        for i, result in enumerate(results):
            metadata = result["metadata"]
            
            # 解析结构化信息
            extracted_info = {}
            if "extracted_info_json" in metadata:
                try:
                    extracted_info = json.loads(metadata["extracted_info_json"])
                except:
                    pass
            
            context_parts.append(f"""
文档 {i+1}{metadata.get('file_name', '未知文件')}
VLM描述:{result['content']}
结构化数据:{json.dumps(extracted_info, ensure_ascii=False, indent=2)}
相似度:{result['similarity']:.2f}
""")
        
        # 3. 构建LLM提示词
        context = "\n".join(context_parts)
        
        prompt = f"""你是一个专业的房产顾问,请根据提供的户型信息回答用户问题。

用户问题:{question}

可用的户型信息:
{context}

请根据以上信息回答用户问题,要求:
1. 直接、准确地回答问题
2. 如果涉及具体数据(面积、尺寸等),请引用准确数值
3. 如果问题涉及多个户型,请进行对比
4. 保持回答简洁明了
5. 在回答末尾注明信息来源

回答:"""

        # 4. 调用LLM生成答案
        print("正在调用LLM生成智能答案...")
        response = self.llm_client.chat.completions.create(
            model=self.model_name,
            messages=[
                {"role": "system", "content": "你是一个专业的房产顾问,擅长分析户型信息并回答客户问题。"},
                {"role": "user", "content": prompt}
            ],
            max_tokens=1000,
            temperature=0.1
        )
        
        answer = response.choices[0].message.content
        
        print(f"LLM答案生成完成!")
        print("="*60)
        
        return {
            "answer": answer,
            "sources": [{
                "file_id": result["metadata"].get("file_id"),
                "file_name": result["metadata"].get("file_name"),
                "similarity": result["similarity"]
            } for result in results],
            "mode": "direct_answer"
        }
    
    def search_images(self, query: str, top_k: int = 5) -> Dict[str, Any]:
        """
        图像检索模式 - 也用LLM来生成更智能的检索结果描述
        """
        print(f"\n{'='*60}")
        print("LLM智能检索模式")
        print(f"   查询: {query}")
        print("="*60)
        
        # 1. 向量检索
        results = self.vector_manager.search(query, top_k=top_k * 2)
        
        if not results:
            return {
                "message": f"没有找到与 '{query}' 相关的户型。",
                "images": [],
                "mode": "search_images"
            }
        
        # 2. 按文件聚合(去重)
        file_map = {}
        for result in results:
            file_id = result["metadata"].get("file_id")
            if file_id not in file_map:
                # 解析结构化信息
                extracted_info = {}
                if "extracted_info_json" in result["metadata"]:
                    try:
                        extracted_info = json.loads(result["metadata"]["extracted_info_json"])
                    except:
                        pass
                
                file_map[file_id] = {
                    "file_id": file_id,
                    "file_name": result["metadata"].get("file_name"),
                    "similarity": result["similarity"],
                    "content": result["content"],
                    "extracted_info": extracted_info,
                    "metadata": result["metadata"]
                }
            else:
                # 更新最高相似度
                if result["similarity"] > file_map[file_id]["similarity"]:
                    file_map[file_id]["similarity"] = result["similarity"]
        
        # 3. 按相似度排序
        sorted_files = sorted(
            file_map.values(),
            key=lambda x: x["similarity"],
            reverse=True
        )[:top_k]
        
        # 4. 用LLM生成智能的检索结果描述
        files_info = []
        for file_info in sorted_files:
            files_info.append({
                "file_name": file_info["file_name"],
                "similarity": file_info["similarity"],
                "description": file_info["content"],
                "details": file_info["extracted_info"]
            })
        
        search_prompt = f"""作为房产顾问,请根据检索到的户型信息,回答用户的查询需求。

用户查询:{query}

检索到的户型:
{json.dumps(files_info, ensure_ascii=False, indent=2)}

请:
1. 总结找到了几个相关户型
2. 对每个户型进行简要介绍(户型、面积、特点等)
3. 根据用户查询给出推荐意见
4. 保持专业和友好的语调

回答:"""

        print("正在生成智能检索结果...")
        response = self.llm_client.chat.completions.create(
            model=self.model_name,
            messages=[
                {"role": "system", "content": "你是专业的房产顾问,擅长根据客户需求推荐合适的户型。"},
                {"role": "user", "content": search_prompt}
            ],
            max_tokens=1000,
            temperature=0.3
        )
        
        message = response.choices[0].message.content
        
        print(f"✓ 找到 {len(sorted_files)} 个相关户型")
        print("="*60)
        
        return {
            "message": message,
            "images": [{
                "file_id": f["file_id"],
                "file_name": f["file_name"], 
                "similarity": f["similarity"]
            } for f in sorted_files],
            "mode": "search_images"
        }

    def ask(self, question: str, mode: str = "auto") -> Dict[str, Any]:
        """统一问答接口"""
        # 智能判断模式
        if mode == "auto":
            search_keywords = ["找", "有没有", "哪些", "查找", "搜索", "推荐", "比较"]
            if any(kw in question for kw in search_keywords):
                return self.search_images(question)
            else:
                return self.direct_answer(question)
        
        if mode == "direct_answer":
            return self.direct_answer(question)
        elif mode == "search_images":
            return self.search_images(question)
        else:
            raise ValueError(f"不支持的模式: {mode}")


# 重新创建问答系统
qa_system = IntelligentQA(vector_manager, vlm_client, MODEL_NAME)
print("LLM智能问答系统已就绪!")
LLM智能问答系统已就绪!
# 1. 分析平面图
image_path = "./test_data/house1.png"
question = "请详细分析这张平面图,包括房间布局、尺寸、动线等信息。"
result = analyzer.analyze(image_path, question)
✓图片加载成功: (900, 781)
图片转换为base64: 130.5 KB
正在调用VLM模型...
分析完成!Token使用: 2762
============================================================
print("【分析结果】")
print(result.answer)
print("\n【提取的元数据】")
print(json.dumps(result.extracted_info, ensure_ascii=False, indent=2))
【分析结果】
这张平面图展示了一套三居室的住宅布局,包含客厅、主卧、次卧、儿童房、厨房、主卫、次卫和阳台等功能区。整体布局紧凑,动线合理。

【提取的元数据】
{
  "total_dimensions": {
    "length": 12.949,
    "width": 9.192,
    "unit": "m",
    "total_area": 119.0
  },
  "rooms": [
    {
      "name": "客厅",
      "position": "中央偏左",
      "dimensions": {
        "length": 5.198,
        "width": 3.188,
        "area": 16.58,
        "unit": "m"
      },
      "furniture": [
        "沙发",
        "茶几",
        "电视柜"
      ],
      "connected_to": [
        "餐厅",
        "阳台"
      ],
      "windows": 1,
      "doors": 1
    },
    {
      "name": "主卧",
      "position": "左下角",
      "dimensions": {
        "length": 3.617,
        "width": 4.113,
        "area": 14.88,
        "unit": "m"
      },
      "furniture": [
        "床",
        "衣柜"
      ],
      "connected_to": [
        "主卫"
      ],
      "windows": 1,
      "doors": 1
    },
    {
      "name": "次卧",
      "position": "右上角",
      "dimensions": {
        "length": 3.499,
        "width": 3.188,
        "area": 11.15,
        "unit": "m"
      },
      "furniture": [
        "床",
        "衣柜"
      ],
      "connected_to": [
        "次卫"
      ],
      "windows": 1,
      "doors": 1
    },
    {
      "name": "儿童房",
      "position": "左上角",
      "dimensions": {
        "length": 3.617,
        "width": 3.127,
        "area": 11.31,
        "unit": "m"
      },
      "furniture": [
        "床",
        "衣柜"
      ],
      "connected_to": [
        "厨房"
      ],
      "windows": 1,
      "doors": 1
    },
    {
      "name": "厨房",
      "position": "中央偏上",
      "dimensions": {
        "length": 2.733,
        "width": 2.49,
        "area": 6.8,
        "unit": "m"
      },
      "furniture": [
        "橱柜",
        "灶台"
      ],
      "connected_to": [
        "儿童房",
        "次卫"
      ],
      "windows": 1,
      "doors": 1
    },
    {
      "name": "主卫",
      "position": "左下角",
      "dimensions": {
        "length": 2.22,
        "width": 1.8,
        "area": 3.996,
        "unit": "m"
      },
      "furniture": [
        "浴缸",
        "洗手台"
      ],
      "connected_to": [
        "主卧"
      ],
      "windows": 0,
      "doors": 1
    },
    {
      "name": "次卫",
      "position": "右上角",
      "dimensions": {
        "length": 2.49,
        "width": 1.5,
        "area": 3.735,
        "unit": "m"
      },
      "furniture": [
        "淋浴",
        "洗手台"
      ],
      "connected_to": [
        "次卧",
        "厨房"
      ],
      "windows": 0,
      "doors": 1
    }
  ],
  "annotations": [
    {
      "type": "dimension",
      "value": "12949",
      "parsed_value": 12.949,
      "unit": "m",
      "description": "外墙总长"
    },
    {
      "type": "dimension",
      "value": "9192",
      "parsed_value": 9.192,
      "unit": "m",
      "description": "外墙总宽"
    }
  ],
  "symbols": [
    {
      "type": "door",
      "count": 7,
      "positions": [
        "客厅-餐厅",
        "主卧-主卫",
        "次卧-次卫",
        "儿童房-厨房"
      ]
    }
  ],
  "circulation": {
    "main_entrance": "右下角",
    "main_path": "主入口 → 玄关 → 客厅 → 各房间",
    "layout_type": "分隔式"
  },
  "design_notes": [
    "主卧带独立卫生间",
    "动线流畅",
    "客厅与餐厅相连"
  ]
}
import uuid
# 2. 存储到向量数据库
file_id = str(uuid.uuid4())
file_name = "house1.png"
chunk_count = vector_manager.add_document(
    file_id=file_id,
    file_name=file_name,
    content=result.answer,  # 将VLM的回答作为文本内容
    extracted_info=result.extracted_info  # 结构化的元数据
)
添加文档到向量库: house1.png
  文本分割为 1 个块
文档已添加,共 1 个文本块
print(f"\n户型已成功存入向量数据库!")
print(f"  文件ID: {file_id}")
print(f"  文本块数: {chunk_count}")
户型已成功存入向量数据库!
  文件ID: d7ea1633-51f4-433c-8b9a-0cc1ba813e21
  文本块数: 1
# 示例1:直接问答
result1 = qa_system.ask("这个户型有几个卧室?")

print(f"回答:{result1['answer']}")
Failed to send telemetry event CollectionQueryEvent: capture() takes 1 positional argument but 3 were given



============================================================
LLM智能问答模式
   问题: 这个户型有几个卧室?
============================================================

执行向量检索: 这个户型有几个卧室?...
✓ 找到 3 个相关结果
正在调用LLM生成智能答案...
LLM答案生成完成!
============================================================
回答:这个户型有三个卧室,分别是主卧、次卧和儿童房。

信息来源:文档 1、文档 2、文档 3。
result2 = qa_system.ask("找一下有没有3室2厅的户型?")
print(f"\n问题:找一下有没有3室2厅的户型?")
print(f"回答:{result2['message']}")
============================================================
LLM智能检索模式
   查询: 找一下有没有3室2厅的户型?
============================================================

执行向量检索: 找一下有没有3室2厅的户型?...


Number of requested results 10 is greater than number of elements in index 3, updating n_results = 3


✓ 找到 3 个相关结果
正在生成智能检索结果...
✓ 找到 3 个相关户型
============================================================

问题:找一下有没有3室2厅的户型?
回答:感谢您的查询!根据您的需求,我们找到了三个符合条件的3室2厅的户型。以下是每个户型的简要介绍:

1. **户型一**
   - **面积**: 总面积为119平方米。
   - **特点**: 该户型布局紧凑,动线合理,包含客厅、主卧、次卧、儿童房、厨房、主卫、次卫和阳台等功能区。主卧带有独立卫生间,客厅与餐厅相连,采光良好。

2. **户型二**
   - **面积**: 总面积为119平方米。
   - **特点**: 该户型设计为分隔式布局,包含客厅、主卧、次卧、儿童房、厨房、主卫、次卫、餐厅和入户玄关。主卧同样带有独立卫生间,客餐厅一体化设计,动线流畅。

3. **户型三**
   - **面积**: 总面积为119平方米。
   - **特点**: 该户型为开放式布局,客厅和餐厅连通,动线流畅。包含客厅、主卧、次卧、儿童房、厨房、主卫、次卫和入户玄关。主卧带有独立卫生间,采光良好。

**推荐意见**:
根据您的需求,这三个户型都符合3室2厅的标准,并且每个户型都有其独特的设计特点。若您偏好动线流畅且采光良好的设计,户型三可能是一个不错的选择。若您更注重客餐厅一体化设计,户型二则可能更适合您。希望这些信息能帮助您做出更好的选择!

如需进一步了解或预约看房,请随时与我们联系。我们很乐意为您提供更多帮助!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值