摘要
本文系统介绍了RAG(检索增强生成)系统中的索引构建与优化技术。核心内容包括:向量嵌入(Embedding)的原理、发展历程及选型方法;多模态嵌入技术(以CLIP和bge-visualized-m3为例);向量数据库(FAISS、Milvus等)的工作机制与实战应用;以及两种关键索引优化策略------上下文扩展优化(句子窗口检索)和结构化索引优化。文章还探讨了LLM知识时效性、模型更新机制及框架选择等延伸问题,为构建生产级RAG系统提供完整技术指南。
1. 向量嵌入基础
1.1. Embedding简介
向量嵌入(Embedding)是一种将真实世界中复杂、高维的数据对象(如文本、图像、音频、视频等)转换为数学上易于处理的、低维、稠密的连续数值向量的技术。
想象一下,我们将每一个词、每一段话、每一张图片都放在一个巨大的多维空间里,并给它一个独一无二的坐标。这个坐标就是一个向量,它"嵌入"了原始数据的所有关键信息。这个过程,就是 Embedding。
- 数据对象:任何信息,如文本"你好世界",或一张猫的图片。
- Embedding 模型:一个深度学习模型,负责接收数据对象并进行转换。
- 输出向量 :一个固定长度的一维数组,例如
[0.16, 0.29, -0.88, ...]。这个向量的维度(长度)通常在几百到几千之间。

1.1.1. 向量空间的语义表示
Embedding的真正意义在于,它产生的向量不是随机数值的堆砌,而是对数据语义的数学编码。
- 核心原则:在 Embedding 构建的向量空间中,语义上相似的对象,其对应的向量在空间中的距离会更近;而语义上不相关的对象,它们的向量距离会更远。
- 关键度量:我们通常使用以下数学方法来衡量向量间的"距离"或"相似度":
-
- 余弦相似度 (Cosine Similarity) :计算两个向量夹角的余弦值。值越接近 1,代表方向越一致,语义越相似。这是最常用的度量方式。
- 点积 (Dot Product) :计算两个向量的乘积和。在向量归一化后,点积等价于余弦相似度。
- 欧氏距离 (Euclidean Distance) :计算两个向量在空间中的直线距离。距离越小,语义越相似。
1.1.2. Embedding 在 RAG 中的作用
在RAG流程中,Embedding 扮演着无可替代的重要角色。
1.1.2.1. 语义检索的基础
RAG 的"检索"环节通常以基于 Embedding 的语义搜索为核心。通用流程如下:
- 离线索引构建:将知识库内文档切分后,使用 Embedding 模型将每个文档块(Chunk)转换为向量,存入专门的向量数据库中。
- 在线查询检索 :当用户提出问题时,使用同一个 Embedding 模型将用户的问题也转换为一个向量。
- 相似度计算:在向量数据库中,计算"问题向量"与所有"文档块向量"的相似度。
- 召回上下文:选取相似度最高的 Top-K 个文档块,作为补充的上下文信息,与原始问题一同送给大语言模型(LLM)生成最终答案。
1.1.2.2. 决定检索质量的关键
Embedding 的质量直接决定了 RAG 检索召回内容的准确性与相关性。一个优秀的 Embedding 模型能够精准捕捉问题和文档之间的深层语义联系,即使用户的提问和原文的表述不完全一致。反之,一个劣质的 Embedding 模型可能会因为无法理解语义而召回不相关或错误的信息,从而"污染"提供给 LLM 的上下文,导致最终生成的答案质量低下。
1.2. Embedding 技术发展
Embedding 技术的发展与自然语言处理(NLP)的进步紧密相连,尤其是在 RAG 框架出现后,对嵌入技术提出了新的要求。其演进路径大致可分为以下几个关键阶段。
1.2.1. 静态词嵌入:上下文无关的表示
- 代表模型:Word2Vec (2013), GloVe (2014)
- 主要原理 :为词汇表中的每个单词生成一个固定的、与上下文无关的向量。例如,
Word2Vec通过 Skip-gram 和 CBOW 架构,利用局部上下文窗口学习词向量,并验证了向量运算的语义能力(如国王 - 男人 + 女人 ≈ 王后)。GloVe则融合了全局词-词共现矩阵的统计信息。 - 局限性:无法处理一词多义问题。在"苹果公司发布了新手机"和"我吃了一个苹果"中,"苹果"的词向量是完全相同的,这限制了其在复杂语境下的语义表达能力。
1.2.2. 动态上下文嵌入
2017年,Transformer 架构的诞生带来了自注意力机制(Self-Attention),它允许模型在生成一个词的向量时,动态地考虑句子中所有其他词的影响。基于此,2018年 BERT 模型利用 Transformer 的编码器,通过掩码语言模型(MLM)等自监督任务进行预训练,生成了深度上下文相关的嵌入。同一个词在不同语境中会生成不同的向量,这有效解决了静态嵌入的一词多义难题。
1.2.3. RAG 对嵌入技术的新要求
开篇就提到了 RAG 框架的提出,是为了解决大型语言模型知识固化(内部知识难以更新)和 幻觉(生成的内容可能不符合事实且无法溯源)的问题 。 它通过"检索-生成"范式,动态地为 LLM 注入外部知识。这一过程的核心是语义检索,很大程度上依赖于高质量的向量嵌入。
后续 RAG 的兴起对嵌入技术提出了更高、更具体的要求:
- 领域自适应能力:通用的嵌入模型在专业领域(如法律、医疗)往往表现不佳,这就要求嵌入模型具备领域自适应的能力,能够通过微调或使用指令(如 INSTRUCTOR 模型)来适应特定领域的术语和语义。
- 多粒度与多模态支持:RAG 系统需要处理的不仅仅是短句,还可能包括长文档、代码,甚至是图像和表格。这就要求嵌入模型能够处理不同长度和类型的输入数据。
- 检索效率与混合检索:嵌入向量的维度和模型大小直接影响存储成本和检索速度。同时,为了结合语义相似性(密集检索)和关键词匹配(稀疏检索)的优点,支持混合检索的嵌入模型(如 BGE-M3)应运而生,在某些任务中成为提升召回率的关键。
1.3. 嵌入模型训练原理
了解了嵌入模型的发展,我们来简单探究一下当前主流的嵌入模型(通常是基于BERT的变体)是如何通过训练获得强大的语义理解能力的。
现代嵌入模型的核心通常是 Transformer 的编码器(Encoder)部分,BERT 就是其中的典型代表。它通过堆叠多个 Transformer Encoder 层来构建一个深度的双向表示学习网络。
1.3.1. 主要训练任务
BERT 的成功很大程度上归功于自监督学习 策略,它允许模型从海量的、无标注的文本数据中学习知识。
1.3.1.1. 任务一:掩码语言模型 (Masked Language Model, MLM)
- 过程:
-
- 随机地将输入句子中 15% 的词元(Token)替换为一个特殊的
[MASK]标记。 - 让模型去预测这些被遮盖住的原始词元是什么。
- 随机地将输入句子中 15% 的词元(Token)替换为一个特殊的
- 目标:通过这个任务,模型被迫学习每个词元与其上下文之间的关系,从而掌握深层次的语境语义。
1.3.1.2. 任务二:下一句预测 (Next Sentence Prediction, NSP)
- 过程:
-
- 构造训练样本,每个样本包含两个句子 A 和 B。
- 其中 50% 的样本,B 是 A 的真实下一句(IsNext);另外 50% 的样本,B 是从语料库中随机抽取的句子(NotNext)。
- 让模型判断 B 是否是 A 的下一句。
- 目标:这个任务让模型学习句子与句子之间的逻辑关系、连贯性和主题相关性。
- 重要说明:后续的研究(如 RoBERTa)发现,NSP 任务可能过于简单,甚至会损害模型性能。因此,许多现代的预训练模型(如 RoBERTa、SBERT)在预训练阶段移除了 NSP。
1.3.2. 效果增强策略
虽然 MLM 和 NSP 赋予了模型强大的基础语义理解能力,但为了在检索任务中表现更佳,现代嵌入模型通常会引入更具针对性的训练策略。
- 度量学习 (Metric Learning) :
-
- 思想:直接以"相似度"作为优化目标。
- 方法 :收集大量相关的文本对(例如,(问题,答案)、(新闻标题,正文))。训练的目标是优化向量空间中的相对距离:让"正例对"的向量表示在空间中被"拉近",而"负例对"的向量表示被"推远"。关键在于优化排序关系,而非追求绝对的相似度值(如 1 或 0),因为过度追求极端值可能导致模型过拟合。
- 对比学习 (Contrastive Learning) :
-
- 思想:在向量空间中,将相似的样本"拉近",将不相似的样本"推远"。
- 方法 :构建一个三元组(Anchor, Positive, Negative)。其中,Anchor 和 Positive 是相关的(例如,同一个问题的两种不同问法),Anchor 和 Negative 是不相关的。训练的目标是让
distance(Anchor, Positive)尽可能小,同时让distance(Anchor, Negative)尽可能大。
1.4. 嵌入模型选型指南
1.4.1. 从 MTEB 排行榜开始
MTEB (Massive Text Embedding Benchmark) 是一个由 Hugging Face 维护的、全面的文本嵌入模型评测基准。它涵盖了分类、聚类、检索、排序等多种任务,并提供了公开的排行榜,为评估和选择嵌入模型提供了重要的参考依据。

下面这张图是网站中的模型评估图像,直观地展示了在选择开源嵌入模型时需要权衡的四个核心维度:
- 横轴 - 模型参数量 (Number of Parameters) :代表了模型的大小。通常,参数量越大的模型(越靠右),其潜在能力越强,但对计算资源的要求也越高。
- 纵轴 - 平均任务得分 (Mean Task Score) :代表了模型的综合性能。这个分数是模型在分类、聚类、检索等一系列标准 NLP 任务上的平均表现。分数越高(越靠上),说明模型的通用语义理解能力越强。
- 气泡大小 - 嵌入维度 (Embedding Size) :代表了模型输出向量的维度。气泡越大,维度越高,理论上能编码更丰富的语义细节,但同时也会占用更多的存储和计算资源。
- 气泡颜色 - 最大处理长度 (Max Tokens) :代表了模型能处理的文本长度上限。颜色越深,表示模型能处理的 Token 数量越多,对长文本的适应性越好。

