没有理想的人不伤心

RAG 技术大全

2025/10/26
5
0

RAG(Retrieval-Augmented Generation,检索增强生成)技术:通过引入外部知识库,利用检索模块(Retriever)从大量文档中提取相关信息,并将这些信息传递给生成模块(Generator,一般是大模型),从而生成更准确且有用的回答。

核心思想在于通过检索与生成的有机结合,弥补大模型在处理领域问题和实时任务时的不足

1. 简介

解决的问题:大模型在知识、理解、推理方面展现了卓越的能力,然而又很多无法忽视的局限性:

  • 领域知识缺乏:大模型的知识来源于训练数据,这些数据主要来自公开的互联网和开源数据集,无法覆盖特定领域或高度专业化的内部知识
  • 信息过时:模型难以处理实时信息,因为训练过程耗时且成本高昂,模型一旦训练完成,就难以获取和处理新信息。
  • 幻觉问题:大模型基于概率生成文本,有时候的会”一本正经的说胡话“
  • 数据安全性:如何在保证企业数据安全的前提下,使大模型有效利用私有数据进行推理和生成

RAG:通过将非参数化的外部数据库、文档与大模型结合,使大模型在生成内容前,检索相关信息,以弥补模型在知识专业性和时效性上的不足,减少生成不确定性,在确保数据安全的同时,充分利用领域知识和私有数据。

非参数化的理解:训练模型时,训练数据在训练过程中转化为模型参数,而非参数化,就是不将外部文档等作为训练数据,而是保持独立,让大模型检索

为什么要使用 rag,直接将所有的知识库交给大模型处理不行吗(一股脑全扔给大模型):模型能够处理的 token 数有限,输入过多 token 会增加成本,而且提供少量相关的关键信息能带来更优质的回答

rag 和微调的选择:RAG 更适用于需要动态响应、频繁更新外部知识的场景,而微调则适合固定领域内的深度优化与推理。当应用场景中既需要利用最新的外部知识,又需要保持高水平的领域推理能力时,可以考虑结合使用 RAG 和微调,以实现最优的性能和效果。

2. RAG 的标准技术流程

image.png

RAG 的标准流程由三个节点组成:

  • 索引阶段:处理多种来源、格式的文档,提取其中的文本,将其切分为标准长度的文本块(chunk),并进行嵌入向量化(embedding),向量存储在向量数据库中
  • 检索节点:用户输入的查询(query)被转化为向量表示,通过相似度匹配从向量数据库中检索出最相关的文本块
  • 生成阶段:将检索到的相关文本与原始查询共同构成提示词(prompt),输入到大模型,生成回答

2.1 索引

索引包含四个关键步骤:

  1. 首先,将各类数据源及其格式(如书籍、教材、领域数据、企业文档等,txt、markdown、doc、ppt、excel、pdf、html、json 等格式)统一解析为纯文本格式。
  2. 接着,根据文本的语义或文档结构,将文档分割为小而语义完整的文本块(chunks),确保系统能够高效检索和利用这些块中包含的信息。
  3. 然后,使用文本嵌入模型(embedding model),将这些文本块向量化,生成高维稠密向量,转换为计算机可理解的语义表示。
  4. 最后,将这些向量存储在向量数据库 (vector database) 中,并构建索引,完成知识库的构建。这一流程成功将外部文档转化为可检索的向量,支撑后续的检索和生成环节。、

2.2 检索

检索是连接用户查询与知识库的核心环节。

  1. 首先,用户输入的问题通过同样的文本嵌入模型转换为向量表示,将查询映射到与知识库内容相同的向量空间中。
  2. 通过相似度度量方法,检索模块从向量数据库中筛选出与查询最相关的前 K 个文本块,这些文本块将作为生成阶段输入的一部分。
  3. 通过相似性搜索,检索模块有效获取了与用户查询切实相关的外部知识,为生成阶段提供了精确且有意义的上下文支持。

2.3 生成

将检索到的相关文本块与用户的原始查询整合为增强提示词(Prompt),并输入到大语言模型(LLM)中。LLM 基于这些输入生成最终的回答,确保生成内容既符合用户的查询意图,又充分利用了检索到的上下文信息,使得回答更加准确和相关,充分使用到知识库中的知识。通过这一过程,RAG 实现了具备领域知识和私有信息的精确内容生成。

3. RAG 应用实战

3.1 技术选型:

技术选型如下所示:

具体内容 技术选型 描述
操作系统 Linux
编程语言 Python python >= 3.8
RAG 技术框架 LangChain LLM 开发框架
索引 - 文档解析模块 pypdf 用于处理 pdf 文档
索引 - 文档分块模块 RecursiveCharacterTextSplitter ClangChain 默认的文本分割器,该分割器通过层次化的分隔符(从双换行符到单字符)拆分文本,旨在保持文本的结构和连贯性,优先考虑自然边界如段落和句子。
索引/检索 - 向量库 Faiss 全称 Facebook AI Similarity Search,由 Facebook AI Research 团队开源的向量库,因其稳定性和高效性在向量检索领域广受欢迎。
大模型 Qwen 阿里旗下大模型

技术选型流程图:
image.png

这里用到了 bge 模型,用于向量嵌入,[[bge 模型]]

3.2 环境搭建

  1. 创建并激活虚拟环境
python3 -m venv rag_env  # 创建名为 rag_env 的虚拟环境
source rag_env/bin/activate  # 激活虚拟环境
  1. 安装依赖
pip install langchain langchain_community pypdf sentence-transformers faiss-cpu dashscope
  1. 下载 bge-small-zh-v1.5 模型
git clone https://gitee.com/techleadcy/rag_app.git
  1. 阿里百炼大模型平台获取 secret key

3.3 RAG 核心流程的基础实现

仅仅简单的走通了从索引、检索、由千问大模型生成回答的核心流程

  1. 模块导入及密钥获取
from langchain_community.document_loaders import PyPDFLoader # PDF 文档提取
from langchain_text_splitters import RecursiveCharacterTextSplitter # 文档拆分 chunk
from sentence_transformers import SentenceTransformer # 加载和使用 Embedding 模型
import faiss # Faiss 向量库
import numpy as np # 处理嵌入向量数据,用于 Faiss 向量检索
import dashscope #调用 Qwen 大模型
from http import HTTPStatus #检查与 Qwen 模型 HTTP 请求状态

import os # 引入操作系统库,后续配置环境变量与获得当前文件路径使用
os.environ["TOKENIZERS_PARALLELISM"] = "false" # 不使用分词并行化操作, 避免多线程或多进程环境中运行多个模型引发冲突或死锁

# 设置 Qwen 系列具体模型及对应的调用 API 密钥,从阿里云百炼大模型服务平台获得
qwen_model = "qwen-turbo"
qwen_api_key = "sk-6fa20792379a46faa269a17d5015a4c8"

def load_embedding_model():
    """
    加载 bge-small-zh-v1.5 模型
    :return: 返回加载的 bge-small-zh-v1.5 模型
    """
    print(f"加载 Embedding 模型中")
    # SentenceTransformer 读取绝对路径下的 bge-small-zh-v1.5 模型,非下载
    embedding_model = SentenceTransformer(os.path.abspath('rag_app/bge-small-zh-v1.5'))
    print(f"bge-small-zh-v1.5 模型最大输入长度: {embedding_model.max_seq_length}") 
    return embedding_model

以上代码导入了我们在 RAG 流程中需要使用的核心模块及大模型参数等配置,这些模块和配置将在后续的索引、检索和生成流程中调用使用。

  1. 索引流程
def indexing_process(pdf_file,embedding_model):
    """
    索引流程:加载 PDF 文件,并将其内容分割成小块,计算这些小块的嵌入向量并将其存储在 FAISS 向量数据库中。
    :param pdf_file:PDF 文件路径
    :param embedding_model: 预加载的嵌入模型
    :return: 返回 FAISS 嵌入向量索引和分割后的文本块原始内容列表
    """
    # PyPDFLoader 加载 PDF 文件,忽略图片提取
    pdf_loader = PyPDFLoader(pdf_file,extract_images=False)
    # 配置 RecursiveCharacterTextSplitter 分割文本块库参数,每个文本块的大小为 768 字符(非 token),相邻文本块之间的重叠 256 字符(非 token)
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=512,chunk_overlap=128
    )
    # 加载 PDF 文档,提取所有页的文本内容
    pdf_content_list = pdf_loader.load()
    # 将每页的文本内容用换行符连接,合并为 PDF 文档的完整文本
    pdf_text = "\n".join([page.page_content for page in pdf_content_list])
    print(f"PDF 文档的总字符数: {len(pdf_text)}") 

    # 将 PDF 文档文本分割成文本块 Chunk
    chunks = text_splitter.split_text(pdf_text)
    print(f"分割的文本 Chunk 数量: {len(chunks)}") 

    # 文本块转化为嵌入向量列表,normalize_embeddings 表示对嵌入向量进行归一化,用于准确计算相似度
    embeddings = []
    for chunk in chunks:
        embedding = embedding_model.encode(chunk,normalize_embeddings=True)
        embeddings.append(embedding)

    print("文本块 Chunk 转化为嵌入向量完成")

    # 将嵌入向量列表转化为 numpy 数组,FAISS 索引操作需要 numpy 数组输入
    embeddings_np = np.array(embeddings)

    # 获取嵌入向量的维度(每个向量的长度)
    dimension = embeddings_np.shape[1]

    # 使用余弦相似度创建 FAISS 索引
    index = faiss.IndexFlatIP(dimension)
    # 将所有的嵌入向量添加到 FAISS 索引中,后续可以用来进行相似性检索
    index.add(embeddings_np)

    print("索引过程完成.")

    return index,chunks

上述代码实现了 RAG 技术中的索引流程,首先使用 PyPDFLoader 加载并预处理 PDF 文档,将其内容提取并合并为完整文本。接着,利用 RecursiveCharacterTextSplitter 将文本分割为每块 512 字符(非 token)、重叠 128 字符(非 token)的文本块,并通过预加载的 bge-small-zh-v1.5 嵌入模型将这些文本块转化为归一化的嵌入向量。最终,这些嵌入向量被存储在基于余弦相似度的 Faiss 向量库中,以支持后续的相似性检索和生成任务。

