LLM工程师手册——RAG 推理管道

回顾第4章,我们实现了检索增强生成(RAG)特征管道,用于填充向量数据库(DB)。在特征管道中,我们从数据仓库中收集数据,进行清理、分块和嵌入文档,最终将它们加载到向量数据库中。因此,到目前为止,向量数据库已经充满了文档,并且准备好用于RAG。

基于RAG方法论,您可以将软件架构拆分为三个模块:一个用于检索,一个用于增强提示,另一个用于生成答案。我们将遵循类似的模式,首先实现一个检索模块来查询向量数据库。在该模块中,我们将实现先进的RAG技术来优化搜索。之后,我们不会为增强提示专门创建一个模块,因为那样做会过度工程化,这也是我们要避免的。然而,我们会编写一个推理服务,该服务输入用户查询和上下文,构建提示并调用LLM生成答案。总之,我们将实现两个核心Python模块,一个用于检索,另一个用于根据用户输入和上下文调用LLM。当我们将这两个模块结合起来时,我们就会拥有一个端到端的RAG流。

在第5章和第6章中,我们对LLM Twin模型进行了微调,在第8章中,我们学习了如何优化其推理。因此,到目前为止,LLM已经准备好投入生产。接下来需要做的就是构建和部署上述两个模块。

我们将在下一章中完全专注于将微调后的LLM Twin模型部署到AWS SageMaker,作为AWS SageMaker推理端点。因此,本章的重点是深入探讨先进的RAG检索模块的实现。我们将整个章节专门用于检索步骤,因为这是RAG系统中"魔法发生"的地方。在检索步骤(而不是调用LLM时),您编写大部分RAG推理代码。此步骤是您需要处理数据的地方,以确保从向量数据库中检索到最相关的数据点。因此,先进的RAG逻辑大部分都包含在检索步骤中。

总的来说,本章将涵盖以下内容:

  • 理解LLM Twin的RAG推理管道
  • 探索LLM Twin的高级RAG技术
  • 实现LLM Twin的RAG推理管道

到本章结束时,您将知道如何实现一个高级的RAG检索模块,使用检索到的上下文增强提示,并调用LLM生成最终答案。最终,您将了解如何构建一个生产就绪的端到端RAG推理管道。

理解LLM Twin的RAG推理管道

在实现RAG推理管道之前,我们首先讨论其软件架构和高级RAG技术。图9.1展示了RAG推理流程的概述。推理管道从输入查询开始,使用检索模块(基于查询)检索上下文,并调用LLM SageMaker服务生成最终答案。

在实现RAG推理管道之前,我们首先讨论其软件架构和高级RAG技术。图9.1展示了RAG推理流程的概述。推理管道从输入查询开始,使用检索模块(基于查询)检索上下文,并调用LLM SageMaker服务生成最终答案。

特征管道和检索模块,如图9.1所示,是独立的处理过程。特征管道在不同的机器上按计划运行,用于填充向量数据库(DB)。同时,检索模块在推理管道中按需调用,每次用户请求时都会触发。

通过将这两个组件的职责分离,向量数据库始终填充最新的数据,确保特征的新鲜度,而检索模块则可以在每次请求时访问最新的特征。RAG检索模块的输入是用户的查询,基于该查询,我们需要从向量数据库中返回最相关和相似的数据点,这些数据点将用于引导LLM生成最终答案。

为了全面理解RAG推理管道的动态,让我们逐步回顾图9.1中的架构流程:

  1. 用户查询:我们从用户发起查询开始,例如"写一篇关于...的文章"。
  2. 查询扩展:我们扩展初始查询,以生成反映原始用户查询不同方面或解释的多个查询。因此,除了一个查询,我们将使用xN个查询。通过多样化搜索词,检索模块增加了捕获相关数据点的全面性概率。此步骤对于原始查询过于狭窄或模糊时尤其重要。
  3. 自查询:我们从原始查询中提取有用的元数据,如作者的名字。提取的元数据将用作向量搜索操作的过滤器,消除查询向量空间中的冗余数据点(使搜索更准确、更快速)。
  4. 过滤向量搜索:我们对每个查询进行嵌入并执行相似度搜索,以找到每个查询的前K个数据点。我们执行xN次搜索,对应扩展查询的数量。我们将此步骤称为过滤向量搜索,因为我们利用从自查询步骤中提取的元数据作为查询过滤器。
  5. 收集结果:我们为每个搜索操作获取最多xK个与特定扩展查询解释最接近的结果。此外,我们将所有xN次搜索的结果聚合,最终得到一个包含N x K结果的列表,其中包含文章、帖子和存储库块的混合。这些结果包括更广泛的潜在相关块,基于原始查询的不同方面提供多个相关角度。
  6. 重新排序:为了从N x K潜在项目列表中保留最相关的前K个结果,我们必须进一步筛选该列表。我们将使用重新排序算法,根据与初始用户查询的相关性和重要性为每个数据块打分。我们将利用神经交叉编码器模型来计算分数,该分数的值介于0和1之间,其中1表示结果与查询完全相关。最终,我们根据分数对N x K结果进行排序,挑选出前K个项目。因此,输出是一个排名列表,前K个数据点是最相关的。
  7. 构建提示并调用LLM:我们将最相关的K个数据块的最终列表映射到一个字符串,用于构建最终的提示。我们使用提示模板、检索到的上下文和用户的查询来创建提示。最终,增强的提示被发送到LLM(托管在AWS SageMaker上,作为API端点暴露)。
  8. 答案:我们等待生成的答案。当LLM处理完提示后,RAG逻辑通过将生成的响应发送给用户来完成。

以上概述了RAG推理管道的流程。接下来,我们将深入探讨细节。

探索LLM Twin的高级RAG技术

现在我们已经理解了RAG推理管道的整体流程,接下来让我们探讨在检索模块中使用的高级RAG技术:

  • 预检索步骤:查询扩展和自查询
  • 检索步骤:过滤向量搜索
  • 后检索步骤:重新排序

在深入了解每个方法之前,我们先来看看我们将在本节中使用的Python接口,这些接口可在此处找到。

首先是一个提示模板工厂,用于标准化我们如何实例化提示模板。作为接口,它继承自ABC,并暴露了create_template()方法,该方法返回一个LangChain的PromptTemplate实例。即使我们尽量避免过度依赖LangChain,因为我们希望自己实现所有功能,以了解背后的工程实现,但一些对象(如PromptTemplate类)对于加快开发进度是非常有帮助的,同时不会隐藏太多功能:

python 复制代码
from abc import ABC, abstractmethod
from langchain.prompts import PromptTemplate
from pydantic import BaseModel

class PromptTemplateFactory(ABC, BaseModel):
    @abstractmethod
    def create_template(self) -> PromptTemplate:
        pass

我们还希望定义一个RAGStep接口,用于标准化高级RAG步骤(如查询扩展和自查询)的接口。由于这些步骤通常依赖于其他LLM,因此它有一个mock属性,在开发过程中可以减少成本和调试时间:

