基于 RAG 的生成式 AI——使用 LlamaIndex、Deep Lake 和 OpenAI 构建基于索引的 RAG

286 阅读40分钟

索引不仅提高了精度和速度,还提供了更多优势。索引通过增加透明度的层次,彻底改变了基于检索增强生成的 AI。使用索引,RAG 模型生成的响应来源完全可追溯,使我们能够清楚地看到所用数据的具体位置和详细内容。这一改进不仅减轻了偏见和幻觉等问题,还解决了版权和数据完整性方面的担忧。

在本章中,我们将探讨如何通过索引数据实现对生成式 AI 应用的更大控制。如果输出不理想,原因不再神秘,因为索引使我们能够识别并检查导致问题的确切数据来源。这一能力使我们能够优化数据输入、调整系统配置或切换组件(如向量存储软件和生成模型),以获得更好的结果。

我们将从建立基于索引的 RAG 流水线架构开始,增强速度、精度和可追溯性。我们将展示如何无缝集成 LlamaIndex、Deep Lake 和 OpenAI,而无需自己创建所有必要的功能,从而提供一个坚实的基础。在此之后,我们将介绍程序中将使用的主要索引类型,如向量、树、列表和关键词索引。然后,我们将构建一个特定领域的无人机技术 LLM RAG 代理,用户可以与之交互。无人机技术正扩展到火灾检测、交通信息和体育赛事等各个领域,因此我们决定在示例中使用该主题。本章的目标是准备一个无人机技术的 LLM 数据集,并将在下一章中用多模态数据增强该数据集。此外,我们还将通过代码展示关键索引类型。

在本章结束时,您将熟练掌握基于索引的 RAG 操作,通过向量存储、数据集和 LLM 优化检索系统,确保全面的可追溯性。您将发现,我们集成的工具包——结合 LlamaIndex、Deep Lake 和 OpenAI——不仅简化了技术复杂性,还为您节省了时间,使您能够更专注于提升分析技能,更深入地理解 RAG 驱动的生成式 AI。

在本章中,我们将涵盖以下主题:

  • 使用 LlamaIndex 框架和索引方法构建语义搜索引擎
  • 填充 Deep Lake 向量存储
  • 集成 LlamaIndex、Deep Lake 和 OpenAI
  • 排名评分和余弦相似度指标
  • 增强元数据以实现可追溯性
  • 查询设置和生成配置
  • 引入自动文档排序
  • 向量、树、列表和关键词索引类型

为什么要使用基于索引的 RAG?

基于索引的搜索将高级 RAG 驱动的生成式 AI 提升到一个新的水平。当面对大数据量时,它显著提高了检索速度,使我们可以从原始数据块到组织化、索引化的节点进行转换,能够从输出追溯到文档的源和其具体位置。

让我们通过分析基于索引的 RAG 的架构来理解向量相似性搜索与索引搜索之间的差异。

架构

在 RAG 中,基于索引的搜索比基于向量的搜索更快,因为它使用索引直接访问相关数据,而向量搜索则逐一比较所有记录中的嵌入数据。我们在第 2 章《使用 Deep Lake 和 OpenAI 构建 RAG 嵌入向量存储》中实现了一个基于向量相似性搜索的程序,如图 3.1 所示:

  • 我们在流水线 #1 中收集并准备了数据:数据收集和准备
  • 在流水线 #2 中将数据嵌入并将准备好的数据存储在向量存储中:嵌入和向量存储
  • 然后在流水线 #3 中运行检索查询和生成式 AI,以处理用户输入,基于向量相似性搜索运行检索,增强输入,生成响应,并应用性能指标。

这种方法灵活,因为它提供了多种实现每个组件的方法,能够根据项目的需求进行调整。

image.png

然而,实施基于索引的搜索将引领我们进入 AI 的未来,使其更快速、更精确并且更具可追溯性。我们将遵循与第 2 章相同的三条流水线流程,以确保您已准备好在任务专业化的团队中工作。由于我们使用的流水线与第 2 章相同,让我们添加该章中的功能,如图 3.1 所示:

  • 流水线组件 #1 和 D2-索引:我们将收集数据并进行预处理。不过这次,我们将一次准备一个文档数据源,并将它们分别存储在不同的文件中。然后,将文档的名称和位置添加到加载到向量存储中的元数据中。元数据将帮助我们将生成的响应完全追溯到检索功能处理的确切文件,使响应与其基础数据直接关联。
  • 流水线组件 #2 和 D3-索引:我们将通过安装和使用创新的集成包 llama-index-vector-stores-deeplake 来将数据加载到向量存储中。这个包在优化的起始场景中包含我们所需的一切:分块、嵌入、存储,甚至 LLM 集成。这样,一旦我们有一个稳固的程序,我们可以根据需要自定义和扩展这些流水线,就像在第 2 章中那样,我们明确选择了 LLM 模型和分块大小。
  • 流水线组件 #3 和 D4-索引:我们将通过安装和使用 llama-index-vector-stores-deeplake 包将数据加载到数据集中,开启基于索引的检索和生成功能,包括自动排名和评分。这一过程流畅且极具生产力。我们将利用 LlamaIndex 和 Deep Lake 来简化信息检索和处理。一个集成的检索器将高效地从 Deep Lake 仓库中提取相关数据,而一个 LLM 代理则会智能地综合和交互这些检索到的信息,以生成有意义的见解或行动。索引设计用于快速检索,我们将实现几种索引方法。
  • 流水线组件 #3 和 E1-索引:我们将添加时间和评分指标以评估输出。

在前一章中,我们实现了基于向量的相似性搜索和检索,将文档嵌入为高维向量以转换数据。然后,通过计算向量之间的距离来进行检索。在本章中,我们将进一步创建一个向量存储。但我们会将数据加载到一个数据集中,并使用检索索引类型对其重新组织。表 3.1 显示了基于向量和基于索引的搜索和检索方法的差异:

