智能体13-RAG

RAG 简介

什么是 RAG?

RAG(Retrieval-Augmented Generation)是一种旨在解决大语言模型(LLM)"知其然不知其所以然"问题的技术范式。它的核心是将模型内部学到的"参数化知识 "(模型权重中固化的、模糊的"记忆"),与来自外部知识库的"非参数化知识"(精准、可随时更新的外部数据)相结合。其运作逻辑就是在 LLM 生成文本前,先通过检索机制从外部知识库中动态获取相关信息,并将这些"参考资料"融入生成过程,从而提升输出的准确性和时效性

技术原理

检索阶段:寻找"非参数化知识"

  • 知识向量化嵌入模型(Embedding Model) 充当了"连接器"的角色。它将外部知识库编码为向量索引(Index),存入向量数据库
  • 语义召回 :当用户发起查询时,检索模块利用同样的嵌入模型将问题向量化,并通过相似度搜索(Similarity Search) ,从海量数据中精准锁定与问题最相关的文档片段。

生成阶段:融合两种知识

  • 上下文整合生成模块接收检索阶段送来的相关文档片段以及用户的原始问题。
  • 指令 引导生成 :该模块会遵循预设的 Prompt 指令,将上下文与问题有效整合,并引导 LLM(如 DeepSeek)进行可控的、有理有据的文本生成。

技术演进

为什么要使用 RAG?

技术选型:RAG vs. 微调

在选择具体的技术路径时,一个重要的考量是成本与效益的平衡。通常,我们应优先选择对模型改动最小、成本最低的方案,所以技术选型路径往往遵循的顺序是提示词工程(Prompt Engineering) -> 检索增强生成 -> 微调(Fine-tuning)

我们可以从两个维度来理解这些技术的区别。如图 1-3 所示,横轴 代表" LLM 优化" ,即对模型本身进行多大程度的修改。从左到右,优化的程度越来越深,其中提示工程和 RAG 完全不改变模型权重,而微调则直接修改模型参数。纵轴代表"上下文优化" ,是对输入给模型的信息进行多大程度的增强。从下到上,增强的程度越来越高,其中提示工程只是优化提问方式,而 RAG 则通过引入外部知识库,极大地丰富了上下文信息。

基于此,我们的选择路径就清晰了:

  • 先尝试提示工程:通过精心设计提示词来引导模型,适用于任务简单、模型已有相关知识的场景。
  • 再选择 RAG:如果模型缺乏特定或实时知识而无法回答,则使用 RAG,通过外挂知识库为其提供上下文信息。
  • 最后考虑微调:当目标是改变模型"如何做"(行为/风格/格式)而不是"知道什么"(知识)时,微调是最终且最合适的选择。例如,让模型学会严格遵循某种独特的输出格式、模仿特定人物的对话风格,或者将极其复杂的指令"蒸馏"进模型权重中

关键优势

(1)准确性与可信度的双重提升

RAG 最核心的价值在于突破了模型预训练知识的限制。它不仅能补充专业领域的知识盲区 ,还能通过提供具体的参考材料,有效抑制"一本正经胡说八道"的幻觉现象 。论文研究还表明,RAG 生成的内容在具体性多样性 上也显著优于纯 LLM。更重要的是,RAG 具备可溯源性------每一条回答都能找到对应的原始文档出处,这种"有据可查"的特性极大提高了内容在法律、医疗等严肃场景下的可信度。

(2)时效性保障

在知识更新方面,RAG 解决了 LLM 固有的知识时滞问题 (即模型不知道训练截止日期之后发生的事)。RAG 允许知识库独立于模型进行动态更新------新政策或新数据一旦入库,立刻就能被检索到。这种能力在论文中被称为**"索引热拔插"(Index Hot-swapping)**------就像给机器人换一张存储卡一样,瞬间切换其世界知识库,而无需重新训练模型,实现了知识的实时在线。

(3)显著的综合成本效益

从经济角度看,RAG 是一种高性价比的方案。首先,它避免了高频微调 带来的巨额算力成本;其次,由于有了外部知识的强力辅助,我们在处理特定领域问题时,往往可以使用参数量更小的基础模型来达到类似的效果,从而直接降低了推理成本。这种架构也减少了试图将海量知识强行"塞入"模型权重中所需的计算资源消耗。

(4)灵活的模块化可扩展性

RAG 的架构具备极强的包容性,支持多源集成 ,无论是 PDF、Word 还是网页数据,都能统一构建进知识库中。同时,其模块化设计实现了检索与生成的解耦,这意味着我们可以独立优化检索组件(比如更换更好的 Embedding 模型),而不会影响到生成组件的稳定性,便于系统的长期迭代。

