二:RAG 的 “语义密码”:向量、嵌入模型与 Milvus 向量数据库实操

上一篇我们知道 RAG 的核心是 "先检索再生成",但 "怎么精准找到相关文档" 是个技术活 ------ 比如用户问 "设备的最大承压",系统要能定位到文档中 "工作压力上限 100MPa" 的片段,而不是 "维护流程" 的内容。

这背后依赖三大核心技术:向量(文本的数字密码)嵌入模型(密码生成器)向量数据库(密码检索柜)。今天从 "原理 + 代码" 双维度拆解,还会手把手教部署 Milvus 并测试相似性检索。

一、向量:让计算机 "理解语义" 的数字语言

我们一般通过 "意思" 判断文本相关性(比如 "承压" 和 "工作压力" 是一回事),但计算机只能处理数字 ------ 向量就是把 "语义" 翻译成 "数字数组" 的工具。

1. 向量的核心特性:3 个关键指标

  • 维度:向量是 N 个数字组成的数组,N 就是维度。你的文档中用了 BGE-small-zh 模型,输出向量是 384 维;OpenAI 的 ada-002 是 1536 维。维度越高,语义捕捉越细,但存储 / 计算成本越高。
  • 语义相关性 :相似文本的向量 "距离近",不相关的 "距离远"。比如:
    • "设备最大工作压力 100MPa"→向量 A:[0.21, 0.35, -0.12, ..., 0.47](384 维)
    • "设备的最大承压是 100MPa"→向量 B:[0.22, 0.34, -0.11, ..., 0.46]
    • 两者的 "余弦相似度" 接近 0.98(最大值 1),计算机就知道它们语义几乎一致。
  • 唯一性:即使是微小的语义差异,向量也会不同。比如 "100MPa" 改成 "120MPa",向量中至少 10% 的数字会变化。

2. 向量计算的关键方法:余弦相似度

判断两个向量是否相关,最常用 "余弦相似度"------ 计算两个向量在高维空间中的夹角余弦值,范围是 [-1,1]:

  • 0.8~1.0:极相似(如 "工作压力" 和 "承压");
  • 0.5~0.8:较相似(如 "工作压力" 和 "运行压力");
  • 0~0.5:弱相关(如 "工作压力" 和 "维护周期");
  • 负数:不相关(如 "工作压力" 和 "员工考勤")。

Milvus 就是用这个方法快速找到 "与问题向量最像" 的 Top-K 个文档向量。

二、文本分割:不止 "固定长度",更要 "语义完整"

文本分割是 "影响检索精度的关键步骤"------ 若把 "设备最大工作压力 100MPa" 拆成 "设备最大工作压力" 和 "100MPa",检索时就会丢失关键信息。我们补充 3 种实用的分割策略:

1. 3 种核心分割方式对比

分割方式 原理 优点 缺点 适用场景
句分割 按句号、感叹号、换行符分割(如 "。!?\n") 保留完整语义(如一个完整的参数描述) 块大小不一(短句 10 字,长句 200 字) 技术手册、法律文档(语义连贯优先)
固定长度分割 按固定 token 数分割(如 512token / 块) 块大小均匀,便于向量化和存储 可能拆分完整语义(如拆分参数句) 新闻、报告(长文本需均匀分块)
语义窗口分割 按段落、标题分割,块间保留 10%~20% 重叠 兼顾语义完整与检索精度(重叠避免拆分) 实现稍复杂,需解析文档结构 带标题的文档(如产品手册章节)

2. LlamaIndex 实操:语义窗口分割代码

使用SentenceSplitter实现分割,我们补充完整代码(含重叠配置):

python 复制代码
from llama_index.core.node_parser import SentenceSplitter