MTEB 榜单可以帮助我们快速筛选掉大量不合适的模型。但需要注意,榜单上的得分是在通用数据集上评测的,可能无法完全反映模型在你特定业务场景下的表现。
1.4.2. 关键评估维度
在查看榜单时,除了分数,还需要关注以下几个关键维度:
- 任务 (Task) :对于 RAG 应用,需要重点关注模型在
Retrieval(检索) 任务下的排名。 - 语言 (Language) :模型是否支持你的业务数据所使用的语言?对于中文 RAG,应选择明确支持中文或多语言的模型。
- 模型大小 (Size) :模型越大,通常性能越好,但对硬件(显存)的要求也越高,推理速度也越慢。需要根据你的部署环境和性能要求来权衡。
- 维度 (Dimensions) :向量维度越高,能编码的信息越丰富,但也会占用更多的存储空间和计算资源。
- 最大 Token 数 (Max Tokens) :这决定了模型能处理的文本长度上限。这个参数是你设计文本分块(Chunking)策略时必须考虑的重要依据,块大小不应超过此限制。
- 得分与机构 (Score & Publisher) :结合模型的得分排名和其发布机构的声誉进行初步筛选。知名机构发布的模型通常质量更有保障。
- 成本 (Cost) :如果是使用 API 服务的模型,需要考虑其调用成本;如果是自部署开源模型,则需要评估其对硬件资源的消耗(如显存、内存)以及带来的运维成本。
1.4.3. 迭代测试与优化
不要只依赖公开榜单做最终决定。
- 确定基线 (Baseline) :根据上述维度,选择几个符合要求的模型作为你的初始基准模型。
- 构建私有评测集 :根据真实业务数据,手动创建一批高质量的评测样本,每个样本包含一个典型用户问题和它对应的标准答案(或最相关的文档块)。
- 迭代优化 : - 使用基线模型在你的私有评测集上运行,评估其召回的准确率和相关性。 - 如果效果不理想,可以尝试更换模型,或者调整 RAG 流程的其他环节(如文本分块策略)。 - 通过几轮的对比测试和迭代优化,最终选出在你的特定场景下表现最佳的那个"心仪"模型。
2. 多模态嵌入
现代 AI 的一项重要突破,是将简单的词向量发展成了能统一理解图文、音视频的复杂系统。**这一发展建立在注意力机制、Transformer 架构和对比学习等关键技术之上,**它们解决了在共享向量空间中对齐不同数据模态的核心挑战。其发展环环相扣:Word2Vec 为 BERT 的上下文理解铺路,而 BERT 又为 CLIP 等模型的跨模态能力奠定了基础。
2.1. 多模态嵌入的作用
前面的章节介绍了如何为文本创建向量嵌入。然而,仅有文本的世界是不完整的。现实世界的信息是多模态的,包含图像、音频、视频等。传统的文本嵌入无法理解"那张有红色汽车的图片"这样的查询,因为文本向量和图像向量处于相互隔离的空间,存在一堵"模态墙"。
多模态嵌入 (Multimodal Embedding) 的目标正是为了打破这堵墙。其目的是将不同类型的数据(如图像和文本)映射到同一个共享的向量空间。在这个统一的空间里,一段描述"一只奔跑的狗"的文字,其向量会非常接近一张真实小狗奔跑的图片向量。
实现这一目标的关键,在于解决 跨模态对齐 (Cross-modal Alignment) 的挑战。以对比学习、视觉 Transformer (ViT) 等技术为代表的突破,让模型能够学习到不同模态数据之间的语义关联,最终催生了像 CLIP 这样的模型。
2.2. CLIP 模型浅析
在图文多模态领域,OpenAI 的 CLIP (Contrastive Language-Image Pre-training) 是一个很有影响力的模型,它为多模态嵌入定义了一个有效的范式。
CLIP 的架构清晰简洁。它采用双编码器架构 (Dual-Encoder Architecture),包含一个图像编码器和一个文本编码器,分别将图像和文本映射到同一个共享的向量空间中。

图:CLIP 的工作流程。(1) 通过对比学习训练双编码器,对齐图文向量空间。(2)和(3) 展示了如何利用该空间,通过图文相似度匹配实现零样本预测。
为了让这两个编码器学会"对齐"不同模态的语义,CLIP 在训练时采用了对比学习 (Contrastive Learning) 策略。在处理一批图文数据时,模型的目标是:最大化正确图文对的向量相似度,同时最小化所有错误配对的相似度。通过这种"拉近正例,推远负例"的方式,模型从海量数据中学会了将语义相关的图像和文本在向量空间中拉近。
这种大规模的对比学习赋予了CLIP 有效的零样本(Zero-shot)识别能力。它能将一个传统的分类任务,转化为一个"图文检索"问题------例如,要判断一张图片是不是猫,只需计算图片向量与"a photo of a cat"文本向量的相似度即可。这使得 CLIP 无需针对特定任务进行微调,就能实现对视觉概念的泛化理解。
2.3. 常用多模态嵌入模型(以bge-visualized-m3为例)
虽然 CLIP 为图文预训练提供了重要基础,但多模态领域的研究迅速发展,涌现了许多针对不同目标和场景进行优化的模型。例如,BLIP 系列专注于提升细粒度的图文理解与生成能力,而 ALIGN 则证明了利用海量噪声数据进行大规模训练的有效性。
在众多优秀的模型中,由北京智源人工智能研究院(BAAI)开发的 bge-visualized-m3(Visualized-BGE 的 M3 版本) 是一个很有代表性的现代多模态嵌入模型。它是在 BGE-M3(文本嵌入底座)的基础上引入图像能力而来,体现了当前技术向"更统一、更全面"发展的趋势。
bge-visualized-m3 的核心特性也可以概括为"M3"(主要继承自其文本底座 BGE-M3):
- 多语言性 (Multi-Linguality):支持超过 100 种语言的文本表示,可用于跨语言的图文检索(文本侧)。
- 多功能性 (Multi-Functionality):在文本检索场景下,可按需求使用密集检索(Dense Retrieval)、多向量检索(Multi-Vector Retrieval)等不同范式。
- 多粒度性 (Multi-Granularity):文本侧可处理从短句到长达 8192 个 token 的长文档,覆盖更广泛的应用需求。
在技术架构上,bge-visualized-m3会先用视觉编码器提取图像的patch token,再将其映射到与文本同维度的"图像 token",与文本 token 一起送入 BGE 的 Transformer 编码器进行联合建模,最终得到可用于图文检索的统一向量表示。
2.4. 多模态嵌入模型代码示例
2.4.1. 环境准备
步骤1:安装 visual_bge 模块
# 进入 visual_bge 目录
cd code/C3/visual_bge
# 安装 visual_bge 模块及其依赖
pip install -e .
# 返回上级目录
cd ..Copy to clipboardErrorCopied
步骤2:下载模型权重
# 运行模型下载脚本
python download_model.pyCopy to clipboardErrorCopied
模型下载脚本会自动检查 ../../models/bge/ 目录下是否存在模型文件,如果不存在则从 Hugging Face 镜像站下载。
2.4.2. 多模态相似度计算示例
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
import torch
from visual_bge.visual_bge.modeling import Visualized_BGE
# 加载模型
model = Visualized_BGE(model_name_bge="BAAI/bge-base-en-v1.5",
model_weight="../../models/bge/Visualized_base_en_v1.5.pth")
model.eval()
with torch.no_grad():
text_emb = model.encode(text="github开源组织的logo")
img_emb_1 = model.encode(image="../../data/C3/imgs/datawhale01.png")
multi_emb_1 = model.encode(image="../../data/C3/imgs/datawhale01.png", text="datawhale开源组织的logo")
img_emb_2 = model.encode(image="../../data/C3/imgs/datawhale02.png")
multi_emb_2 = model.encode(image="../../data/C3/imgs/datawhale02.png", text="datawhale开源组织的logo")
# 计算相似度
sim_1 = img_emb_1 @ img_emb_2.T
sim_2 = img_emb_1 @ multi_emb_1.T
sim_3 = text_emb @ multi_emb_1.T
sim_4 = multi_emb_1 @ multi_emb_2.T
print("=== 相似度计算结果 ===")
print(f"纯图像 vs 纯图像: {sim_1}")
print(f"图文结合1 vs 纯图像: {sim_2}")
print(f"图文结合1 vs 纯文本: {sim_3}")
print(f"图文结合1 vs 图文结合2: {sim_4}")Copy to clipboardErrorCopied
代码解读:
- 模型架构 :
Visualized_BGE是通过将图像token嵌入集成到BGE文本嵌入框架中构建的通用多模态嵌入模型,具备处理超越纯文本的多模态数据的灵活性。 - 模型参数:
-
model_name_bge: 指定底层BGE文本嵌入模型,继承其强大的文本表示能力。model_weight: Visual BGE的预训练权重文件,包含视觉编码器参数。
- 多模态编码能力: Visual BGE提供了编码多模态数据的多样性,支持纯文本、纯图像或图文组合的格式:
-
- 纯文本编码: 保持原始BGE模型的强大文本嵌入能力。
- 纯图像编码: 使用基于EVA-CLIP的视觉编码器处理图像。
- 图文联合编码: 将图像和文本特征融合到统一的向量空间。
- 应用场景: 主要用于混合模态检索任务,包括多模态知识检索、组合图像检索、多模态查询的知识检索等。
- 相似度计算: 使用矩阵乘法计算余弦相似度,所有嵌入向量都被标准化到单位长度,确保相似度值在合理范围内。
运行结果:
=== 相似度计算结果 ===
纯图像 vs 纯图像: tensor([[0.8318]])
图文结合1 vs 纯图像: tensor([[0.8291]])
图文结合1 vs 纯文本: tensor([[0.7627]])
图文结合1 vs 图文结合2: tensor([[0.9058]])
3. 向量数据库
3.1. 向量数据库的作用
在前面我们学习了如何使用嵌入模型将文本、图像等非结构化数据转换为高维向量。这些向量是RAG系统能够进行语义理解的基础。然而,当向量数量从几百个增长到数百万甚至数十亿时,一个核心问题随之而来:如何快速、准确地从海量向量中找到与用户查询最相似的那几个?
3.1.1. 向量数据库主要功能
向量数据库的核心价值在于其高效处理海量高维向量的能力。其主要功能可以概括为以下几点:
- 高效的相似性搜索:这是向量数据库最重要的功能。它利用专门的索引技术(如 HNSW, IVF),能够在数十亿级别的向量中实现毫秒级的近似最近邻(ANN)查询,快速找到与给定查询最相似的数据。
- 高维数据存储与管理:专门为存储高维向量(通常维度成百上千)而优化,支持对向量数据进行增、删、改、查等基本操作。
- 丰富的查询能力 :除了基本的相似性搜索,还支持按标量字段过滤查询(例如,在搜索相似图片的同时,指定
年份 > 2023)、范围查询和聚类分析等,满足复杂业务需求。 - 可扩展与高可用:现代向量数据库通常采用分布式架构,具备良好的水平扩展能力和容错性,能够通过增加节点来应对数据量的增长,并确保服务的稳定可靠。
- 数据与模型生态集成:与主流的 AI 框架(如 LangChain, LlamaIndex)和机器学习工作流无缝集成,简化了从模型训练到向量检索的应用开发流程。
3.1.2. 向量数据库 vs 传统数据库
传统的数据库(如 MySQL)擅长处理结构化数据的精确匹配查询(例如,WHERE age = 25),但它们并非为处理高维向量的相似性搜索而设计的。在庞大的向量集合中进行暴力、线性的相似度计算,其计算成本和时间延迟无法接受。向量数据库 (Vector Database) 很好的解决了这一问题,它是一种专门设计用于高效存储、管理和查询高维向量的数据库系统。在 RAG 流程中,它扮演着"知识库"的角色,是连接数据与大语言模型的关键桥梁。
向量数据库与传统数据库的主要差异如下:
|------------|-------------------------|---------------------------|
| 维度 | 向量数据库 | 传统数据库 (RDBMS) |
| 核心数据类型 | 高维向量 (Embeddings) | 结构化数据 (文本、数字、日期) |
| 查询方式 | 相似性搜索 (ANN) | 精确匹配 |
| 索引机制 | HNSW, IVF, LSH 等 ANN 索引 | B-Tree, Hash Index |
| 主要应用场景 | AI 应用、RAG、推荐系统、图像/语音识别 | 业务系统 (ERP, CRM)、金融交易、数据报表 |
| 数据规模 | 轻松应对千亿级向量 | 通常在千万到亿级行数据,更大规模需复杂分库分表 |
| 性能特点 | 高维数据检索性能极高,计算密集型 | 结构化数据查询快,高维数据查询性能呈指数级下降 |
| 一致性 | 通常为最终一致性 | 强一致性 (ACID 事务) |
向量数据库和传统数据库并非相互替代的关系,而是互补关系。在构建现代 AI 应用时,通常会将两者结合使用:利用传统数据库存储业务元数据和结构化信息,而向量数据库则专门负责处理和检索由 AI 模型产生的海量向量数据。
3.2. 工作原理
向量数据库的核心是高效处理高维向量的相似性搜索。向量是一组有序的数值,可以表示文本、图像、音频等复杂数据的特征或属性。在RAG 系统中,向量一般通过嵌入模型将原始数据转换为高维向量表示,比如上一节的图文示例。向量数据库通常采用四层架构,通过存储层、索引层、查询层和服务层 的协同工作来实现高效相似性搜索,其中存储层负责存储向量数据和元数据,优化存储效率并支持分布式存储;索引层维护索引算法(HNSW、LSH、PQ等),负责索引的创建与优化,并支持索引调整;查询层处理查询请求,支持混合查询并实现查询优化;服务层管理客户端连接,提供监控和日志能力,并实现安全管理。
主要技术手段包括:
- 基于树的方法:如 Annoy 使用的随机投影树,通过树形结构实现对数复杂度的搜索
- 基于哈希的方法 :如 LSH(局部敏感哈希),通过哈希函数将相似向量映射到同一"桶"
- 基于图的方法 :如 HNSW(分层可导航小世界图),通过多层邻近图结构实现快速搜索
- 基于量化的方法:如 Faiss 的 IVF 和 PQ,通过聚类和量化压缩向量
3.3. 三、主流向量数据库介绍