为更清晰地展示 RAG 流程的各个细节,当前代码未涉及多文档处理、嵌入模型的效率优化与并行处理。此外,Faiss 向量目前仅在内存中存储,未考虑持久化存储问题。以及文档解析、文本分块、嵌入模型与向量库的技术选型,后续课程将逐步深入探讨,并以上述代码作为基础,持续优化。

  1. 检索流程
def retrieval_process(query,index,chunks,embedding_model,top_k=3):
    """
    检索流程:将用户查询 Query 转化为嵌入向量,并在 Faiss 索引中检索最相似的前 k 个文本块。
    :param query: 用户查询语句
    :param index: 已建立的 Faiss 向量索引
    :param chunks: 原始文本块内容列表
    :param embedding_model: 预加载的嵌入模型
    :param top_k: 返回最相似的前 K 个结果
    :return: 返回最相似的文本块及其相似度得分
    """
    # 将查询转化为嵌入向量,normalize_embeddings 表示对嵌入向量进行归一化
    query_embedding = embedding_model.encode(query,normalize_embeddings=True)
    # 将嵌入向量转化为 numpy 数组,Faiss 索引操作需要 numpy 数组输入
    query_embedding = np.array([query_embedding])

    # 在 Faiss 索引中使用 query_embedding 进行搜索,检索出最相似的前 top_k 个结果。
    # 返回查询向量与每个返回结果之间的相似度得分(在使用余弦相似度时,值越大越相似)排名列表 distances,最相似的 top_k 个文本块在原始 chunks 列表中的索引 indices。
    distances,indices = index.search(query_embedding,top_k)

    print(f"查询语句: {query}")
    print(f"最相似的前{top_k}个文本块:")

    # 输出查询出的 top_k 个文本块及其相似度得分
    results = []
    for i in range(top_k):
        # 获取相似文本块的原始内容
        result_chunk = chunks[indices[0][i]]
        print(f"文本块 {i}:\n{result_chunk}") 

        # 获取相似文本块的相似度得分
        result_distance = distances[0][i]
        print(f"相似度得分: {result_distance}\n")

        # 将相似文本块存储在结果列表中
        results.append(result_chunk)

    print("检索过程完成.")
    return results

上述代码实现了 RAG 技术中的检索流程。首先,用户的查询(Query)被预加载的 bge-small-zh-v1.5 嵌入模型转化为归一化的嵌入向量,进一步转换为 numpy 数组以适配 Faiss 向量库的输入格式。然后,利用 Faiss 向量库中的向量检索功能,计算查询向量与存储向量之间的余弦相似度,从而筛选出与查询最相似的前 top_k 个文本块。这些文本块及其相应的相似度得分被逐一输出,相似文本块存储在结果列表中,最终返回供后续生成过程使用。

  1. 生成流程
def generate_process(query,chunks):
    """
    生成流程:调用 Qwen 大模型云端 API,根据查询和文本块生成最终回复。
    :param query: 用户查询语句
    :param chunks: 从检索过程中获得的相关文本块上下文 chunks
    :return: 返回生成的响应内容
    """
    # 设置 Qwen 系列具体模型及对应的调用 API 密钥,从阿里云大模型服务平台百炼获得
    llm_model = qwen_model
    dashscope.api_key = qwen_api_key

    # 构建参考文档内容,格式为“参考文档 1: \n 参考文档 2: \n ...”等
    context = ""
    for i,chunk in enumerate(chunks):
        context += f"参考文档{i+1}: \n{chunk}\n\n"

    # 构建生成模型所需的 Prompt,包含用户查询和检索到的上下文
    prompt = f"根据参考文档回答问题:{query}\n\n{context}"
    print("###############################################################")
    print(f"生成模型的 Prompt: {prompt}")
    print("###############################################################")

    # 准备请求消息,将 prompt 作为输入
    messages = [{'role': 'user', 'content':prompt}]

    # 调用大模型 API 云服务生成响应
    try:
        responses = dashscope.Generation.call(
            model = llm_model,
            messages=messages,
            result_format='message',  # 设置返回格式为"message"
            stream=True,              # 启用流式输出
            incremental_output=True   # 获取流式增量输出
        )
        # 初始化变量以存储生成的响应内容
        generated_response = ""
        print("生成过程开始:")
        # 逐步获取和处理模型的增量输出
        for response in responses:
            if response.status_code == HTTPStatus.OK:
                content = response.output.choices[0]['message']['content']
                generated_response += content
                print(content,end='')  # 实时输出模型生成的内容
            else:
                print(f"请求失败: {response.status_code} - {response.message}")
                return None  # 请求失败时返回 None
        print("\n 生成过程完成.")
        return generated_response
    except Exception as e:
        print(f"大模型生成过程中发生错误: {e}")
        return None

上述代码实现了 RAG 流程中的生成过程。首先,结合用户查询与检索到的文本块内容组织成大模型提示词(Prompt)。随后,代码通过调用 Qwen 大模型云端 API,将构建好的 Prompt 发送给大模型,并利用流式输出的方式逐步获取模型生成的响应内容,实时输出并汇总为最终的生成结果。

这里相当于是把通过 RAG 检索出来的与用户问题相似度高的 chunk,拼接到了 prompt 中

  1. 执行
def main():
    print("RAG 过程开始.")

    query="下面报告中涉及了哪几个行业的案例以及总结各自面临的挑战?"
    embedding_model = load_embedding_model()

    # 索引流程:加载 PDF 文件,分割文本块,计算嵌入向量,存储在 FAISS 索引中(内存)
    index,chunks = indexing_process('rag_app/test_lesson2.pdf',embedding_model)

    # 检索流程:将用户查询转化为嵌入向量,检索最相似的文本块
    retrieval_chunks = retrieval_process(query,index,chunks,embedding_model)

    # 生成流程:调用 Qwen 大模型生成响应
    generate_process(query,retrieval_chunks)

    print("RAG 过程结束.")

if __name__ == "__main__":
    main()
source rag_env/bin/activate  # 激活虚拟环境
python rag_app/rag_app_lesson2.py # **执行 RAG 应用脚本**

测试代码通过 main() 函数串联各个步骤,从索引到生成,确保 RAG 的各个环节顺畅执行,准确完成“下面报告中涉及了哪几个行业的案例以及总结各自面临的挑战?”的 RAG 问答任务。

运行结果:

RAG 过程开始.
加载 Embedding 模型中
bge-small-zh-v1.5 模型最大输入长度 token:512
PDF 文档的总字符数:9135
分割的文本 Chunk 数量:24
文本块 Chunk 转化为嵌入向量完成
索引过程完成.

查询语句: 下面报告中涉及了哪几个行业的案例以及总结各自面临的挑战?
最相似的前 3 个文本块:
文本块 0: 面的数字化转型。2.3.2 面临的挑战:......相似度得分:0.5915016531944275
文本块 1: ...... 相似度得分:0.5728524327278137
文本块 2: ...... 相似度得分:0.5637902617454529
检索过程完成.

生成模型的 Prompt: 根据参考文档回答问题:下面报告中涉及了哪几个行业的案例以及总结各自面临的挑战?
参考文档 1: 面的数字化转型。2.3.2 面临的挑战...
参考文档 2: ......
参考文档 3: ......

生成过程开始:
参考文档中涉及了三个行业的案例及其面临的挑战:
1.### 制造业...... 2. ### 零售业......  3. ### 金融业......
### 数字化转型解决方案概述
-**制造业**:...... -**零售业**:...... -**金融业**:......
这些案例强调了不同行业在数字化转型过程中面临的独特挑战......
生成过程完成.
RAG 过程结束.

3.4 文档解析

选用适合业务场景的支持多格式、多版式、高精度、高效率的文档解析技术,是构建成功 RAG 系统的基础。

image.png

RAG 系统的应用场景主要集中在专业领域和企业场景。这些场景中,除了关系型和非关系型数据库,更多的数据以 PDF、TXT、Word、PPT、Excel、CSV、Markdown、XML、HTML 等多种格式存储。尤其是 PDF 文件,凭借其统一的排版和多样化的结构形式,成为了最为常见的文档数据存储与交换格式。文档解析技术不仅需要支持上述所有常见格式,还需要特别强化对于 PDF 的解析能力,包括对电子档和扫描档的处理,支持多种版面形式的解析、不同类型版面元素的识别,并能够还原正确的阅读顺序。

此外,由于 PDF 文档往往篇幅巨大、页数众多,且企业及专业领域 PDF 文件数据量庞大,因此文档解析技术还需具备极高的处理性能,以确保知识库的高效构建和实时更新。

LangChain 提供了一套功能强大的文档加载器(Document Loaders),涵盖 PDF、TXT、Word、PPT、Excel、CSV、Markdown、XML 和 HTML 格式,帮助开发者轻松地将数据源中的内容加载为文档对象。

LangChain 定义了 BaseLoader 类和 Document 类,其中 BaseLoader 类负责定义如何从不同数据源加载文档,而 Document 类则统一描述了不同文档类型的元数据。

开发者可以基于 BaseLoader 类为特定数据源创建自定义加载器,并将其内容加载为 Document 对象。使用预构建的加载器比自行编写更加便捷。例如,PyPDF 加载器能够处理 PDF 文件,将多页文档分解为独立的、可分析的单元,并附带内容及诸如源信息、页码等重要元数据。

langchain_community 是 LangChain 与常用第三方库相结合的拓展库。各类开源库和企业库基于 BaseLoader 类在 langchain_community 库中扩展了不同文档类型的加载器,这些加载器被归类于 langchain_community.document_loaders 模块中。每个加载器都可以输入对应的参数,如指定文档解析编码、解析特定元素等,以及对 Document 类进行提取或检索等操作。目前,已有超过 160 种数据加载器,覆盖了本地文件、云端文件、数据库、互联网平台、Web 服务等多种数据源。详情可以在 LangChain 官网查看。
image.png
Document Loader 模块是封装好的各种文档解析库集成 SDK,项目中使用还需要安装对应的文档解析库。例如,当我们项目中使用 from langchain_community.document_loaders import PDFPlumberLoader 时,需要先通过命令行 pip install pdfplumber 安装 pdfplumber 库。某些特殊情况下,还需要额外的依赖库,比如使用 UnstructuredMarkdownLoader 时,需要安装 unstructured 库来提供底层文档解析,还需要 markdown 库来支持 Markdown 文档格式更多能力。此外,对于像 .doc 这种早期的文档类型,还需要安装 libreoffice 软件库才能进行解析。