def get_splitter(file_type: str):
    """
    根据文件类型选择分割器:
    - file_type: "tech"(技术文档)/"normal"(普通文档)
    """
    if file_type == "tech":
        # 技术文档:块小(300token)、重叠高(20%),避免参数拆分
        return SentenceSplitter(
            chunk_size=300,
            chunk_overlap=60,  # 300*20%=60
            separator="\n"  # 按换行符分割,保留段落语义
        )
    else:
        # 普通文档:块大(512token)、重叠低(10%),减少冗余
        return SentenceSplitter(
            chunk_size=512,
            chunk_overlap=51,  # 512*10%≈51
            separator="。"  # 按句号分割,保留句子语义
        )

# 示例:分割技术文档
splitter = get_splitter("tech")
nodes = splitter.get_nodes_from_documents(documents)  # documents是读取的文档列表

优化:分割时可添加 "文档标题 + 章节" 前缀,让块包含上下文(如 "【文档:设备手册】【章节:参数规格】设备最大工作压力 100MPa"),避免检索时 "不知道块来自哪里"。

三、嵌入模型:文本转向量的 "密码生成器"

向量不会自己生成,需要 "嵌入模型" 来完成 "文本→向量" 的转换。下面是 2 类常用模型,我们对比它们的差异和实操代码:

1. 嵌入模型的 2 大分类

类型 代表模型 优势 劣势 你的文档使用场景
闭源模型 OpenAI ada-002 语义捕捉准,支持多语言 需 API 调用,有成本,无法本地化 快速测试、小批量数据
开源模型 BGE-small-zh、M3E 免费,可本地部署,支持中文优化 大模型(如 BGE-large)速度慢 私有化部署、中文文档(你的选择)