当前主流的向量数据库产品包括:
- Pinecone是一款完全托管的向量数据库服务,采用Serverless架构设计。它提供存储计算分离、自动扩展和负载均衡等企业级特性,并保证99.95%的SLA。Pinecone支持多种语言SDK,提供极高可用性和低延迟搜索(<100ms),特别适合企业级生产环境、高并发场景和大规模部署。
- Milvus是一款开源的分布式向量数据库,采用分布式架构设计,支持GPU加速和多种索引算法。它能够处理亿级向量检索,提供高性能GPU加速和完善的生态系统。Milvus特别适合大规模部署、高性能要求的场景,以及需要自定义开发的开源项目。
- Qdrant 是一款高性能的开源向量数据库,采用Rust开发,支持二进制量化技术。它提供多种索引策略和向量混合搜索功能,能够实现极高的性能(RPS>4000)和低延迟搜索。Qdrant特别适合性能敏感应用、高并发场景以及中小规模部署。
- Weaviate是一款支持GraphQL的AI集成向量数据库,提供20+AI模块和多模态支持。它采用GraphQL API设计,支持RAG优化,特别适合AI开发、多模态处理和快速开发场景。Weaviate具有活跃的社区支持和易于集成的特点。
- Chroma 是一款轻量级的开源向量数据库,采用本地优先设计,无依赖。它提供零配置安装、本地运行和低资源消耗等特性,特别适合原型开发、教育培训和小规模应用。Chroma的部署简单,适合快速原型开发。
选择建议:
- 新手入门/小型项目 :从
ChromaDB或FAISS开始是最佳选择。它们与 LangChain/LlamaIndex 紧密集成,几行代码就能运行,且能满足基本的存储和检索需求。 - 生产环境/大规模应用 :当数据量超过百万级,或需要高并发、实时更新、复杂元数据过滤时,应考虑更专业的解决方案,如
Milvus、Weaviate或云服务Pinecone。
3.4. 本地向量存储:以 FAISS 为例
FAISS (Facebook AI Similarity Search) 是一个由 Facebook AI Research 开发的高性能库,专门用于高效的相似性搜索和密集向量聚类。当与 LangChain 结合使用时,它可以作为一个强大的本地向量存储方案,非常适合快速原型设计和中小型应用。
与 ChromaDB 等数据库不同,FAISS 本质上是一个算法库,它将索引直接保存为本地文件(一个 .faiss 索引文件和一个 .pkl 映射文件),而非运行一个数据库服务。这种方式轻量且高效。
3.4.1. 环境准备
在开始之前,请确保已安装所有必需的库:当前requirements.txt安装的 faiss-cpu****是 CPU 版本。如果你的机器有 GPU,可以安装 faiss-gpu****以获得更好的性能。
3.4.2. FAISS查询示例
下面的代码演示了使用 LangChain 和 FAISS 完成一个完整的"创建 -> 保存 -> 加载 -> 查询"流程。
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_core.documents import Document
# 1. 示例文本和嵌入模型
texts = [
"张三是法外狂徒",
"FAISS是一个用于高效相似性搜索和密集向量聚类的库。",
"LangChain是一个用于开发由语言模型驱动的应用程序的框架。"
]
docs = [Document(page_content=t) for t in texts]
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
# 2. 创建向量存储并保存到本地
vectorstore = FAISS.from_documents(docs, embeddings)
local_faiss_path = "./faiss_index_store"
vectorstore.save_local(local_faiss_path)
print(f"FAISS index has been saved to {local_faiss_path}")
# 3. 加载索引并执行查询
# 加载时需指定相同的嵌入模型,并允许反序列化
loaded_vectorstore = FAISS.load_local(
local_faiss_path,
embeddings,
allow_dangerous_deserialization=True
)
# 相似性搜索
query = "FAISS是做什么的?"
results = loaded_vectorstore.similarity_search(query, k=1)
print(f"\n查询: '{query}'")
print("相似度最高的文档:")
for doc in results:
print(f"- {doc.page_content}")
运行结果与解读:
当你运行上述脚本时,会看到类似以下的输出:
FAISS index has been saved to ./faiss_index_store
查询: 'FAISS是做什么的?'
相似度最高的文档:
- FAISS是一个用于高效相似性搜索和密集向量聚类的库。
索引创建实现细节: 通过深入 LangChain 源码,可以发现索引创建是一个分层、解耦的过程,主要涉及以下几个方法的嵌套调用:
- from_documents**(封装层)**:
-
- 这是我们直接调用的方法。它的职责很简单:从输入的
Document对象列表中提取出纯文本内容 (page_content) 和元数据 (metadata)。 - 然后,它将这些提取出的信息传递给核心的
from_texts方法。
- 这是我们直接调用的方法。它的职责很简单:从输入的
- from_texts**(向量化入口)**:
-
- 这个方法是面向用户的入口。它接收文本列表,并执行关键的第一步:调用
embedding.embed_documents(texts),将所有文本批量转换为向量。 - 完成向量化后,它并不直接处理索引构建,而是将生成的向量和其他所有信息(文本、元数据等)传递给一个内部的辅助方法
__from。
- 这个方法是面向用户的入口。它接收文本列表,并执行关键的第一步:调用
- __from**(构建索引框架)**:
-
- 一个内部方法,负责搭建 FAISS 向量存储的"空框架"。
- 它会根据指定的距离策略(默认为 L2 欧氏距离)初始化一个空的 FAISS 索引结构(如
faiss.IndexFlatL2)。 - 同时,它也准备好了用于存储文档原文的
docstore和用于连接 FAISS 索引与文档的index_to_docstore_id映射。 - 最后,它调用另一个内部方法
__add来完成数据的填充。
- __add**(填充数据)**:
-
- 真正执行数据添加操作的核心。它接收到向量、文本和元数据后,执行以下关键操作:
-
-
- 添加向量 : 将向量列表转换为 FAISS 需要的
numpy数组,并调用self.index.add(vector)将其批量添加到 FAISS 索引中。 - 存储文档 : 将文本和元数据打包成
Document对象,存入docstore。 - 建立映射 : 更新
index_to_docstore_id字典,建立起 FAISS 内部的整数 ID(如 0, 1, 2...)到我们文档唯一 ID 的映射关系。
- 添加向量 : 将向量列表转换为 FAISS 需要的
-
4. Milvus向量数据库实战
Milvus 是一个开源的、专为大规模向量相似性搜索和分析而设计的向量数据库。它诞生于 Zilliz 公司,并已成为 LF AI & Data 基金会的顶级项目,在AI领域拥有广泛的应用。与 FAISS、ChromaDB 等轻量级本地存储方案不同,Milvus 从设计之初就瞄准了生产环境。其采用云原生架构,具备高可用、高性能、易扩展的特性,能够处理十亿、百亿甚至更大规模的向量数据。
4.1. Milvus数据库核心组件
4.1.1. Collection (集合)
可以用一个图书馆的比喻来理解 Collection:
- Collection (集合) : 相当于一个图书馆,是所有数据的顶层容器。一个 Collection 可以包含多个 Partition,每个 Partition 可以包含多个 Entity。
- Partition (分区) : 相当于图书馆里的不同区域(如"小说区"、"科技区"),将数据物理隔离,让检索更高效。
- Schema (模式) : 相当于图书馆的图书卡片规则,定义了每本书(数据)必须登记哪些信息(字段)。
- Entity (实体) : 相当于一本具体的书,是数据本身。
- Alias (别名) : 相当于一个动态的推荐书单(如"本周精选"),它可以指向某个具体的 Collection,方便应用层调用,实现数据更新时的无缝切换。
Collection 是 Milvus 中最基本的数据组织单位,类似于关系型数据库中的一张表 (Table)*。是我们存储、管理和查询向量及相关元数据的容器。所有的数据操作,如插入、删除、查询等,都是围绕 Collection 展开的。一个 Collection 由其 Schema 定义,并包含以下重要的子概念和特性:
4.1.2. Schema
在创建 Collection 之前,必须先定义它的 Schema 。 Schema 规定了 Collection 的数据结构,定义了其中包含的所有字段 (Field) 及其属性。一个设计良好的 Schema 是能够保证数据一致性并提升查询性能。
Schema 通常包含以下几类字段:
- 主键字段 (Primary Key Field): 每个 Collection 必须有且仅有一个主键字段,用于唯一标识每一条数据(实体)。它的值必须是唯一的,通常是整数或字符串类型。
- 向量字段 (Vector Field): 用于存储核心的向量数据。一个 Collection 可以有一个或多个向量字段,以满足多模态等复杂场景的需求。
- 标量字段 (Scalar Field): 用于存储除向量之外的元数据,如字符串、数字、布尔值、JSON 等。这些字段可以用于过滤查询,实现更精确的检索。