实际研发场景中,使用 Document Loader 文档加载器模块时,需要根据具体的业务需求编写自定义的文档后处理逻辑。针对业务需求,开发者可以自行编写和实现对不同文档内容的解析,例如对标题、段落、表格、图片等元素的特殊处理。在本课程的案例中,我们将从 Document 类中提取所有文本内容,进行下一步的文档分块处理。
image.png

按文档加载器的使用:

  1. 先安装相关依赖
source rag_env/bin/activate  # 激活虚拟环境
pip install unstructured pdfplumber python-docx python-pptx markdown openpyxl pandas

安装 .doc 文件的支持软件 LibreOffice:

sudo apt-get install libreoffice # Linux 系统执行这条指令
  1. 解析文档
from langchain_community.document_loaders import(
    PDFPlumberLoader,
    TextLoader,
    UnstructuredWordDocumentLoader,
    UnstructuredPowerPointLoader,
    UnstructuredExcelLoader,
    CSVLoader,
    UnstructuredMarkdownLoader,
    UnstructuredXMLLoader,
    UnstructuredHTMLLoader,
) # 从 langchain_community.document_loaders 模块中导入各种类型文档加载器类

def load_document(file_path):
    """
    解析各种文档格式的文件,返回文档内容字符串
    :param file_path: 文档文件路径
    :return: 返回文档内容的字符串
    """

    # 定义文档解析加载器字典,根据文档类型选择对应的文档解析加载器类和输入参数
    DOCUMENT_LOADER_MAPPING = {
        ".pdf": (PDFPlumberLoader, {}),
        ".txt": (TextLoader, {"encoding": "utf8"}),
        ".doc": (UnstructuredWordDocumentLoader, {}),
        ".docx": (UnstructuredWordDocumentLoader, {}),
        ".ppt": (UnstructuredPowerPointLoader, {}),
        ".pptx": (UnstructuredPowerPointLoader, {}),
        ".xlsx": (UnstructuredExcelLoader, {}),
        ".csv": (CSVLoader, {}),
        ".md": (UnstructuredMarkdownLoader, {}),
        ".xml": (UnstructuredXMLLoader, {}),
        ".html": (UnstructuredHTMLLoader, {}),
    }

    ext = os.path.splitext(file_path)[1]  # 获取文件扩展名,确定文档类型
    loader_tuple = DOCUMENT_LOADER_MAPPING.get(ext)  # 获取文档对应的文档解析加载器类和参数元组

    if loader_tuple: # 判断文档格式是否在加载器支持范围
        loader_class,loader_args = loader_tuple  # 解包元组,获取文档解析加载器类和参数
        loader = loader_class(file_path, **loader_args)  # 创建文档解析加载器实例,并传入文档文件路径
        documents = loader.load()  # 加载文档
        content = "\n".join([doc.page_content for doc in documents])  # 多页文档内容组合为字符串
        print(f"文档 {file_path} 的部分内容为: {content[:100]}...")  # 仅用来展示文档内容的前 100 个字符
        return content  # 返回文档内容的多页拼合字符串

    print(file_path+f",不支持的文档类型: '{ext}'") # 若文件格式不支持,输出信息,返回空字符串。
    return""

上述代码实现了解析多种文档格式并返回文档内容的字符串的方法 load_document。函数通过检查文件的扩展名 ext,动态选择合适的文档加载器 Document Loader,使用相应的加载器调用对应库读取文档内容 documents。支持的文档格式与对应的加载器类和参数在字典 DOCUMENT_LOADER_MAPPING 中进行了映射。根据文件的扩展名,函数会实例化对应的加载器,并将文档内容加载为字符串 content,支持多页文档的合并处理。

  1. 调整 RAG 索引流程方法,处理文件夹中所有类型的文档文件,修改 indexing_process() 方法
def indexing_process(folder_path,embedding_model):
    """
    索引流程:加载文件夹中的所有文档文件,并将其内容分割成文档块,计算这些小块的嵌入向量并将其存储在 Faiss 向量数据库中。
    :param folder_path: 文档文件夹路径
    :param embedding_model: 预加载的嵌入模型
    :return: 返回 Faiss 嵌入向量索引和分割后的文本块原始内容列表
    """
    
    # 初始化空的 chunks 列表,用于存储所有文档文件的文本块
    all_chunks = []

    # 遍历文件夹中的所有文档文件
    for filename in os.listdir(folder_path):
        file_path = os.path.join(folder_path,filename)
        
        # 检查是否为文件
        if os.path.isfile(file_path):
            # 解析文档文件,获得文档字符串内容
            document_text = load_document(file_path)
            print(f"文档 {filename} 的总字符数: {len(document_text)}")
            
            # 配置 RecursiveCharacterTextSplitter 分割文本块库参数,每个文本块的大小为 512 字符(非 token),相邻文本块之间的重叠 128 字符(非 token)
            text_splitter = RecursiveCharacterTextSplitter(
                chunk_size=512,chunk_overlap=128
            )
            
            # 将文档文本分割成文本块 Chunk
            chunks = text_splitter.split_text(document_text)
            print(f"文档 {filename} 分割的文本 Chunk 数量: {len(chunks)}")
            
            # 将分割的文本块添加到总 chunks 列表中
            all_chunks.extend(chunks)

    # 文本块转化为嵌入向量列表,normalize_embeddings 表示对嵌入向量进行归一化,用于准确计算相似度
    embeddings = []
    for chunk in all_chunks:
        embedding = embedding_model.encode(chunk,normalize_embeddings=True)
        embeddings.append(embedding)

    print("所有文本块 Chunk 转化为嵌入向量完成")

    # 将嵌入向量列表转化为 numpy 数组,FAISS 索引操作需要 numpy 数组输入
    embeddings_np = np.array(embeddings)

    # 获取嵌入向量的维度(每个向量的长度)
    dimension = embeddings_np.shape[1]

    # 使用余弦相似度创建 FAISS 索引
    index = faiss.IndexFlatIP(dimension)
    # 将所有的嵌入向量添加到 FAISS 索引中,后续可以用来进行相似性检索
    index.add(embeddings_np)

    print("索引过程完成.")

    return index,all_chunks

上述代码是在上一讲的 indexing_process 函数基础上进行了迭代,新增了批量处理多种格式文档文件的功能。调整部分为函数遍历文件夹中的所有文档文件,通过调用 load_document 获取文档的字符串内容,并将其切分为文本块 chunks,然后将所有文档的 chunks 汇总到一个总列表 all_chunks 中。当前的实现主要聚焦于文档解析技术,chunks 和对应的嵌入向量 index 暂时存储在内存中,持久化存储的部分将在后续的向量库课程中详细讲解。

关于 pdf 的解析
image.png
PDF 文件的显示效果不受设备、软件或系统的影响,但对计算机而言,它是一种非数据结构化的格式,储存的信息无法直接被理解。此外,大模型的训练数据中不包含直接的 PDF 文件,无法直接理解。

PDF 文件分为电子版和扫描版,电子版基于规则来解析,而扫描版通过深度学习来解析

  • 电子版:PDF 电子版可以通过规则解析,提取出文本、表格等文档元素。目前,有许多开源库可以支持,例如 pyPDF2、PyMuPDF、pdfminer、pdfplumber 和 papermage 等。这些库在 langchain_community.document_loaders 中基本都有对应的加载器,方便在不同场景下切换使用。
  • 扫描版:PDF 扫描版需要经过文本识别和表格识别 PDF 扫描图像,才能提取出文档中的各类元素。同时要真正实现文档解析的目标,无论扫描版还是电子版均需进行版面分析和阅读顺序的还原,将内容解析为一个包含所有文档元素并且具有正确阅读顺序的 MarkDown 文件。单纯依赖规则解析是无法实现这一目标的,目前支持这些功能的多为基于深度学习的开源库,如 Layout-parser、PP-StructureV2、PDF-Extract-Kit、pix2text、MinerU、marker 等。这些库并未集成在 langchain 中
    此外,还需要进一步探索 PDF 中的图像内容理解,不仅限于文字模态,还包括对图片中非文字内容的解析,如常见的折线图、柱状图等,也包含重要的内容信息。将这些内容转换为文字形式并嵌入到 MarkDown 文件中,通常需要依赖端到端的多模态大模型,如 GPT-4o 或 Gemini。然而,目前这些模型在效率和成本方面仍存在挑战,但其未来潜力巨大,值得期待。

3.5 分块(chunking)策略和 Embedding 技术

文档数据(Documents)经过解析后,通过分块技术将信息内容划分为适当大小的文档片段(chunks),从而使 RAG 系统能够高效处理和精准检索这些片段信息。分块的本质在于依据一定逻辑或语义原则,将较长文本拆解为更小的单元。分块策略有多种,各有侧重,选择适合特定场景的分块策略是提升 RAG 系统召回率的关键。

文档通常包含丰富的上下文信息和复杂的语义结构,通过将文档分块,模型可以更有效地提取关键信息,并减少不相关内容的干扰。分块的目标在于确保每个片段在保留核心语义的同时,具备相对独立的语义完整性,从而使模型在处理时不必依赖广泛的上下文信息,增强检索召回的准确性。

分块的重要性在于它直接影响 RAG 系统的生成质量。首先,合理的分块能够确保检索到的片段与用户查询信息高度匹配,避免信息冗余或丢失。其次,分块有助于提升生成内容的连贯性,精心设计的独立语义片段可以降低模型对上下文的依赖,从而增强生成的逻辑性与一致性。最后,分块策略的选择还会影响系统的响应速度与效率,模型能够更快、更准确地处理和生成内容。