特性基于向量的相似性搜索和检索基于索引的向量、树、列表和关键词搜索和检索
灵活性中等(预计算结构)
速度大数据集下较慢快速且优化以实现快速检索
可扩展性受限于实时处理高度可扩展,适用于大数据集
复杂性设置较简单更复杂,需要索引步骤
更新频率易于更新更新需要重新索引

表 3.1:基于向量和基于索引的特点

接下来,我们将使用 Deep Lake、LlamaIndex 和 OpenAI 构建一个基于语义索引的 RAG 程序。

构建用于无人机技术的语义搜索引擎和生成代理

在本节中,我们将使用 Deep Lake 向量存储、LlamaIndex 和 OpenAI 构建一个基于语义索引的搜索引擎和生成式 AI 代理引擎。如前所述,无人机技术正在扩展到火灾检测和交通控制等领域。因此,程序的目标是为无人机技术的问答提供一个基于索引的 RAG 代理。该程序将展示无人机如何使用计算机视觉技术来识别车辆和其他物体。我们将实现本章架构部分描述的图 3.1 中的架构。

打开本章 GitHub 仓库中的文件 2-Deep_Lake_LlamaIndex_OpenAI_indexing.ipynb。本节的标题与 notebook 中的标题一致,因此您可以将解释与代码对应起来。

我们首先将安装环境。然后,我们将构建程序的三个主要流水线:

  • 流水线 1:收集和准备文档。使用 GitHub 和 Wikipedia 等来源收集和清理要索引的文档。
  • 流水线 2:创建并填充 Deep Lake 向量存储。使用准备好的文档创建并填充一个 Deep Lake 向量存储。
  • 流水线 3:基于索引的 RAG 用于查询处理和生成。应用时间和评分性能指标,结合 LLM 和余弦相似度度量。

在可能的情况下,将项目分解为独立的流水线,以便团队能够独立并行推进。本章的流水线示例展示了如何实现这一点,但具体做法可以根据项目需要进行调整。目前,我们将从安装环境开始。

安装环境

环境设置与上一章大致相同。我们将重点介绍用于集成 LlamaIndex、Deep Lake 向量存储以及 OpenAI 模块的包。这种集成是跨平台无缝实现的重要一步:

!pip install llama-index-vector-stores-deeplake==0.1.6

程序还需要额外的 Deep Lake 功能:

!pip install deeplake==3.9.8

此外,程序还需要 LlamaIndex 的功能:

!pip install llama-index==0.10.64

现在让我们检查是否可以从 llama-index 成功导入包括 Deep Lake 向量存储在内的相关包:

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Document
from llama_index.vector_stores.deeplake import DeepLakeVectorStore

至此,我们完成了环境安装。接下来,我们将收集并准备文档。

流水线 1:收集和准备文档

在本节中,我们将收集并准备与无人机相关的文档,并包含用于追溯文档来源的元数据。目标是将生成响应的内容追溯到具体的已检索数据块,以找到其来源。首先,我们将在其中加载文档的 data 目录:

!mkdir data

接下来,我们将使用 BeautifulSoup 处理无人机技术数据的异构语料库:

import requests
from bs4 import BeautifulSoup
import re
import os
urls = [
    "https://github.com/VisDrone/VisDrone-Dataset",
    "https://paperswithcode.com/dataset/visdrone",
    "https://openaccess.thecvf.com/content_ECCVW_2018/papers/11133/Zhu_VisDrone-DET2018_The_Vision_Meets_Drone_Object_Detection_in_Image_Challenge_ECCVW_2018_paper.pdf",
    "https://github.com/VisDrone/VisDrone2018-MOT-toolkit",
    "https://en.wikipedia.org/wiki/Object_detection",
    "https://en.wikipedia.org/wiki/Computer_vision",
    …
]

该语料库包含与无人机、计算机视觉及相关技术有关的多个站点的列表。然而,列表中还包括了一些噪声链接,如 keras.io/pytorch.org/,这些链接并不包含我们需要的特定信息。

在实际项目中,我们并不总能奢望拥有完美、相关、结构化且格式良好的数据。我们的 RAG 流水线必须足够健壮,能够在噪声环境中检索相关数据。

在本案例中,我们处理的是与无人机技术相关的各种格式和质量不一的非结构化数据。当然,在封闭的环境中,我们可以与生成文档的个人或组织合作,但在快速发展的数字化世界中,我们必须准备好处理任何类型的文档。

代码将如第 2 章中那样获取并清理数据:

def clean_text(content):
    # 移除引用和不需要的字符
    content = re.sub(r'[\d+]', '', content)  # 移除引用
    content = re.sub(r'[^\w\s.]', '', content)  # 移除标点(除了句号)
    return content

def fetch_and_clean(url):
    try:
        response = requests.get(url)
        response.raise_for_status()  # 处理响应异常(如 404 错误)
        soup = BeautifulSoup(response.content, 'html.parser')
        # 优先选择 "mw-parser-output",若未找到则使用 "content" 类
        content = soup.find('div', {'class': 'mw-parser-output'}) or soup.find('div', {'id': 'content'})
        if content is None:
            return None
        # 移除特定章节,包括嵌套内容
        for section_title in ['References', 'Bibliography', 'External links', 'See also', 'Notes']:
            section = content.find('span', id=section_title)
            while section:
                for sib in section.parent.find_next_siblings():
                    sib.decompose()
                section.parent.decompose()
                section = content.find('span', id=section_title)
        # 提取并清理文本
        text = content.get_text(separator=' ', strip=True)
        text = clean_text(text)
        return text
    except requests.exceptions.RequestException as e:
        print(f"Error fetching content from {url}: {e}")
        return None  # 出现错误时返回 None

每个项目都需要为原始数据定义特定的名称和路径。在本案例中,我们将引入一个额外的函数,通过基于 URL 创建关键字来保存每段文本及其数据源的名称:

# 保存输出文件的目录
output_dir = './data/'
os.makedirs(output_dir, exist_ok=True)
# 处理每个 URL 并将其内容写入单独的文件
for url in urls:
    article_name = url.split('/')[-1].replace('.html',")  # 处理 .html 扩展名
    filename = os.path.join(output_dir, article_name + '.txt')  # 为文章创建文件名
    clean_article_text = fetch_and_clean(url)
    with open(filename, 'w', encoding='utf-8') as file:
        file.write(clean_article_text)
print(f"Content (可能的部分) 已写入 '{output_dir}' 目录中的文件。")

输出显示目标已实现,尽管有些文档无法解码:

WARNING:bs4.dammit:某些字符无法解码,已替换为替代字符。
Content (可能的部分) 已写入 './data/' 目录中的文件。

根据项目的目标,您可以选择进一步调查以确保所有文档均被检索,或者认为现有数据足够用于用户查询。

如果检查 ./data/,可以看到每篇文章现在都位于单独的文件中,如该目录的内容所示。

image.png

程序现在从 ./data/ 目录加载文档:

# 加载文档
documents = SimpleDirectoryReader("./data/").load_data()

LlamaIndex 的 SimpleDirectoryReader 类设计用于处理非结构化数据。它会递归扫描目录并识别所有支持的文件类型,如 .txt.pdf.docx。然后,它从每个文件中提取内容,并返回包含文本和元数据(如文件名和文件路径)的文档对象列表。让我们显示该文档字典列表的第一个条目:

documents[0]

输出显示目录读取器已提供其数据来源的完全透明信息,包括文档名称,例如在本例中为 1804.06985.txt

'/content/data/1804.06985.txt', 'file_name': '1804.06985.txt', 'file_type': 'text/plain', 'file_size': 3698, 'creation_date': '2024-05-27', 'last_modified_date': '2024-05-27'}, excluded_embed_metadata_keys=['file_name', 'file_type', 'file_size', 'creation_date', 'last_modified_date', 'last_accessed_date'], excluded_llm_metadata_keys=['file_name', 'file_type', 'file_size', 'creation_date', 'last_modified_date', 'last_accessed_date'], relationships={}, text='High Energy Physics  Theory arXiv1804.06985 hepth Submitted on 19 Apr 2018 Title A Near Horizon Extreme Binary Black Hole Geometry Authors Jacob Ciafre  Maria J. Rodriguez View a PDF of the paper titled A Near Horizon Extreme Binary Black Hole Geometry by Jacob Ciafre and Maria J. Rodriguez View PDF Abstract A new solution of fourdimensional vacuum General Relativity is presented…

该文档的内容包含一些似乎与我们所寻找的无人机技术信息无关的噪声。但这正是该程序的要点,目标如下:

  • 从所有能够获得的原始、非结构化的、与无人机松散相关的数据开始
  • 模拟实际项目的起点
  • 评估基于索引的 RAG 生成式 AI 程序在充满挑战的环境中的表现

接下来,我们将以完全透明的方式创建并填充一个 Deep Lake 向量存储。

管道 2:创建和填充 Deep Lake 向量存储

在本节中,我们将创建一个 Deep Lake 向量存储,并使用文档中的数据填充它。我们将实现一个标准的张量配置,包括以下字段:

  • text (str) :文本内容是字典中列出的文本文件之一的内容。该过程是无缝的,并优化了文本分块,将文本划分为有意义的块。
  • metadata (json) :在本例中,元数据将包含每个文本块的文件名来源,以实现完全透明和可控性。我们将展示如何在代码中访问此信息。
  • embedding (float32) :使用 OpenAI 嵌入模型进行无缝嵌入,该模型直接由 LlamaIndex-Deep Lake-OpenAI 包调用。
  • id (str, auto-populated) :系统自动为每个文本块分配一个唯一 ID。向量存储还包含一个索引,这是一个从 0 到 n 的数字,但该索引在每次修改数据集时都会变化,因此无法在语义上使用。然而,唯一 ID 字段将保持不变,直到我们决定用基于索引的搜索策略进行优化(见后续的“管道 3:基于索引的 RAG”部分)。

程序首先定义了向量存储和数据集路径:

from llama_index.core import StorageContext
vector_store_path = "hub://denis76/drone_v2"
dataset_path = "hub://denis76/drone_v2"

将向量存储和数据集路径替换为您的账户名称和所使用的数据集名称:

vector_store_path = "hub://[YOUR VECTOR STORE/

然后,我们创建一个向量存储,填充数据,并在文档上创建一个索引:

# overwrite=True 将覆盖数据集,False 将追加到现有数据集
vector_store = DeepLakeVectorStore(dataset_path=dataset_path, overwrite=True)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
# 在文档上创建索引
index = VectorStoreIndex.from_documents(documents, storage_context=storage_context)

请注意,overwrite 设置为 True 以创建向量存储并覆盖任何现有的数据集。如果 overwrite=False,数据集将被追加。

创建的索引将由索引方法进行重新组织,这些方法将在需要时重新排列并创建新索引。然而,响应将始终提供数据的原始来源。输出确认数据集已创建且数据已上传:

Your Deep Lake dataset has been successfully created!
Uploading data to deeplake dataset.
100%|██████████| 41/41 [00:02<00:00, 18.15it/s]

输出还显示了数据集在填充后的结构:

Dataset(path='hub://denis76/drone_v2', tensors=['text', 'metadata', 'embedding', 'id'])

数据存储在不同类型和形状的张量中:

image.png

现在我们将数据集加载到内存中:

import deeplake
ds = deeplake.load(dataset_path)  # 加载数据集

我们可以通过输出中提供的链接在线可视化数据集:

此数据集可以在 Jupyter Notebook 中通过 `ds.visualize()` 进行可视化,或在 https://app.activeloop.ai/denis76/drone_v2 查看。
hub://denis76/drone_v2 加载成功。