上图以一篇新闻文章为例,展示了一个典型的多模态、混合向量 Schema 设计。它将一篇文章拆解为:唯一的 Article (ID)、文本元数据(如 Title、Author Info)、图像信息(Image URL),并为图像和摘要内容分别生成了密集向量(Image Embedding, Summary Embedding)和稀疏向量(Summary Sparse Embedding)。
4.1.3. Partition (分区)
Partition 是 Collection 内部的一个逻辑划分。每个 Collection 在创建时都会有一个名为 _default 的默认分区。我们可以根据业务需求创建更多的分区,将数据按特定规则(如类别、日期等)存入不同分区。
为什么使用分区?
- 提升查询性能: 在查询时,可以指定只在一个或几个分区内进行搜索,从而大幅减少需要扫描的数据量,显著提升检索速度。
- 数据管理: 便于对部分数据进行批量操作,如加载/卸载特定分区到内存,或者删除整个分区的数据。
一个 Collection 最多可以有 1024 个分区。合理利用分区是 Milvus 性能优化的重要手段之一。
4.1.4. Alias (别名)
Alias (别名) 是为 Collection 提供的一个"昵称"。通过为一个 Collection 设置别名,我们可以在应用程序中使用这个别名来执行所有操作,而不是直接使用真实的 Collection 名称。
为什么使用别名?
- 安全地更新数据:想象一下,你需要对一个在线服务的 Collection 进行大规模的数据更新或重建索引。直接在原 Collection 上操作风险很高。正确的做法是:
-
- 创建一个新的 Collection (
collection_v2) 并导入、索引好所有新数据。 - 将指向旧 Collection (
collection_v1) 的别名(例如my_app_collection)原子性地切换到新 Collection (collection_v2) 上。
- 创建一个新的 Collection (
- 代码解耦:整个切换过程对上层应用完全透明,无需修改任何代码或重启服务,实现了数据的平滑无缝升级。
4.1.5. 索引 (Index)
如果说 Collection 是 Milvus 的骨架,那么索引 (Index) 就是其加速检索的神经系统。从宏观上看,索引本身就是一种为了加速查询而设计的复杂数据结构。对向量数据创建索引后,Milvus 可以极大地提升向量相似性搜索的速度,代价是会占用额外的存储和内存资源。

上图清晰地展示了 Milvus 向量索引的内部组件及其工作流程:
- 数据结构:这是索引的骨架,定义了向量的组织方式(如 HNSW 中的图结构)。
- 量化(可选):数据压缩技术,通过降低向量精度来减少内存占用和加速计算。
- 结果精炼(可选):在找到初步候选集后,进行更精确的计算以优化最终结果。
Milvus支持对标量字段和向量字段分别创建索引。
- 标量字段索引 :主要用于加速元数据过滤,常用的有
INVERTED、BITMAP等。通常使用推荐的索引类型即可。 - 向量字段索引:这是 Milvus 的核心。选择合适的向量索引是在查询性能、召回率和内存占用之间做出权衡的艺术。
4.2. 主要向量索引类型
Milvus 提供了多种向量索引算法,以适应不同的应用场景。以下是几种最核心的类型:
- FLAT (精确查找)
-
- 原理:暴力搜索(Brute-force Search)。它会计算查询向量与集合中所有向量之间的实际距离,返回最精确的结果。
- 优点:100% 的召回率,结果最准确。
- 缺点:速度慢,内存占用大,不适合海量数据。
- 适用场景:对精度要求极高,且数据规模较小(百万级以内)的场景。
- IVF 系列 (倒排文件索引)
-
- 原理 :类似于书籍的目录。它首先通过聚类将所有向量分成多个"桶"(
nlist),查询时,先找到最相似的几个"桶",然后只在这几个桶内进行精确搜索。IVF_FLAT、IVF_SQ8、IVF_PQ是其不同变体,主要区别在于是否对桶内向量进行了压缩(量化)。 - 优点:通过缩小搜索范围,极大地提升了检索速度,是性能和效果之间很好的平衡。
- 缺点:召回率不是100%,因为相关向量可能被分到了未被搜索的桶中。
- 适用场景:通用场景,尤其适合需要高吞吐量的大规模数据集。
- 原理 :类似于书籍的目录。它首先通过聚类将所有向量分成多个"桶"(
- HNSW (基于图的索引)
-
- 原理:构建一个多层的邻近图。查询时从最上层的稀疏图开始,快速定位到目标区域,然后在下层的密集图中进行精确搜索。
- 优点:检索速度极快,召回率高,尤其擅长处理高维数据和低延迟查询。
- 缺点:内存占用非常大,构建索引的时间也较长。
- 适用场景:对查询延迟有严格要求(如实时推荐、在线搜索)的场景。
- DiskANN (基于磁盘的索引)
-
- 原理:一种为在 SSD 等高速磁盘上运行而优化的图索引。
- 优点:支持远超内存容量的海量数据集(十亿级甚至更多),同时保持较低的查询延迟。
- 缺点:相比纯内存索引,延迟稍高。
- 适用场景:数据规模巨大,无法全部加载到内存的场景。
4.3. 如何选择索引?
选择索引没有唯一的"最佳答案",需要根据业务场景在数据规模、内存限制、查询性能和召回率之间进行权衡。
|-------------------|------------------------|-----------------------|
| 场景 | 推荐索引 | 备注 |
| 数据可完全载入内存,追求低延迟 | HNSW | 内存占用较大,但查询性能和召回率都很优秀。 |
| 数据可完全载入内存,追求高吞吐 | IVF_FLAT / IVF_SQ8 | 性能和资源消耗的平衡之选。 |
| 数据量巨大,无法载入内存 | DiskANN | 在 SSD 上性能优异,专为海量数据设计。 |
| 追求 100% 准确率,数据量不大 | FLAT | 暴力搜索,确保结果最精确。 |
在实际应用中,通常需要通过测试来找到最适合自己数据和查询模式的索引类型及其参数。
4.4. 检索
4.4.1. 基础向量检索 (ANN Search)
拥有了数据容器 (Collection) 和检索引擎 (Index) 后,最后一步就是从海量数据中高效地检索信息。这是 Milvus 的核心功能之一,近似最近邻 (Approximate Nearest Neighbor, ANN) 检索。与需要计算全部数据的暴力检索(Brute-force Search)不同,ANN 检索利用预先构建好的索引,能够极速地从海量数据中找到与查询向量最相似的 Top-K 个结果。这是一种在速度和精度之间取得极致平衡的策略。
- 主要参数:
-
anns_field: 指定要在哪个向量字段上进行检索。data: 传入一个或多个查询向量。limit(或top_k): 指定需要返回的最相似结果的数量。search_params: 指定检索时使用的参数,例如距离计算方式 (metric_type) 和索引相关的查询参数。
4.4.2. 增强检索
在基础的 ANN 检索之上,Milvus 提供了多种增强检索功能,以满足更复杂的业务需求。
4.4.2.1. 过滤检索 (Filtered Search)
在实际应用中,我们很少只进行单纯的向量检索。更常见的需求是"在满足特定条件的向量中,查找最相似的结果",这就是过滤检索。它将向量相似性检索 与标量字段过滤结合在一起。
- 工作原理 :先根据提供的过滤表达式 (
filter) 筛选出符合条件的实体,然后仅在这个子集内执行 ANN 检索。这极大地提高了查询的精准度。 - 应用示例:
-
- 电商:"检索与这件红色连衣裙最相似的商品,但只看价格低于500元且有库存的。"
- 知识库:"查找与'人工智能'相关的文档,但只从'技术'分类下、且发布于2023年之后的文章中寻找。"
4.4.2.2. 范围检索 (Range Search)
有时我们关心的不是最相似的 Top-K 个结果,而是"所有与查询向量的相似度在特定范围内的结果"。
- 工作原理:范围检索允许定义一个距离(或相似度)的阈值范围。Milvus 会返回所有与查询向量的距离落在这个范围内的实体。
- 应用示例:
-
- 人脸识别:"查找所有与目标人脸相似度超过 0.9 的人脸",用于身份验证。
- 异常检测:"查找所有与正常样本向量距离过大的数据点",用于发现异常。
4.4.2.3. 多向量混合检索 (Hybrid Search)
这是 Milvus 提供的一种极其强大的高级检索模式,它允许在一个请求中同时检索多个向量字段,并将结果智能地融合在一起。
- 工作原理:
-
- 并行检索:应用针对不同的向量字段(如一个用于文本语义的密集向量,一个用于关键词匹配的稀疏向量,一个用于图像内容的多模态向量)分别发起 ANN 检索请求。
- **结果融合 (Rerank)**:Milvus 使用一个重排策略(Reranker)将来自不同检索流的结果合并成一个统一的、更高质量的排序列表。常用的策略有
RRFRanker(平衡各方结果)和WeightedRanker(可为特定字段结果加权)。
- 应用示例:
-
- 多模态商品检索 :用户输入文本"安静舒适的白色耳机",系统可以同时检索商品的文本描述向量 和图片内容向量,返回最匹配的商品。
- 增强型 RAG : 结合密集向量 (捕捉语义)和稀疏向量(精确匹配关键词),实现比单一向量更精准的文档检索效果。
4.4.2.4. 分组检索 (Grouping Search)
分组检索解决了一个常见的痛点:检索结果多样性不足。想象一下,你检索"机器学习",返回的前10篇文章都来自同一本教科书不同章节。这显然不是理想的结果。
- 工作原理 :分组检索允许指定一个字段(如
document_id)对结果进行分组。Milvus 会在检索后,确保返回的结果中每个组(每个document_id)只出现一次(或指定的次数),且返回的是该组内与查询最相似的那个实体。 - 应用示例:
-
- 视频检索:检索"可爱的猫咪",确保返回的视频来自不同的博主。
- 文档检索:检索"数据库索引",确保返回的结果来自不同的书籍或来源。
通过这些灵活的检索功能组合,开发者可以构建出满足各种复杂业务需求的向量检索应用。
4.5. milvus多模态code实践
我们将通过一个完整的示例,演示如何使用Milvus 和 Visualized-BGE模型构建一个端到端的图文多模态检索引擎。
4.5.1. 初始化与工具定义
首先导入所有必需的库,定义好模型路径、数据目录等常量。为了代码的整洁和复用,将 Visualized-BGE 模型的加载和编码逻辑封装在一个 Encoder 类中,并定义了一个 visualize_results 函数用于后续的结果可视化。
import os
from tqdm import tqdm
from glob import glob
import torch
from visual_bge.visual_bge.modeling import Visualized_BGE
from pymilvus import MilvusClient, FieldSchema, CollectionSchema, DataType
import numpy as np
import cv2
from PIL import Image
# 1. 初始化设置
MODEL_NAME = "BAAI/bge-base-en-v1.5"
MODEL_PATH = "../../models/bge/Visualized_base_en_v1.5.pth"
DATA_DIR = "../../data/C3"
COLLECTION_NAME = "multimodal_demo"
MILVUS_URI = "http://localhost:19530"
# 2. 定义工具 (编码器和可视化函数)
class Encoder:
"""编码器类,用于将图像和文本编码为向量。"""
def __init__(self, model_name: str, model_path: str):
self.model = Visualized_BGE(model_name_bge=model_name, model_weight=model_path)
self.model.eval()
def encode_query(self, image_path: str, text: str) -> list[float]:
with torch.no_grad():
query_emb = self.model.encode(image=image_path, text=text)
return query_emb.tolist()[0]
def encode_image(self, image_path: str) -> list[float]:
with torch.no_grad():
query_emb = self.model.encode(image=image_path)
return query_emb.tolist()[0]
def visualize_results(query_image_path: str, retrieved_images: list, img_height: int = 300, img_width: int = 300, row_count: int = 3) -> np.ndarray:
"""从检索到的图像列表创建一个全景图用于可视化。"""
panoramic_width = img_width * row_count
panoramic_height = img_height * row_count
panoramic_image = np.full((panoramic_height, panoramic_width, 3), 255, dtype=np.uint8)
query_display_area = np.full((panoramic_height, img_width, 3), 255, dtype=np.uint8)
# 处理查询图像
query_pil = Image.open(query_image_path).convert("RGB")
query_cv = np.array(query_pil)[:, :, ::-1]
resized_query = cv2.resize(query_cv, (img_width, img_height))
bordered_query = cv2.copyMakeBorder(resized_query, 10, 10, 10, 10, cv2.BORDER_CONSTANT, value=(255, 0, 0))
query_display_area[img_height * (row_count - 1):, :] = cv2.resize(bordered_query, (img_width, img_height))
cv2.putText(query_display_area, "Query", (10, panoramic_height - 20), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2)
# 处理检索到的图像
for i, img_path in enumerate(retrieved_images):
row, col = i // row_count, i % row_count
start_row, start_col = row * img_height, col * img_width
retrieved_pil = Image.open(img_path).convert("RGB")
retrieved_cv = np.array(retrieved_pil)[:, :, ::-1]
resized_retrieved = cv2.resize(retrieved_cv, (img_width - 4, img_height - 4))
bordered_retrieved = cv2.copyMakeBorder(resized_retrieved, 2, 2, 2, 2, cv2.BORDER_CONSTANT, value=(0, 0, 0))
panoramic_image[start_row:start_row + img_height, start_col:start_col + img_width] = bordered_retrieved
# 添加索引号
cv2.putText(panoramic_image, str(i), (start_col + 10, start_row + 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
return np.hstack([query_display_area, panoramic_image])
4.5.2. 创建 Collection
这是与 Milvus 交互的开始。首先初始化 Milvus 客户端,然后定义 Collection 的 Schema,它规定了集合的数据结构。
# 3. 初始化客户端
print("--> 正在初始化编码器和Milvus客户端...")
encoder = Encoder(MODEL_NAME, MODEL_PATH)
milvus_client = MilvusClient(uri=MILVUS_URI)
# 4. 创建 Milvus Collection
print(f"\n--> 正在创建 Collection '{COLLECTION_NAME}'")
if milvus_client.has_collection(COLLECTION_NAME):
milvus_client.drop_collection(COLLECTION_NAME)
print(f"已删除已存在的 Collection: '{COLLECTION_NAME}'")
image_list = glob(os.path.join(DATA_DIR, "dragon", "*.png"))
if not image_list:
raise FileNotFoundError(f"在 {DATA_DIR}/dragon/ 中未找到任何 .png 图像。")
dim = len(encoder.encode_image(image_list[0]))
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=dim),
FieldSchema(name="image_path", dtype=DataType.VARCHAR, max_length=512),
]
# 创建集合 Schema
schema = CollectionSchema(fields, description="多模态图文检索")
print("Schema 结构:")
print(schema)
# 创建集合
milvus_client.create_collection(collection_name=COLLECTION_NAME, schema=schema)
print(f"成功创建 Collection: '{COLLECTION_NAME}'")
print("Collection 结构:")
print(milvus_client.describe_collection(collection_name=COLLECTION_NAME))
输出结果:
--> 正在创建 Collection 'multimodal_demo'
Schema 结构:
{
'auto_id': True,
'description': '多模态图文检索',
'fields': [
{'name': 'id', 'description': '', 'type': <DataType.INT64: 5>, 'is_primary': True, 'auto_id': True},
{'name': 'vector', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 768}},
{'name': 'image_path', 'description': '', 'type': <DataType.VARCHAR: 21>, 'params': {'max_length': 512}}
],
'enable_dynamic_field': False
}
成功创建 Collection: 'multimodal_demo'
Collection 结构:
{
'collection_name': 'multimodal_demo',
'auto_id': True,
'num_shards': 1,
'description': '多模态图文检索',
'fields': [
{'field_id': 100, 'name': 'id', 'description': '', 'type': <DataType.INT64: 5>, 'params': {}, 'auto_id': True, 'is_primary': True},
{'field_id': 101, 'name': 'vector', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 768}},
{'field_id': 102, 'name': 'image_path', 'description': '', 'type': <DataType.VARCHAR: 21>, 'params': {'max_length': 512}}
],
'functions': [],
'aliases': [],
'collection_id': 459243798405253751,
'consistency_level': 2,
'properties': {},
'num_partitions': 1,
'enable_dynamic_field': False,
'created_timestamp': 459249546649403396,
'update_timestamp': 459249546649403396
}
上面的输出详细展示了刚刚创建的 multimodal_demo Collection 的完整结构。其 Schema 包含了三个核心字段(Field ):一个自增的 id 作为主键 ,一个 768 维的 vector 向量字段 用于存储图像嵌入,以及一个 image_path 标量字段来记录原始图片路径。
4.5.3. 准备并插入数据
创建好 Collection 后,需要将数据填充进去。通过遍历指定目录下的所有图片,将它们逐一编码成向量,然后与图片路径一起组织成符合 Schema 结构的格式,最后批量插入到 Collection 中。
# 5. 准备并插入数据
print(f"\n--> 正在向 '{COLLECTION_NAME}' 插入数据")
data_to_insert = []
for image_path in tqdm(image_list, desc="生成图像嵌入"):
vector = encoder.encode_image(image_path)
data_to_insert.append({"vector": vector, "image_path": image_path})
if data_to_insert:
result = milvus_client.insert(collection_name=COLLECTION_NAME, data=data_to_insert)
print(f"成功插入 {result['insert_count']} 条数据。")
4.5.4. 创建索引
为了实现快速检索,需要为向量字段创建索引。这里选择HNSW索引,它在召回率和查询性能之间有着很好的平衡。创建索引后,必须调用 load_collection 将集合加载到内存中才能进行搜索。
# 6. 创建索引
print(f"\n--> 正在为 '{COLLECTION_NAME}' 创建索引")
index_params = milvus_client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="HNSW",
metric_type="COSINE",
params={"M": 16, "efConstruction": 256}
)
milvus_client.create_index(collection_name=COLLECTION_NAME, index_params=index_params)
print("成功为向量字段创建 HNSW 索引。")
print("索引详情:")
print(milvus_client.describe_index(collection_name=COLLECTION_NAME, index_name="vector"))
milvus_client.load_collection(collection_name=COLLECTION_NAME)
print("已加载 Collection 到内存中。")
输出结果:
--> 正在为 'multimodal_demo' 创建索引
成功为向量字段创建 HNSW 索引。
索引详情:
{'M': '16', 'efConstruction': '256', 'metric_type': 'COSINE', 'index_type': 'HNSW', 'field_name': 'vector', 'index_name': 'vector', 'total_rows': 0, 'indexed_rows': 0, 'pending_index_rows': 0, 'state': 'Finished'}
已加载 Collection 到内存中。
可以看出,索引创建成功,在 vector 字段上成功创建了 HNSW 索引,并使用 COSINE 作为距离度量。M: '16' 和 efConstruction: '256' 是 HNSW 索引的两个关键参数,分别控制着图中每个节点的最大连接数和索引构建时的搜索范围,这些参数直接影响检索的性能和准确性。state: 'Finished' 状态表明索引已成功构建。
4.5.5. 执行多模态检索
这里通过定义一个包含图片和文本的组合查询,将其编码为查询向量,然后调用 search 方法在 Milvus 中执行近似最近邻搜索。
# 7. 执行多模态检索
print(f"\n--> 正在 '{COLLECTION_NAME}' 中执行检索")
query_image_path = os.path.join(DATA_DIR, "dragon", "query.png")
query_text = "一条龙"
query_vector = encoder.encode_query(image_path=query_image_path, text=query_text)
search_results = milvus_client.search(
collection_name=COLLECTION_NAME,
data=[query_vector],
output_fields=["image_path"],
limit=5,
search_params={"metric_type": "COSINE", "params": {"ef": 128}}
)[0]
retrieved_images = []
print("检索结果:")
for i, hit in enumerate(search_results):
print(f" Top {i+1}: ID={hit['id']}, 距离={hit['distance']:.4f}, 路径='{hit['entity']['image_path']}'")
retrieved_images.append(hit['entity']['image_path'])
输出结果:
--> 正在 'multimodal_demo' 中执行检索
检索结果:
Top 1: ID=459243798403756667, distance=0.9411, 路径='../../data/C3\dragon\dragon01.png'
Top 2: ID=459243798403756668, distance=0.5818, 路径='../../data/C3\dragon\dragon02.png'
Top 3: ID=459243798403756671, distance=0.5731, 路径='../../data/C3\dragon\dragon05.png'
Top 4: ID=459243798403756670, distance=0.4894, 路径='../../data/C3\dragon\dragon04.png'
Top 5: ID=459243798403756669, distance=0.4100, 路径='../../data/C3\dragon\dragon03.png'
这段输出展示了与图文组合查询最相似的5个实体 (Entity) 。 distance****字段代表了 余弦相似度 ,值越接近 1 表示越相似。可以看到,Top 1 结果正是查询图片本身,其相似度得分最高(0.9411),这说明了检索的有效性。其余结果也都是龙的图片,并按相似度从高到低精确排列。
4.5.6. 可视化与清理
最后,将检索到的图片路径用于可视化,生成一张直观的结果对比图。在完成所有操作后,应该释放 Milvus 中的资源,包括从内存中卸载 Collection 和删除整个 Collection。
# 8. 可视化与清理
print(f"\n--> 正在可视化结果并清理资源")
if not retrieved_images:
print("没有检索到任何图像。")
else:
panoramic_image = visualize_results(query_image_path, retrieved_images)
combined_image_path = os.path.join(DATA_DIR, "search_result.png")
cv2.imwrite(combined_image_path, panoramic_image)
print(f"结果图像已保存到: {combined_image_path}")
Image.open(combined_image_path).show()
milvus_client.release_collection(collection_name=COLLECTION_NAME)
print(f"已从内存中释放 Collection: '{COLLECTION_NAME}'")
milvus_client.drop_collection(COLLECTION_NAME)
print(f"已删除 Collection: '{COLLECTION_NAME}'")