2. 本地嵌入模型实操:BGE-small-zh 配置(来自你的 embeddings.py

文档中embeddings.py专门配置了本地嵌入模型,代码可直接复用,关键步骤如下:

python 复制代码
# embeddings.py:本地嵌入模型配置
from llama_index.embeddings.huggingface import HuggingFaceEmbedding

def embed_model_local_bge_small():
    """加载本地BGE-small-zh嵌入模型"""
    # model_name: HuggingFace上的模型地址
    # embed_batch_size:批量处理文本的大小,根据内存调整(建议16/32)
    # max_length:模型支持的最大文本长度(BGE-small-zh默认512)
    embed_model = HuggingFaceEmbedding(
        model_name="BAAI/bge-small-zh-v1.5",
        embed_batch_size=16,
        max_length=512,
        # 模型参数:device="cuda"用GPU,"cpu"用CPU(本地测试用cpu)
        device="cpu"
    )
    return embed_model

# 在base_rag.py中全局配置:
from llama_index.core import Settings
from embeddings import embed_model_local_bge_small
Settings.embed_model = embed_model_local_bge_small()  # 所有分块/查询都用这个模型

3. 模型调用测试:看文本如何转向量

python 复制代码
# 测试嵌入模型:文本→向量
embed_model = embed_model_local_bge_small()
text = "设备最大工作压力100MPa"
vector = embed_model.get_text_embedding(text)
print(f"向量维度:{len(vector)}")  # 输出384,符合BGE-small-zh的配置
print(f"前5个数字:{vector[:5]}")  # 输出如[0.023, -0.051, 0.124, 0.087, -0.032]

四、Milvus 向量数据库:存储与检索的 "高速抽屉"

当你有 10 万份文档,每份拆成 100 个片段,会生成 1000 万个向量 ------ 普通数据库(如 MySQL)无法高效查询这些向量,必须用 Milvus 这类专门的向量数据库。

1. Milvus 工具类(utils/milvus.py)

python 复制代码
# utils/milvus.py:Milvus向量数据库工具类
import os
from pymilvus import MilvusClient
from rag.config import RagConfig  # 从配置文件读取参数

# 单例模式:全局唯一Milvus客户端
class MilvusUtils:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            # 从配置文件初始化客户端(避免硬编码)
            cls._instance.client = MilvusClient(uri=RagConfig.milvus_uri)
        return cls._instance

    def list_collections(self) -> list:
        """查看所有集合(类似数据库的"表")"""
        return self.client.list_collections()

    def create_collection(self, collection_name: str, dim: int):
        """
        创建集合:
        - collection_name:集合名
        - dim:向量维度(BGE-small-zh是384维)
        """
        if not self.client.has_collection(collection_name):
            self.client.create_collection(
                collection_name=collection_name,
                dimension=dim,
                # 索引配置:小数据用IVF_FLAT(精准),大数据用HNSW(快速)
                index_params={"index_type": "IVF_FLAT", "nlist": 1024}
            )
            print(f"集合 {collection_name} 创建成功")
        else:
            print(f"集合 {collection_name} 已存在")

    def insert_vectors(self, collection_name: str, vectors: list, metadatas: list):
        """
        插入向量:
        - vectors:向量列表(如[[0.1,0.2,...],[0.3,0.4,...]])
        - metadatas:元数据列表(如[{"file_path":"xxx.pdf","page":1}])
        """
        data = [
            {"vector": vec, "metadata": meta, "id": i} 
            for i, (vec, meta) in enumerate(zip(vectors, metadatas))
        ]
        self.client.insert(collection_name=collection_name, data=data)
        print(f"成功插入 {len(vectors)} 个向量到 {collection_name}")

    def search_vectors(self, collection_name: str, query_vector: list, top_k: int = 3):
        """
        搜索相似向量:
        - query_vector:查询向量
        - top_k:返回前k个相似结果
        """
        return self.client.search(
            collection_name=collection_name,
            data=[query_vector],
            limit=top_k,
            output_fields=["metadata"]  # 返回元数据(如文件路径)
        )[0]  # 返回第一个查询的结果

    def drop_collection(self, collection_name: str):
        """删除集合(谨慎使用!)"""
        if self.client.has_collection(collection_name):
            self.client.drop_collection(collection_name=collection_name)
            print(f"集合 {collection_name} 删除成功")
        else:
            print(f"集合 {collection_name} 不存在")

2. 工具类使用示例

python 复制代码
# 初始化工具类
milvus_utils = MilvusUtils()

# 1. 创建集合(向量维度384,对应BGE-small-zh)
milvus_utils.create_collection("device_manual", dim=384)

# 2. 插入向量(假设vectors是文本块向量,metadatas是元数据)
vectors = [embed_model.get_text_embedding(chunk.text) for chunk in chunks]
metadatas = [{"file_path": chunk.metadata["file_path"]} for chunk in chunks]
milvus_utils.insert_vectors("device_manual", vectors, metadatas)

# 3. 搜索相似向量(用户问题向量)
query_vector = embed_model.get_query_embedding("设备最大工作压力是多少?")
results = milvus_utils.search_vectors("device_manual", query_vector, top_k=3)

# 4. 打印结果
for res in results:
    print(f"相似度:{res['distance']:.3f},来源:{res['entity']['metadata']['file_path']}")

3. Milvus 的核心优势(你的文档重点强调)

  • 高速检索:支持每秒百万级向量的相似性搜索,比传统数据库快 100 倍以上;
  • 分布式部署:可横向扩展,支持 TB 级向量存储(适合企业级数据);
  • 多索引类型:支持 IVF_FLAT(精准搜索)、HNSW(快速搜索)等,兼顾精度和速度;
  • 与 LlamaIndex 无缝集成 :你的文档中base_rag.py直接调用 MilvusVectorStore,无需额外适配。

4. Milvus 部署与连接(详细实操步骤)

步骤 1:安装 Milvus(本地测试用 Docker)

python 复制代码
# 1. 下载Milvus Docker Compose文件
wget https://github.com/milvus-io/milvus/releases/download/v2.3.0/milvus-standalone-docker-compose.yml -O docker-compose.yml

# 2. 启动Milvus服务(需Docker已安装)
docker-compose up -d

# 3. 检查服务状态(确保milvus-standalone启动成功)
docker-compose ps

步骤 2:Milvus 连接配置(来自你的 rag/config.py)

config.py用 Pydantic 定义了 Milvus 的配置,支持从环境变量读取参数,避免硬编码:

python 复制代码
# rag/config.py:Milvus及其他组件的配置
import os
from pydantic import BaseModel, Field

class RAGConfig(BaseModel):
    # Milvus连接地址:默认本地19530端口(Docker启动的默认端口)
    milvus_uri: str = Field(default=os.getenv("MILVUS_URI", "http://localhost:19530"), 
                           description="Milvus服务地址")
    # 嵌入模型维度:必须与BGE-small-zh的384维一致,否则报错
    embedding_model_dim: int = Field(default=384, 
                                     description="嵌入模型输出向量的维度")
    # Milvus集合名:相当于数据库的"表",默认用default
    milvus_collection: str = Field(default=os.getenv("MILVUS_COLLECTION", "rag_collection"),
                                   description="Milvus中的集合名")

# 单例实例:全局唯一配置对象
rag_config = RAGConfig()

步骤 3:Milvus 向量存储集成(来自你的 base_rag.py)

base_rag.py中,你定义了create_index方法,专门用于将向量存入 Milvus,关键代码解析:

python 复制代码
# base_rag.py:创建Milvus远程索引
from llama_index.vector_stores.milvus import MilvusVectorStore
from llama_index.core import VectorStoreIndex, StorageContext
from .config import rag_config

async def create_index_milvus(self, data):
    """
    把文档数据存入Milvus:
    data:经OCR/分块后的文档列表
    """
    # 1. 初始化Milvus向量存储
    vector_store = MilvusVectorStore(
        uri=rag_config.milvus_uri,  # 从配置读取连接地址
        collection_name=rag_config.milvus_collection,  # 集合名
        dim=rag_config.embedding_model_dim,  # 向量维度(384)
        overwrite=False,  # 避免覆盖已有集合(首次创建可设为True)
        # 索引配置:用HNSW索引,适合快速搜索
        index_params={"index_type": "HNSW", "M": 8, "efConstruction": 64}
    )

    # 2. 创建存储上下文:关联Milvus向量存储
    storage_context = StorageContext.from_defaults(vector_store=vector_store)

    # 3. 生成向量并存入Milvus
    index = VectorStoreIndex.from_documents(
        data,
        storage_context=storage_context,
        show_progress=True  # 显示处理进度(方便调试)
    )
    return index

5. Milvus 检索测试:找 "最像" 的向量

python 复制代码
# 测试Milvus检索:用问题向量找相似文档向量
from llama_index.core import QueryBundle
from .base_rag import RAG

# 1. 加载之前存入Milvus的索引
rag = RAG(files=[])
index = await rag.load_index_milvus()  # 自定义加载方法,参考你的base_rag.py

# 2. 构造问题向量
query_text = "设备的最大工作压力是多少?"
query_bundle = QueryBundle(query_text)
query_vector = Settings.embed_model.get_query_embedding(query_text)

# 3. 检索Top-3相似片段
retriever = index.as_retriever(similarity_top_k=3)
retrieved_nodes = await retriever.aretrieve(query_bundle)

# 4. 输出结果
for i, node in enumerate(retrieved_nodes):
    print(f"第{i+1}个相似片段:")
    print(f"文本:{node.text}")
    print(f"相似度得分:{node.score}")  # 得分越高越相似(0~1)
    print(f"来源文件:{node.metadata['file_path']}")  # 从PostgreSQL关联的元数据

五、多模态 RAG 初步:不止文本,还能处理图片

传统 RAG 仅处理文本,而多模态 RAG 可处理图片、图表等,这是企业场景的重要扩展。

1. 多模态 RAG 的核心流程(图片处理)

  1. 图片预处理:用户上传设备图片(如标注 "100MPa" 的压力表照片),调用 OCR 工具(如 Umi-OCR)提取文字;
  2. 多模态向量化:用视觉语言模型(VLM,如 NeVA 22B)将图片转换为向量,同时将 OCR 文本也转换为向量;
  3. 混合检索:用户提问 "图片中设备的压力是多少?" 时,同时检索图片向量和文本向量,找到最相关结果;
  4. 生成答案:将图片向量对应的原文(OCR 结果)和文本片段一起送入 LLM,生成答案。

2. 图片 OCR 提取实操(基于 Umi-OCR)

python 复制代码
# ocr.py:Umi-OCR图片文字提取
import requests
import json
from rag.config import RagConfig  # 从配置读取OCR服务地址

def ocr_image_to_text(image_path: str) -> str:
    """
    用Umi-OCR提取图片文字:
    - image_path:图片路径
    """
    # 1. 图片转Base64(Umi-OCR要求)
    import base64
    with open(image_path, "rb") as f:
        base64_str = base64.b64encode(f.read()).decode("utf-8")
    
    # 2. 调用Umi-OCR HTTP接口
    url = f"{RagConfig.ocr_base_url}/api/ocr"
    data = {
        "base64": base64_str,
        "options": {"data.format": "text"}
    }
    response = requests.post(url, data=json.dumps(data), headers={"Content-Type": "application/json"})
    response.raise_for_status()  # 报错时抛出异常
    
    # 3. 返回提取的文本
    return response.json().get("data", "")

# 示例:提取压力表图片中的文字
text = ocr_image_to_text("pressure_gauge.jpg")
print("OCR结果:", text)  # 预期输出:"设备最大工作压力:100MPa"

六、向量与元数据的 "联动":Milvus+PostgreSQL 的配合

特别强调:Milvus 只存向量,原文和元数据(文件路径、页数)存在 PostgreSQL,两者通过 "向量 ID" 关联。这个设计的核心逻辑是:

  1. 存储分工:Milvus 擅长向量检索,PostgreSQL 擅长结构化数据(元数据)查询,各司其职;
  2. 检索流程
    • 第一步:问题向量→Milvus→返回相似向量的 ID 列表;
    • 第二步:向量 ID→PostgreSQL→查询对应的原文片段 + 文件路径 + 页数;
    • 第三步:原文片段→组合 Prompt→发给 LLM 生成答案。

PostgreSQL 的表结构设计:

python 复制代码
-- PostgreSQL建表语句:存储向量ID与元数据的映射
CREATE TABLE rag_document_nodes (
    node_id VARCHAR(64) PRIMARY KEY,  -- 与Milvus的向量ID一一对应
    text TEXT NOT NULL,  -- 文档片段原文
    file_path VARCHAR(255) NOT NULL,  -- 原始文件在MinIO的路径
    page_num INT,  -- 片段所在的页码(PDF适用)
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP  -- 创建时间
);

小结

向量是 "语义密码",嵌入模型是 "密码生成器",Milvus 是 "密码检索柜"------ 这三者构成了 RAG 检索能力的基石。我们不仅掌握了向量和嵌入模型的基础,还学会了 "语义优先" 的文本分割策略、Milvus 工具类的封装(避免重复代码),甚至初步接触了多模态 RAG 的图片处理流程。下一篇我们将搭建 Web 界面,用 Chainlit 实现 "文件上传→OCR→检索→生成" 的完整交互,让 RAG 从 "代码" 变成 "可用工具"。

相关推荐
小二·2 小时前
mac下解压jar包
ide·python·pycharm
艾醒(AiXing-w)2 小时前
探索大语言模型(LLM):大模型微调方式全解析
人工智能·语言模型·自然语言处理
科兴第一吴彦祖2 小时前
基于Spring Boot + Vue 3的乡村振兴综合服务平台
java·vue.js·人工智能·spring boot·推荐算法
姚瑞南2 小时前
【AI 风向标】四种深度学习算法(CNN、RNN、GAN、RL)的通俗解释
人工智能·深度学习·算法
少女续续念3 小时前
从工具到生态:揭秘 Gitee 成为 60% 头部银行首选的底层逻辑
git
努力的白熊嗨3 小时前
多台服务器文件共享存储
服务器·后端
调试人生的显微镜3 小时前
CSS开发工具推荐与实战经验,让样式开发更高效、更精准
后端
渣哥3 小时前
多环境配置利器:@Profile 在 Spring 项目中的实战价值
javascript·后端·面试
东百牧码人3 小时前
还在使用ToList太Low了
后端