Bootstrap

大模型高级RAG从理论到LlamaIndex实现

最近这一两周看到不少互联网公司都已经开始秋招提前批了。

不同以往的是,当前职场环境已不再是那个双向奔赴时代了。求职者在变多,HC 在变少,岗位要求还更高了。

最近,我们又陆续整理了很多大厂的面试题,帮助一些球友解惑答疑,分享技术面试中的那些弯弯绕绕。

喜欢本文记得收藏、关注、点赞。更多实战和面试交流,文末加入我们星球


论文《Retrieval-Augmented Generation for Large Language Models: A Survey》对检索增强生成(RAG)总结了如下三种方式:

  • Naive RAG

  • 高级RAG

  • 模块化RAG

高级RAG旨在解决Naive RAG的局限性。本文把这些技术,可分为预检索(pre-retrieval)、检索(retrieval)和检索后(post-retrieval)优化。

在下半部分中,首先使用Llamaindex实现一个简单的RAG管道,然后通过选择以下高级RAG技术将其增强为高级RAG管道:

  • 检索前优化:语句窗口检索

  • 检索优化:混合搜索

  • 检索后优化:重新排序

一、高级RAG

高级RAG与Naive RAG对比,如下图所示:

图片

1.1 预检索优化

预检索优化侧重于数据索引优化和查询优化。数据索引优化技术旨在提高存储数据的检索效率,例如:

  • 滑动窗口使用块之间的重叠,是最简单的技术之一。

  • 增强数据粒度应用数据清理技术,如删除无关信息、确认事实准确性、更新过时信息等。

  • 添加元数据,可以过滤如日期、目的或章节等信息。

  • 优化索引结构涉及到对数据进行索引的不同策略,例如调整块大小或使用多索引策略。我们将在本文中实现的一种技术是句子窗口检索,它使用单个嵌入句子进行检索,并在推理时用更大的文本窗口替换它们。

图片

此外,预检索技术并不局限于数据索引,还可以涵盖推理时的技术,如查询路由、查询重写和查询扩展。

1.2 检索优化

检索阶段旨在识别最相关的上下文。通常,检索是基于向量搜索的,向量搜索计算查询和索引数据之间的语义相似性。因此,大多数检索优化技术都围绕着嵌入模型[1]:

  • 微调嵌入模型可根据特定领域的上下文定制嵌入模型,尤其是对于具有不断演变或罕见术语的领域。例如,BAAI/bge-small-en是一种可以微调的高性能嵌入模型(请参阅微调指南[1])。

  • 动态嵌入会根据上下文使用的单词动态给出每个单词的向量,而静态嵌入则为每个单词使用一个向量。例如,OpenAI的embeddings—da-02是一个复杂的动态嵌入模型,可以捕捉上下文理解。

除了矢量搜索之外,还有其他检索技术,例如混合搜索,是指将矢量搜索与基于关键字的搜索相结合。如果您的检索需要精确的关键字匹配,那么混合搜索是非常合适的。

有时候需要对检索到的上下文进行额外的处理,比如检索到的内容超出上下文窗口限制或引入了噪声之类的问题。下面介绍一些检索后处理技术:

  • 提示压缩通过删除不相关的上下文并突出显示重要上下文来减少整个提示长度。

  • 重新排序使用机器学习模型来重新计算检索到的上下文的相关性得分。

图片

二、代码实现

2.1 安装所需的包

pip install llama-index

在本文中,我们将使用LlamaIndex v0.10。如果您是从较旧的LlamaIndex版本升级,则需要运行以下命令才能正确安装和运行LlamaIndex:

pip uninstall llama-index
pip install llama-index --upgrade --no-cache-dir --force-reinstall

LlamaIndex提供了一种将矢量嵌入存储在本地JSON文件中以进行持久存储的功能,这对快速构建想法的原型非常有用。然而,在生产环境中,我们将使用矢量数据库进行持久存储。

由于除了存储矢量嵌入之外,我们还需要元数据存储和混合搜索功能,因此我们将使用支持这些功能的开源矢量数据库Weaviate(v3.26.2)。

pip install weaviate-client llama-index-vector-stores-weaviate

2.2 API密钥