嵌入模型(Embedding Model)负责将文本数据映射到高维向量空间中,将输入的文档片段转换为对应的嵌入向量(embedding vectors)。这些向量捕捉了文本的语义信息,并被存储在向量库(VectorStore)中,以便后续检索使用。用户查询(Query)同样通过嵌入模型的处理生成查询嵌入向量,这些向量用于在向量数据库中通过向量检索(Vector Retrieval)匹配最相似的文档片段。根据不同的场景需求,评估并选择最优的嵌入模型,以确保 RAG 的检索性能符合要求。

image.png

3.5.1 分块

分块策略最大的挑战在于确定分块的大小。如果片段过大,可能导致向量无法精确捕捉内容的特定细节并且计算成本增加;若片段过小,则可能丢失上下文信息,导致句子碎片化和语义不连贯。较小的块适用于需要细粒度分析的任务,例如情感分析,能够精确捕捉特定短语或句子的细节。更大的块则更为合适需要保留更广泛上下文的场景,例如文档摘要或主题检测。因此,块大小的确定必须在计算效率和上下文信息之间取得平衡。

分块策略应该按照应用场景具体决定
多种分块策略从本质上来看,由以下三个关键组成部分构成:

  1. 大小:每个文档块所允许的最大字符数。
  2. 重叠:在相邻数据块之间,重叠字符的数量。
  3. 拆分:通过段落边界、分隔符、标记,或语义边界来确定块边界的位置。

image.png
分块策略:

  1. 固定大小分块: 以固定的大小进行分块,最为简单,但缺乏灵活性,容易丢失上下文语义关系
  2. 重叠分块(overlap chunking):通过滑动窗口技术切分文本块,使新文本块与前一个块的内容部分重叠,从而保留块边界处的重要上下文信息,增强系统的语义相关性。虽然这种方法增加了存储需求和冗余信息,但它有效避免了在块之间丢失关键语义或句法结构,提升分块之间的连续性
  3. 递归分块(recursive chunking):通过预定义的文本分隔符(如换行符\n\n、\n,句号、逗号、感叹号、空格等)迭代地将文本分解为更小的块,以实现段大小的均匀性和语义完整性。此过程中,文本首先按较大的逻辑单元分割(如段落 \n\n),然后逐步递归到较小单元(如句子 \n 和单词),确保在分块大小限制内保留最强的语义片段。
  4. 文档特定分块(document specific chunking):根据文档的格式(如 Markdown、Latex、或编程语言如 Python 等)进行定制化分割的技术。此方法依据文档的特定格式和结构规则,例如 Markdown 的标题、列表项,或 Python 代码中的函数和类定义等,来确定分块边界。通过这种方式,确保分块能够准确反映文档的格式特点,优化保留这些语义完整的单元,提升后续的处理和分析效果。但依赖性强,不同格式之间的分块策略不通用,并且无法处理格式不规范及混合多种格式的情况。
  5. 语义分块(semantic chunking):基于文本的自然语言边界(如句子、段落或主题中断)进行分段的技术,需要使用 NLP 技术根据语义分词分句,旨在确保每个分块都包含语义连贯的信息单元。语义分块保留了较高的上下文保留,并确保每个块都包含连贯的信息,但需要更多的计算资源。常用的分块策略有 spaCy 和 NLTK 的 NLP 库,spaCy 适用于需要高效、精准语义切分的大规模文本处理,NLTK 更适合教学、研究和需要灵活自定义的语义切分任务。
  6. 混合分块(Mix chunking):混合分块是一种结合多种分块方法的技术,通过综合利用不同分块技术的优势,提高分块的精准性和效率。例如,在初始阶段使用固定长度分块快速整理大量文档,而在后续阶段使用语义分块进行更精细的分类和主题提取。根据实际业务场景,设计多种分块策略的混合,能够灵活适应各种需求,提供更强大的分块方案。

langchain 提供了多种分块方法,在 langchain_text_splitters 库中对应的具体方法类如下:
image.png

分块策略具体实现:
SpacyTextSplitter 和 NLTKTextSplitter 需要额外安装 Python 依赖库,其中 SpacyTextSplitter 还需要按照文档的语言对应安装额外的语言模型。

source rag_env/bin/activate  # 激活虚拟环境
pip install spacy nltk -i https://pypi.tuna.tsinghua.edu.cn/simple
python -m spacy download zh_core_web_sm # 如果需要进行中文分块,安装 spacy 中文语言模型
python -m spacy download en_core_web_sm # 如果需要进行英文分块,安装 spacy 英文语言模型

导入 langchain.text_splitter 中各种文档分块类代码:

from langchain.text_splitter import(
    CharacterTextSplitter,
    RecursiveCharacterTextSplitter,
    MarkdownTextSplitter,
    PythonCodeTextSplitter,
    LatexTextSplitter,
    SpacyTextSplitter,
    NLTKTextSplitter
) # 从 langchain.text_splitter 模块中导入各种文档分块类

indexing_process 方法中切分文本块库代码:

# 配置 SpacyTextSplitter 分割文本块库
#text_splitter = SpacyTextSplitter(
#    chunk_size=512,chunk_overlap=128,pipeline="zh_core_web_sm")

# 配置 RecursiveCharacterTextSplitter 分割文本块
# 可以更换为 CharacterTextSplitter、MarkdownTextSplitter、PythonCodeTextSplitter、LatexTextSplitter、NLTKTextSplitter 等
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=512,chunk_overlap=128)

CharacterTextSplitter、RecursiveCharacterTextSplitter、MarkdownTextSplitter、PythonCodeTextSplitter、LatexTextSplitter、NLTKTextSplitter 替换原有 text_splitter 参数的赋值类即可。需要额外处理的是 SpacyTextSplitter,需要参数 pipeline 指定具体的语言模型才可以运行。

3.5.2 Embedding

Embedding 嵌入是指将文本、图像、音频、视频等形式的信息映射为高维空间中的密集向量表示。这些向量在语义空间中起到坐标的作用,捕捉对象之间的语义关系和隐含的意义。通过在向量空间中进行计算(例如余弦相似度),可以量化和衡量这些对象之间的语义相似性。
image.png

向量检索(Vector Retrieval)是一种基于向量表示的搜索技术,通过计算查询向量与已知文本向量的相似度来识别最相关的文本数据。向量检索的高效性在于,它能在大规模数据集中快速、准确地找到与查询最相关的内容,这得益于向量表示中蕴含的丰富语义信息。

嵌入模型
自 2013 年以来,word2vec、GloVe、fastText 等嵌入模型通过分析大量文本数据,学习得出单词的嵌入向量。近年来,随着 transformer 模型的突破,嵌入技术以惊人的速度发展。BERT、RoBERTa、ELECTRA 等模型将词嵌入推进到上下文敏感的阶段。这些模型在为文本中的每个单词生成嵌入时,会充分考虑其上下文环境,因此同一个单词在不同语境下的嵌入向量可以有所不同,从而大大提升了模型理解复杂语言结构的能力。

image.png
在 RAG 流程中,文档首先被分割成多个片段,每个片段随后通过 Embedding Model 进行嵌入处理。生成的文档嵌入向量被存储在 VectorStore 中,供后续检索使用。用户查询会通过 Embedding Model 转换为查询嵌入向量,这些向量用于在向量数据库中匹配最相似的文档片段,最终组合生成指令(Prompt),大模型生成回答。

嵌入模型如何选择
嵌入模型评估:
image.png
在选择适合的嵌入模型时,需要综合考虑多个因素,包括特定领域的适用性、检索精度、支持的语言、文本块长度、模型大小以及检索效率等因素。同时以广泛受到认可的 MTEB(Massive Text Embedding Benchmark)和 C-MTEB(Chinese Massive Text Embedding Benchmark)榜单作为参考,通过涵盖分类、聚类、语义文本相似性、重排序和检索等多个数据集的评测,开发者可以根据不同任务的需求,评估并选择最优的向量模型,以确保在特定应用场景中的最佳性能。
榜单如下:
image.png

榜单每日更新,切换语言为 Chinese,可以看到中文嵌入模型的排名。由于 RAG 是一项检索任务,我们需要按“Retrieval Average”(检索平均值)列对排行榜进行排序,图中显示的就是检索任务效果排序后的结果。在检索任务中,我们需要在榜单顶部看到最佳的检索模型,并且专注于以下几个关键列:

  • Retrieval Average 检索平均值:较高的检索平均值表示模型更擅长在检索结果列表中将相关项目排在较高的位置,检索效果更好。
  • Model Size 模型大小:模型的大小(以 GB 为单位)。虽然检索性能随模型大小而变化,但要注意,模型大小也会对延迟产生直接影响。因此,在选择模型时,建议筛选掉那些在硬件资源有限的情况下不可行的过大模型。在生产环境中,性能与效率之间的权衡尤为重要。
  • Max Tokens 最大 Token 数:可压缩到单个文本块中的最大 Token 数。因为文档块我们希望不要过大而降低目标信息块的精准度,因此,即使最大 tokens 数为 512 的模型在大部分场景下也足够使用。
  • Embedding Dimensions:嵌入向量的维度。越少的嵌入维度提供更快的推理速度,存储效率更高,而更多的维度可以捕获数据中的细微特征。我们需要在模型的性能和效率之间取得良好的权衡。
  • 实验至关重要,在排行榜上表现良好的模型不一定在你的任务上表现良好,试验各种高得分的模型至关重要。我们参考 MTEB 排行榜,选择多个适合我们场景的嵌入模型作为备选,并在我们的业务场景数据集上进行评估测试,以选出最适合我们 RAG 系统的嵌入模型。

嵌入模型代码实战
这里使用 SentenceTransformers 作为加载嵌入模型的 Python 模块。

SentenceTransformers(又名 SBERT)是一个用于训练和推理文本嵌入模型的 Python 模块,可以在 RAG 系统中计算嵌入向量。使用 SentenceTransformers 进行文本嵌入转换非常简单:只需导入模块库、加载模型,并调用 encode 方法即可。执行时,SentenceTransformers 会自动下载相应的模型库,当然也可以手动下载并指定模型库的路径。所有可用的模型都可以在 SentenceTransformers 模型库 查看,超过 8000 个发布在 Hugging Face 上的嵌入模型库可以被使用。