python 复制代码
from typing import Any
from llm_engineering.domain.queries import Query

class RAGStep(ABC):
    def __init__(self, mock: bool = False) -> None:
        self._mock = mock

    @abstractmethod
    def generate(self, query: Query, *args, **kwargs) -> Any:
        pass

最后,我们需要了解如何建模Query领域实体,以便将用户的输入与高级RAG所需的其他元数据结合起来。因此,让我们看一下它的实现。首先,我们导入必要的类:

javascript 复制代码
from pydantic import UUID4, Field
from llm_engineering.domain.base import VectorBaseDocument
from llm_engineering.domain.types import DataCategory

接着,我们定义Query实体类,它继承自VectorBaseDocument对象向量映射(OVM)类,该类在第4章中进行了讨论。因此,每个查询都可以轻松地从向量数据库中保存或检索:

python 复制代码
class Query(VectorBaseDocument):
    content: str
    author_id: UUID4 | None = None
    author_full_name: str | None = None
    metadata: dict = Field(default_factory=dict)

class Config:
    category = DataCategory.QUERIES

需要注意的是类的属性,这些属性用于将用户的查询与一组元数据字段结合起来:

  • content:包含输入查询的字符串。
  • author_id:一个可选的UUID4标识符,从查询中提取,用作向量搜索操作中的过滤器,只检索由特定作者写的片段。
  • author_full_name :一个可选的字符串,用于查询author_id
  • metadata:一个字典,用于存储任何附加的元数据,默认初始化为空字典。

除了领域类的标准定义外,我们还定义了一个from_str()类方法,用于直接从字符串创建Query实例。这使我们能够标准化在构建查询对象之前如何清理查询字符串,例如去除首尾空白和换行符:

less 复制代码
@classmethod
def from_str(cls, query: str) -> "Query":
    return Query(content=query.strip("\n "))

此外,还有一个实例方法replace_content(),用于创建一个具有更新内容的新Query实例,同时保留原始查询的id、author_id、author_full_name和metadata:

python 复制代码
def replace_content(self, new_content: str) -> "Query":
    return Query(
        id=self.id,
        content=new_content,
        author_id=self.author_id,
        author_full_name=self.author_full_name,
        metadata=self.metadata,
    )

这在修改查询文本时尤其有用,例如在预处理或归一化过程中,而不会丢失相关的元数据或标识符。在定义了Query类之后,我们定义了EmbeddedQuery类:

kotlin 复制代码
class EmbeddedQuery(Query):
    embedding: list[float]
    
    class Config:
        category = DataCategory.QUERIES

EmbeddedQuery类通过添加embedding字段扩展了QueryEmbeddedQuery实体封装了执行向量搜索操作所需的所有数据和元数据,适用于Qdrant(或其他向量数据库)。

高级RAG预检索优化:查询扩展与自查询

我们实现了两种方法来优化预检索步骤:查询扩展和自查询。这两种方法与过滤向量搜索步骤密切配合,后者将在下一节中讨论。不过,现在我们将首先了解查询扩展的代码,并继续实现自查询。

在这两种方法中,我们将利用OpenAI的API来生成基于原始查询的多个变体,以便在查询扩展步骤中使用,并在自查询算法中提取必要的元数据。当我们写这本书时,我们在所有示例中使用了GPT-4o-mini,但随着OpenAI的模型快速更新,模型可能会被弃用。但这不是问题,因为您可以通过配置OPENAI_MODEL_ID环境变量,在.env文件中快速更改模型。

查询扩展

典型的检索步骤中的问题是,你使用原始问题的单一向量表示来查询向量数据库。此方法只覆盖了嵌入空间的一个小区域,这可能会导致限制。如果嵌入空间没有包含查询的所有必要信息或细微差别,检索到的上下文可能不相关。这意味着一些语义相关但不靠近查询向量的关键文档可能会被忽略。

解决方案基于查询扩展,它提供了一种克服这一限制的方法。通过使用LLM基于初始问题生成多个查询,你可以创建多种视角,捕捉查询的不同方面。这些扩展查询在嵌入后,针对嵌入空间中的其他相关区域,这增加了从向量数据库中检索到更多相关文档的可能性。

实现查询扩展可以像编写详细的零样本提示来引导LLM生成这些替代查询一样简单。因此,在实现查询扩展后,你将不再只有一个查询来搜索相关上下文,而是拥有xN个查询,从而执行xN次搜索。

增加搜索的数量可能会影响延迟,因此你必须实验生成的查询数量,以确保检索步骤满足应用要求。你还可以通过并行化搜索来优化它们,大大减少延迟,这将在本章末尾实现的ContextRetriever类中完成。

查询扩展也被称为多查询,但原理是相同的。例如,LangChain实现中的MultiQueryRetriever就是一个例子:python.langchain.com/docs/how_to...

接下来,让我们深入研究代码。我们首先导入查询扩展所需的模块和类:

javascript 复制代码
from langchain_openai import ChatOpenAI
from llm_engineering.domain.queries import Query
from llm_engineering.settings import settings
from .base import RAGStep
from .prompt_templates import QueryExpansionTemplate

接下来,我们定义QueryExpansion类,用于生成扩展的查询版本。类的实现可以在此处找到:

python 复制代码
class QueryExpansion(RAGStep):
    def generate(self, query: Query, expand_to_n: int) -> list[Query]:
        assert expand_to_n > 0, f"'expand_to_n' should be greater than 0. Got {expand_to_n}."
        if self._mock:
            return [query for _ in range(expand_to_n)]

generate方法中,我们首先确保请求的扩展次数(expand_to_n)大于零。如果实例处于模拟模式(self._mock为True),它仅返回一个包含原始查询副本的列表,以模拟扩展而不实际调用API。如果不处于模拟模式,我们继续创建提示并初始化语言模型:

ini 复制代码
        query_expansion_template = QueryExpansionTemplate()
        prompt = query_expansion_template.create_template(expand_to_n - 1)
        model = ChatOpenAI(model=settings.OPENAI_MODEL_ID, api_key=settings.OPENAI_API_KEY, temperature=0)

在这里,我们实例化QueryExpansionTemplate并创建一个提示,以生成expand_to_n - 1个新的查询(不包括原始查询)。我们使用指定的设置初始化ChatOpenAI模型,并将temperature设置为0,以确保输出是确定性的。接着,我们通过将提示与模型组合来创建一个LangChain链,并通过用户的查询来调用它:

ini 复制代码
chain = prompt | model
response = chain.invoke({"question": query})
result = response.content

通过将提示传递给模型(prompt | model),我们设置了一个链,在使用原始查询调用时生成扩展查询。模型的响应将被捕获在result对象中。收到响应后,我们解析并清理扩展的查询:

ini 复制代码
queries_content = result.strip().split(query_expansion_template.separator)
queries = [query]
queries += [
    query.replace_content(stripped_content)
    for content in queries_content
    if (stripped_content := content.strip())
]
return queries

我们使用模板中定义的分隔符将结果拆分,以获取单个查询。首先创建一个包含原始查询的列表,然后在去除多余空白后将每个扩展查询添加到列表中。

最后,我们定义QueryExpansionTemplate类,用于构建用于查询扩展的提示。该类和其他提示模板可以在此处访问:

python 复制代码
from langchain.prompts import PromptTemplate
from .base import PromptTemplateFactory

class QueryExpansionTemplate(PromptTemplateFactory):
    prompt: str = """You are an AI language model assistant. Your task is to generate {expand_to_n}
    different versions of the given user question to retrieve relevant documents from a vector
    database. By generating multiple perspectives on the user question, your goal is to help
    the user overcome some of the limitations of the distance-based similarity search.
    Provide these alternative questions separated by '{separator}'.
    Original question: {question}"""

    @property
    def separator(self) -> str:
        return "#next-question#"

    def create_template(self, expand_to_n: int) -> PromptTemplate:
        return PromptTemplate(
            template=self.prompt,
            input_variables=["question"],
            partial_variables={
                "separator": self.separator,
                "expand_to_n": expand_to_n,
            },
        )

这个类定义了一个提示,指示语言模型生成用户问题的多个版本。它使用了占位符,例如{expand_to_n}{separator}{question},来定制提示。

它将expand_to_n作为输入参数,用于定义我们希望生成多少个查询,同时在构建PromptTemplate实例时传递。separator属性提供了一个唯一的字符串,用于分割生成的查询。expand_to_nseparator变量作为partial_variables传递,这使得它们在运行时不可变。同时,每次调用LLM链时,{question}占位符将被替换。

现在,我们已经完成了查询扩展实现的学习,接下来让我们看看如何使用QueryExpansion类的示例。我们可以使用以下代码来运行这个python -m llm_engineering.application.rag.query_expansion命令:

ini 复制代码
query = Query.from_str("Write an article about the best types of advanced RAG methods.")
query_expander = QueryExpansion()
expanded_queries = query_expander.generate(query, expand_to_n=3)
for expanded_query in expanded_queries:
    logger.info(expanded_query.content)

我们得到了原始查询的以下变体。正如您所观察到的,查询扩展方法成功地提供了更多细节和不同的视角,例如突出了先进RAG方法的有效性或这些方法的概述(记住,第一个查询是原始查询):

yaml 复制代码
2024-09-18 17:51:33.529 | INFO  - Write an article about the best types of advanced RAG methods.
2024-09-18 17:51:33.529 | INFO  - What are the most effective advanced RAG methods, and how can they be applied?
2024-09-18 17:51:33.529 | INFO  - Can you provide an overview of the top advanced retrieval-augmented generation techniques?

现在,让我们进入下一个预检索优化方法:自查询。

自查询

当将查询嵌入到向量空间中时,问题在于无法确保查询所需的所有方面都以足够的信号存在于嵌入向量中。例如,您可能希望100%确保检索依赖于用户输入中提供的标签。遗憾的是,您无法控制嵌入中强调标签的信号。仅嵌入查询提示时,您无法确保标签在嵌入向量中得到了足够的表示,或者在计算与其他向量的距离时标签的信号足够强。

这个问题适用于任何其他希望在搜索中展示的元数据,例如ID、名称或类别。

解决方案是使用自查询提取查询中的标签或其他关键信息,并将它们与向量搜索一起作为过滤器。自查询使用LLM来提取对您的业务用例至关重要的各种元数据字段,例如标签、ID、评论数、点赞数、分享数等。然后,您可以完全控制在检索过程中如何考虑提取的元数据。在我们的LLM Twin用例中,我们提取了作者的名字,并将其作为过滤器。自查询与过滤向量搜索协同工作,我们将在下一节中解释这一点。

现在,让我们进入代码部分。我们首先导入我们的代码所依赖的必要模块和类:

javascript 复制代码
from langchain_openai import ChatOpenAI
from llm_engineering.application import utils
from llm_engineering.domain.documents import UserDocument
from llm_engineering.domain.queries import Query
from llm_engineering.settings import settings
from .base import RAGStep
from .prompt_templates import SelfQueryTemplate

接下来,我们定义SelfQuery类,该类继承自RAGStep并实现了generate()方法。该类的实现可以在此处找到:

ruby 复制代码
class SelfQuery(RAGStep):
    def generate(self, query: Query) -> Query:
        if self._mock:
            return query

generate()方法中,我们首先检查_mock属性是否设置为True。如果是,我们将返回原始的查询对象不做修改。这允许我们在测试和调试时绕过调用模型。如果不在模拟模式下,我们将创建提示模板并初始化语言模型:

scss 复制代码
        prompt = SelfQueryTemplate().create_template()
        model = ChatOpenAI(model=settings.OPENAI_MODEL_ID, api_key=settings.OPENAI_API_KEY, temperature=0)

在这里,我们使用SelfQueryTemplate工厂类实例化提示,并创建一个ChatOpenAI模型实例(类似于查询扩展实现)。然后,我们将提示与模型结合成链,并使用用户的查询来调用它:

ini 复制代码
        chain = prompt | model
        response = chain.invoke({"question": query})
        user_full_name = response.content.strip("\n ")

我们从LLM响应中提取内容,并去除任何前导或尾随的空格,以获得user_full_name值。接着,我们检查模型是否能够提取到用户信息。

ini 复制代码
        if user_full_name == "none":
            return query

如果响应为"none",则表示在查询中未找到用户名称,因此我们返回原始查询对象。如果找到了用户名,我们将使用一个工具函数将user_full_name拆分成first_namelast_name变量。然后,根据用户的详细信息,我们检索或创建一个UserDocument用户实例:

ini 复制代码
        first_name, last_name = utils.split_user_full_name(user_full_name)
        user = UserDocument.get_or_create(first_name=first_name, last_name=last_name)

最后,我们使用提取的作者信息更新查询对象并返回它:

ini 复制代码
        query.author_id = user.id
        query.author_full_name = user.full_name
        return query

更新后的查询现在包含了author_idauthor_full_name值,可以在RAG管道的后续步骤中使用。

让我们来看一下SelfQueryTemplate类,它定义了提取用户信息的提示:

python 复制代码
from langchain.prompts import PromptTemplate
from .base import PromptTemplateFactory

class SelfQueryTemplate(PromptTemplateFactory):
    prompt: str = """You are an AI language model assistant. Your task is to extract information from a user question.
    The required information that needs to be extracted is the user name or user id.
    Your response should consist of only the extracted user name (e.g., John Doe) or id (e.g. 1345256), nothing else.
    If the user question does not contain any user name or id, you should return the following token: none.
   
    For example:
    QUESTION 1:
    My name is Paul Iusztin and I want a post about...
    RESPONSE 1:
    Paul Iusztin
   
    QUESTION 2:
    I want to write a post about...
    RESPONSE 2:
    none
   
    QUESTION 3:
    My user id is 1345256 and I want to write a post about...
    RESPONSE 3:
    1345256
   
    User question: {question}"""
    
    def create_template(self) -> PromptTemplate:
        return PromptTemplate(template=self.prompt, input_variables=["question"])