适用场景风险分级

如何快速上手RAG

(1)数据准备与清洗:这是系统的地基。我们需要将 PDF、Word 等多源异构数据标准化,并采用合理的分块策略(如按语义段落切分而非固定字符数),避免信息在切割中支离破碎。

(2)索引构建:将切分好的文本通过嵌入模型转化为向量,并存入数据库。可以在此阶段关联元数据(如来源、页码),这对后续的精确引用很有帮助。

(3)检索策略优化:不要依赖单一的向量搜索。可以采用混合检索(向量+关键词)等方式来提升召回率,并引入重排序模型对检索结果进行二次精选,确保 LLM 看到的都是精华。

(4)生成与提示工程:最后,设计一套清晰的 Prompt 模板,引导 LLM 基于检索到的上下文回答用户问题,并明确要求模型"不知道就说不知道",防止幻觉。

数据准备

数据加载

文档加载器

RAG 系统中,数据加载是整个流水线的第一步,也是不可或缺的一步。文档加载器负责将各种格式的非结构化文档(如PDF、Word、Markdown、HTML等)转换为程序可以处理的结构化数据。数据加载的质量会直接影响后续的索引构建、检索效果和最终的生成质量。

文档加载器在 RAG 的数据管道中一般需要完成三个核心任务,一是解析不同格式的原始文档,将 PDF、Word、Markdown 等内容提取为可处理的纯文本,二是在解析过程中同时抽取文档来源、页码、作者等关键信息作为元数据,三是把文本和元数据整理成统一的数据结构,方便后续进行切分、向量化和入库,其整体流程与传统数据工程中的抽取、转换、加载相似,目标都是把杂乱的原始文档清洗并对齐为适合检索和建模的标准化语料。

当前主流RAG文档加载器

Unstructured文档处理库

Unstructured 的核心优势

Unstructured (1)是一个专业的文档处理库,专门设计用于RAG和AI微调场景的非结构化数据预处理。提供了统一的接口来处理多种文档格式,是目前应用较广泛的文档加载解决方案之一。Unstructured 在格式支持和内容解析方面具有明显优势,它一方面支持 PDF、Word、Excel、HTML、Markdown 等多种文档格式,并通过统一的 API 接口避免为不同格式分别编写代码,另一方面可以自动识别标题、段落、表格、列表等文档结构,同时保留相应的元数据信息。

支持的文档元素类型

文本分块

理解文本分块

文本分块(Text Chunking)是构建 RAG 流程的关键步骤。它的原理是将加载后的长篇文档,切分成更小、更易于处理的单元。这些被切分出的文本块,是后续向量检索和模型处理的基本单位

文本分块重要性

满足模型上下文限制

将文本分块的首要原因,是为了适应 RAG 系统中两个核心组件的硬性限制:

  • 嵌入模型 (Embedding Model) : 负责将文本块转换为向量。这类模型有严格的输入长度上限。例如,许多常用的嵌入模型(如 bge-base-zh-v1.5)的上下文窗口为512个token。任何超出此限制的文本块在输入时都会被截断,导致信息丢失,生成的向量也无法完整代表原文的语义。因此,文本块的大小必须小于等于嵌入模型的上下文窗口。
  • 大语言模型 ( LLM ) : 负责根据检索到的上下文生成答案。LLM同样有上下文窗口限制(尽管通常比嵌入模型大得多,从几千到上百万token不等)。检索到的所有文本块,连同用户问题和提示词,都必须能被放入这个窗口中。如果单个块过大,可能会导致只能容纳少数几个相关的块,限制了LLM回答问题时可参考的信息广度。
为何"块"不是越大越好

假设嵌入模型最多能处理 8192 个 token,是否应该把块切得尽可能大(比如8000个token)呢?答案是否定的。块的大小并非越大越好,过大的块会严重影响RAG系统的性能。

2.2.1 嵌入过程中的信息损失

大多数嵌入模型都基于 Transformer 编码器。其工作流程大致如下:

  • 分词 (Tokenization) : 将输入的文本块分解成一个个 token。
  • 向量化 (Vectorization) : Transformer 为每个 token 生成一个高维向量表示。
  • 池化 ( Pooling ) : 通过某种方法(如取 [CLS] 位的向量、对所有token向量求平均 mean pooling 等),将所有 token 的向量压缩 成一个单一的向量,这个向量代表了整个文本块的语义。

[CLS] 是BERT等Transformer模型在输入文本开头添加的特殊标记,它通过自注意力机制动态聚合整个序列的上下文信息,其最终向量被训练用作代表全局语义的嵌入。