我们还可以选择添加代码来显示数据集。首先,我们将数据加载到一个 pandas DataFrame 中:

import json
import pandas as pd
import numpy as np
# 假设 'ds' 是已加载的 Deep Lake 数据集
# 创建一个字典来存储数据
data = {}
# 遍历数据集中的张量
for tensor_name in ds.tensors:
    tensor_data = ds[tensor_name].numpy()
    # 检查张量是否是多维的
    if tensor_data.ndim > 1:
        # 将多维张量展平
        data[tensor_name] = [np.array(e).flatten().tolist() for e in tensor_data]
    else:
        # 将一维张量直接转换为列表,并解码文本
        if tensor_name == "text":
            data[tensor_name] = [t.tobytes().decode('utf-8') if t else "" for t in tensor_data]
        else:
            data[tensor_name] = tensor_data.tolist()
# 从字典创建 Pandas DataFrame
df = pd.DataFrame(data)

接着,我们创建一个函数来显示一条记录:

# 显示选定记录的函数
def display_record(record_number):
    record = df.iloc[record_number]
    display_data = {
        "ID": record["id"] if "id" in record else "N/A",
        "Metadata": record["metadata"] if "metadata" in record else "N/A",
        "Text": record["text"] if "text" in record else "N/A",
        "Embedding": record["embedding"] if "embedding" in record else "N/A"
    }

最后,我们可以选择一条记录并显示每个字段:

# 调用函数显示记录
rec = 0  # 替换为所需的记录号
display_record(rec)

ID 是一个唯一的字符串代码:

ID:
['a89cdb8c-3a85-42ff-9d5f-98f93f414df6']

元数据字段包含了我们追溯内容来源到原始文件及其路径所需的信息,以及我们理解该记录所需的所有信息,从数据来源到嵌入向量。它还包含了从记录数据生成的节点信息,可用于我们将在管道 3 中运行的索引引擎:

  • file_path:数据集中文件的路径(例如 /content/data/1804.06985.txt)。

  • file_name:文件名(例如 1804.06985.txt)。

  • file_type:文件类型(例如 text/plain)。

  • file_size:文件大小(以字节为单位,如 3700)。

  • creation_date:文件的创建日期(例如 2024-08-09)。

  • last_modified_date:文件的最后修改日期(例如 2024-08-09)。

  • _node_content:节点的详细内容,包括以下主要项目:

    • id_ :节点的唯一标识符(例如 a89cdb8c-3a85-42ff-9d5f-98f93f414df6)。
    • embedding:与文本相关的嵌入(为空)。
    • metadata:文件的重复元数据。
    • excluded_embed_metadata_keys:嵌入元数据中排除的键(嵌入时不必要)。
    • excluded_llm_metadata_keys:LLM 元数据中排除的键(对 LLM 不必要)。
    • relationships:关于与其他节点关系的信息。
    • text:文档的实际文本内容,可以是文本本身、摘要、概要,或任何其他优化搜索功能的方式。
    • start_char_idx:文本的起始字符索引。
    • end_char_idx:文本的结束字符索引。
    • text_template:用于显示包含元数据的文本的模板。
    • metadata_template:显示元数据的模板。
    • metadata_seperator:用于元数据显示的分隔符。
    • class_name:节点的类型(例如 TextNode)。
    • _node_type:节点的类型(例如 TextNode)。
    • document_id:文档的标识符(例如 61e7201d-0359-42b4-9a5f-32c4d67f345e)。
    • doc_id:文档 ID,与 document_id 相同。
    • ref_doc_id:参考文档 ID,与 document_id 相同。

文本字段包含了该数据块的内容,而不是整个原始文本:

['High Energy Physics Theory arXiv1804.06985 hepth Submitted on 19 Apr 2018 Title A Near Horizon Extreme Binary Black Hole Geometry Authors Jacob Ciafre Maria J. Rodriguez View a PDF of the paper titled A Near Horizon Extreme Binary…']

嵌入字段包含文本内容的嵌入向量:

[-0.0009671939187683165, 0.010151553899049759, -0.010979819111526012, -0.003061748342588544, -0.00865076668560505, 0.02144993655383587, -0.01412297785282135, -0.02674516849219799, -0.008693241514265537, -0.03383851423859596, 0.011404570192098618, 0.015956487506628036, -0.013691147789359093, 0.008856062777340412, …]

RAG 数据集的结构和格式在不同领域或项目中各有不同。不过,该数据集的以下四列对 AI 发展的理解提供了宝贵信息:

  • idid 是我们将用来组织数据集中文本块的索引。这些文本块将被转换为节点,这些节点可以包含原始文本、文本摘要和附加信息,比如存储在 metadata 列中的用于生成输出的数据信息。我们在本笔记本的 Pipeline 2 中创建向量存储时创建了此索引。然而,我们也可以在没有索引的现有数据库中生成内存索引,具体操作将在第 4 章《多模态模块化 RAG 与无人机技术》中展示。
  • metadatametadata 是在 Pipeline 1 中自动生成的,当时 Deep Lake 的 SimpleDirectoryReader 将源文件加载到 documents 对象中,并在创建向量存储时生成。在第 2 章《使用 Deep Lake 和 OpenAI 的 RAG 嵌入向量存储》中,我们的数据来源仅有一个文件,而在本章中,我们将每个数据源的内容单独存储在一个文件中(每个 URL 一个文件)。
  • text:Deep Lake 的向量存储创建功能在 Pipeline 2 中处理了文本,并自动对数据进行了分块,而我们无需配置块的大小(如第 2 章《准备好的一批文件》中那样)。这个过程是无缝的。本章的 Pipeline 3 中的优化分块部分将展示如何进行智能分块。
  • embedding:每个数据块的嵌入是通过一个嵌入模型生成的,而我们无需进行配置。我们可以选择一个嵌入模型(如第 2 章《数据嵌入与存储》中那样),自行编写函数进行嵌入。在本程序中,Deep Lake 自动选择嵌入模型并生成数据嵌入,而我们不需写任何代码。