SelfQueryTemplate类中,我们定义了一个提示,指导AI模型从输入的问题中提取用户的名字或ID。该提示通过少量示例学习(few-shot learning)来指导模型如何在不同场景中响应。当模板被调用时,{question}占位符将被实际的用户问题替换。

通过实现自查询,我们确保了在检索过程中,所有关键的元数据(如用户的名字或ID)都会被明确提取并使用。这种方法克服了仅依赖于嵌入语义来捕获查询所有必要方面的局限性。

现在我们已经实现了SelfQuery类,下面提供一个示例。使用python -m llm_engineering.application.rag.self_query命令运行以下代码:

ini 复制代码
query = Query.from_str("I am Paul Iusztin. Write an article about the best types of advanced RAG methods.")
self_query = SelfQuery()
query = self_query.generate(query)
logger.info(f"Extracted author_id: {query.author_id}")
logger.info(f"Extracted author_full_name: {query.author_full_name}")

我们得到以下结果,其中作者的全名和ID被正确提取:

yaml 复制代码
2024-09-18 18:02:10.362 | INFO - Extracted author_id: 900fec95-d621-4315-84c6-52e5229e0b96
2024-09-18 18:02:10.362 | INFO - Extracted author_full_name: Paul Iusztin

现在我们了解了自查询是如何工作的,接下来我们将探讨它如何与过滤向量搜索一起,在检索优化步骤中使用。

高级RAG检索优化:过滤向量搜索

向量搜索在基于语义相似度检索相关信息方面起着至关重要的作用。然而,简单的向量搜索会引发一些显著的挑战,这些挑战会影响信息检索的准确性和延迟。这主要是因为它仅基于向量嵌入的数值接近度进行操作,而没有考虑可能对相关性至关重要的上下文或类别细节。

简单向量搜索的主要问题之一是检索到语义相似但上下文无关的文档。由于向量嵌入捕获的是一般性的语义含义,它们可能会对共享语言模式或主题的内容赋予较高的相似度分数,但这些内容可能与查询的具体意图或约束并不对齐。例如,搜索"Java"可能会检索到关于编程语言或印度尼西亚岛屿的文档,单纯依赖语义相似度会导致模糊或误导的结果。

此外,随着数据集的规模增大,简单的向量搜索可能会遇到可扩展性问题。由于缺乏过滤机制,搜索算法必须计算整个向量空间中的相似度,这会显著增加延迟。

这种全量搜索会减慢响应时间,并消耗更多计算资源,使其在实时或大规模应用中效率低下。

过滤向量搜索作为一种解决方案,通过在计算向量相似度之前根据附加标准(如元数据标签或类别)进行过滤,从而减少搜索空间。通过应用这些过滤器,搜索算法将潜在的结果范围缩小到与查询意图上下文相关的内容。这种有针对性的方法通过消除那些由于语义相似性可能被认为相关的无关文档,从而提高了准确性。

此外,过滤向量搜索通过减少算法需要执行的比较次数来提高延迟。处理一个更小、更相关的数据子集可以减少计算开销,从而加快响应时间。这种效率对于需要实时交互或处理大查询的应用至关重要。

由于在过滤向量搜索中使用的元数据通常是用户输入的一部分,我们必须在查询向量数据库之前提取它。这正是我们在自查询步骤中所做的,在该步骤中,我们提取了作者的名字,并仅将向量空间缩小到该作者的内容。因此,在自查询步骤中处理查询时,它属于预检索优化类别,而当过滤向量搜索优化查询时,它属于检索优化范畴。

例如,在使用Qdrant时,要在每个文档的元数据中添加一个查找匹配的author_id的过滤器,你必须实现以下代码:

ini 复制代码
from qdrant_client.models import FieldCondition, Filter, MatchValue

records = qdrant_connection.search(
    collection_name="articles",
    query_vector=query_embedding,
    limit=3,
    with_payload=True,
    query_filter=Filter(
        must=[
            FieldCondition(
                key="author_id",
                match=MatchValue(
                    value=str("1234"),
                ),
            )
        ]
    ),
)

本质上,虽然简单的向量搜索为语义检索提供了基础,但它的局限性会在实际应用中降低性能。过滤向量搜索通过结合向量嵌入与上下文过滤的优势,解决了这些挑战,从而在RAG系统中实现了更精确、高效的信息检索。优化我们RAG管道的最后一步是研究重新排序。

高级RAG后检索优化:重新排序

在RAG系统中,问题在于检索到的上下文可能包含无关的片段,这些片段仅会:

  1. 增加噪音:检索到的上下文可能无关紧要,杂乱的信息可能会干扰语言模型的理解。
  2. 使提示变大:包括不必要的片段会增加提示的大小,从而导致更高的计算成本。此外,语言模型通常对上下文的开头和结尾部分偏向。如果添加了大量上下文,有很大可能错过重要信息。
  3. 与问题不对齐:片段是基于查询与片段嵌入之间的相似性进行检索的。问题在于,嵌入模型可能没有针对你的问题进行调优,导致与问题不完全相关的片段也可能得到较高的相似度分数。

解决方法是使用重新排序(reranking),根据每个片段与初始问题的相关性对所有N × K个检索到的片段进行排序,其中第一个片段是最相关的,最后一个是最不相关的。N表示查询扩展后的搜索次数,而K是每次搜索时检索的片段数。因此,我们总共检索了N × K个片段。在RAG系统中,重新排序是一个关键的后检索步骤,用于优化从检索模型中得到的初始结果。

我们通过应用重新排序算法来评估每个片段与原始查询的相关性,这通常使用像神经交叉编码器(neural cross-encoders)这样的高级模型。这些模型比基于嵌入和余弦相似度距离的初步检索方法能够更准确地评估查询和每个片段之间的语义相似性,具体细节可以参考第4章中的"高级RAG概述"部分。

最终,我们从排序后的N × K个项目中选出最相关的前K个片段。重新排序在与查询扩展结合时表现良好。首先,让我们了解在没有查询扩展的情况下,重新排序是如何工作的:

  1. 检索 > K 个片段:检索多个片段以获得更广泛的潜在相关信息。
  2. 使用重新排序进行重新排序:对这个更大的片段集应用重新排序,评估每个片段与查询的实际相关性。
  3. 取前K个片段:选择最相关的前K个片段,用于最终提示。