在这个压缩过程中,信息损失是不可避免的。一个768维的向量需要概括整个文本块的所有信息。文本块越长,包含的语义点越多,这个单一向量所承载的信息就越稀释,导致其表示变得笼统,关键细节被模糊化,从而降低了检索的精度。

2.2.2 生成过程的"大海捞针" (Lost in the Middle)

即使将检索到的多个大块文本都塞进LLM的长上下文窗口中,也会出现关键信息被"淹没"在大量无关内容里的问题。有研究表明 (1),当LLM处理非常长的、充满大量信息的上下文时,它倾向于更好地记住开头和结尾的信息,而忽略中间部分的内容。

如果提供给LLM的上下文块又大又杂,充满了与问题无关的噪音,模型就很难从中提取出最关键的信息来形成答案,从而导致回答质量下降或产生幻觉。

2.2.3 主题稀释导致检索失败

一个好的文本块应该聚焦于一个明确、单一的主题。如果一个块包含太多不相关的主题,它的语义就会被稀释,导致在检索时无法被精确匹配。

举个栗子🌰:

假设有一个关于《王者荣耀》英雄鲁班七号的攻略文档。

  • 糟糕的分块策略:将"技能介绍"、"推荐出装"和"背景故事"这三个完全不同主题的内容,全部放在一个巨大的文本块里。

    • 当玩家查询"鲁班七号怎么出装?"时,这个大块虽然包含了出装信息,但由于被技能说明和英雄故事等无关主题严重稀释,其整体的检索相关性得分可能会很低,导致无法被召回。
  • 优秀的分块策略:将"技能"、"出装"和"故事"分别切分为三个独立的、主题聚焦的块。

    • 当玩家再次查询时,"推荐出装"这个块会因为与查询高度相关而获得极高的分数,从而被精准地检索出来。

通过合理分块,可以有效提升检索的信噪比,确保了后续生成环节能得到最优质、最相关的上下文

基础分块策略

固定大小分块

这是最简单直接的分块方法。根据LangChain源码,这种方法的工作原理分为两个主要阶段:

(1)按段落分割CharacterTextSplitter 采用默认分隔符 "\n\n",使用正则表达式将文本按段落进行分割,通过 _split_text_with_regex 函数处理。

(2)智能合并 :调用继承自父类的 _merge_splits 方法,将分割后的段落依次合并。该方法会监控累积长度,当超过 chunk_size 时形成新块,并通过重叠机制(chunk_overlap)保持上下文连续性,同时在必要时发出超长块的警告。

需要注意,CharacterTextSplitter 实际实现的并非严格的固定大小分块。根据 _merge_splits 源码逻辑,这种方法会:

  • 优先保持段落完整性 :只有当添加新段落会导致总长度超过 chunk_size 时,才会结束当前块
  • 处理超长段落 :如果单个段落超过 chunk_size,系统会发出警告但仍将其作为完整块保留
  • 应用重叠机制 :通过 chunk_overlap 参数在块之间保持内容重叠,确保上下文连续性

所以,LangChain 的实现更准确地应该称为"段落感知的自适应分块",块大小会根据段落边界动态调整。

python 复制代码
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.document_loaders import TextLoader

loader = TextLoader("../../data/C2/txt/蜂医.txt")
docs = loader.load()

text_splitter = CharacterTextSplitter(
    chunk_size=200,    # 每个块的目标大小为100个字符
    chunk_overlap=10   # 每个块之间重叠10个字符,以缓解语义割裂
)

chunks = text_splitter.split_documents(docs)

print(f"文本被切分为 {len(chunks)} 个块。\n")
print("--- 前5个块内容示例 ---")
for i, chunk in enumerate(chunks[:5]):
    print("=" * 60)
    # chunk 是一个 Document 对象,需要访问它的 .page_content 属性来获取文本
    print(f'块 {i+1} (长度: {len(chunk.page_content)}): "{chunk.page_content}"')

这种方法的主要优势在于实现简单、处理速度快且计算开销小。劣势在于可能会在语义边界处切断文本,影响内容的完整性和连贯性。实际的固定大小分块实现(如LangChain的 CharacterTextSplitter)通常会结合分隔符来减少这种问题,在段落边界处优先切分,只有在必要时才会强制按大小切断。因此,这种方法在日志分析、数据预处理等场景中仍有其应用价值。

递归 字符分块

在前面的章节中,已经尝试了使用 RecursiveCharacterTextSplitter 的默认配置来处理文档分块。现在让我们深入了解 RecursiveCharacterTextSplitter 的实现。这种分块器通过分隔符层级递归处理,相对与固定大小分块,改善了超长文本的处理效果。