在中文领域,智源研究院的 BGE 系列模型 是较为知名的开源嵌入模型,在 C-MTEB 上表现出色。BGE 系列目前包含 23 个嵌入模型,涵盖多种维度、多种最大 Token 数和模型大小,用户可以根据需求进行测试和使用。

load_embedding_model 方法中使用 SentenceTransformer 加载嵌入模型代码:

# 绝对路径:SentenceTransformer 读取绝对路径下的 bge-small-zh-v1.5 模型,如需使用其他模型,下载其他模型,并且更换绝对路径即可
embedding_model = SentenceTransformer(os.path.abspath('rag_app/bge-small-zh-v1.5'))

# 自动下载:SentenceTransformer 库自动下载 BAAI/bge-large-zh-v1.5 模型,如需下载其他模型,输入其他模型名称即可
# embedding_model = SentenceTransformer('BAAI/bge-large-zh-v1.5')

indexing_process 方法中将文本转化为嵌入向量代码:

# 文本块转化为嵌入向量列表,normalize_embeddings 表示对嵌入向量进行归一化,用于后续流程准确计算向量相似度
embeddings = []
for chunk in all_chunks:
      embedding = embedding_model.encode(chunk,normalize_embeddings=True)
      embeddings.append(embedding)

3.6 RAG 向量数据库

用于存储嵌入向量及文档元数据,并高效地进行相似性检索

根据是否开源以及是否为专用向量库,分为:

  1. 第一类是开源的专用向量数据库,如 Chroma、Vespa、LanceDB、Marqo、Qdrant 和 Milvus,这些数据库专门设计用于处理向量数据。
  2. 第二类是支持向量搜索的开源数据库,如 OpenSearch、PostgreSQL、ClickHouse 和 Cassandra,它们是常规数据库,但支持向量搜索功能。
  3. 第三类是商用的专用向量数据库,如 Weaviate 和 Pinecone,它们专门用于处理向量数据,但属于商业产品或通过商业许可获得源码。
  4. 第四类是支持向量搜索的商用数据库,如 Elasticsearch、Redis、Rockset 和 SingleStore,这些常规数据库支持向量搜索功能,同时属于商业产品或可通过商业许可获得源码。
    image.png

向量数据库的核心在于其能够基于向量之间的相似性,快速、精确地定位和检索数据。这类数据库不仅为向量嵌入提供了优化的存储和查询功能,同时也继承了传统数据库的诸多优势,如性能、可扩展性和灵活性,满足了充分利用大规模数据的需求。相比之下,传统的基于标量的数据库由于无法应对数据复杂性和规模化处理的挑战,难以有效提取洞察并实现实时分析。

存储
向量数据库的存储通过建立索引,来加速查找,常用算法:

  • 层次化可导航小世界图(HNSW 图)通过在多层结构中将相似向量连接在一起,快速缩小搜索范围。
  • 产品量化(PQ) 则通过压缩高维向量,减少内存占用并加速检索,
  • 位置敏感哈希(LSH) 则通过哈希函数将相似向量聚集在一起,便于快速定位。

搜索
向量数据库的搜索机制不是追求精确匹配,而是通过近似最近邻(ANN)算法在速度与准确性之间找到最佳平衡。ANN 算法通过允许一定程度的误差,在显著提高搜索速度的同时,依然能够找到与查询相似度较高的向量。这种策略对于需要实时、高精度响应的应用场景尤为重要。
常见的向量搜索方法:

  • 余弦相似度:主要用于文本处理和信息检索,关注向量之间的角度,以捕捉语义相似性;
  • 欧几里得距离:则测量向量之间的实际距离,适用于密集特征集的聚类或分类;
  • 曼哈顿距离:则通过计算笛卡尔坐标中的绝对差值之和,适用于稀疏数据的处理。

向量数据库的工作流程涵盖了从数据处理、向量化、向量存储、向量索引到最终检索的全链条操作,确保在复杂的数据环境中实现高效的存储、索引和相似性搜索。

在 RAG 中,向量数据库主要用于存储向量、建立索引以及检索的过程
image.png

常用向量数据库:
image.png
根据上面所示特点,

  • 对于需要快速开发和轻量化部署的项目,Chroma、Qdrant 是不错的选择。
  • 对于追求高性能和可扩展性的企业级应用,可以考虑 Milvus/Zilliz。
  • FAISS 是适合对性能有极致要求、不要求持久化和数据管理的场景。
  • Weaviate、LanceDB 在处理多模态数据方面表现突出,适用于需要管理多种数据类型(如图像、文本、音频等)的 AI 应用。
  • 如果需要无缝集成现有数据库并进行向量搜索,PGVector、Elasticsearch、Redis 是理想的方案。
  • 不希望管理基础设施的用户则可以选择 Pinecone 这样的全托管服务。

向量数据库代码实战——以 Chroma 为例:

Chroma 是一种简单且易于持久化的向量数据库,它以轻量级、开箱即用的特性著称。Chroma 支持内存中操作和磁盘持久化,能够高效地管理和查询向量数据,非常适合快速集成和开发。其设计简洁且不需要复杂的配置,使开发者能够专注于核心功能的实现而无需担心底层存储的复杂性。

安装依赖:

pip install -U pip chromadb langchain langchain_community sentence-transformers dashscope unstructured pdfplumber python-docx python-pptx markdown openpyxl pandas -i https://pypi.tuna.tsinghua.edu.cn/simple

有部分依赖已经安装

代码主要改动内容:

  1. 引入 Chroma 向量数据库 chromadb,引入 uuid 模块用于为每个文本块生成唯一的 ID。
  2. 在 main 方法中,创建了 Chroma 本地存储实例 client 和存储集合 collection,实例数据库存储在相对路径 rag_app/chroma_db 下,数据存储在 documents 集合中。
  3. 在 indexing_process 方法中,将文档切块后的文本块的 ID、嵌入向量和原始文本块内容存储到 ChromaDB 的 documents 集合中。
  4. 在 retrieval_process 方法中,使用 Chroma 向量数据库检索与查询(query)最相似的 top_k 个文本块。

具体改动:

  1. 引入依赖库:
import chromadb # 引入 Chroma 向量数据库
import uuid # 生成唯一 ID
import shutil # 文件操作模块,为了避免既往数据的干扰,在每次启动时清空 ChromaDB 存储目录中的文件
  1. main 方法:
def main():
    print("RAG 过程开始.")

    # 为了避免既往数据的干扰,在每次启动时清空 ChromaDB 存储目录中的文件
    chroma_db_path = os.path.abspath("rag_app/chroma_db")
    if os.path.exists(chroma_db_path):
        shutil.rmtree(chroma_db_path)

    # 创建 ChromaDB 本地存储实例和 collection
    client = chromadb.PersistentClient(chroma_db_path)
    collection = client.get_or_create_collection(name="documents")
    embedding_model = load_embedding_model()

    indexing_process('rag_app/data_lesson5',embedding_model,collection)
    query = "下面报告中涉及了哪几个行业的案例以及总结各自面临的挑战?"
    retrieval_chunks = retrieval_process(query,collection,embedding_model)
    generate_process(query,retrieval_chunks)
    print("RAG 过程结束.")
  1. indexing_process 方法:
def indexing_process(folder_path,embedding_model,collection):
    all_chunks = []
    all_ids = []

    for filename in os.listdir(folder_path):
        file_path = os.path.join(folder_path,filename)

        if os.path.isfile(file_path):
            document_text = load_document(file_path)
            if document_text:
                print(f"文档 {filename} 的总字符数: {len(document_text)}")

                text_splitter = RecursiveCharacterTextSplitter(chunk_size=512,chunk_overlap=128)
                chunks = text_splitter.split_text(document_text)
                print(f"文档 {filename} 分割的文本 Chunk 数量: {len(chunks)}")

                all_chunks.extend(chunks)
                # 生成每个文本块对应的唯一 ID
                all_ids.extend([str(uuid.uuid4())for _ in range(len(chunks))])

    embeddings = [embedding_model.encode(chunk,normalize_embeddings=True).tolist()for chunk in all_chunks]

    # 将文本块的 ID、嵌入向量和原始文本块内容添加到 ChromaDB 的 collection 中
    collection.add(ids=all_ids,embeddings=embeddings,documents=all_chunks)
    print("嵌入生成完成,向量数据库存储完成.")
    print("索引过程完成.")
    print("********************************************************")

retrieval_process 方法:

def retrieval_process(query,collection,embedding_model=None,top_k=6):
    query_embedding = embedding_model.encode(query,normalize_embeddings=True).tolist()

    # 使用向量数据库检索与 query 最相似的 top_k 个文本块
    results = collection.query(query_embeddings=[query_embedding],n_results=top_k)

    print(f"查询语句: {query}")
    print(f"最相似的前{top_k}个文本块:")

    retrieved_chunks = []
    # 打印检索到的文本块 ID、相似度和文本块信息
    for doc_id,doc,score in zip(results['ids'][0],results['documents'][0],results['distances'][0]):
        print(f"文本块 ID: {doc_id}")
        print(f"相似度: {score}")
        print(f"文本块信息:\n{doc}\n")
        retrieved_chunks.append(doc)

    print("检索过程完成.")
    print("********************************************************")
    return retrieved_chunks

3.7 混合检索和重排序技术

当前主流的 RAG 检索方式主要采用向量检索(Vector Search),通过语义相似度来匹配文本切块,然而,向量检索并非万能,某些场景下无法替代传统关键词检索的优势,如需要精确搜索某个订单 id、品牌名称等等

传统关键词检索的优势场景:

  1. 精确匹配,如产品名称、姓名、产品编号;
  2. 少量字符的匹配,用户习惯于输入几个关键词,而少量字符进行向量检索时效果可能较差;
  3. 低频词汇的匹配,低频词汇往往承载了关键意义,如在“你想跟我去喝咖啡吗?”这句话中,“喝”“咖啡”比“你”“吗”更具重要性。

image.png
在上述案例中,虽然依靠关键词检索可以精确找到与“订单 12345”匹配的特定信息,但它无法提供与订单相关的更广泛上下文。另一方面,语义匹配虽然能够识别“订单”和“配送”等相关概念,但在处理具体的订单 ID 时,往往容易出错。

3.7.1 混合检索