因此,当与查询扩展结合使用时,我们从空间的多个点收集潜在有价值的上下文,而不是仅仅在一个位置寻找超过K个样本。现在流程是这样的:

  1. 检索 N × K 个片段:使用扩展查询检索多个片段集。
  2. 使用重新排序进行重新排序:根据相关性重新排序所有检索到的片段。
  3. 取前K个片段:选择最相关的片段作为最终提示的上下文。

将重新排序集成到RAG管道中,可以提高检索上下文的质量和相关性,并有效利用计算资源。让我们看看如何实现LLM Twin的重新排序步骤,以帮助我们理解上述内容,可以通过以下GitHub链接访问该实现:github.com/PacktPublis...

我们首先导入进行重新排序所需的模块和类:

javascript 复制代码
from llm_engineering.application.networks import CrossEncoderModelSingleton
from llm_engineering.domain.embedded_chunks import EmbeddedChunk
from llm_engineering.domain.queries import Query
from .base import RAGStep

接下来,我们定义Reranker类,该类负责根据片段与查询的相关性对检索到的文档进行重新排序:

python 复制代码
class Reranker(RAGStep):
    def __init__(self, mock: bool = False) -> None:
        super().__init__(mock=mock)
        self._model = CrossEncoderModelSingleton()

Reranker类的初始化方法中,我们通过创建CrossEncoderModelSingleton实例来实例化我们的交叉编码器模型。这个交叉编码器模型用于根据每个文档片段与查询的相关性为其打分。

Reranker 类的核心功能在 generate() 方法中实现:

ini 复制代码
def generate(self, query: Query, chunks: list[EmbeddedChunk], keep_top_k: int) -> list[EmbeddedChunk]:
    if self._mock:
        return chunks
    query_doc_tuples = [(query.content, chunk.content) for chunk in chunks]
    scores = self._model(query_doc_tuples)
    scored_query_doc_tuples = list(zip(scores, chunks, strict=False))
    scored_query_doc_tuples.sort(key=lambda x: x[0], reverse=True)
    reranked_documents = scored_query_doc_tuples[:keep_top_k]
    reranked_documents = [doc for _, doc in reranked_documents]
    return reranked_documents

generate() 方法接收一个查询、一个文档块列表和要保留的前 k 个文档数(keep_top_k)。如果处于模拟模式下,它会直接返回原始的文档块。否则,它执行以下步骤:

  1. 创建查询内容和每个文档块内容的配对
  2. 使用交叉编码器模型为每个配对打分,评估文档块与查询的匹配程度
  3. 将得分与对应的文档块配对,创建得分的元组列表
  4. 根据得分对这个列表进行降序排序
  5. 选择排名前 keep_top_k 的文档块
  6. 从元组中提取文档块并将其作为重新排名的文档返回

在定义 CrossEncoder 类之前,我们导入必要的组件:

javascript 复制代码
from sentence_transformers.cross_encoder import CrossEncoder
from .base import SingletonMeta

我们从 sentence_transformers 库导入 CrossEncoder 类,该类提供了为文本对打分的功能。同时,我们从基础模块中导入 SingletonMeta,以确保我们的模型类遵循单例模式,即整个应用程序中只有一个模型实例。

接下来,我们定义 CrossEncoderModelSingleton 类:

python 复制代码
class CrossEncoderModelSingleton(metaclass=SingletonMeta):
    def __init__(
        self,
        model_id: str = settings.RERANKING_CROSS_ENCODER_MODEL_ID,
        device: str = settings.RAG_MODEL_DEVICE,
    ) -> None:
        """
        提供一个用于为输入文本对打分的预训练交叉编码器模型的单例类。
        """
        self._model_id = model_id
        self._device = device
        self._model = CrossEncoder(
            model_name=self._model_id,
            device=self._device,
        )
        self._model.model.eval()

该类使用从 .env 文件加载的全局设置中的 model_iddevice 初始化交叉编码器模型。我们使用 self._model.model.eval() 将模型设置为评估模式,以确保模型准备好进行推理。

CrossEncoderModelSingleton 类包含一个可调用方法,用于为文本对打分:

python 复制代码
def __call__(self, pairs: list[tuple[str, str]], to_list: bool = True) -> NDArray[np.float32] | list[float]:
    scores = self._model.predict(pairs)
    if to_list:
        scores = scores.tolist()
    return scores

__call__ 方法允许我们传入一个文本对列表(每个对由查询和文档块组成),并返回它们的相关性得分。该方法使用模型的 predict() 函数调用模型并计算得分。

CrossEncoderModelSingleton 类是对 CrossEncoder 类的封装,我们编写它有两个目的。第一个目的是为了实现单例模式,这样我们可以在应用程序的任何地方轻松访问同一个交叉编码器模型实例,而不需要每次使用时都重新加载模型到内存中。第二个目的是通过编写我们的封装器,我们定义了交叉编码器模型(或任何用于重新排序的其他模型)的接口。这使得代码具备了未来适应性,因为如果我们需要不同的重新排序实现或策略,例如使用 API,我们只需要编写一个遵循相同接口的新封装器,并将旧类替换为新类。这样,我们可以引入新的重新排序方法,而不需要修改其他代码部分。

现在,我们已经了解了架构中使用的所有高级 RAG 技术。在下一部分,我们将研究 ContextRetriever 类,它将所有这些方法连接起来,并解释如何使用检索模块与 LLM(大语言模型)结合,构建一个端到端的 RAG 推理管道。

实现 LLM Twin 的 RAG 推理管道

正如本章开头所解释的,RAG 推理管道主要可以分为三部分:检索模块、提示词创建和答案生成,这归结为使用增强的提示词调用 LLM。在这一部分,我们的主要关注点将是实现检索模块,因为大部分的代码和逻辑都集中在这里。之后,我们将讨论如何使用检索到的上下文和用户查询来构建最终的提示词。

最终,我们将探讨如何将检索模块、提示词创建逻辑和 LLM 结合起来,形成一个端到端的 RAG 工作流。不幸的是,在完成第10章之前,我们无法测试 LLM,因为我们尚未将微调后的 LLM Twin 模块部署到 AWS SageMaker。

因此,在本节结束时,您将学会如何实现 RAG 推理管道,虽然您只能在完成第10章后进行端到端的测试。现在,让我们从检索模块的实现开始。

实现检索模块

让我们深入了解 ContextRetriever 类的实现,它通过整合我们之前使用的所有高级技术(如查询扩展、自查询、重新排序和过滤向量搜索)来协调 RAG 系统中的检索步骤。该类可以在 GitHub 上找到,地址为:github.com/PacktPublis...

ContextRetriever 类的入口函数是 search() 方法,它调用了本章讨论的所有高级步骤。图 9.2 更详细地展示了 search 方法如何将所有步骤结合起来,以搜索与用户查询相似的结果。它突出了从自查询步骤中提取的作者详情如何在过滤向量搜索中使用。同时,它还深入探讨了搜索操作本身:对于每个查询,我们会向向量数据库发起三次搜索,查找与查询相似的文章、帖子或代码库。对于每次搜索(在 N 次搜索中),我们希望检索最多 K 个结果。因此,我们对每个数据类别(因为我们有三个类别)检索最多 K / 3 个项目。因此,总的来说,我们将得到一个 ≤ K 的文档块列表。当某个特定数据类别或更多类别在应用作者过滤后返回的项目少于 K / 3 时(由于该特定作者或数据类别缺少文档块),检索到的列表将小于等于 K(而不是等于 K)。