算法流程 : (1)寻找有效分隔符 : 从分隔符列表中从前到后遍历,找到第一个在当前文本中存在 的分隔符。如果都不存在,使用最后一个分隔符(通常是空字符串 "")。

(2)切分与分类处理: 使用选定的分隔符切分文本,然后遍历所有片段:

  • 如果片段不超过块大小 : 暂存到 _good_splits 中,准备合并

  • 如果片段超过块大小:

    • 首先,将暂存的合格片段通过 _merge_splits 合并成块

    • 然后,检查是否还有剩余分隔符:

      • 有剩余分隔符 : 递归调用 _split_text 继续分割
      • 无剩余分隔符: 直接保留为超长块

(3)最终处理: 将剩余的暂存片段合并成最后的块

实现细节

  • 批处理 机制 : 先收集所有合格片段(_good_splits),遇到超长片段时才触发合并操作。
  • 递归 终止条件 : 关键在于 if not new_separators 判断。当分隔符用尽时(new_separators 为空),停止递归,直接保留超长片段。确保算法不会无限递归。

与固定大小分块的关键差异

  • 固定大小分块遇到超长段落时只能发出警告并保留。
  • 递归分块会继续使用更细粒度的分隔符(句子→单词→字符)直到满足大小要求。
ini 复制代码
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader

loader = TextLoader("../../data/C2/txt/蜂医.txt")
docs = loader.load()

text_splitter = RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", "。", ",", " ", ""],  # 分隔符优先级
    chunk_size=200,
    chunk_overlap=10,
)

chunks = text_splitter.split_text(docs)

递归字符分块的原理是采用一组有层次结构的分隔符(如段落、句子、单词)进行递归分割,旨在有效平衡语义完整性与块大小控制。在 RecursiveCharacterTextSplitter 的实现中,该分块器首先尝试使用最高优先级的分隔符(如段落标记)来切分文本。如果切分后的块仍然过大,会继续对这个大块应用下一优先级分隔符(如句号),如此循环往复,直到块满足大小限制。这种分层处理的机制,能够在尽可能保持高级语义结构完整性的同时,有效控制块大小。

语义分块

语义分块(Semantic Chunking)是一种更智能的方法,这种方法不依赖于固定的字符数或预设的分隔符,而是尝试根据文本的语义内涵来切分。其核心是:在语义主题发生显著变化的地方进行切分 。这使得每个分块都具有高度的内部语义一致性。LangChain 提供了 langchain_experimental.text_splitter.SemanticChunker 来实现这一功能。

实现原理

SemanticChunker 的工作流程可以概括为以下几个步骤:

(1)句子分割 (Sentence Splitting) :首先,使用标准的句子分割规则(例如,基于句号、问号、感叹号)将输入文本拆分成一个句子列表。

(2)上下文感知嵌入 (Context-Aware Embedding) :这是 SemanticChunker 的一个关键设计。该分块器不是对每个句子独立进行嵌入,而是通过 buffer_size 参数(默认为1)来捕捉上下文信息。对于列表中的每一个句子,这种方法会将其与前后各 buffer_size 个句子组合起来,然后对这个临时的、更长的组合文本进行嵌入。这样,每个句子最终得到的嵌入向量就融入了其上下文的语义。

(3)计算语义距离 (Distance Calculation) :计算每对相邻句子的嵌入向量之间的余弦距离。这个距离值量化了两个句子之间的语义差异------距离越大,表示语义关联越弱,跳跃越明显。

(4)识别 断点 ( Breakpoint Identification)SemanticChunker 会分析所有计算出的距离值,并根据一个统计方法(默认为 percentile)来确定一个动态阈值。例如,它可能会将所有距离中第95百分位的值作为切分阈值。所有距离大于此阈值的点,都被识别为语义上的"断点"。

(5)合并成块 (Merging into Chunks) :最后,根据识别出的所有断点位置,将原始的句子序列进行切分,并将每个切分后的部分内的所有句子合并起来,形成一个最终的、语义连贯的文本块。

断点 识别方法 ( breakpoint_threshold_type )