可以看到,嵌入、分块、索引和其他数据处理功能现在已被封装在一些平台和框架中,例如 Activeloop Deep Lake、LlamaIndex、OpenAI、LangChain、Hugging Face 和 Chroma 等。逐渐地,生成式 AI 模型和 RAG 的最初热潮将消退,它们将被工业化、封装化,成为 AI 管道中的常见组件。AI 正在发展,因此建立一个基于有效实践的默认配置平台可能是有帮助的。之后,我们可以根据项目需求,在基本配置的基础上进行定制和扩展。

我们现在已经准备好运行基于索引的 RAG。

Pipeline 3:基于索引的 RAG

在本节中,我们将使用 LlamaIndex 实现一个基于索引的 RAG 管道,利用 Deep Lake 处理的准备数据。我们将从包含噪声的异构无人机相关文档集合中检索相关信息,并通过 OpenAI 的 LLM 模型综合生成响应。我们将实现四种索引引擎:

  • 向量存储索引引擎:从文档中创建向量存储索引,支持高效的相似度搜索。
  • 树形索引:基于文档构建一个分层的树形索引,提供一种替代的检索结构。
  • 列表索引:从文档构建一个简单的列表索引。
  • 关键词表索引:基于从文档中提取的关键词创建索引。

我们将使用 LLM 进行查询:

  • 查询响应和来源:通过用户输入查询索引,检索相关文档,并返回综合响应及其来源信息。

我们还将通过时间加权平均分数、LLM 评分和余弦相似度来评估响应,计算基于检索和相似度评分的时间加权平均分数。由于实现的随机算法,内容和执行时间可能在不同运行中有所不同。

用户输入和查询参数

用户输入将作为我们运行的四个索引引擎的参考问题。我们将基于各个索引引擎的检索结果评估每个响应,并使用时间和评分比率衡量输出。用户输入如下:

user_input = "How do drones identify vehicles?"

实现的四个查询引擎(在本例中为 OpenAI 模型)将使用相同的参数无缝调用。我们设置的三个关键参数如下:

# 设置最相似的结果数目
k = 3
# 设置温度
temp = 0.1
# 输出最大字数
mt = 1024

这些关键参数的含义如下:

  • k = 3:查询引擎将被要求找到最有可能的前三个响应,通过设置 top-k 参数为 3。在本例中,k 将作为排名函数,强制 LLM 选择最佳的文档。
  • temp = 0.1:温度值为 0.1 会鼓励 LLM 生成精确的结果。如果将温度提高到 0.9,响应将会更具创造性。然而,本例探讨的是无人机技术,因此需要精确的回答。
  • mt = 1024:此参数将限制输出的最大 token 数量为 1,024。

用户输入和参数将应用于四个查询引擎。现在,让我们来构建余弦相似度指标。

余弦相似度指标

余弦相似度指标在第 2 章的“使用余弦相似度评估输出”部分中进行了描述。如果有需要,可以重新查看该部分内容。这里,我们将为生成的响应创建一个函数:

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sentence_transformers import SentenceTransformer

# 使用 Hugging Face 的 SentenceTransformer 模型
model = SentenceTransformer('all-MiniLM-L6-v2')

def calculate_cosine_similarity_with_embeddings(text1, text2):
    embeddings1 = model.encode(text1)
    embeddings2 = model.encode(text2)
    similarity = cosine_similarity([embeddings1], [embeddings2])
    return similarity[0][0]

该函数使用 sklearn 和 Hugging Face 的 SentenceTransformer。程序首先创建向量存储引擎。

向量存储索引查询引擎

VectorStoreIndex 是 LlamaIndex 中的一种索引类型,它实现了向量嵌入,用于从文档中表示和检索信息。具有相似意义的文档在向量空间中的嵌入会更接近,正如我们在上一章中所探讨的那样。然而,这次 VectorStoreIndex 并不会自动使用现有的 Deep Lake 向量存储,而是可以创建一个新的内存中的向量索引,对文档重新进行嵌入,并生成新的索引结构。我们将在第 4 章《面向无人机技术的多模态模块化 RAG》中进一步研究这一方法,当时将实现一个不包含索引或嵌入的数据集。

选择适合项目的索引方法并没有万全之策!选择最佳方法的方式是测试本章介绍的向量、树、列表和关键词索引。

我们首先创建向量存储索引:

from llama_index.core import VectorStoreIndex
vector_store_index = VectorStoreIndex.from_documents(documents)

然后,展示我们创建的向量存储索引:

print(type(vector_store_index))

我们会得到如下输出,确认引擎已成功创建:

<class 'llama_index.core.indices.vector_store.base.VectorStoreIndex'>

接下来,我们需要一个查询引擎,用于检索并通过 LLM(在这里,我们使用 OpenAI 模型,安装命令为 !pip install llama-index-vector-stores-deeplake==0.1.2)合成检索到的文档内容:

vector_query_engine = vector_store_index.as_query_engine(similarity_top_k=k, temperature=temp, num_output=mt)

我们在“用户输入和查询参数”小节中定义了查询引擎的参数。现在可以对数据集进行查询并生成响应。

查询响应与数据来源

定义一个函数来管理查询,并返回响应内容的相关信息:

import pandas as pd
import textwrap

def index_query(input_query):
    response = vector_query_engine.query(input_query)
    # 可选:格式化输出响应内容(如不需要,可删除)
    print(textwrap.fill(str(response), 100))
    
    node_data = []
    for node_with_score in response.source_nodes:
        node = node_with_score.node
        node_info = {
            'Node ID': node.id_,
            'Score': node_with_score.score,
            'Text': node.text
        }
        node_data.append(node_info)
    
    df = pd.DataFrame(node_data)
    # 返回 DataFrame 和响应对象,而不是直接打印
    return df, response