图 9.3 展示了我们如何处理通过 xN 次搜索返回的结果。由于每次搜索返回的项目数 ≤ K,我们最终将得到 ≤ N x K 个文档块,这些文档块会被汇总成一个列表。由于某些结果可能在不同的搜索之间有重叠,因此我们必须对汇总的列表进行去重,以确保每个文档块都是唯一的。最终,我们将结果发送到重新排序模型,根据重新排序的得分对其进行排序,并选择最相关的前 K 个文档块,这些文档块将作为 RAG 的上下文使用。

让我们理解如何在 ContextRetriever 类中实现图 9.2 和 9.3 中的所有步骤。首先,我们通过设置 QueryExpansionSelfQueryReranker 类的实例来初始化该类:

ini 复制代码
class ContextRetriever:
    def __init__(self, mock: bool = False) -> None:
        self._query_expander = QueryExpansion(mock=mock)
        self._metadata_extractor = SelfQuery(mock=mock)
        self._reranker = Reranker(mock=mock)

search() 方法中,我们将用户的输入字符串转换为查询对象。然后,我们使用 SelfQuery 实例从查询中提取 author_idauthor_full_name

ini 复制代码
def search(
    self,
    query: str,
    k: int = 3,
    expand_to_n_queries: int = 3,
) -> list:
    query_model = Query.from_str(query)
    query_model = self._metadata_extractor.generate(query_model)
    logger.info(
        "Successfully extracted the author_id from the query.",
        author_id=query_model.author_id,
    )

接下来,我们使用 QueryExpansion 实例扩展查询,生成多个语义相似的查询:

ini 复制代码
    n_generated_queries = self._query_expander.generate(query_model, expand_to_n=expand_to_n_queries)
    logger.info(
        "Successfully generated queries for search.",
        num_queries=len(n_generated_queries),
    )

然后,我们使用线程池并发地对所有扩展后的查询进行搜索。每个查询都由 _search() 方法处理,我们稍后会深入探讨。搜索结果被平坦化、去重,并汇总成一个单一的列表:

ini 复制代码
    with concurrent.futures.ThreadPoolExecutor() as executor:
        search_tasks = [executor.submit(self._search, _query_model, k) for _query_model in n_generated_queries]
        n_k_documents = [task.result() for task in concurrent.futures.as_completed(search_tasks)]
        n_k_documents = utils.misc.flatten(n_k_documents)
        n_k_documents = list(set(n_k_documents))
    logger.info("All documents retrieved successfully.", num_documents=len(n_k_documents))

检索到文档后,我们根据它们与原始查询的相关性对它们进行重新排序,只保留前 K 个文档:

arduino 复制代码
    if len(n_k_documents) > 0:
        k_documents = self.rerank(query, chunks=n_k_documents, keep_top_k=k)
    else:
        k_documents = []
    return k_documents

_search() 方法执行跨不同数据类别(如帖子、文章和代码库)的过滤向量搜索。它使用 EmbeddingDispatcher 将查询转换为 EmbeddedQuery,该对象包括查询的嵌入向量和任何提取的元数据:

ini 复制代码
def _search(self, query: Query, k: int = 3) -> list[EmbeddedChunk]:
    assert k >= 3, "k should be >= 3"
    def _search_data_category(
        data_category_odm: type[EmbeddedChunk], embedded_query: EmbeddedQuery
    ) -> list[EmbeddedChunk]:
        if embedded_query.author_id:
            query_filter = Filter(
                must=[
                    FieldCondition(
                        key="author_id",
                        match=MatchValue(
                            value=str(embedded_query.author_id),
                        ),
                    )
                ]
            )
        else:
            query_filter = None
        return data_category_odm.search(
            query_vector=embedded_query.embedding,
            limit=k // 3,
            query_filter=query_filter,
        )
    embedded_query: EmbeddedQuery = EmbeddingDispatcher.dispatch(query)

我们使用与 RAG 特性管道中相同的 EmbeddingDispatcher 来嵌入查询,这样我们就可以对存储在向量数据库中的文档块进行嵌入。使用相同的类可以确保我们在摄取和查询时使用相同的嵌入模型,这对检索步骤至关重要。

我们通过利用本地的 _search_data_category() 函数分别搜索每个数据类别。在 _search_data_category() 函数中,我们应用从 embedded_query 对象提取的过滤条件。例如,如果 author_id 存在,我们使用它来过滤搜索结果,只包含该作者的文档。然后,来自所有类别的结果会被合并:

ini 复制代码
    post_chunks = _search_data_category(EmbeddedPostChunk, embedded_query)
    articles_chunks = _search_data_category(EmbeddedArticleChunk, embedded_query)
    repositories_chunks = _search_data_category(EmbeddedRepositoryChunk, embedded_query)
    retrieved_chunks = post_chunks + articles_chunks + repositories_chunks
    return retrieved_chunks

最后,rerank() 方法接受原始查询和检索到的文档列表,根据相关性对它们进行重新排序:

python 复制代码
def rerank(self, query: str | Query, chunks: list[EmbeddedChunk], keep_top_k: int) -> list[EmbeddedChunk]:
    if isinstance(query, str):
        query = Query.from_str(query)
    reranked_documents = self._reranker.generate(query=query, chunks=chunks, keep_top_k=keep_top_k)
    logger.info("Documents reranked successfully.", num_documents=len(reranked_documents))
    return reranked_documents

通过利用 ContextRetriever 类,我们可以仅用几行代码从任何查询中检索上下文。例如,看看以下代码片段,我们通过简单地调用 search() 方法,调用了整个高级 RAG 架构:

python 复制代码
from loguru import logger
from llm_engineering.application.rag.retriever import ContextRetriever

query = """
        My name is Paul Iusztin.
       
        Could you draft a LinkedIn post discussing RAG systems?
        I'm particularly interested in:
            - how RAG works
            - how it is integrated with vector DBs and large language models (LLMs).
        """

retriever = ContextRetriever(mock=False)
documents = retriever.search(query, k=3)

logger.info("Retrieved documents:")
for rank, document in enumerate(documents):
    logger.info(f"{rank + 1}: {document}")

使用以下 CLI 命令调用上述代码:poetry poe call-rag-retrieval-module。这会输出如下内容:

csharp 复制代码
2024-09-18 19:01:50.588 | INFO - Retrieved documents:
2024-09-18 19:01:50.588 | INFO - 1: id=UUID('541d6c22-d15a-4e6a-924a-68b7b1e0a330') content='4 Advanced RAG Algorithms You Must Know by Paul Iusztin Implement 4 advanced RAG retrieval techniques to optimize your vector DB searches. Integrate the RAG retrieval module into a production LLM system..." platform='decodingml.substack.com' document_id=UUID('32648f33-87e6-435c-b2d7-861a03e72392') author_id=UUID('900fec95-d621-4315-84c6-52e5229e0b96') author_full_name='Paul Iusztin' metadata={'embedding_model_id': 'sentence-transformers/all-MiniLM-L6-v2', 'embedding_size': 384, 'max_input_length': 256} link='https://decodingml.substack.com/p/the-4-advanced-rag-algorithms-you?r=1ttoeh'
2024-09-18 19:01:50.588 | INFO - 2: id=UUID('5ce78438-1314-4874-8a5a-04f5fcf0cb21') content='Overview of advanced RAG optimization techniquesA production RAG system is split into 3 main components ingestion clean, chunk, embed, and load your data to a vector DBretrieval query your vector DB for ..." platform='medium' document_id=UUID('bd9021c9-a693-46da-97e7-0d06760ee6bf') author_id=UUID('900fec95-d621-4315-84c6-52e5229e0b96') author_full_name='Paul Iusztin' metadata={'embedding_model_id': 'sentence-transformers/all-MiniLM-L6-v2', 'embedding_size': 384, 'max_input_length': 256} link='https://medium.com/decodingml/the-4-advanced-rag-algorithms-you-must-know-to-implement-5d0c7f1199d2'
2024-09-18 19:02:45.729 | INFO  - 3: id=UUID('0405a5da-4686-428a-91ca-446b8e0446ff') content='Every Medium article will be its own lesson An End to End Framework for Production Ready LLM Systems by Building Your LLM TwinThe Importance of Data Pipelines in the Era of Generative AIChange Data Capture Enabling Event Driven ..." platform='medium' document_id=UUID('bd9021c9-a693-46da-97e7-0d06760ee6bf') author_id=UUID('900fec95-d621-4315-84c6-52e5229e0b96') author_full_name='Paul Iusztin' metadata={'embedding_model_id': 'sentence-transformers/all-MiniLM-L6-v2', 'embedding_size': 384, 'max_input_length': 256} link='https://medium.com/decodingml/the-4-advanced-rag-algorithms-you-must-know-to-implement-5d0c7f1199d2'

正如您在上面的输出中看到的,除了检索到的内容外,我们还可以访问各种元数据,例如用于检索的嵌入模型或文档块的来源链接。这些元数据可以在生成用户结果时快速添加到参考列表中,从而增加最终结果的可信度。

现在我们已经理解了检索模块的工作原理,让我们迈出最后一步,考察端到端的 RAG 推理管道。

将所有内容整合到 RAG 推理管道中

为了完全实现 RAG 流程,我们还需要使用检索模型中的上下文构建提示,并调用 LLM 来生成答案。本节将讨论这两个步骤,并将所有内容整合成一个单一的 rag() 函数。本节中的函数可以在 GitHub 上访问:github.com/PacktPublis...

让我们首先看看 call_llm_service() 函数,它负责与 LLM 服务进行交互。该函数接收用户的查询和可选的上下文,设置语言模型端点,执行推理,并返回生成的答案。上下文是可选的,您可以像与任何其他 LLM 交互一样,在没有上下文的情况下调用 LLM:

python 复制代码
def call_llm_service(query: str, context: str | None) -> str:
    llm = LLMInferenceSagemakerEndpoint(
        endpoint_name=settings.SAGEMAKER_ENDPOINT_INFERENCE, inference_component_name=None
    )
    answer = InferenceExecutor(llm, query, context).execute()
    return answer

此函数向我们的微调 LLM Twin 模型发出 HTTP 请求,该模型作为 AWS SageMaker 推理端点托管。我们将在下一章深入探讨所有与 SageMaker 相关的细节,包括 LLMInferenceSagemakerEndpointInferenceExecutor 类。目前,重要的是要了解我们使用此函数调用微调后的 LLM,但我们必须强调,传递给 InferenceExecutor 类的查询和上下文是如何转化为最终提示的。我们使用一个简单的提示模板,通过用户查询和检索到的上下文来定制:

ini 复制代码
prompt = f"""
You are a content creator. Write what the user asked you to while using the provided context as the primary source of information for the content.
User query: {query}
Context: {context}
"""

接下来是 rag() 函数,这是 RAG 逻辑的汇聚点。它处理根据查询检索相关文档,将文档映射到将被注入提示的上下文中,并从 LLM 获取最终答案:

ini 复制代码
def rag(query: str) -> str:
    retriever = ContextRetriever(mock=False)
    documents = retriever.search(query, k=3)
    context = EmbeddedChunk.to_context(documents)
    answer = call_llm_service(query, context)
    return answer

由于我们将所有 RAG 步骤模块化成独立的类,我们将高层次的 rag() 函数减少到了仅五行代码(封装了系统的所有复杂性),类似于我们在 LangChain、LlamaIndex 或 Haystack 等工具中看到的实现。与它们的高层次实现不同,我们学习了如何从头开始构建一个先进的 RAG 服务。而且,通过清晰地分离每个类的责任,我们可以像使用 LEGO 一样使用这些类。因此,您可以快速独立地调用 LLM(不带上下文),或将检索模块作为查询引擎放在您的向量数据库之上。在下一章,我们将在将微调后的 LLM 部署到 AWS SageMaker 推理端点后看到 rag() 函数的实际应用。

在结束本章之前,我们想讨论一些您可以添加到 RAG 推理管道中的潜在改进。由于我们正在构建一个聊天机器人,第一个改进是添加一个会话记忆,存储所有用户的提示和生成的答案。这样,在与聊天机器人交互时,它将能够记住整个对话,而不仅仅是最新的提示。在向 LLM 提示时,除了新的用户输入和上下文,我们还会传递记忆中的对话历史。由于对话历史可能会变长,为了避免超出上下文窗口或增加成本,您需要实现一种方式来减少记忆的大小。正如图 9.4 所示,最简单的一种方法是只保留聊天历史中的最新 K 项。不幸的是,使用这种策略,LLM 永远不会了解整个对话。

因此,另一种将聊天历史添加到提示中的方法是保留对话的摘要和最新的 K 条回复。有多种方法可以计算这个摘要,如果我们深入讨论,可能会偏离本书的目的,但最简单的方法是每次用户提示时更新摘要并生成答案。

对于每次搜索,我们会向向量数据库发送三个查询,每个查询对应一个数据类别。因此,第二个改进是添加一个路由器,在查询和搜索之间进行调度。这个路由器将是一个多类别分类器,它预测我们必须检索的特定查询的数据类别。因此,代替每次搜索都发起三个请求,我们通常可以将其减少到一个或两个请求。例如,如果用户想为一篇文章写一个关于 RAG 的理论段落,那么最有可能的做法是只查询文章集合。在这种情况下,路由器将预测文章类别,我们可以使用该类别来决定必须查询哪个集合。