通过上图可以看出,这个多模态检索引擎成功地理解了"一条龙"这个图文组合查询的意图,并从图库中找到了最相关的几张图片并进行排序。
5. 索引优化
在RAG系统中,常常面临一个权衡问题:使用小块文本进行检索可以获得更高的精确度,但小块文本缺乏足够的上下文,可能导致大语言模型(LLM)无法生成高质量的答案;而使用大块文本虽然上下文丰富,却容易引入噪音,降低检索的相关性。为了解决这一矛盾,LlamaIndex提出了一种实用的索引策略------句子窗口检索(Sentence Window Retrieval) 。该技术巧妙地结合了两种方法的优点:它在检索时聚焦于高度精确的单个句子,在送入LLM生成答案前,又智能地将上下文扩展回一个更宽的"窗口",从而同时保证检索的准确性和生成的质量。
可以先设想一个图书馆咨询的场景。传统 RAG 方法就如同让一位图书管理员要么只取回单个关键词所在的句子,虽然精确,却失去了前因后果;要么直接搬来整本书,虽然信息全,但可能充斥着大量无关内容。句子窗口检索的妙处在于,它像一位经验丰富的专家:检索时精确捕捉到包含关键词的句子,但在回答时,会把这个句子连同它前后的"上下文段落"一同给你,从而兼顾了精确与全面。这种"索引和生成分离"的策略,其精妙之处在于将整个流程分为索引和查询两大阶段。
5.1. 上下文扩展优化
5.1.1. 上下文扩展优化原理
句子窗口检索的思想可以概括为:为检索精确性而索引小块,为上下文丰富性而检索大块。其工作流程如下:
- 索引阶段 :在构建索引时,文档被分割成单个句子 。每个句子都作为一个独立的"节点(Node)"存入向量数据库。同时,每个句子节点都会在元数据(metadata)中存储其上下文窗口,即该句子原文中的前N个和后N个句子。这个窗口内的文本不会被索引,仅仅是作为元数据存储。
- 检索阶段 :当用户发起查询时,系统会在所有单一句子节点上执行相似度搜索。因为句子是表达完整语义的最小单位,所以这种方式可以非常精确地定位到与用户问题最相关的核心信息。
- 后处理阶段 :在检索到最相关的句子节点后,系统会使用一个名为
MetadataReplacementPostProcessor的后处理模块。该模块会读取到检索到的句子节点的元数据,并用元数据中存储的完整上下文窗口来替换节点中原来的单一句子内容。 - 生成阶段:最后,这些被替换了内容的、包含丰富上下文的节点被传递给LLM,用于生成最终的答案。
5.1.2. 索引阶段:为每句话搭建一个"场景"
在这个阶段,系统会为每个句子创建一个包含其"周边环境"的独立节点。
- 精准分割 :首先使用
SentenceWindowNodeParser工具将文档精确地拆解成一个一个的独立句子节点。 - 储存上下文 :对于每一个句子节点,系统会分析其上下文,并自动在它的元数据(Metadata) 中保存一个包含前后若干句子的"窗口"。
- 关键细节 :最为关键的一步是,系统只为"当前这个单句"创建用于检索的向量嵌入(Vector Embeddings),而完整丰富的上下文窗口则安静地存放在元数据中备用。这保证了检索时信号的精炼。
5.1.3. 查询阶段:检索用单句,生成用窗口
在查询时,系统通过"两步走"的策略,实现从精确检索到丰富生成的过渡。
- 步骤一:精准检索:当用户发起提问时,系统在所有单句向量中进行相似度搜索,快速锁定与问题最相关的那几个"种子句子"。
- 步骤二:窗口替换 :接着,MetadataReplacementPostProcessor 处理器会立即介入,它就像一位"智能替换员",用检索到的句子所对应的、储存在元数据中的"完整上下文窗口"替换掉原本的单句。
- 步骤三:高质量生成:最终,大语言模型(LLM)接收到的信息不再是孤立的一句话,而是一段连贯、完整的上下文段落,从而能生成更准确、更全面的答案。
5.1.4. 核心对比:句子窗口检索 vs. 传统RAG
为了更清晰地展示其优势,这里总结出两种方法的核心区别:
|-----------|---------------------------------------------------------------------------|-----------------------------------------|
| 特性 | 句子窗口检索 (Sentence Window Retrieval) | 传统 RAG (Traditional RAG) |
| 分块粒度 | 以单个句子为基本检索单元 | 按固定的字符数或语义切分为固定段落块 |
| 检索精度 | 高。能精准定位与问题高度相关的句子 | 中/低。大块文本易引入噪声,小块文本可能信息不足 |
| 上下文信息 | 丰富。通过扩展窗口为LLM提供完整的上下文 | 有限。LLM仅能依靠检索到的固定块中的信息 |
| 核心优势 | 兼顾检索精度与生成质量,尤其适用于需要极高检索精度且上下文依赖强的问答场景 | 实现简单,适用于大多数通用场景 |
| 核心技术 | SentenceWindowNodeParser (索引) 和 MetadataReplacementPostProcessor (查询) | 标准 Document 加载和 VectorStoreIndex 构建 |
此外,与传统 RAG 中可能出现的"中间丢失(Lost in the Middle)"现象(即关键信息位于长文本块中间而被模型忽略)相比,句子窗口检索通过始终将最相关的"种子句子"置于窗口中心附近,有效规避了这一问题,进一步确保了生成的质量
5.1.5. 上下文扩展优化代码实现示例
下面通过 LlamaIndex 官网的示例,来演示如何实现句子窗口检索,并与常规的检索方法进行对比。该示例将加载一份PDF格式的IPCC气候报告,并就其中的专业问题进行提问。
核心代码如下:
# 假设 Settings.llm 和 Settings.embed_model 已经预先配置好
# 1. 加载文档
documents = SimpleDirectoryReader(
input_files=["../../data/C3/pdf/IPCC_AR6_WGII_Chapter03.pdf"]
).load_data()
# 2. 创建节点与构建索引
# 2.1 句子窗口索引
node_parser = SentenceWindowNodeParser.from_defaults(
window_size=3,
window_metadata_key="window",
original_text_metadata_key="original_text",
)
sentence_nodes = node_parser.get_nodes_from_documents(documents)
sentence_index = VectorStoreIndex(sentence_nodes)
根据 LlamaIndex 的底层源码,SentenceWindowNodeParser 的核心逻辑位于 build_window_nodes_from_documents 方法中。其实现过程可以分解为以下几个关键步骤:
- 句子切分 ( sentence_splitter**)** :解析器首先接收一个文档(
Document),然后调用self.sentence_splitter(doc.text)方法。这个sentence_splitter是一个可配置的函数,默认为split_by_sentence_tokenizer,它负责将文档的全部文本精确地切分成一个句子列表(text_splits)。 - 创建基础节点 ( build_nodes_from_splits**)** :切分出的
text_splits列表被传递给build_nodes_from_splits工具函数。这个函数会为列表中的每一个句子 都创建一个独立的TextNode。此时,每个TextNode的text属性就是这个句子的内容。 - 构建窗口并填充元数据 (主要循环) :接下来,解析器会遍历所有新创建的
TextNode。对于位于第i个位置的节点,它会执行以下操作:
- 定位窗口 :通过列表切片
nodes[max(0, i - self.window_size) : min(i + self.window_size + 1, len(nodes))]来获取一个包含中心句子及其前后window_size(默认为3)个邻近节点的列表(window_nodes)。这个切片操作很巧妙地处理了文档开头和结尾的边界情况。 - 组合窗口文本 :将
window_nodes列表中所有节点的text(即所有在窗口内的句子)用空格拼接成一个长字符串。 - 填充元数据 :将上一步生成的长字符串(完整的上下文窗口)存入当前节点(第
i个节点)的元数据中,键为self.window_metadata_key(默认为"window")。同时,也会将节点自身的文本(原始句子)存入元数据,键为self.original_text_metadata_key(默认为"original_text")。
- 设置元数据排除项 :这是一个非常关键的细节。在填充完元数据后,代码会执行
node.excluded_embed_metadata_keys.extend(...)和node.excluded_llm_metadata_keys.extend(...)。这行代码的作用是告诉后续的嵌入模型和LLM,在处理这个节点时,应当忽略"window"和"original_text"这两个元数据字段。这确保了只有单个句子的纯净文本被用于生成向量嵌入,从而保证了检索的高精度。而"window"字段仅供后续的MetadataReplacementPostProcessor使用。
通过以上步骤,SentenceWindowNodeParser 最终返回一个 TextNode 列表。列表中的每个节点都代表一个独立的句子,其 text 属性用于精确检索,而其 metadata 中则"隐藏"了用于生成答案的丰富上下文窗口。
# 2.2 常规分块索引 (基准)
base_parser = SentenceSplitter(chunk_size=512)
base_nodes = base_parser.get_nodes_from_documents(documents)
base_index = VectorStoreIndex(base_nodes)
# 3. 构建查询引擎
sentence_query_engine = sentence_index.as_query_engine(
similarity_top_k=2,
node_postprocessors=[
MetadataReplacementPostProcessor(target_metadata_key="window")
],
)
base_query_engine = base_index.as_query_engine(similarity_top_k=2)
# 4. 执行查询并对比结果
query = "What are the concerns surrounding the AMOC?"
print(f"查询: {query}\n")
print("--- 句子窗口检索结果 ---")
window_response = sentence_query_engine.query(query)
print(f"回答: {window_response}\n")
print("--- 常规检索结果 ---")
base_response = base_query_engine.query(query)
print(f"回答: {base_response}\n")
- 构建句子窗口索引 :这一步利用了
SentenceWindowNodeParser。它将文档解析为以单个句子为单位的Node,同时将包含上下文的"窗口"文本(默认为前后各3个句子)存储在每个Node的元数据中。这一步是实现"为检索精确性而索引小块"思想的关键。 - 构建查询引擎与后处理:查询引擎的构建是实现"为生成质量而扩展上下文"的关键。
- 在创建
sentence_query_engine时,配置中加入了一个重要的后处理器MetadataReplacementPostProcessor。 - 它的作用是:当检索器根据用户查询找到最相关的节点(也就是单个句子)后,这个后处理器会立即介入。
- 它会从该节点的元数据中读取出预先存储的完整"窗口"文本,并用它替换掉节点中原来的单个句子内容。
- 这样,最终传递给大语言模型的就不再是孤立的句子,而是包含丰富上下文的完整文本段落,从而确保了生成答案的质量和连贯性。
我们向两个引擎提出的问题是:"关于大西洋经向翻转环流(AMOC),人们主要担忧什么?" (What are the concerns surrounding the AMOC?)。
代码输出如下:
查询: What are the concerns surrounding the AMOC?
--- 句子窗口检索结果 ---
回答: The Atlantic Meridional Overturning Circulation (AMOC) is projected to decline over the 21st century with high confidence, though there is low confidence in quantitative projections of this decline. Observational records since the mid-2000s are too short to determine the relative contributions of internal variability, natural forcing, and anthropogenic forcing to AMOC changes. Additionally, there is low confidence in reconstructed and modeled AMOC changes for the 20th century due to limited agreement in quantitative trends. While an abrupt collapse before 2100 is not expected, the decline could have significant implications for global climate patterns.
--- 常规检索结果 ---
回答: The concerns surrounding the Atlantic Meridional Overturning Circulation (AMOC) primarily involve its projected decline over the 21st century across all Shared Socioeconomic Pathway (SSP) scenarios. While an abrupt collapse before 2100 is not expected, there is high confidence in this decline, though quantitative projections remain uncertain. Observational records since the mid-2000s are too short to clearly distinguish the contributions of internal variability, natural forcing, and anthropogenic forcing to these changes. This uncertainty highlights the need for further research to better understand and predict AMOC behavior and its broader climate impacts.Copy to clipboardErrorCopied
从输出结果中可以观察到:
- 两个答案都抓住了核心:两个引擎都正确地识别出,对AMOC的主要担忧是其在21世纪预计的衰退。
- 句子窗口检索的答案更详尽、更连贯:句子窗口检索的回答不仅指出了衰退的趋势,还补充了关于"定量预测的置信度低"、"观测记录时间过短"、"20世纪重建和模拟的变化置信度低"等多个维度的细节。这使得答案的信息量更大,上下文更完整,更像一个综述。
- 常规检索的答案相对宽泛:常规检索的回答虽然正确,但内容相对概括,最后以"需要进一步研究"这样较为笼同的结论收尾。
这种差异正是句子窗口检索策略优势的体现。它通过"精确检索小文本块(单个句子),再扩展上下文(句子窗口)"的方式,为大语言模型提供了高度相关且信息丰富的上下文,从而生成了质量更高的答案。
5.2. 结构化索引优化
随着知识库的规模不断扩大(例如,包含数百个PDF文件),传统的RAG方法(即对所有文本块进行top-k相似度搜索)会遇到瓶颈。当一个查询可能只与其中一两个文档相关时,在整个文档库中进行无差别的向量搜索,不仅效率低下,还容易被不相关的文本块干扰,导致检索结果不精确。
为了解决这个问题,一个有效的方法是利用结构化索引 。其原理是在索引文本块的同时,为其附加结构化的元数据(Metadata)。这些元数据可以是任何有助于筛选和定位信息的标签,例如:
- 文件名
- 文档创建日期
- 章节标题
- 作者
- 任何自定义的分类标签

