二: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 个文档向量。
二、文本分割:不止 “固定长度”,更要 “语义完整”
新文档《2-LLamaindex RAG 基础聊天实现.pdf》2.2.1 节指出,文本分割是 “影响检索精度的关键步骤”—— 若把 “设备最大工作压力 100MPa” 拆成 “设备最大工作压力” 和 “100MPa”,检索时就会丢失关键信息。我们补充 3 种实用的分割策略:
1. 3 种核心分割方式对比
分割方式 | 原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
句分割 | 按句号、感叹号、换行符分割(如 “。!?\n”) | 保留完整语义(如一个完整的参数描述) | 块大小不一(短句 10 字,长句 200 字) | 技术手册、法律文档(语义连贯优先) |
固定长度分割 | 按固定 token 数分割(如 512token / 块) | 块大小均匀,便于向量化和存储 | 可能拆分完整语义(如拆分参数句) | 新闻、报告(长文本需均匀分块) |
语义窗口分割 | 按段落、标题分割,块间保留 10%~20% 重叠 | 兼顾语义完整与检索精度(重叠避免拆分) | 实现稍复杂,需解析文档结构 | 带标题的文档(如产品手册章节) |
2. LlamaIndex 实操:语义窗口分割代码
新文档《4-LlamaIndex 传统 RAG 系统设计及开发.pdf》2.4 节使用SentenceSplitter
实现分割,我们补充完整代码(含重叠配置):
from llama_index.core.node_parser import SentenceSplitterdef 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%=60separator="\n" # 按换行符分割,保留段落语义)else:# 普通文档:块大(512token)、重叠低(10%),减少冗余return SentenceSplitter(chunk_size=512,chunk_overlap=51, # 512*10%≈51separator="。" # 按句号分割,保留句子语义)# 示例:分割技术文档
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
专门配置了本地嵌入模型,代码可直接复用,关键步骤如下:
# embeddings.py:本地嵌入模型配置
from llama_index.embeddings.huggingface import HuggingFaceEmbeddingdef 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. 模型调用测试:看文本如何转向量
# 测试嵌入模型:文本→向量
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)
# utils/milvus.py:Milvus向量数据库工具类
import os
from pymilvus import MilvusClient
from rag.config import RagConfig # 从配置文件读取参数# 单例模式:全局唯一Milvus客户端
class MilvusUtils:_instance = Nonedef __new__(cls):if cls._instance is None:cls._instance = super().__new__(cls)# 从配置文件初始化客户端(避免硬编码)cls._instance.client = MilvusClient(uri=RagConfig.milvus_uri)return cls._instancedef 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. 工具类使用示例
# 初始化工具类
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)
# 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 的配置,支持从环境变量读取参数,避免硬编码:
# rag/config.py:Milvus及其他组件的配置
import os
from pydantic import BaseModel, Fieldclass 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集合名:相当于数据库的“表”,默认用defaultmilvus_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,关键代码解析:
# base_rag.py:创建Milvus远程索引
from llama_index.vector_stores.milvus import MilvusVectorStore
from llama_index.core import VectorStoreIndex, StorageContext
from .config import rag_configasync 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. 生成向量并存入Milvusindex = VectorStoreIndex.from_documents(data,storage_context=storage_context,show_progress=True # 显示处理进度(方便调试))return index
5. Milvus 检索测试:找 “最像” 的向量
# 测试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 初步:不止文本,还能处理图片
新文档《2-LLamaindex RAG 基础聊天实现.pdf》3.4 节和《4-LlamaIndex 传统 RAG 系统设计及开发.pdf》1 节,介绍了多模态 RAG 的概念 —— 传统 RAG 仅处理文本,而多模态 RAG 可处理图片、图表等,这是企业场景的重要扩展。
1. 多模态 RAG 的核心流程(图片处理)
- 图片预处理:用户上传设备图片(如标注 “100MPa” 的压力表照片),调用 OCR 工具(如 Umi-OCR)提取文字;
- 多模态向量化:用视觉语言模型(VLM,如 NeVA 22B)将图片转换为向量,同时将 OCR 文本也转换为向量;
- 混合检索:用户提问 “图片中设备的压力是多少?” 时,同时检索图片向量和文本向量,找到最相关结果;
- 生成答案:将图片向量对应的原文(OCR 结果)和文本片段一起送入 LLM,生成答案。
2. 图片 OCR 提取实操(基于 Umi-OCR)
新文档《5-LlamaIndex RAG 知识库对话及 OCR 识别.pdf》2.3 节提供了 Umi-OCR 的调用代码,我们补充简化版(需先启动 Umi-OCR 的 HTTP 服务):
# 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 base64with 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” 关联。这个设计的核心逻辑是:
- 存储分工:Milvus 擅长向量检索,PostgreSQL 擅长结构化数据(元数据)查询,各司其职;
- 检索流程:
- 第一步:问题向量→Milvus→返回相似向量的 ID 列表;
- 第二步:向量 ID→PostgreSQL→查询对应的原文片段 + 文件路径 + 页数;
- 第三步:原文片段→组合 Prompt→发给 LLM 生成答案。
PostgreSQL 的表结构设计:
-- 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 从 “代码” 变成 “可用工具”。