我们将使用Weaviate嵌入式,您可以在不注册API密钥的情况下免费使用。然而,本教程使用了来自OpenAI的嵌入模型和LLM,为此您需要一个OpenAI API密钥。要获得一个,您需要一个OpenAI帐户,然后在API密钥下“创建新密钥”。

接下来,在根目录中创建一个本地.env文件,并在其中定义API密钥:

OPENAI_API_KEY="<YOUR_OPENAI_API_KEY>"

之后,使用以下代码加载API密钥:

# !pip install python-dotenv
import os
from dotenv import load_dotenv,find_dotenv

load_dotenv(find_dotenv())

2.3 用LlamaIndex实现Naive RAG

本节讨论如何使用LlamaIndex实现一个简单的RAG管道,完整的Naive RAG管道可以参考这个Jupyter笔记本[2]。对于使用LangChain实现Naive RAG管道,可以参考[3]。

步骤1:定义嵌入模型和LLM

首先,可以在全局设置对象中定义嵌入模型和LLM。这样做意味着不必再次在代码中显式指定模型。

嵌入模型:用于为文档块和查询生成矢量嵌入。

LLM:用于根据用户查询和相关上下文生成答案。

from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from llama_index.core.settings import Settings

Settings.llm = OpenAI(model="gpt-3.5-turbo", temperature=0.1)
Settings.embed_model = OpenAIEmbedding()

步骤2:加载数据

接下来,将在根目录中创建一个名为data的本地目录,并从LlamaIndex GitHub存储库(MIT许可证)[4]下载一些示例数据。

!mkdir -p 'data'
!wget '<https://raw.githubusercontent.com/run-llama/llama_index/main/docs/examples/data/paul_graham/paul_graham_essay.txt>' -O 'data/paul_graham_essay.txt'

之后,可以加载数据以进行进一步处理:

from llama_index.core import SimpleDirectoryReader

# Load data
documents = SimpleDirectoryReader(
        input_files=["./data/paul_graham_essay.txt"]
).load_data()

步骤3:将文档分块到节点中

由于整个文档太大,无法放入LLM的上下文窗口,因此需要将其划分为较小的文本块,这些文本块在LlamaIndex中称为Nodes。使用SimpleNodeParser将加载的文档解析为块大小为1024的节点。

from llama_index.core.node_parser import SimpleNodeParser

node_parser = SimpleNodeParser.from_defaults(chunk_size=1024)

# Extract nodes from documents
nodes = node_parser.get_nodes_from_documents(documents)

步骤4:建立索引

接下来,需要构建一个索引,将所有外部知识存储在开源矢量数据库Weaviate中。

首先,需要连接到Weaviate实例。在这种情况下,我们使用WeaviateEmbedded,它允许您在没有API密钥的情况下免费在笔记本中进行实验。对于生产就绪的解决方案,建议自己部署Weaviate,例如通过Docker或使用托管服务。

import weaviate

# Connect to your Weaviate instance
client = weaviate.Client(
    embedded_options=weaviate.embedded.EmbeddedOptions(), 
)

接下来,将从Weaviate客户端构建一个VectorStoreIndex,用于存储数据并与之交互。

from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.vector_stores.weaviate import WeaviateVectorStore

index_name = "MyExternalContext"

# Construct vector store
vector_store = WeaviateVectorStore(
    weaviate_client = client, 
    index_name = index_name
)

# Set up the storage for the embeddings
storage_context = StorageContext.from_defaults(vector_store=vector_store)

# Setup the index
# build VectorStoreIndex that takes care of chunking documents
# and encoding chunks to embeddings for future retrieval
index = VectorStoreIndex(
    nodes,
    storage_context = storage_context,
)

步骤5:设置查询引擎

最后,将把索引设置为查询引擎。

# The QueryEngine class is equipped with the generator
# and facilitates the retrieval and generation steps
query_engine = index.as_query_engine()

步骤6:运行一个简单的RAG查询

现在,可以对数据运行一个简单的RAG查询,如下所示:

# Run your naive RAG query
response = query_engine.query(
    "What happened at Interleaf?"
)

2.4 用LlamaIndex实现高级RAG

接下来,我们如下高级RAG技术来提高Naive RAG管道效果:

  • 预检索优化:语句窗口检索

  • 检索优化:混合搜索

  • 检索后优化:重新排序