如何定义"显著的语义跳跃"是语义分块的关键。SemanticChunker 提供了几种基于统计的方法来识别断点:

  • percentile (百分位法 - 默认方法):

    • 逻辑: 计算所有相邻句子的语义差异值,并将这些差异值进行排序。当一个差异值超过某个百分位阈值时,就认为该差异值是一个断点。
    • 参数 : breakpoint_threshold_amount (默认为 95),表示使用第95个百分位作为阈值。这意味着,只有最显著的5%的语义差异点会被选为切分点。
  • standard_deviation (标准差法):

    • 逻辑: 计算所有差异值的平均值和标准差。当一个差异值超过"平均值 + N * 标准差"时,被视为异常高的跳跃,即断点。
    • 参数 : breakpoint_threshold_amount (默认为 3),表示使用3倍标准差作为阈值。
  • interquartile (四分位距法):

    • 逻辑 : 使用统计学中的四分位距(IQR)来识别异常值。当一个差异值超过 Q3 + N * IQR 时,被视为断点。
    • 参数 : breakpoint_threshold_amount (默认为 1.5),表示使用1.5倍的IQR。
  • gradient (梯度法):

    • 逻辑: 这是一种更复杂的方法。它首先计算差异值的变化率(梯度),然后对梯度应用百分位法。对于那些句子间语义联系紧密、差异值普遍较低的文本(如法律、医疗文档)特别有效,因为这种方法能更好地捕捉到语义变化的"拐点"。
    • 参数 : breakpoint_threshold_amount (默认为 95)。
基于文档结构的分块

对于具有明确结构标记的文档格式(如Markdown、HTML、LaTex),可以利用这些标记来实现更智能、更符合逻辑的分割。

Markdown 结构分块为例