index_query(input_query) 使用向量查询引擎执行查询,并将结果整理为结构化格式。该函数接收一个输入查询,利用查询引擎检索相关信息,并将结果存储在一个 pandas DataFrame 中,包括节点 ID、评分、文件路径、文件名和文本内容。

以下代码将调用该查询:

import time

# 开始计时
start_time = time.time()

df, response = index_query(user_input)

# 停止计时
end_time = time.time()

# 计算并打印执行时间
elapsed_time = end_time - start_time
print(f"Query execution time: {elapsed_time:.4f} seconds")

print(df.to_markdown(index=False, numalign="left", stralign="left"))  # 使用 markdown 格式显示 DataFrame

我们将评估查询检索相关数据并生成 LLM(在此为 OpenAI 模型)合成响应所需的时间。语义搜索的输出首先返回 LLM 合成的响应:

无人机可以通过不同视角和硬件规格的摄像头,利用再识别方法自动识别车辆。

输出显示查询的耗时:

Query execution time: 0.8831 seconds

接下来,输出显示每个节点的信息。k=3 的三个文档节点及其文本片段与评分已成功检索。

image.png

节点 ID 确保了数据的完全透明性,即使在索引引擎重新索引数据集后也可以追溯到原始文档。我们可以通过以下代码获取第一个节点的来源:

nodeid = response.source_nodes[0].node_id
nodeid

输出将显示节点 ID:

4befdb13-305d-42db-a616-5d9932c17ac8

我们还可以深入查询,检索包含该节点的完整文本内容,即被 LLM 合成的文档文本:

response.source_nodes[0].get_text()

输出将显示如下文本内容:

['这些活动可以通过多种方法进行,包括摄影测量 SfM 热成像 多光谱图像 3D 场地扫描 NDVI 图等。农业、林业和环境研究编辑 主要条目 农业无人机 随着全球对粮食生产需求的指数增长,资源逐渐枯竭,农田...

我们还可以查看节点的块大小信息。

优化分块

我们可以预先定义块大小,或者让 LlamaIndex 自动选择块大小。在此示例中,代码会自动确定块大小:

for node_with_score in response.source_nodes:
    node = node_with_score.node  # 从 NodeWithScore 中提取 Node 对象
    chunk_size = len(node.text)
    print(f"Node ID: {node.id_}, Chunk Size: {chunk_size} characters")

自动块大小的优点在于其可变性。例如,在此情况下,输出节点的块大小大约在 4000 到 5500 个字符之间:

Node ID: 83a135c6-dddd-402e-9423-d282e6524160, Chunk Size: 4417 characters
Node ID: 7b7b55fe-0354-45bc-98da-0a715ceaaab0, Chunk Size: 1806 characters
Node ID: 18528a16-ce77-46a9-bbc6-5e8f05418d95, Chunk Size: 3258 characters

分块功能不会简单地线性切割内容,而是优化块以适应语义搜索的需求。

性能指标

我们还将实现基于查询准确性和耗时的性能指标。该函数计算并输出查询的性能指标及其执行时间。该指标基于检索信息的加权平均相关性得分除以获取结果所需的时间。得分越高,性能越好。

我们首先计算分数的总和和平均分数,然后将加权平均值除以执行查询的耗时:

import numpy as np
def info_metrics(response):
  # 计算性能(处理 None 值的分数)
  scores = [node.score for node in response.source_nodes if node.score is not None]
  if scores:  # 检查是否有有效的分数
      weights = np.exp(scores) / np.sum(np.exp(scores))
      perf = np.average(scores, weights=weights) / elapsed_time
  else:
      perf = 0  # 如果所有分数均为 None,则设置为 0 或其他默认值

结果是基于平均权重除以耗时的比率:

perf = np.average(scores, weights=weights) / elapsed_time

然后,我们可以调用该函数:

info_metrics(response)

输出提供了对响应质量的估计:

平均分数: 0.8374
查询执行时间: 1.3266 秒
性能指标: 0.6312

此性能指标不是绝对值,而是一个用于在本章中比较不同索引引擎输出的参考指标。由于机器学习算法的随机性,它的结果可能会在每次运行中有所不同。此外,输出质量也取决于用户的主观感受。在任何情况下,此指标都将有助于比较各个查询引擎的性能。

我们可以看到,即使我们在数据集中加载了异质且有时不相关的文档,平均分数仍然令人满意。LlamaIndex、Deep Lake 和 OpenAI 的集成检索与合成功能已证明其高效性。

树索引查询引擎

LlamaIndex 中的树索引通过创建一个层次结构来有效管理和查询文本文档。不过,不要将其想象为传统的层次结构!树索引引擎会优化节点的层次结构、内容和顺序,如图 3.5 所示。

image.png

树索引将文档组织成树形结构,在较高层次上提供更广泛的摘要,而在较低层次上提供详细信息。树中的每个节点都会总结其所覆盖的文本。对于大型数据集,树索引效率更高,可以通过将文档分解成可管理的优化块来快速查询大量文档。树结构的优化使得检索能够通过遍历相关节点而快速完成,无需浪费时间。

在管道中组织这部分并调整树深度和摘要方法等参数,可以成为团队成员的专门任务。根据项目需求和工作量,树结构的构建可以在创建和填充向量存储的Pipeline 2中完成,或者在每个会话开始时将树结构存储在内存中。树结构和索引引擎的灵活性和实现让RAG驱动的生成式AI团队中的专业化变得更加有趣且有价值。

在这个索引模型中,LLM(此处为OpenAI模型)在查询时类似于回答多项选择题。它分析查询,将其与当前节点子节点的摘要进行比较,选择最合适的路径,以找到最相关的信息。

本章中的集成LlamaIndex-Deep Lake-OpenAI流程实现了无缝工业化,推动了AI的进步。现在,LLM模型可以用于嵌入、文档排序和对话代理。市场上提供了各种语言模型,包括来自OpenAI、Cohere、AI21 Labs和Hugging Face的模型。LLM已从早期的“魔法”发展到成为无缝、工业化、多功能和集成的广泛AI管道的组成部分。

让我们用两行代码创建一个树索引:

from llama_index.core import TreeIndex
tree_index = TreeIndex.from_documents(documents)

代码随后检查我们刚刚创建的类:

print(type(tree_index))

输出确认我们在TreeIndex类中:

<class 'llama_index.core.indices.tree.base.TreeIndex'>

现在可以将树索引作为查询引擎:

python
复制代码
tree_query_engine = tree_index.as_query_engine(similarity_top_k=k, temperature=temp, num_output=mt)

LLM的参数在"用户输入和查询参数"部分定义。代码现在调用查询、测量所用时间并处理响应:

import time
import textwrap
# 开始计时
start_time = time.time()
response = tree_query_engine.query(user_input)
# 停止计时
end_time = time.time()
# 计算并输出执行时间
elapsed_time = end_time - start_time
print(f"Query execution time: {elapsed_time:.4f} seconds")
print(textwrap.fill(str(response), 100))

查询时间和响应都令人满意:

Query execution time: 4.3360 seconds
Drones identify vehicles using computer vision technology related to object detection. This
technology involves detecting instances of semantic objects of a certain class, such as vehicles, in
digital images and videos. Drones can be equipped with object detection algorithms, such as YOLOv3
models trained on datasets like COCO, to detect vehicles in real-time by analyzing the visual data
captured by the drone's cameras.

接下来,我们对输出应用性能度量。

性能度量

该性能度量将计算用户输入与我们RAG管道响应之间的余弦相似度,具体定义见“余弦相似度度量”部分:

similarity_score = calculate_cosine_similarity_with_embeddings(user_input, str(response))
print(f"Cosine Similarity Score: {similarity_score:.3f}")
print(f"Query execution time: {elapsed_time:.4f} seconds")
performance = similarity_score / elapsed_time
print(f"Performance metric: {performance:.4f}")

输出结果显示尽管响应质量令人满意,但执行时间较慢,从而降低了性能度量值:

Cosine Similarity Score: 0.731
Query execution time: 4.3360 seconds
Performance metric: 0.1686

当然,执行时间取决于服务器的计算能力和数据的质量(噪声影响)。正如前面所说,由于算法的随机性,执行时间可能会因每次运行而有所不同。此外,当数据集的体量增加时,所有索引类型的执行时间可能会发生变化。

接下来,我们来测试列表索引查询引擎,看看它在这种情况下是否表现得更好。让我们运行并验证结果。

列表索引查询引擎

不要将ListIndex简单地视为一系列节点。查询引擎将用户输入和每个文档作为提示传递给LLM。LLM会评估文档与查询之间的语义相似关系,从而隐式地对节点进行排名并选择最相关的节点。LlamaIndex会基于获得的排名来筛选文档,并且还可以进一步从多个节点和文档中合成信息。

可以看出,使用LLM的选择过程并非基于规则。没有预先定义的标准,选择是基于将用户输入与文档集合结合的提示。LLM独立评估列表中的每个文档,并根据其与查询的相关性分配分数。这个分数不是相对于其他文档的,而是LLM认为当前文档能多好地回答问题的衡量。然后,如果需要,查询引擎保留排名靠前的k个文档,就像本节中的函数使用的那样。

和树索引类似,列表索引也可以通过两行代码创建:

from llama_index.core import ListIndex
list_index = ListIndex.from_documents(documents)

代码会验证我们正在使用的类:

print(type(list_index))

输出确认我们处于列表类中:

<class 'llama_index.core.indices.list.base.SummaryIndex'>

列表索引是一个SummaryIndex,表明在后台运行了大量的文档摘要优化!现在我们可以在LlamaIndex提供的无缝框架中将列表索引用作查询引擎:

list_query_engine = list_index.as_query_engine(similarity_top_k=k, temperature=temp, num_output=mt)

LLM参数保持不变,以便我们可以比较不同的索引类型。现在,我们可以运行查询、包装响应并显示输出:

#start the timer
start_time = time.time()
response = list_query_engine.query(user_input)
# Stop the timer
end_time = time.time()
# Calculate and print the execution time
elapsed_time = end_time - start_time
print(f"Query execution time: {elapsed_time:.4f} seconds")
print(textwrap.fill(str(response), 100))

输出显示了较长的执行时间,但响应尚可接受:

Query execution time: 16.3123 seconds
Drones can identify vehicles through computer vision systems that process image data captured by
cameras mounted on the drones. These systems use techniques like object recognition and detection to
analyze the images and identify specific objects, such as vehicles, based on predefined models or
features. By processing the visual data in real-time, drones can effectively identify vehicles in
their surroundings.

执行时间较长,因为查询通过的是一个列表,而不是一个经过优化的树结构。然而,我们无法据此得出结论,因为每个项目甚至每个子任务的需求都不同。接下来,让我们应用性能度量。

性能度量

我们将使用余弦相似度,正如在树索引中所做的那样,来评估相似性得分:

similarity_score = calculate_cosine_similarity_with_embeddings(user_input, str(response))
print(f"Cosine Similarity Score: {similarity_score:.3f}")
print(f"Query execution time: {elapsed_time:.4f} seconds")
performance = similarity_score / elapsed_time
print(f"Performance metric: {performance:.4f}")

由于执行时间较长,该性能度量低于树索引:

Cosine Similarity Score: 0.775
Query execution time: 16.3123 seconds
Performance metric: 0.0475

同样需要记住,由于所使用的随机算法,执行时间可能会因每次运行而有所不同。

回顾各索引类型的性能度量,我们可以看到,目前为止,向量存储索引是最快的。然而,不能轻易得出结论。每个项目可能会产生不同的结果,具体取决于数据的类型和复杂性。接下来,让我们研究关键词索引。

关键词索引查询引擎

KeywordTableIndex 是 LlamaIndex 中的一种索引类型,旨在从文档中提取关键词并将其组织成类似表格的结构。这种结构有助于基于特定关键词或主题进行查询和检索相关信息。不过,这并不是简单的关键词列表。提取的关键词会被组织成一个表格形式,每个关键词都关联一个 ID,用于指向相关的节点。

程序通过以下两行代码创建关键词索引:

from llama_index.core import KeywordTableIndex
keyword_index = KeywordTableIndex.from_documents(documents)

接下来,我们提取数据并创建一个 pandas DataFrame,以查看索引的结构:

# 提取数据用于创建 DataFrame
data = []
for keyword, doc_ids in keyword_index.index_struct.table.items():
    for doc_id in doc_ids:
        data.append({"Keyword": keyword, "Document ID": doc_id})
# 创建 DataFrame
df = pd.DataFrame(data)
df

输出显示,每个关键词都关联一个包含文档或摘要的 ID,这取决于 LlamaIndex 对索引的优化方式:

image.png

现在我们将关键词索引定义为查询引擎:

keyword_query_engine = keyword_index.as_query_engine(similarity_top_k=k, temperature=temp, num_output=mt)

运行关键词查询,观察它的响应效果和速度:

import time
# 开始计时
start_time = time.time()
# 执行查询(使用 .query() 方法)
response = keyword_query_engine.query(user_input)
# 停止计时
end_time = time.time()
# 计算并打印执行时间
elapsed_time = end_time - start_time
print(f"Query execution time: {elapsed_time:.4f} seconds")
print(textwrap.fill(str(response), 100))

输出结果令人满意,执行时间也较快:

Query execution time: 2.4282 seconds
Drones can identify vehicles through various means such as visual recognition using onboard cameras, sensors, and image processing algorithms. They can also utilize technologies like artificial intelligence and machine learning to analyze and classify vehicles based on their shapes, sizes, and movement patterns. Additionally, drones can be equipped with specialized software for object detection and tracking to identify vehicles accurately.

接下来,我们将使用性能指标来测量输出效果。

性能指标

代码对树索引和列表索引运行相同的指标计算:

similarity_score = calculate_cosine_similarity_with_embeddings(user_input, str(response))
print(f"Cosine Similarity Score: {similarity_score:.3f}")
print(f"Query execution time: {elapsed_time:.4f} seconds")
performance = similarity_score / elapsed_time
print(f"Performance metric: {performance:.4f}")

性能指标显示结果尚可:

Cosine Similarity Score: 0.801
Query execution time: 2.4282 seconds
Performance metric: 0.3299

再次强调,我们不能得出绝对结论。所有索引类型的结果都较为令人满意。然而,每个项目的数据集复杂性和机器性能不同。此外,由于使用了随机算法,执行时间可能会在不同运行之间有所变化。

至此,我们回顾了几种主要的索引类型和检索策略。接下来,让我们总结本章内容,并进入多模态模块化检索和生成策略的探索。

总结

本章探讨了基于索引的搜索对RAG的变革性影响,并引入了一项关键的进步:完全可追溯性。文档被转换为包含数据块的节点,使查询源头可以追溯到原始数据。随着数据集规模的增加,索引还提高了检索速度,这一点至关重要。本章还介绍了LlamaIndex、Deep Lake和OpenAI等技术的集成,这些技术标志着AI进入了另一个新时代。最先进的AI模型,如OpenAI GPT-4o、Hugging Face和Cohere,正在成为RAG驱动的生成式AI管道中无缝整合的组件,类似于计算机中的GPU。

我们首先详细说明了基于索引的RAG生成式AI管道的架构,展示了如何将这些复杂技术无缝整合,以加速创建高级索引和检索系统。AI实施的复杂性正在改变我们对并行团队项目中的单独管道和功能的组织方式,尤其是当项目需要处理大量数据时。我们展示了如何通过每个生成的响应追溯至其数据源,从而提供对所用信息来源和准确性的清晰可见性。通过无人机技术展示了先进的RAG技术实现。

在本章中,我们介绍了构建这些系统的基本工具,包括向量存储、数据集、分块、嵌入、节点创建、排序和索引方法。我们实现了LlamaIndex框架、Deep Lake向量存储和OpenAI模型,还构建了一个Python程序来收集数据并添加关键元数据,以便在数据集中精确定位每个数据块的来源。我们强调了索引(向量、树、列表和关键词类型)在生成式AI应用中赋予我们更大控制权的关键作用,使我们能够进行精确的调整和改进。

接着,我们通过Python代码详解了基于索引的RAG,指导了如何设置向量存储、进行高级查询并确保AI生成响应的可追溯性。我们还引入了基于响应质量和查询耗时的性能指标。通过LLM探索无人机技术展示了构建坚实的AI管道所需的新技能,我们也了解了无人机技术涉及计算机视觉,从而引入多模态节点。

在下一章中,我们将在数据集中加入多模态数据,扩展多模块RAG。

问题

  1. 索引是否提高了检索增强生成式AI的准确性和速度?
  2. 索引能否提供RAG输出的可追溯性?
  3. 索引搜索在大数据集上是否比向量搜索更慢?
  4. LlamaIndex是否可以无缝集成Deep Lake和OpenAI?
  5. 树、列表、向量和关键词索引是否是唯一的索引类型?
  6. 关键词索引是否依赖语义理解来检索数据?
  7. LlamaIndex是否能够自动处理分块和嵌入?
  8. 元数据增强是否对于确保RAG生成输出的可追溯性至关重要?
  9. 是否可以轻松地将实时更新应用于基于索引的搜索系统?
  10. 余弦相似度是否在本章中用于评估查询准确性?

参考文献

进一步阅读