传统RAG就像在一座巨大的图书馆里,每次提问都把整个图书馆的所有书架翻一遍来找答案------哪怕你只想知道"2023年财务报告"里的一句话。这显然又慢又容易找错。
结构化索引的本质就是:先分类,再检索。它给每个文本块贴上"标签"(元数据),让系统能像人类用文件夹、标签页或Excel表格一样,先快速缩小搜索范围,再进行相似度匹配。
想象你家里有几百本书(知识库),你想找一句话:"公司今年的净利润增长目标"。
- 传统RAG(无差别搜索):你把所有书一本本翻开,每页都扫一遍,看看有没有"净利润增长"。结果可能从小说、菜谱、历史书里都找到这个词,但都不是你要的。
- 结构化索引(先筛选再搜索) :你先从书架上只抽出**标签为"财务报告"**的那几本书(元数据筛选),然后只在这几本书里搜索"净利润增长"。这样又快又准。
5.2.1. 结构化实现原理
- 入库时贴标签:
当把一个PDF文件拆成文本块时,系统自动记录它的元数据:
-
- 文件名 =
2023年度财报.pdf - 章节标题 =
管理层讨论与分析 - 日期 =
2024-03-15
- 文件名 =
这些标签会跟文本块的向量一起存储。
- 查询时先筛选
当用户问:"2023年财报里,管理层对净利润增长的看法是什么?"
系统不是直接做向量搜索,而是先用元数据过滤:
只保留 文件名 包含 "2023年度财报" 且 章节标题 包含 "管理层" 的文本块。
这一步通常使用关键词匹配或结构化查询(如SQL或过滤条件),速度极快。
- 仅在筛选后的集合里做向量搜索
现在需要比较的文本块可能从10万个减少到几十个。再做top-k相似度搜索,既快又准。
|----------|--------------|-----------------------------------|
| 特性 | 传统RAG | 结构化索引RAG |
| 检索方式 | 全库向量相似度搜索 | 元数据过滤 → 小范围向量搜索 |
| 效率 | 随知识库增长线性下降 | 几乎不受总规模影响(只取决于过滤后的大小) |
| 精度 | 容易被跨文档的噪声干扰 | 精准限定文档/领域,减少干扰 |
| 典型场景 | 小规模、单一领域的知识库 | 大规模、多文档、多分类的知识库(如企业档案、法律卷宗、多年度报告) |
结构化索引 = 给文本块打标签 + 查询时先按标签筛掉99%不相关的内容,再在剩下的1%里做精细搜索。 它不改变向量匹配的原理,而是在匹配之前加一道"过滤门",让RAG系统既能处理海量文档,又能保持高精度。
5.2.2. 基于多表格的递归检索代码示例
在更复杂的场景中,结构化数据可能分布在多个来源中,例如一个包含多个工作表(Sheet)的 Excel 文件,每个工作表都代表一个独立的表格。在这种情况下,需要一种更强大的策略:递归检索 3。它能实现"路由"功能,先将查询引导至正确的知识来源(正确的表格),然后再在该来源内部执行精确查询。
下面使用一个包含多个工作表的电影数据 Excel 文件(movie.xlsx)来演示,其中每个工作表(如 年份_1994, 年份_2002 等)都存储了对应年份的电影信息。
# 1. 为每个工作表创建查询引擎和摘要节点
excel_file = '../../data/C3/excel/movie.xlsx'
xls = pd.ExcelFile(excel_file)
df_query_engines = {}
all_nodes = []
for sheet_name in xls.sheet_names:
df = pd.read_excel(xls, sheet_name=sheet_name)
# 为当前工作表创建一个 PandasQueryEngine
query_engine = PandasQueryEngine(df=df, llm=Settings.llm, verbose=True)
# 为当前工作表创建一个摘要节点(IndexNode)
year = sheet_name.replace('年份_', '')
summary = f"这个表格包含了年份为 {year} 的电影信息,可以用来回答关于这一年电影的具体问题。"
node = IndexNode(text=summary, index_id=sheet_name)
all_nodes.append(node)
# 存储工作表名称到其查询引擎的映射
df_query_engines[sheet_name] = query_engine
# 2. 创建顶层索引(只包含摘要节点)
vector_index = VectorStoreIndex(all_nodes)
# 3. 创建递归检索器
vector_retriever = vector_index.as_retriever(similarity_top_k=1)
recursive_retriever = RecursiveRetriever(
"vector",
retriever_dict={"vector": vector_retriever},
query_engine_dict=df_query_engines,
verbose=True,
)
# 4. 创建查询引擎
query_engine = RetrieverQueryEngine.from_args(recursive_retriever)
# 5. 执行查询
query = "1994年评分人数最多的电影是哪一部?"
print(f"查询: {query}")
response = query_engine.query(query)
print(f"回答: {response}")Copy to clipboardErrorCopied
- 创建 PandasQueryEngine :遍历 Excel 中的每个工作表,为每个工作表(即一个独立的 DataFrame)都实例化一个
PandasQueryEngine。其强大之处在于,它能将关于表格的自然语言问题(如"评分人数最多的是哪个")转换成实际的 Pandas 代码(如df.sort_values('评分人数').iloc[-1])来执行。 - 创建摘要节点 ( IndexNode**)** :对每个工作表,都创建一个
IndexNode,其内容是关于这个表格的一段摘要文本。这个节点将作为顶层检索的"指针"。 - 构建顶层索引 :使用所有创建的
IndexNode构建一个VectorStoreIndex。这个索引不包含任何表格的详细数据,只包含指向各个表格的"指针"信息。 - 创建 RecursiveRetriever :这是实现递归检索的核心。将其配置为:
-
retriever_dict: 指定顶层的检索器,即在摘要节点中进行检索的vector_retriever。query_engine_dict: 提供一个从节点 ID(即工作表名称)到其对应查询引擎的映射。当顶层检索器匹配到某个摘要节点后,递归检索器就知道该调用哪个PandasQueryEngine来处理后续查询。
运行结果:
查询: 1994年评分人数最少的电影是哪一部?
> Retrieving with query id None: 1994年评分人数最少的电影是哪一部?
> Retrieved node with id, entering: 年份_1994
> Retrieving with query id 年份_1994: 1994年评分人数最少的电影是哪一部?
> Pandas Instructions:Copy to clipboardErrorCopied
df[df['年份'] == 1994].nsmallest(1, '评分人数')['电影名称'].iloc[0]
> Pandas Output: 燃情岁月
回答: 燃情岁月Copy to clipboardErrorCopied
从输出中可以清晰地看到递归检索的完整流程:
(1)顶层路由 :Retrieving with query id None,系统首先在顶层的摘要索引中检索,根据问题"1994年..."匹配到了摘要节点 年份_1994。
(2)进入子层 :Retrieved node with id, entering: 年份_1994,系统决定进入与"年份_1994"这个工作表关联的查询引擎。
(3)子层查询 :Retrieving with query id 年份_1994,PandasQueryEngine 接管查询,并将问题发送给 LLM,让其生成 Pandas 代码。
(4)代码生成与执行 :LLM 生成了 df[df['年份'] == 1994].nsmallest(1, '评分人数')['电影名称'].iloc[0],引擎执行后得到输出 燃情岁月。
重要安全警告:实际上在 LlamaIndex 的官网有提到, PandasQueryEngine****是一个实验性功能,具有潜在的安全风险。它的工作原理是让 LLM 生成 Python 代码,然后使用 **eval()**函数在本地执行。这意味着,在没有严格沙箱隔离的环境下,理论上可能执行任意代码。因此,强烈不建议在生产环境中使用此工具。
5.2.3. 另一种实现方式
鉴于 PandasQueryEngine 的安全风险,还可以采用一种更安全的方式来实现类似的多表格查询,思路是将路由和检索彻底分离。
这种改进方法的具体步骤如下:
(1)创建两个独立的向量索引:
- 摘要索引(用于路由) :为每个Excel工作表(例如,"1994年电影数据")创建一个非常简短的摘要性
Document,例如:"此文档包含1994年的电影信息"。然后,用所有这些摘要文档构建一个轻量级的向量索引。这个索引的唯一目的就是充当"路由器"。 - 内容索引(用于问答) :将每个工作表的实际数据(例如,整个表格)转换为一个大的文本
Document,并为其附加一个关键的元数据标签,如{"sheet_name": "年份_1994"}。然后,用所有这些包含真实内容的文档构建一个向量索引。
(2)执行两步查询:
- 第一步:路由 。当用户提问(例如,"1994年评分人数最少的电影是哪一部?")时,首先在"摘要索引"中进行检索。由于问题中的"1994年"与"此文档包含1994年的电影信息"这个摘要高度相关,检索器会快速返回其对应的元数据,告诉系统目标是
年份_1994这个工作表。 - 第二步:检索 。拿到
年份_1994这个目标后,系统会在"内容索引"中进行检索,但这次会附加一个元数据过滤器 (MetadataFilter),强制要求只在sheet_name == "年份_1994"的文档中进行搜索。这样,LLM就能在正确的、经过筛选的数据范围内找到问题的答案。
通过这种"先路由,后用元数据过滤检索"的方式,既实现了跨多个数据源的查询能力,又避免了执行代码的安全隐患。LlamaIndex 官方也提供了类似的结构化分层检索可以参考。
6. 索引构建与优化相关问题
6.1. LLM的训练数据是实时吗? 对于当前实时信息,LLM 是怎么来判断呢?
LLM的知识完全来自于其训练阶段所使用的静态数据集。一旦训练完成,它的知识就"冻结"了。它就像一个读过海量书籍(截至某个日期)的学霸,但你无法在他读完书后,再往他脑子里塞一张今天的报纸。
每个LLM都有一个明确的知识截止日期。例如:
- GPT-4(早期版本)的知识截止于2021年9月。
- 其他模型也会有类似的固定时间点。
对于这个日期之后 发生的事件、新出现的数据、当前的股价、天气、新闻等,LLM 根本不知道。
6.2. 那LLM是如何"判断"实时信息的?
这里有一个非常重要的区别:**LLM不会主动"判断"信息是否实时。它只会根据你问的问题和它被训练的方式,来决定如何回应。**当被问到需要实时信息的问题时,LLM的行为可以分为几种情况:
6.2.1. 它会"假装"知道(但可能是错的)
如果你问:"今天特斯拉的股价是多少?" 一个没有实时数据连接的LLM不知道"今天"是哪天,也不知道股价。但它被训练成要给出"有用"的答案。所以它可能会:
- 编造一个看似合理的答案 (这被称为幻觉)。它可能会根据训练数据中特斯拉股价的常见波动范围,胡诌一个数字,比如"145.27美元"。
- 给出一个过时的答案。比如回答:"根据我2021年9月前的知识,特斯拉股价在拆分后约为......"
它不会告诉你"我不知道",除非被特别指令(如系统提示词要求它不确定时拒绝回答)。
6.2.2. 它能理解"实时性"这个词的语义
LLM通过大量文本学习,理解了"实时"、"最新"、"今天"这些词的含义。所以当你问:"请提供最新的科学研究进展",它会理解你想要的不是旧信息。但由于它没有实时数据,它会:
- 描述它已知的、截止到训练日期为止的"最新"进展。
- 或者,它会建议你开启联网搜索功能(如果平台支持)。
6.2.3. 现代LLM的真正处理方式:工具调用
这才是当前最主流、最正确的方法。 先进的LLM(如GPT-4、Claude 3、Gemini等)被设计成不使用其内部知识来回答实时问题。它们的流程是这样的:
- 识别意图:当你问"今天天气怎么样?",模型内部的"功能调用"模块会识别出这需要实时数据。
- 拒绝回答(内部逻辑):模型知道自己的内部知识不包含"今天"的天气。
- 发起外部请求 :它会输出一个特殊的指令,比如:
[调用 get_weather(location="北京", date="2024-05-20")]。 - 外部工具执行:调用方(比如ChatGPT的客户端、你的RAG系统)看到这个指令,就去调用一个真实的天气API。
- 获取结果并回答 :API返回
25°C,晴天。这个结果被重新喂给LLM,然后LLM根据这个实时信息组织语言回答你。
所以,LLM不是自己"判断"实时信息,而是被设计成"知道自己不知道",然后去调用外部工具来获取。
6.2.4. LLM内部知识 vs. 实时信息
|-------------|--------------------------------------------------------|----------------------------|
| 特性 | LLM的内部训练数据 | 实时信息 |
| 来源 | 静态的、历史的数据集(书籍、网页、论文等) | 动态的外部世界(API、数据库、搜索引擎) |
| 时效性 | 截止于训练完成日期,之后不再更新 | 当前、最新 |
| 准确性 | 对于历史事实可能准确,但无法反映新变化 | 可以反映最新状态 |
| LLM如何获取 | 作为其"原生"知识,直接从参数中生成 | 必须通过工具调用(如RAG、搜索、API) |
| 判断机制 | 无主动判断。它被训练成: - 知道实时问题需要外部工具,则发起调用。 - 否则,使用内部知识(可能已过时)。 | 由外部工具保证其"实时性",LLM只负责解读和呈现。 |
6.3. 那大模型的训练是需要不断更新训练? 还是可以在原有模型上继续训练? 将来模型的参数是不是会越来越大?
6.3.1. 如何更新:重新训练 vs. 持续训练
你猜得没错,可以在原有模型上继续训练。为了对抗知识的"时效性",目前主要有两种技术路径:
- 持续预训练 (CPT, Continual Pre-Training):这是保持模型"基础活力"的核心。它是在模型已有知识的基础上,持续注入海量的新领域语料(如最新新闻、专业论文),从而应对数据分布的漂移。
- 持续微调 (CFT, Continual Fine-Tuning):这个阶段更像是"岗前培训"。它针对具体任务进行连续的监督微调(SFT)或人类偏好对齐(RLHF),以提升模型在特定任务上的成功率和输出质量。
与费时费力的全量重新训练相比,业界更倾向于采用下面这些更高效的"轻量级更新"方法。
6.3.2. 如何不"喜新厌旧":应对"灾难性遗忘"
然而,直接在旧模型上训练新知识,会引发一个棘手的"AI健忘症"------灾难性遗忘 (Catastrophic Forgetting),也就是在学习新任务后,模型迅速遗忘了之前学会的技能。
为了解决这一难题,研究人员设计了许多巧妙的策略,你可以看看下面这个对比:
|-------------------|-----------------------------|---------------------------------------------------------------------------|------------------------------------------------|
| 方法类别 | 核心原理 | 优缺点 | 典型代表 |
| 参数高效微调 (PEFT) | 冻结原始模型,只调整并引入极少量额外的"插件"参数。 | ✅ 资源友好 :显存和存储占用极低,互不干扰。 ❌ 能力局限:对新知识的"吸收"不够深入。 | LoRA:只需训练0.01%-0.2%的新参数,效果堪比全量微调。 |
| 动态架构扩展 | 为模型动态增加新的"神经元"或"专家模块"。 | ✅ 专模专用 :为新知识开辟新区域,互不干扰。 ❌ 复杂度高:随着任务增多,模型体积会逐渐膨胀。 | MoE (混合专家模型):只激活与当前任务最相关的部分专家,总参数虽大但计算量可控。 |
| 正则化与记忆回放 | 在学习新知识时,通过特定算法"保护"旧知识的重要参数。 | ✅ 无需旧数据 :如弹性权重巩固(EWC)等技术通过计算参数重要性来保护旧知识。 ❌ 性能折衷:新旧知识间可能存在微妙的平衡难题。 | - |
| 知识编辑 | 像操作数据库一样,精准定位并修改模型中的特定"事实"。 | ✅ 外科手术式精准 :能快速修正错误事实,干扰极小。 ❌ 规模有限:目前主要适用于单点事实更新,难以应对大规模新知识。 | CASE框架:支持千次连续编辑,额外参数不到1MB。 |
6.3.3. 🧬 未来趋势:参数会无限膨胀吗?
短期看,模型参数依然会增长,但速度会放缓。长期看,我们追求的是"更高密度"的智能。
- 增长的"天花板":在经历了过去几年参数的指数级增长后(如GPT-3到GPT-4),行业普遍认为"单纯堆参数"带来的收益正在急剧递减。
- 计算瓶颈:训练万亿参数的模型需要数千张GPU卡连续运算数周,且随着摩尔定律放缓,单纯依靠硬件提升算力将变得越来越昂贵。
- 从"参数崇拜"到"效能革命" :模型的"能力密度",即单位参数所蕴含的智能水平,正成为新的衡量标准 。根据清华大学提出的"密度法则",大约每过3.5个月,参数量减半的模型就能达到当前最先进模型的性能水平。
- 技术的"多极化"发展:未来的大模型世界将不再是一种形态,而是百花齐放。
-
- 顶端追求 :参数量将迈向十万亿级别。这类"超级模型"将作为智能底座,通过MoE架构控制计算成本。
- 底座普及:模型参数增长将更为理性,同时推理成本会大幅下降。
- 端侧爆发 :小模型 (SLM) 正在崛起,它们参数虽小(如38亿参数),但在特定任务上表现出色,适合在手机等终端设备上运行。
- 高效化:量化、剪枝、知识蒸馏等模型压缩技术将成为模型部署的"标配"。
总的来说,未来大模型的发展并非单一维度的参数竞赛,而是形成一种 "一超多强" 的生态:
- "一超":极少数万亿级的"超级大模型"作为通用技术底座,持续进化。
- "多强":大量经过压缩、针对特定场景优化的"高效模型"(百亿级)将无处不在。
6.4. 有些人可能疑惑,为什么不专注于一个框架(如 LlamaIndex 或 LangChain),而是混合使用,甚至造轮子?
框架是加速开发的强大工具,是帮助我们快速跨越技术鸿沟的"桥梁"。但任何桥梁都有其设计边界和局限性。我们的目标不是成为一个熟练的"过桥者",而是成为一个懂得如何设计和建造桥梁的"工程师"。
因此,本教程选择的路径是:
- 以原理为主:我们优先关心的是"它是如何工作的?"而不是"我该调用哪个函数?"。新框架在诞生,老框架,但只要理解了底层的思想,我们将能更快地掌握任何现有或未来的框架。
- 拥抱灵活性 :真实世界的业务需求往往比框架预设的场景更复杂。当框架无法满足需求,或者像本节使用的
PandasQueryEngine那样存在安全隐患时,懂得原理的话,就有能力去修改它,或者像本节的示例一样,用更底层的模块组合出更安全、合适的解决方案。 - 培养解决问题的能力:只学习使用框架,好比是照着菜谱做菜,虽然能快速复刻出指定的菜肴,但一旦缺少某个食材或遇到意外情况,就可能束手无策。而理解原理,则像是学会了烹饪的精髓。这让你不仅能轻松地做出各种美食,还能创造新菜式。
如果你希望深入某个框架的细节,它的官方文档永远是最好、最权威的学习资料。而本教程的使命,是帮助你建立起关于 RAG 的坚实知识体系,让你无论面对何种工具,都能游刃有余。
博文参考
- Lewis et al. (2020).Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks
- RoBERTa: A Modified BERT Model for NLP
- Building Performant RAG Applications for Production
- LlamaIndex - Sentence Window Retrieval
- Recursive Retriever + Query Engine Demo
- Structured Hierarchical Retrieval
- Milvus官网地址 : https://milvus.io/
- Milvus GitHub : https://github.com/milvus-io/milvus