针对结构清晰的 Markdown 文档,利用其标题层级进行分块是一种高效且保留了丰富语义的方法。LangChain 提供了 MarkdownHeaderTextSplitter 来处理。

  • 实现原理: 该分块器的主要逻辑是"先按标题分组,再按需细分"。

    • 定义分割规则 : 用户首先需要提供一个标题层级的映射关系,例如 [ ("#", "Header 1"), ("##", "Header 2") ],告诉分块器 # 是一级标题,## 是二级标题。
    • 内容聚合: 分块器会遍历整个文档,将每个标题下的所有内容(直到下一个同级或更高级别的标题出现前)聚合在一起。每个聚合后的内容块都会被赋予一个包含其完整标题路径的元数据。
  • 元数据注入的优势 : 这是此方法的主要特点。例如,对于一篇关于机器学习的文章,某个段落可能位于"第三章:模型评估"下的"3.2节:评估指标"中。经过分割后,这个段落形成的文本块,其元数据就会是 {"Header 1": "第三章:模型评估", "Header 2": "3.2节:评估指标"}。这种元数据为每个块提供了精确的"地址",极大地增强了上下文的准确性,让大模型能更好地理解信息片段的来源和背景。

  • 局限性与组合使用 : 单纯按标题分割可能会导致一个问题:某个章节下的内容可能非常长,远超模型能处理的上下文窗口。为了解决这个问题,MarkdownHeaderTextSplitter 可以与其它分块器(如 RecursiveCharacterTextSplitter组合使用。具体流程是:

    • 第一步,使用 MarkdownHeaderTextSplitter 将文档按标题分割成若干个大的、带有元数据的逻辑块。
    • 第二步,对这些逻辑块再应用 RecursiveCharacterTextSplitter,将其进一步切分为符合 chunk_size 要求的小块。由于这个过程是在第一步之后进行的,所有最终生成的小块都会继承来自第一步的标题元数据。
  • RAG应用优势: 这种两阶段的分块方法,既保留了文档的宏观逻辑结构(通过元数据),又确保了每个块的大小适中,是处理结构化文档进行RAG的理想方案。

Unstructured:基于文档元素的智能分块

Unstructured是一个强大的文档处理工具,同样提供了实用的分块功能

(1)分区 (Partitioning) : 这是一个重要功能,负责将原始文档(如PDF、HTML)解析成一系列结构化的"元素"(Elements)。每个元素都带有语义标签,如 Title (标题)、NarrativeText (叙述文本)、ListItem (列表项) 等。这个过程本身就完成了对文档的深度理解和结构化。

(2)分块 (Chunking) : 该功能建立在分区的结果之上。分块功能不是对纯文本进行操作,而是将分区产生的"元素"列表作为输入,进行智能组合。Unstructured 提供了两种主要的分块方法:

  • basic : 这是默认方法。这种方法会连续地组合文档元素(如段落、列表项),直到达到 max_characters 上限,尽可能地填满每个块。如果单个元素超过上限,则会对其进行文本分割。
  • by_title : 该方法在 basic 方法的基础上,增加了对"章节"的感知。该方法将 Title 元素视为一个新章节的开始,并强制在此处开始一个新的块,确保同一个块内不会包含来自不同章节的内容。这在处理报告、书籍等结构化文档时非常有用,效果类似于 LangChain 的 MarkdownHeaderTextSplitter,但适用范围更广。

Unstructured 允许将分块作为分区的一个参数在单次调用中完成,也支持在分区之后作为一个独立的步骤来执行分块。这种"先理解、后分割"的策略,使得 Unstructured 能在最大程度上保留文档的原始语义结构,特别是在处理版式复杂的文档时,优势尤为明显。

LlamaIndex:面向节点的解析与转换

LlamaIndex 将数据处理流程抽象为对"节点(Node) "的操作。文档被加载后,首先会被解析成一系列的"节点",分块只是节点转换(Transformation)中的一环。

LlamaIndex 的分块体系有以下特点:

(1)丰富的节点 解析器 (Node Parser ) : LlamaIndex 提供了大量针对特定数据格式和方法的节点解析器,可以大致分为几类:

  • 结构感知型 : 如 MarkdownNodeParser, JSONNodeParser, CodeSplitter 等,能理解并根据源文件的结构(如Markdown标题、代码函数)进行切分。

  • 语义感知型:

    • SemanticSplitterNodeParser: 与 LangChain 的 SemanticChunker 类似,这种解析器使用嵌入模型来检测句子之间的语义"断点",在语义连续性明显减弱的地方切开,从而让每个 chunk 内部尽量连贯。
    • SentenceWindowNodeParser: 这是一种巧妙的方法。该方法将文档切分成单个的句子,但在每个句子节点(Node)的元数据中,会存储其前后相邻的N个句子(即"窗口")。这使得在检索时,可以先用单个句子的嵌入进行精确匹配,然后将包含上下文"窗口"的完整文本送给LLM,极大地提升了上下文的质量。
  • 常规型 : 如 TokenTextSplitter, SentenceSplitter 等,提供基于Token数量或句子边界的常规切分方法。

(2)灵活的转换流水线 : 用户可以构建一个灵活的流水线,例如先用 MarkdownNodeParser 按章节切分文档,再对每个章节节点应用 SentenceSplitter 进行更细粒度的句子级切分。每个节点都携带丰富的元数据,记录着其来源和上下文关系。

(3)良好的 互操作性 : LlamaIndex 提供了 LangchainNodeParser,可以方便地将任何 LangChain 的 TextSplitter 封装成 LlamaIndex 的节点解析器,无缝集成到其处理流程中。

向量嵌入

向量嵌入基础

基础概念

什么是 Embedding

向量嵌入(Embedding)是一种将真实世界中复杂、高维的数据对象(如文本、图像、音频、视频等)转换为数学上易于处理的、低维、稠密的连续数值向量的技术。

想象一下,我们将每一个词、每一段话、每一张图片都放在一个巨大的多维空间里,并给它一个独一无二的坐标。这个坐标就是一个向量,它"嵌入"了原始数据的所有关键信息。这个过程,就是 Embedding。

  • 数据对象:任何信息,如文本"你好世界",或一张猫的图片。
  • Embedding 模型:一个深度学习模型,负责接收数据对象并进行转换。
  • 输出向量 :一个固定长度的一维数组,例如 [0.16, 0.29, -0.88, ...]。这个向量的维度(长度)通常在几百到几千之间。
向量空间 的语义表示

Embedding 的真正意义在于,它产生的向量不是随机数值的堆砌,而是对数据语义的数学编码。

  • 核心原则:在 Embedding 构建的向量空间中,语义上相似的对象,其对应的向量在空间中的距离会更近;而语义上不相关的对象,它们的向量距离会更远。

  • 关键度量:我们通常使用以下数学方法来衡量向量间的"距离"或"相似度":

    • 余弦相似度 ( Cosine Similarity ) :计算两个向量夹角的余弦值。值越接近 1,代表方向越一致,语义越相似。这是最常用的度量方式。
    • 点积 (Dot Product) :计算两个向量的乘积和。在向量归一化后,点积等价于余弦相似度。
    • 欧氏距离 ( Euclidean Distance ) :计算两个向量在空间中的直线距离。距离越小,语义越相似。

Embedding 在 RAG 中的作用

在RAG流程中,Embedding 扮演着无可替代的重要角色。

1.2.1 语义检索的基础

RAG 的"检索"环节通常以基于 Embedding 的语义搜索为核心。通用流程如下: (1)离线索引构建:将知识库内文档切分后,使用 Embedding 模型将每个文档块(Chunk)转换为向量,存入专门的向量数据库中。

(2)在线查询检索 :当用户提出问题时,使用同一个 Embedding 模型将用户的问题也转换为一个向量。

(3)相似度计算:在向量数据库中,计算"问题向量"与所有"文档块向量"的相似度。

(4)召回上下文:选取相似度最高的 Top-K 个文档块,作为补充的上下文信息,与原始问题一同送给大语言模型(LLM)生成最终答案。

1.2.2 决定检索质量的关键

Embedding 的质量直接决定了 RAG 检索召回内容的准确性与相关性。一个优秀的 Embedding 模型能够精准捕捉问题和文档之间的深层语义联系,即使用户的提问和原文的表述不完全一致。反之,一个劣质的 Embedding 模型可能会因为无法理解语义而召回不相关或错误的信息,从而"污染"提供给 LLM 的上下文,导致最终生成的答案质量低下。

Embedding 技术发展

静态词嵌入:上下文无关的表示

  • 代表模型:Word2Vec (2013), GloVe (2014)
  • 主要原理 :为词汇表中的每个单词生成一个固定的、与上下文无关的向量。例如,Word2Vec 通过 Skip-gram 和 CBOW 架构,利用局部上下文窗口学习词向量,并验证了向量运算的语义能力(如 国王 - 男人 + 女人 ≈ 王后)。GloVe 则融合了全局词-词共现矩阵的统计信息。
  • 局限性:无法处理一词多义问题。在"苹果公司发布了新手机"和"我吃了一个苹果"中,"苹果"的词向量是完全相同的,这限制了其在复杂语境下的语义表达能力。

动态上下文嵌入

2017年,Transformer 架构的诞生带来了自注意力机制(Self-Attention),它允许模型在生成一个词的向量时,动态地考虑句子中所有其他词的影响。基于此,2018年 BERT 模型利用 Transformer 的编码器,通过掩码语言模型(MLM)等自监督任务进行预训练,生成了深度上下文相关的嵌入。同一个词在不同语境中会生成不同的向量,这有效解决了静态嵌入的一词多义难题。

RAG 对嵌入技术的新要求

在开篇我们就提到了 RAG 框架的提出(1),是为了解决大型语言模型 知识固化 (内部知识难以更新)和 幻觉 (生成的内容可能不符合事实且无法溯源)的问题。它通过"检索-生成"范式,动态地为 LLM 注入外部知识。这一过程的核心是 语义检索,很大程度上依赖于高质量的向量嵌入。

后续 RAG 的兴起对嵌入技术提出了更高、更具体的要求:

  • 领域自适应能力:通用的嵌入模型在专业领域(如法律、医疗)往往表现不佳,这就要求嵌入模型具备领域自适应的能力,能够通过微调或使用指令(如 INSTRUCTOR 模型)来适应特定领域的术语和语义。
  • 多粒度与多模态支持:RAG 系统需要处理的不仅仅是短句,还可能包括长文档、代码,甚至是图像和表格。这就要求嵌入模型能够处理不同长度和类型的输入数据。
  • 检索效率与混合检索:嵌入向量的维度和模型大小直接影响存储成本和检索速度。同时,为了结合语义相似性(密集检索)和关键词匹配(稀疏检索)的优点,支持混合检索的嵌入模型(如 BGE-M3)应运而生,在某些任务中成为提升召回率的关键。

嵌入模型选型指南

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

索引优化

上下文扩展

在RAG系统中,常常面临一个权衡问题:使用小块文本进行检索可以获得更高的精确度,但小块文本缺乏足够的上下文,可能导致大语言模型(LLM)无法生成高质量的答案;而使用大块文本虽然上下文丰富,却容易引入噪音,降低检索的相关性。为了解决这一矛盾,LlamaIndex 提出了一种实用的索引策略------句子窗口检索(Sentence Window Retrieval) (2)。该技术巧妙地结合了两种方法的优点:它在检索时聚焦于高度精确的单个句子,在送入LLM生成答案前,又智能地将上下文扩展回一个更宽的"窗口",从而同时保证检索的准确性和生成的质量。

主要思路

句子窗口检索的思想可以概括为:为检索精确性而索引小块,为上下文丰富性而检索大块

其工作流程如下:

(1)索引阶段 :在构建索引时,文档被分割成单个句子 。每个句子都作为一个独立的"节点(Node)"存入向量数据库。同时,每个句子节点都会在元数据(metadata)中存储其上下文窗口,即该句子原文中的前N个和后N个句子。这个窗口内的文本不会被索引,仅仅是作为元数据存储。

(2)检索阶段 :当用户发起查询时,系统会在所有单一句子节点上执行相似度搜索。因为句子是表达完整语义的最小单位,所以这种方式可以非常精确地定位到与用户问题最相关的核心信息。

(3)后处理阶段 :在检索到最相关的句子节点后,系统会使用一个名为 MetadataReplacementPostProcessor 的后处理模块。该模块会读取到检索到的句子节点的元数据,并用元数据中存储的完整上下文窗口来替换节点中原来的单一句子内容。

(4)生成阶段:最后,这些被替换了内容的、包含丰富上下文的节点被传递给LLM,用于生成最终的答案。

混合检索-稠密+稀疏检索融合

什么是混合检索

混合检索(Hybrid Search)是一种结合了 稀疏向量( Sparse Vectors)密集向量(Dense Vectors) 优势的先进搜索技术。旨在同时利用稀疏向量的关键词精确匹配能力和密集向量的语义理解能力,以克服单一向量检索的局限性,从而在各种搜索场景下提供更准确、更鲁棒的检索结果。

稀疏向量 vs 密集向量

稀疏向量

密集向量

密集向量,也常被称为"语义向量",是通过深度学习模型学习到的数据(如文本、图像)的低维、稠密的浮点数表示。这些向量旨在将原始数据映射到一个连续的、充满意义的"语义空间"中来捕捉"语义"或"概念"。在理想的语义空间中,向量之间的距离和方向代表了它们所表示概念之间的关系。一个经典的例子是 vector('国王') - vector('男人') + vector('女人') 的计算结果在向量空间中非常接近 vector('女王'),这表明模型学会了"性别"和"皇室"这两个维度的抽象概念。它的代表包括 Word2Vec、GloVe、以及所有基于 Transformer 的模型(如 BERT、GPT)生成的嵌入(Embeddings)。

其主要优点是能够理解同义词、近义词和上下文关系,泛化能力强,在语义搜索任务中表现卓越。但缺点也同样明显:可解释性差(向量中的每个维度通常没有具体的物理意义),需要大量数据和算力进行模型训练,且对于未登录词(OOV)\^1的处理相对困难。

OOV Out-of-Vocabulary )未登录词:指在模型训练时没有出现在词汇表中,但在实际使用时遇到的新词汇。例如,如果模型训练时词汇表中没有"ChatGPT"这个词,那么在实际应用中遇到它时就是OOV。传统的稀疏向量方法(如BM25)对OOV词汇会完全忽略,而现代的密集向量方法通过子词分割(如BPE、WordPiece)可以更好地处理OOV问题。