另一个例子是,如果我们想展示一段代码,演示如何构建 RAG 管道。在这种情况下,路由器必须预测文章和代码库这两个数据类别,因为我们需要在这两个集合中查找示例,以提供完整的上下文。

通常,路由策略根据用户的输入来决定调用哪个模型,例如是使用 GPT-4 还是自托管的 Llama 3.1 模型来处理该特定查询。然而,在我们的具体用例中,我们可以调整路由算法,以优化检索步骤。

我们还可以通过使用混合搜索算法进一步优化检索,该算法结合了基于嵌入的向量搜索和基于关键字的搜索算法,如 BM25。搜索算法使用 BM25(或类似的方法)在向量搜索算法流行之前,在数据库中查找相似项。通过将这些方法合并,混合搜索能够检索匹配确切术语(如 RAG、LLM 或 SageMaker)和查询语义的结果,从而提高检索结果的准确性和相关性。从根本上讲,混合搜索算法遵循以下机制:

  • 并行处理:搜索查询同时通过向量搜索和 BM25 算法进行处理。每个算法根据其标准检索一组相关文档。
  • 得分归一化:两种搜索方法的结果会被分配相关性得分,并进行归一化,以确保可比较性。此步骤至关重要,因为向量搜索和 BM25 的得分机制在不同的尺度上工作,因此在没有归一化的情况下,它们的得分无法直接比较或合并。
  • 结果合并:归一化的得分被合并,通常通过加权求和的方式,生成文档的最终排序。调整权重可以微调语义搜索和关键字搜索算法的侧重点。

总结来说,通过结合语义搜索和精确关键字搜索算法,您可以提高检索步骤的准确性。向量搜索有助于识别同义词或相关概念,确保不会因词汇差异而忽略相关信息。关键字搜索则确保包含关键字的文档被适当地强调,尤其是在涉及特定术语的技术领域中。

最后,我们可以对 RAG 系统进行的一项改进是使用多索引向量结构,而不仅仅是基于内容的嵌入进行索引。让我们详细说明一下多索引的工作原理。与使用单个字段的嵌入进行特定集合的向量搜索不同,多索引方法结合了多个字段。

例如,在我们的 LLM Twin 用例中,我们仅使用了文章、帖子或代码库的内容字段来查询向量数据库。当使用多索引策略时,除了内容字段,我们还可以对内容发布的平台或内容发布的时间进行嵌入索引。这可能会影响检索的最终准确性,因为不同的平台有不同类型的内容,或者更新的内容通常更相关。像 Superlinked 这样的框架使得多索引变得简单。例如,在下面的代码片段中,我们使用 Superlinked 在几行代码中为我们的文章集合定义了一个内容和平台的多索引:

ini 复制代码
from superlinked.framework.common.schema.id_schema_object import IdField
from superlinked.framework.common.schema.schema import schema
from superlinked.framework.common.schema.schema_object import String
... # Other Superlinked imports. 

@schema
class ArticleSchema:
    id: IdField
    platform: String
    content: String

article = ArticleSchema()

articles_space_content = TextSimilaritySpace(
    text=chunk(article.content, chunk_size=500, chunk_overlap=50),
    model=settings.EMBEDDING_MODEL_ID,
)

articles_space_plaform = CategoricalSimilaritySpace(
    category_input=article.platform,
    categories=["medium", "substack", "wordpress"],
    negative_filter=-5.0,
)

article_index = Index(
    [articles_space_content, articles_space_plaform],
    fields=[article.author_id],
)

Superlinked 是一个强大的 Python 工具,适用于任何包含向量计算的用例,如 RAG、推荐系统和语义搜索。它提供了一个生态系统,您可以快速将数据摄取到向量数据库中,基于此编写复杂查询,并将服务部署为 RESTful API。

LLM 和 RAG 的世界是实验性的,类似于其他任何 AI 领域。因此,在构建现实世界产品时,重要的是快速构建一个可行的端到端解决方案,尽管它不一定是最优的。然后,您可以通过各种实验进行迭代,直到完全优化它以适应您的用例。这是业界的标准做法,它使您能够快速迭代,同时在产品生命周期内为业务提供价值,并尽可能快地收集用户反馈。

总结

本章教会了我们如何构建一个先进的 RAG 推理管道。我们首先探讨了 RAG 系统的软件架构。然后,我们深入了解了在检索模块中使用的高级 RAG 方法,例如查询扩展、自查询、过滤向量搜索和重新排序。接着,我们展示了如何编写一个模块化的 ContextRetriever 类,将所有先进的 RAG 组件连接在一个单一的接口下,使得检索相关文档变得轻松。最终,我们讲解了如何将所有缺失的部分连接起来,包括检索、提示增强和 LLM 调用,形成一个统一的 RAG 函数,作为我们的 RAG 推理管道。

正如本章多次强调的那样,我们无法测试我们的微调 LLM,因为它尚未部署到 AWS SageMaker 作为推理端点。因此,在下一章中,我们将学习如何将 LLM 部署到 AWS SageMaker,编写推理接口来调用该端点,并实现一个 FastAPI Web 服务器,作为我们的业务层。

参考文献

4o mini

相关推荐
扫地的小何尚1 小时前
NVIDIA RTX 系统上使用 llama.cpp 加速 LLM
人工智能·aigc·llama·gpu·nvidia·cuda·英伟达
埃菲尔铁塔_CV算法4 小时前
深度学习神经网络创新点方向
人工智能·深度学习·神经网络
艾思科蓝-何老师【H8053】4 小时前
【ACM出版】第四届信号处理与通信技术国际学术会议(SPCT 2024)
人工智能·信号处理·论文发表·香港中文大学
weixin_452600694 小时前
《青牛科技 GC6125:驱动芯片中的璀璨之星,点亮 IPcamera 和云台控制(替代 BU24025/ROHM)》
人工智能·科技·单片机·嵌入式硬件·新能源充电桩·智能充电枪
学术搬运工4 小时前
【珠海科技学院主办,暨南大学协办 | IEEE出版 | EI检索稳定 】2024年健康大数据与智能医疗国际会议(ICHIH 2024)
大数据·图像处理·人工智能·科技·机器学习·自然语言处理
右恩5 小时前
AI大模型重塑软件开发:流程革新与未来展望
人工智能
图片转成excel表格5 小时前
WPS Office Excel 转 PDF 后图片丢失的解决方法
人工智能·科技·深度学习
Just Jump5 小时前
大语言模型LLM综述
llm·大语言模型
ApiHug5 小时前
ApiSmart x Qwen2.5-Coder 开源旗舰编程模型媲美 GPT-4o, ApiSmart 实测!
人工智能·spring boot·spring·ai编程·apihug
哇咔咔哇咔6 小时前
【科普】简述CNN的各种模型
人工智能·神经网络·cnn