混合检索(Hybrid Search,也成为多路召回):通过结合关键词检索和语义匹配的优势,可以首先利用关键词检索精确定位到“订单 12345”的信息,然后通过语义匹配扩展与该订单相关的其他上下文或客户操作的信息,例如“12 开头的订单、包装破损严重”等。这样不仅能够获取精确的订单详情,还能获得与之相关的额外有用信息。

混合检索不限于关键词检索和向量匹配,还可以混合更多的检索方式,如下图:langchain 提供了检索器模块 langchain_community.retrievers,参考:retrievers — 🦜🔗 LangChain documentation

image.png

在 RAG 检索场景中,首要目标是确保最相关的结果能够出现在候选列表中。混合检索提供了一种更加全面精准的搜索方案。

3.7.2 重排序reranking):

仅具备混合检索的能力还不足以满足需求,检索到的见过还需要经过重排序,目的是将混合检索的结果进行整合,并将与用户问题语义最契合的结果排在前列确保最符合用户意图和查询语义的结果优先展示,从而提升用户的搜索体验和结果的准确性。

RAG 流程有两个概念,粗排和精排。粗排检索效率较快,但是召回的内容并不一定强相关。而精排效率较低,因此适合在粗排的基础上进行进一步优化。精排的代表就是重排序(Reranking)。

image.png
在这个案例中,我们通过重排序技术成功找到了与问题语义最契合的结果。系统评分显示,“订单 12345 于 2023 年 8 月 15 日在上海,客户不满意。”与“该 12 开头的订单客户不满意的地方在于包装破损严重。”这两个文档块的相关性分别为 0.9 和 0.8,排序为第一和第二位。

重排序模型大多是基于双塔或交叉编码架构的模型,在此基础上进一步计算更精确的相关性分数,能够捕捉查询词与文档块之间更细致的相关性,从而在细节层面上提高检索精度。因此,尽管向量检索提供了有效的初步筛选,重排序模型则通过更深入的分析和排序,确保最终结果在语义和内容层面上更紧密地契合查询意图,实现了检索质量的提升。

总的来说,重排序技术的作用:

  1. 优化检索结果:将检索到的文档进行进一步的筛选和排序,将最相关、最重要的文档置于前列。
  2. 增强上下文相关性:RAG 系统依赖于检索到的文档作为生成模型的上下文。因此,上下文的质量直接影响生成的结果。重排序技术通过重新评估文档与查询的相关性,确保生成模型优先使用那些与查询最相关的文档,从而提高了生成内容的准确性和连贯性。
  3. 应对复杂查询:对于复杂的查询,初始检索可能会返回一些表面上相关但实际上不太匹配的文档。重排序技术可以根据查询的复杂性和具体需求,对这些结果进行更细致的分析和排序,优先展示那些能够提供深入见解或关键信息的文档。

重排序模型:将查询与每个文档块之间计算对应的相关性分数,并根据这些分数对文档进行重新排序,确保文档按照从最相关到最不相关的顺序排列,并返回前 top-k 个结果。
image.png

与嵌入模型不同,重排序模型将用户的查询(Query)和文档块作为输入,直接输出相似度评分,而非生成嵌入向量。目前,市面上可用的重排序模型并不多,商用的有 Cohere,开源的有 BGE、Sentence、Mixedbread、T5-Reranker 等,甚至可以使用指令(Prompt)让大模型(GPT、Claude、通义千问、文心一言等)进行重排

在生产环境中使用重排序模型会面临资源和效率问题,包括计算资源消耗高、推理速度慢以及模型参数量大等问题。这些问题主要源于重排序模型在对候选项进行精细排序时,因其较大参数量而导致的高计算需求和复杂耗时的推理过程,从而对 RAG 系统的响应时间和整体效率产生负面影响。因此,在实际应用中,需要根据实际资源情况,在精度与效率之间进行平衡

3.7.3 混合检索技术代码实战

使用 rank_bm25 作为 RAG 项目的关键词搜索技术。BM25 是一种强大的关键词搜索算法,通过分析词频(TF)和逆向文档频率(IDF)来评估文档与查询的相关性。具体来说,BM25 检查查询词在文档中的出现频率,以及该词在所有文档中出现的稀有程度。如果一个词在特定文档中频繁出现,但在其他文档中较少见,那么 BM25 会将该文档评为高度相关。
此外,BM25 还通过调整文档长度的影响,防止因文档长度不同而导致的词频偏差。正是这种结合了词频和文档长度平衡的机制,使得 BM25 在关键词搜索中能够提供精准的检索结果,在 RAG 项目中尤为有效。

安装依赖库:

pip install -U pip jieba rank_bm25 chromadb langchain langchain_community sentence-transformers dashscope unstructured pdfplumber python-docx python-pptx markdown openpyxl pandas -i https://pypi.tuna.tsinghua.edu.cn/**simple**

代码中新增内容:

  1. 引入了 rank_bm25 库中的 BM25Okapi 类,用于实现 BM25 算法的检索功能。
  2. 引入了 jieba 库,用于对中文文本进行分词处理,这对于 BM25 算法处理中文文本起关键作用。
  3. 在 retrieval_process 方法中,从 Chroma 的 collection 中提取所有存储的文档内容,并使用 jieba 对这些文档进行中文分词,将分词结果存储为 tokenized_corpus,为后续的 BM25 检索做准备。
  4. 利用分词后的文档集合实例化 BM25Okapi 对象,并对查询语句进行分词处理。
  5. 计算查询语句与每个文档之间的 BM25 相关性得分 (bm25_scores),然后选择得分最高的前 top_k 个文档,并提取这些文档的内容。
  6. 返回合并后的全部检索结果,包含向量检索和 BM25 检索的结果。

在此代码实现中,没有使用混合检索的 RRF(递归折减融合)排名,课程下半部分会对检索结果进行进一步的重排序,所以这节课直接返回了向量检索和 BM25 检索的结果,并按顺序合并,集中展示 BM25 关键词检索的代码实战。

具体代码改动:

  1. 引入依赖库
from rank_bm25 import BM25Okapi # 从 rank_bm25 库中导入 BM25Okapi 类,用于实现 BM25 算法的检索功能
import jieba # 导入 jieba 库,用于对中文文本进行分词处理
  1. retrieval_process 方法:
def retrieval_process(query,collection,embedding_model=None,top_k=3):

    query_embedding = embedding_model.encode(query,normalize_embeddings=True).tolist()
    vector_results = collection.query(query_embeddings=[query_embedding],n_results=top_k)

    # 从 Chroma collection 中提取所有文档
    all_docs = collection.get()['documents']

    # 对所有文档进行中文分词
    tokenized_corpus = [list(jieba.cut(doc))for doc in all_docs]

    # 使用分词后的文档集合实例化 BM25Okapi,对这些文档进行 BM25 检索的准备工作
    bm25 = BM25Okapi(tokenized_corpus)
    # 对查询语句进行分词处理,将分词结果存储为列表
    tokenized_query = list(jieba.cut(query))
    # 计算查询语句与每个文档的 BM25 得分,返回每个文档的相关性分数
    bm25_scores = bm25.get_scores(tokenized_query)
    # 获取 BM25 检索得分最高的前 top_k 个文档的索引
    bm25_top_k_indices = sorted(range(len(bm25_scores)),key=lambda i:bm25_scores[i],reverse=True)[:top_k]
    # 根据索引提取对应的文档内容
    bm25_chunks = [all_docs[i] for i in bm25_top_k_indices]

    # 打印 向量 检索结果
    print(f"查询语句: {query}")
    print(f"向量检索最相似的前 {top_k} 个文本块:")
    vector_chunks = []
    for rank, (doc_id,doc)in enumerate(zip(vector_results['ids'][0],vector_results['documents'][0])):
        print(f"向量检索排名: {rank + 1}")
        print(f"文本块 ID: {doc_id}")
        print(f"文本块信息:\n{doc}\n")
        vector_chunks.append(doc)

    # 打印 BM25 检索结果
    print(f"BM25 检索最相似的前 {top_k} 个文本块:")
    for rank,doc in enumerate(bm25_chunks):
        print(f"BM25 检索排名: {rank + 1}")
        print(f"文档内容:\n{doc}\n")

    # 合并结果,将 向量 检索的结果放在前面,然后是 BM25 检索的结果
    combined_results = vector_chunks + bm25_chunks

    print("检索过程完成.")
    print("********************************************************")

    # 返回合并后的全部结果,共 2*top_k 个文档块
    return combined_results

3.7.4 重排序技术代码实战

在实战中,我们使用来自北京人工智能研究院 BGE 的 bge-reranker-v2-m3 作为 RAG 项目的重排序模型,这是一种轻量级的开源和多语言的重排序模型。更多模型相关信息参考huggingface.co

依赖安装:

pip install -U pip FlagEmbedding Peft jieba rank_bm25 chromadb langchain langchain_community sentence-transformers dashscope unstructured pdfplumber python-docx python-pptx markdown openpyxl pandas -i https://pypi.tuna.tsinghua.edu.cn/simple

主要增加内容

  1. 引入 FlagEmbedding 库中的 FlagReranker 类,用于对嵌入结果进行重新排序的工具类。
  2. 增加 reranking 方法,对初始检索到的文档块进行重新排序。该方法初始化了使用 BAAI/bge-reranker-v2-m3 的 FlagReranker 模型,并通过计算每个 query 与 chunk 的语义相似性得分对文档块进行排序。最后,返回排名前 top_k 的文档块。
  3. 在 retrieval_process 方法中,新增了对检索结果进行重排序 reranking 的步骤,返回重排序后的前 top_k 个文档块。

具体代码改动

  1. 引入依赖库:
from FlagEmbedding import FlagReranker # 用于对嵌入结果进行重新排序的工具类
  1. 增加 reranking 方法:
def reranking(query,chunks,top_k=3):
    # 初始化重排序模型,使用 BAAI/bge-reranker-v2-m3
    reranker = FlagReranker('BAAI/bge-reranker-v2-m3',use_fp16=True)
    # 构造输入对,每个 query 与 chunk 形成一对
    input_pairs = [[query,chunk] for chunk in chunks]
    # 计算每个 chunk 与 query 的语义相似性得分
    scores = reranker.compute_score(input_pairs,normalize=True)
    print("文档块重排序得分:",scores)
    # 对得分进行排序并获取排名前 top_k 的 chunks
    sorted_indices = sorted(range(len(scores)),key=lambda i:scores[i],reverse=True)
    reranking_chunks = [chunks[i] for i in sorted_indices[:top_k]]
    # 打印前三个 score 对应的文档块
    for i in range(top_k):
        print(f"重排序文档块{i+1}: 相似度得分:{scores[sorted_indices[i]]},文档块信息:{reranking_chunks[i]}\n")
    return reranking_chunks

retrieval_process 方法:
    # 使用重排序模型对检索结果进行重新排序,输出重排序后的前 top_k 文档块
    reranking_chunks = reranking(query,vector_chunks + bm25_chunks,top_k)

    print("检索过程完成.")
    print("********************************************************")

    # 返回重排序后的前 top_k 个文档块
    return reranking_chunks
  1. retrieval_process 方法:
def retrieval_process(query,collection,embedding_model=None,top_k=6):

    query_embedding = embedding_model.encode(query,normalize_embeddings=True).tolist()
    vector_results = collection.query(query_embeddings=[query_embedding],n_results=top_k)

    all_docs = collection.get()['documents']

    tokenized_corpus = [list(jieba.cut(doc))for doc in all_docs]

    bm25 = BM25Okapi(tokenized_corpus)
    tokenized_query = list(jieba.cut(query))
    bm25_scores = bm25.get_scores(tokenized_query)
    
    bm25_top_k_indices = sorted(range(len(bm25_scores)),key=lambda i:bm25_scores[i],reverse=True)[:top_k]
    bm25_chunks = [all_docs[i] for i in bm25_top_k_indices]

    print(f"查询语句: {query}")
    print(f"向量检索最相似的前 {top_k} 个文本块:")
    vector_chunks = []
    for rank, (doc_id,doc)in enumerate(zip(vector_results['ids'][0],vector_results['documents'][0])):
        print(f"向量检索排名: {rank + 1}")
        print(f"文本块 ID: {doc_id}")
        print(f"文本块信息:\n{doc}\n")
        vector_chunks.append(doc)

    print(f"BM25 检索最相似的前 {top_k} 个文本块:")
    for rank,doc in enumerate(bm25_chunks):
        print(f"BM25 检索排名: {rank + 1}")
        print(f"文档内容:\n{doc}\n")

    # 使用重排序模型对检索结果进行重新排序,输出重排序后的前 top_k 文档块
    reranking_chunks = reranking(query,vector_chunks + bm25_chunks,top_k)

    print("检索过程完成.")
    print("********************************************************")

    # 返回重排序后的前 top_k 个文档块
    return reranking_chunks

3.8 大模型与 Prompt

RAG 的本质是通过为大模型提供外部知识来增强其理解和回答领域问题的能力,类似于为大语言模型配备插件,使其能够结合外部知识作出更为精准和符合上下文的回答。大模型在 RAG 系统中起到大脑中枢的作用,尤其在面对复杂且多样化的 RAG 任务时,大模型的性能直接决定了整个系统的效果和响应质量,可以说大模型是整个系统的大脑。

一个 prompt 通常包含以下内容:

  1. 指令(Instruction):指明模型要执行的特定任务或操作。
  2. 上下文(Context):为模型提供额外信息或背景,可以帮助引导模型生成更准确的响应。
  3. 输入数据(Input Data):我们希望模型回答的问题或感兴趣的输入内容。
  4. 输出指示符(Output Indicator):指定模型的输出类型或格式,例如格式、是否要求生成代码、总结文本或回答具体问题。
    image.png

如何提升 prompt 的质量:

  1. 具体指令法:
    通过向大模型提供具体、清晰的指令,能够提高输出的准确性。模糊的指示往往导致模型产生不理想的结果,而具体指令则有助于模型明确任务目标,生成更符合预期的内容。
    如:
请根据上传的银行业报告,简洁总结当前的市场趋势,重点分析政策变化对行业的影响,输出为以下 Markdown 格式:
- **市场趋势**
- **政策影响**
- **竞争风险**
  1. 示例学习:
    通过给模型提供多个参考示例,模型可以基于这些示例进行模式识别,进而模仿、思考并生成类似的答案。这种方法在无需对模型进行进一步训练的情况下,有效提升了模型的输出质量。
    如:
以下是两个关于银行业的分析示例,请按照这种格式对新的报告进行分析:
- 示例 1:**市场趋势**:由于政策放宽,银行贷款增长迅速。
- 示例 2:**政策影响**:新的利率政策可能会对中小企业贷款产生负面影响。


请对下面报告进行同样的分析。
  1. 默认回复策略:
    当模型无法从文档中获取足够信息时,通过设定默认回复策略,避免模型产生“幻觉”,即生成虚假的答案。这可以确保模型仅基于文档中的事实进行回答。
    如:
如果文档中没有足够的事实回答问题,请返回{无法从文档中获得相关内容},而不是进行推测。
  1. 任务角色设定
    通过为模型设定特定的角色身份,可以帮助模型更好地理解任务要求和角色责任,从而输出更加一致、专业的内容。
    如:
你的角色: 知识库专家
- 背景:分析银行业市场数据
- 目标:生成一份详细的行业趋势分析
- 限制:仅根据报告中的数据生成分析
  1. 解释理由法:
    在编写提示时,向模型解释为什么某些任务需要特定的处理方式。这样可以帮助模型更好地理解任务背景,从而提高输出的质量和相关性。
    如:
请生成一份简明扼要的银行业报告摘要,不要逐字重复段落内容。原因:读者可以访问完整文档,如果需要可以详细阅读全文。
  1. 文档基础说明
    为模型提供文档的背景信息和文本来源可以帮助奠定任务基础,让模型更好地进行任务推理和回答。
    如:
以下是关于银行业政策变化的相关规则,它们将用于回答有关政策对银行业影响的问题。

在提示工程中,过于具体的指令可能会限制模型的创造性,过于宽泛的提示则可能导致生成偏差。如何在提示设计中找到合适的权衡点,既能够引导模型生成高质量结果,又不过度限制模型的灵活性,是提示工程的重中之重

需要不断调试优化提示词才能找到合适的平衡点,引导模型生成高质量结果,而又不过度限制模型的灵活性

3.9 检索精度优化

上述内容的讲解实际上已经涵盖了一些能够提升 RAG 检索效果的关键技术。这些技术包括:处理多种文档格式、版面布局及阅读顺序还原的高精度、高效率文档解析技术,适用于特定场景的多样化分块策略,综合考虑特定领域精度、效率和文本块长度的嵌入模型,支持高效索引、检索和存储的向量数据库,结合多种检索技术的混合检索方法,以及能够捕捉查询词与文档块相关性的重排序技术。每个技术的细节优化都可以进一步提升整体检索精度。

3.9.1 数据清洗和预处理

在 RAG 索引流程中,文档解析之后、文本块切分之前,进行数据清洗和预处理能够有效减少脏数据和噪声,提升文本的整体质量和信息密度。通过清除冗余信息、统一格式、处理异常字符等手段,数据清洗和预处理过程确保文档更加规范和高质量,从而提高 RAG 系统的检索效果和信息准确性。

  1. 处理冗余的模板内容

文档中出现大量的重复段落或内容,特别是在合同、报告中,这类重复内容并没有重复的意义,只会增加存储负担、影响检索效率

  1. 清除文档中的额外空白和格式不一致

文档中的多余的空行、缩进或其他格式不一致的地方,这些多余格式可能会影响文本块的切分和向量化过程,如递归分块时,若根据空行,则就会影响

  1. 去除文档脚注、页眉页脚、版权信息等无实际意义的内容

在文档解析时,可能会从网页或 PDF 中提取出脚注、版权声明、页眉页脚等无关信息。这些内容会增加数据的噪声,影响向量生成的精度。

3.9.2 查询扩展

在 RAG 系统的典型检索步骤中,用户的查询会转化为向量后进行检索,但单个向量查询只能覆盖向量空间中的一个有限区域。如果查询中的嵌入向量未能包含所有关键信息,那么检索到的文档块可能不相关或缺乏必要的上下文。因此,单点查询的局限性会限制系统在庞大文档库中的搜索范围,导致错失与查询语义相关的内容。

查询扩展策略:借助大模型,从原始的查询语句(即用户输入)生成多个语义相关的查询,可以覆盖向量空间中的不同区域,从而提高检索的全面性和准确性。这些查询在嵌入后能够击中不同的语义区域,确保系统能够从更广泛的文档中检索到与用户需求相关的有用信息。

如下查询扩展的 prompt:

你是一个 AI 语言模型助手。
你的任务是生成五个不同版本的用户问题,以便从向量数据库中检索相关文档。
通过从多个角度生成用户问题,你的目标是帮助用户克服基于距离的相似性搜索的一些局限性。
请将这些替代问题用换行符分隔。原始问题:{查询原文}

原始查询问题: 下面报告中涉及了哪几个行业的案例以及总结各自面临的挑战?

查询扩展后:

请问报告中提到的案例涉及了哪些行业?这些行业各自面临的挑战有哪些?
报告中有哪些行业的案例被讨论?每个行业在报告中描述的挑战是什么?
这个报告中具体提到了哪些行业的案例?能否总结一下这些行业当前面临的主要挑战?
该报告中涵盖了哪些行业案例,并对各行业的挑战进行了哪些讨论?
在报告中提到的行业案例有哪些?这些行业分别遇到的主要问题和挑战是什么?

通过这种查询扩展策略,原始问题被分解为多个子查询,每个子查询独立检索相关文档并生成相应的结果。随后,系统将所有子查询的检索结果进行合并和重新排序。此方法能够有效扩展用户的查询意图,确保在复杂信息库中进行更全面的文档检索,从而避免遗漏与查询语义密切相关的重要内容。

3.9.3 自查询

自查询策略通过大语言模型自动提取查询中对业务场景至关重要的元数据字段(如标签、作者 ID、评论数量等关键信息),并将这些信息结合到嵌入检索过程中。通过这种方式,可以确保嵌入向量中包含这些关键信息,从而提高检索的全面性与精确性。