混合检索

通过上文可以看出稀疏向量和密集向量各有千秋,那么将它们结合起来,实现优势互补,就成了一个不错的选择。混合检索便是基于这个思路,通过结合多种搜索算法(最常见的是稀疏与密集检索)来提升搜索结果相关性和召回率。

  • 主要目标 :解决单一检索技术的局限性。例如,关键词检索无法理解语义,而向量检索则可能忽略掉必须精确匹配的关键词(如产品型号、函数名等)。混合检索旨在同时利用稀疏向量的精确性 和密集向量的泛化性,以应对复杂多变的搜索需求。

2.1 技术原理与融合方法

混合检索通常并行执行两种检索算法,然后将两组异构的结果集融合成一个统一的排序列表。以下是两种主流的融合策略:

通过调整 α 的值,可以灵活地控制语义相似性与关键词匹配在最终排序中的贡献比例。例如,在电商搜索中,可以调高关键词的权重;而在智能问答中,则可以侧重于语义。

2.2 优势与局限

优势 局限
召回率与准确率高:能同时捕获关键词和语义,显著优于单一检索。 计算资源消耗大:需要同时维护和查询两套索引。
灵活性强:可通过融合策略和权重调整,适应不同业务场景。 参数调试复杂:融合权重等超参数需要反复实验调优。
容错性好:关键词检索可部分弥补向量模型对拼写错误或罕见词的敏感性。 可解释性仍是挑战:融合后的结果排序理由难以直观分析。
相关推荐
小林ixn1 小时前
远程 MCP 实战:让 AI 无缝调用地图、浏览器与文件系统
人工智能·agent·mcp
chanalbert2 小时前
Agent 工程化指南(一):概念与认知架构 — 智能体到底是什么、怎么思考
llm·agent
小林ixn2 小时前
AI Agent的“万能工具箱”:MCP协议实战,让工具调用不再受语言和进程束缚
人工智能·agent·mcp
一个处女座的程序猿3 小时前
Agent之Memory:claude-mem的简介、安装和使用方法、案例应用之详细攻略
agent·memory·claude-mem
To_OC4 小时前
MCP 入门实战:写一个能读本地文件的极简服务
人工智能·agent·mcp
倾颜15 小时前
AI 聊天长期记忆的向量化处理:从"关键词匹配"到"换种说法也能找到"
llm·agent·next.js
阿里云大数据AI技术19 小时前
Elasticsearch 智能助手:Agent 让运维从经验驱动迈向智能协同
人工智能·elasticsearch·agent
DigitalOcean19 小时前
GPU 算力降本指南:拆解大模型推理账单的 4 个隐藏加价坑
llm·agent
用户77833661321120 小时前
# 从零搭一个能搜 Google 的 AI Agent:完整工程流程
agent