a)索引优化示例:语句窗口检索

对于语句窗口检索技术,需要进行两个调整:首先,必须调整数据的存储和后处理方式,使用PensioneWindowNodeParser替换SimpleNodeParser。

from llama_index.core.node_parser import SentenceWindowNodeParser

# create the sentence window node parser w/ default settings
node_parser = SentenceWindowNodeParser.from_defaults(
    window_size=3,
    window_metadata_key="window",
    original_text_metadata_key="original_text",
)

PensioneWindowNodeParser主要实现如下两个步骤:

  1. 它将文档分隔为单个句子,这些句子将被嵌入;

  2. 对于每个句子,它都会创建一个上下文窗口。如果指定window_size=3,则生成的窗口将有三个句子长,包括嵌入句子的前一个句子和后面一个句子。该窗口将作为元数据存储。

    在检索过程中,将返回与查询最匹配的句子。检索后,您需要通过定义MetadataReplacementPostProcessor并在node_postprocessors列表中使用它,将句子替换为元数据中的整个窗口。

from llama_index.core.postprocessor import MetadataReplacementPostProcessor

# The target key defaults to `window` to match the node_parser's default
postproc = MetadataReplacementPostProcessor(
    target_metadata_key="window"
)

...

query_engine = index.as_query_engine( 
    node_postprocessors = [postproc],
)

b)检索优化示例:混合搜索

如果底层矢量数据库支持混合搜索查询,那么在LlamaIndex中实现混合搜索就像对query_engine进行两个参数更改一样容易。alpha参数指定矢量搜索和基于关键字的搜索之间的权重,其中alpha=0表示基于关键字的检索,alpha=1表示纯矢量搜索。

query_engine = index.as_query_engine(
    ...,
    vector_store_query_mode="hybrid", 
    alpha=0.5,
    ...
)

c)检索后优化示例:重新排序

在高级RAG管道中添加重新排序只需三个简单步骤:

  1. 首先,定义一个重新排序模型。在这里,我们使用Huggingface中的BAAI/bge-reranker-base;

  2. 在查询引擎中,将reranker模型添加到node_postprocessors列表中;

  3. 增加查询引擎中的similarity_top_k以检索更多的上下文段落,重新排序后可以减少到top_n。

# !pip install torch sentence-transformers
from llama_index.core.postprocessor import SentenceTransformerRerank

# Define reranker model
rerank = SentenceTransformerRerank(
    top_n = 2, 
    model = "BAAI/bge-reranker-base"
)

...

# Add reranker to query engine
query_engine = index.as_query_engine(
    similarity_top_k = 6,
    ...,
                node_postprocessors = [rerank],
    ...,
)

三、总结

本文介绍了高级RAG的概念,其中包括一组技术来解决原始RAG范式的局限性。在概述了高级RAG技术(可分为预检索、检索和后检索技术)之后,本文使用LlamaIndex进行编排,实现了一个简单而高级的RAG管道。

RAG管道组件是OpenAI的语言模型、托管在Hugging Face上的BAAI的重新排序模型和Weaviate矢量数据库。

我们在Python中使用LlamaIndex实现了以下技术选择:

  • 检索前优化:语句窗口检索

  • 检索优化:混合搜索

  • 检索后优化:重新排序

您可以在此处找到包含完整端到端管道的Jupyter笔记本:

LlamaIndex中的Naive RAG[2]

LlamaIndex中的高级RAG[5]

技术交流

在这里插入图片描述

通俗易懂讲解大模型系列

参考文献:

[1] https://betterprogramming.pub/fine-tuning-your-embedding-model-to-maximize-relevance-retrieval-in-rag-pipeline-2ea3fa231149

[2] https://github.com/weaviate/recipes/blob/main/integrations/llamaindex/retrieval-augmented-generation/naive_rag.ipynb

[3] https://medium.com/towards-data-science/retrieval-augmented-generation-rag-from-theory-to-langchain-implementation-4e9bd5f6a4f2

[4] https://github.com/run-llama/llama_index

[5] https://github.com/weaviate/recipes/blob/main/integrations/llamaindex/retrieval-augmented-generation/advanced_rag.ipynb

;