自查询 prompt 如下:

你是一个 AI 语言模型助手。  
你的任务是从用户问题中提取关键信息,你的回复应仅包含提取的关键信息。  
用户问题:{查询原文}

原始查询问题为:“下面报告中涉及了哪几个行业的案例以及总结各自面临的挑战?”

通过自查询指令生成如下内容:

行业,案例,挑战

通过这种自查询策略,系统能够精准提取查询中的关键信息,结合关键词检索及向量检索,确保这些元数据在向量检索中得以充分利用,从而提高检索结果的相关性和准确性。

3.9.4 提示压缩

提示压缩旨在减少上下文中的噪声,并突出最相关的信息,从而提高检索精度和生成质量。在 RAG 系统中,检索到的文档通常包含大量无关的文本,这些无关内容可能会掩盖与查询高度相关的信息,导致生成结果的相关性下降。提示压缩通过精简上下文、过滤掉不相关的信息,确保系统只处理与查询最相关、最重要的内容

提示压缩 prompt 如下:

你是一个 AI 语言模型助手,负责对检索到的文档进行上下文压缩。
你的目标是从文档中提取与用户查询高度相关的段落,并删除与查询无关或噪声较大的部分。
你应确保保留所有能够直接回答用户查询的问题核心信息。

输入:
用户查询:{用户的原始查询}
检索到的文档:{检索到的文档内容}

输出要求:
提取与用户查询最相关的段落和信息。
删除所有与查询无关的内容,包括噪声、背景信息或扩展讨论。
压缩后的内容应简洁清晰,直指用户的核心问题。

输出格式:
{压缩段落 1}
{压缩段落 2}
{压缩段落 3}

通过提示压缩,系统能够准确提取出与查询高度相关的核心信息,去除冗余内容,并返回简洁的压缩结果。组合成为新的指令,输入大模型获得回复,提高 RAG 系统答案准确度。

3.10 RAG 效果评估

评估方式:

  • 大模型打分:通过使用大语言模型对 RAG 的输出进行自动评分。这类评估方式效率高,能够快速处理大规模的评估任务,但在准确性上可能受到模型本身偏差的影响。
  • 人工打分:由人类评审员对 RAG 的输出进行逐一打分。人工评估方式可以提供更为精确、细致的反馈,特别是在检测生成答案中的细微错误和幻觉时,但其耗时较长,成本较高。

评估指标:

  1. CR 检索相关性(Context Relevancy):该指标用于测量检索到的信息与查询上下文的相关性。如果检索到的信息偏离了原始查询,后续的生成任务就会受到负面影响。
  2. AR 答案相关性(Answer Relevancy):衡量生成答案与原始问题之间的相关性。该指标主要评估生成的答案是否能够解决用户的问题,且内容是否逻辑连贯。
  3. F 可信度(Faithfulness):评估生成的答案中是否存在幻觉(hallucination)或不准确之处。这一指标尤为重要,因为虚假的答案会极大影响用户对 RAG 系统的信任度。

image.png

RAG 效果评估是 RAG 系统完成搭建后的一个持续优化流程。通过设定打分标准和评估指标,综合评分能够准确反映 RAG 系统的整体性能。针对不同的应用场景,还可以引入更多评估指标,如 Top5 召回率、Top3 召回率、Top1 召回率以及其他常用的 NLP 评估指标。通过灵活组合这些评估方式与指标,可以更加精确地衡量 RAG 系统在特定场景中的表现,并为后续优化提供方向。

4 RAG 优化大全

有部分内容和之前的重叠,主要作为参考,面试过程中有内容可以扯就行

按照理论来说,这叫做 ** advanced RAG**范式。
按照检索前、检索、检索后的策略分类

4.1 检索前优化

检索前优化通过索引、分块、查询优化以及内容向量化等技术手段,提高检索内容的精确性和生成内容的相关性。

  1. 滑动窗口方法:经典的分块技术,通过在相邻的文本块之间创建重叠区域,确保关键信息不会因简单的分段而丢失。这种方法在索引过程中通过在块之间保留重复部分,保证了检索时上下文信息的连贯性,进而提高了检索的精度。
  2. 元数据添加:为每个分块添加元数据(如创建日期、章节名称、文档类型等),能够使系统在检索时快速过滤掉无关内容。例如,用户在查询时可以通过元数据筛选特定时间段的文档,减少不相关信息的干扰。
  3. 分层索引:在索引过程中,可以采用句子级、段落级甚至文档级的多层次嵌入方法。这样,系统可以根据查询的具体要求,灵活地在不同层次进行检索。比如,当用户输入较为复杂的长句查询时,段落级别的嵌入能够提供更加全面的语义匹配;而对于简短查询,句子级的嵌入能够提供更精确的结果。
  4. 句子窗口检索:这种方法通过将文档中的每个句子独立嵌入,从而提高检索的精确度。在检索过程中,系统找到与查询最相关的句子后,会扩展句子前后的上下文窗口,保证生成模型能够获取足够的背景信息进行推理。这种方式既能够精准定位关键信息,又能确保生成的上下文连贯性。
  5. 查询重写:针对用户输入的原始查询进行重新表述,使其更加清晰易懂,并且与检索任务匹配。例如,针对用户模糊或含糊的提问,系统可以通过重写,使查询更加具体化,从而检索到更加精准的内容。
  6. 查询扩展:查询扩展通过增加同义词、相关词汇或概念扩展用户的原始查询,增加了检索结果的广度。这样,当用户输入简短或不完整的查询时,系统能够通过扩展词汇找到更多潜在相关的内容,从而提升检索效果。
  7. 长短不一的内容向量化:RAG 系统中,文档或查询的长度对向量化过程有着显著的影响。对于短句子或短语,其生成的向量更加聚焦于具体细节,能够实现更精确的句子级别匹配。段落或文档级别的向量化涵盖了更广泛的上下文信息,能够捕捉到内容的整体语义。检索优化

4.2 检索优化

检索优化是 RAG 系统中直接影响检索效果和质量的核心环节。通过增强向量搜索、动态嵌入模型、混合检索等技术手段,系统能够高效、精准地找到与用户查询最相关的内容。

  1. 动态嵌入:RAG 系统通过动态嵌入模型根据上下文变化实时调整单词的嵌入表示,能够捕捉单词在不同上下文中的不同含义。例如,“bank”在“river bank”(河岸)和“financial bank”(银行)中的语义完全不同,动态嵌入可以根据具体语境生成合适的向量,从而提高检索的精准性。
  2. 领域特定嵌入微调:在实际应用中,不同领域的数据语境差异较大,通用的嵌入模型往往无法覆盖某些领域的专业术语或特定语义。通过对嵌入模型进行微调,可以增强其在特定领域中的表现。例如,针对医学、法律等专业领域,可以对嵌入模型进行定制化训练 / 微调,使其更好地理解这些领域中的特有词汇和语境。
  3. 假设文档嵌入:假设文档嵌入(Hypothetical Document Embeddings,HyDE)是一种创新的检索技术。HyDE 方法通过生成假设文档并将其向量化,以提升查询与检索结果之间的语义匹配度。当用户输入一个查询时,LLM 首先基于查询生成一个假设性答案,这个答案不一定是真实存在的文档内容,但它反映了查询的核心语义。然后,系统将该假设性答案向量化,与数据库中的向量进行匹配,寻找最接近的文档。例如,用户询问“拔除智齿需要多长时间?”,系统会生成一个假设性回答“拔智齿通常需要 30 分钟到两小时”,然后根据该假设文档进行检索,系统可能最终找到类似的真实文档,如“拔智齿的过程通常持续几分钟到 20 分钟以上”。通过假设文档,系统可以捕捉到更准确的相关文档。
  4. 混合检索:混合检索是结合向量搜索与关键词搜索等多种检索方法的混合方法,能够同时利用语义匹配与关键词匹配的优势。
  5. 小到大检索:这种方法首先通过较小的内容块(如单个句子或短段落)进行嵌入和检索,确保模型能找到与查询最匹配的小范围上下文。检索到相关内容后,再在生成阶段使用对应的较大文本块(如完整段落或全文)为模型提供更广泛的上下文支持。小块检索有助于提高精度,而大块生成则提供丰富的背景信息,使得生成的内容更加全面。
  6. 递归块合并:通过逐级扩展检索内容,确保生成阶段能够捕捉到更全面的上下文信息。该技术在细粒度的子块检索后,自动将相关的父块合并,以便提供完整的上下文供生成模型参考。

4.3 检索后优化

检索后优化目的是对已经检索到的内容进行进一步的处理和筛选,常用的技术包括重排序、提示压缩等,以确保最终生成的答案具有高度的相关性和准确性。

  1. 重排序:在 RAG 系统中,虽然初始检索可以找到多个与查询相关的内容块,但这些内容的相关性可能存在差异,因此需要进一步排序以优化生成结果。重排序通过重排序模型根据上下文的重要性、相关性评分等因素对已检索内容重新打分,以确保最相关的信息被优先处理。
  2. 提示压缩:通过删除冗余信息、合并相关内容、突出关键信息等方式来压缩提示,为生成模型提供更简洁、更相关的输入。
  3. 上下文重构:通过对检索到的内容进行再加工或重组,以便更好地符合查询的需求。常见的做法是将多个检索到的上下文片段整合成一个更具连贯性的文本块,减少重复或冲突的内容,从而为生成模型提供一个统一、清晰的输入。
  4. 内容过滤:根据预先设定的规则进行,包括过滤掉与查询无直接关联的内容、语义相似度较低的片段、冗长且无关的背景信息等,避免对生成结果产生负面影响。
  5. 多跳推理:系统通过多个推理步骤,逐步整合信息,以回答复杂查询。通常用于需要跨多个上下文或多步推理的问题。例如,用户询问“某个技术的演化历程”,系统可能先检索到该技术的某个时间点的关键事件,然后再通过进一步检索,找到关于这些关键事件的详细说明,最终给出完整的回答。
  6. 知识注入:在检索后通过外部知识库或预定义的领域知识,增强生成的上下文内容。这种方式适用于对准确性要求较高的场景,尤其是在特定领域或技术场景下,系统需要补充额外的专业知识。