解决现代RAG实际生产问题


LLM很棒,但我们可以用它们来回答我们对私人数据的查询吗?这就是检索增强一代(RAG)的用武之地。RAG 的使用一直在快速增长,因为大多数公司拥有大量专有数据,并且他们希望他们的聊天机器人或其他基于文本的人工智能专门针对他们的公司。 RAG 是LLM的一个非常有趣的用例,它们与LLM不断增加的上下文长度直接竞争,我不知道这两者中哪一个会占上风。但我确信,为创建更好的 RAG 而开发的许多技术将在未来的系统中使用,RAG 可能会也可能不会在几年内消失,但一些有趣的技术可能会激发下一代系统。因此,事不宜迟,让我们来看看创建下一代人工智能系统的细节。

目录

  • 什么是RAG?
  • 构建基本的 RAG 管道
  • 总体挑战
  • 现代 RAG 管道的 9 个挑战和解决方案
  • 可扩展性
  • 结论

第2部分:

解决现代 RAG 系统中的生产问题-II

解决 RAG 中的生产和扩展问题。复杂的表格和 PDF 解析。 RAG 的未来:人工智能代理?

媒体网站

什么是RAG?

简而言之,RAG 是一种为我们的LLM提供额外背景的技术,以生成更好、更具体的答复。LLM接受了公开数据的培训,它们确实是智能系统,但它们无法回答我们的具体问题,因为它们缺乏回答这些查询的上下文。通过 RAG,我们提供了必要的背景,以便我们可以优化我们出色的LLM的使用。

image-20240415223025967

如果您想复习一下LLM,请查看这篇文章:

忙碌的人LLM简介

涵盖LLM领域的所有主要更新

媒体网站

RAG 是一种将新知识或能力插入到我们的LLM中的方法,尽管这种知识插入不是永久性的。向LLM添加新知识或能力的另一种方法是根据我们的特定数据微调LLM。

通过微调添加新知识是相当棘手、困难、昂贵且永久的。通过微调添加新功能甚至会影响其先前拥有的知识。在微调过程中,我们无法控制哪些权重将被改变,从而哪些能力将增加或减少。

image-20240415223042915

现在,我们是否进行微调、RAG 或两者的组合完全取决于手头的任务。没有一个人适合所有人。

构建基本的 RAG 管道

image-20240415223054556

过程:

  • 将文档分割成均匀的块。
  • 每个块都是一段原始文本。
  • 为每个块生成嵌入(例如 OpenAl 嵌入、sentence_transformer)
  • 将每个块存储在矢量数据库中。
  • 从向量数据库集合中查找 Top-k 最相似的块
  • 插入 LLM 响应综合模块。

image-20240415223102652

原生的RAG

!pip install llama-index

# My OpenAI Key
import os
os.environ['OPENAI_API_KEY'] = ""


import logging
import sys
import requests

logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

from llama_index import VectorStoreIndex, SimpleDirectoryReader
from IPython.display import Markdown, display

# download paul graham's essay
response = requests.get("https://www.dropbox.com/s/f6bmb19xdg0xedm/paul_graham_essay.txt?dl=1")
essay_txt = response.text
with open("pg_essay.txt", "w") as fp:
  fp.write(essay_txt)
  
  
  # load documents
documents = SimpleDirectoryReader(input_files=['pg_essay.txt']).load_data()


index = VectorStoreIndex.from_documents(documents)


# set Logging to DEBUG for more detailed outputs
query_engine = index.as_query_engine(similarity_top_k=2)

response = query_engine.query(
    "What did the author do growing up?",
)


print(response.source_nodes[0].node.get_text())

上面的代码展示了如何制作一个简单的 RAG 管道。我们只需加载一篇文章,将其分块,然后使用 llama-index 库创建一个 Naive RAG 管道。

朴素的 RAG 方法往往适用于解决针对简单、小型文档集的简单问题。
● “特斯拉面临的主要风险因素是什么?” (超过 Tesla 2021 10K)
● “作者在 YC 期间做了什么?” (保罗·格雷厄姆文章)

但现实生活很少如此简单。因此,在下一节中,让我们看看挑战和可能的补救措施。然后,定义此类系统的未来。

整体挑战

但在我们研究每个痛点之前,让我们先定义一下总体挑战。人工智能系统与当前的软件系统有很大不同。

人工智能驱动的软件由一组黑盒参数定义。很难推理出功能空间是什么样子的。模型参数已调整,而周围参数(提示模板)
未调整。

如果系统的一个组件是黑匣子,则系统的所有组件都变成黑匣子。组件越多,
我们需要调整的参数就越多。每个参数都会影响整个 RAG 管道的性能。用户应该调整哪些参数?有太多的选择!

RAG 管道中的不同挑战

错误检索

  • 低精度:并非检索到的集合中的所有块都是相关的
    - 幻觉 + 迷失在中间问题
  • 低召回率:现在所有相关块都被检索。
    — LLM 缺乏足够的背景来综合答案
  • 过时的信息:数据冗余或过时。

不良响应生成

  • 幻觉:模型给出了上下文中不存在的答案。
  • 不相关:模型给出的答案没有回答问题。
  • 毒性/偏见:模型给出的答案是有害的/令人反感的。

因此,最佳实践是根据特定的痛点对我们的 RAG 管道进行分类,并单独解决它们。让我们在下一节中看看具体的问题及其解决方案。

现代 RAG 管道的 9 个挑战和解决方案

响应质量相关

  1. 知识库中上下文缺失

  2. 初始检索过程中上下文缺失

  3. 重新排序后上下文缺失

  4. 上下文未提取

  5. 输出格式错误

  6. 输出的特异性级别不正确

  7. 输出不完整

可扩展性

  1. 无法扩展到更大的数据量
  2. 速率限制错误

最近有一篇名为“设计检索增强生成系统时的七个故障点”的论文,讨论了为什么创建生产级 RAG 如此困难。今天,我们正在研究这些工程挑战,并试图提出一种新的创新方法。

image-20240415223252533

图片来源:设计检索增强生成系统时的七个故障点

知识库中缺少上下文

这很容易理解,您提出的问题需要一些上下文来回答,如果您的 RAG 系统没有选择正确的文档块或者源数据本身缺少上下文,它只会给出一个通用答案,不够具体,无法解决用户的查询。

我们提出了一些建议的解决方案:

清理您的数据:

如果您的源数据质量很差,例如包含冲突的信息,那么无论我们将 RAG 管道构建得多么好,它都无法从我们提供的垃圾中输出黄金。

有一些常见的数据清理策略,仅举几例:

  • 删除噪音和不相关信息:这包括删除特殊字符、停用词(常见词,如“the”和“a”)和 HTML 标签。
  • 识别并纠正错误:这包括拼写错误、拼写错误和语法错误。拼写检查器和语言模型等工具可以帮助解决这个问题。
  • 重复数据删除:删除可能使检索过程产生偏差的重复记录或类似记录。

Unstructed.io在其核心库中提供了一组清理功能来帮助解决此类数据清理需求。值得一看。

更好的提示:

通过使用诸如“如果您不确定答案,请告诉我您不知道”之类的提示来指导系统,您可以鼓励模型承认其局限性并更透明地传达不确定性。无法保证 100% 的准确性,但精心设计提示是清理数据后可以做出的最佳努力之一。

添加元数据:

将全局上下文注入每个块

初始检索过程中上下文缺失

重要文档可能不会出现在系统检索组件返回的顶部结果中。正确答案被忽略,导致系统无法提供准确的响应。该论文暗示,“问题的答案在文档中,但排名不够高,无法返回给用户”。

针对这个痛点有两种解决方案:

块大小和 top-k 的超参数调整:

chunk_size都是similarity_top_k用于管理 RAG 模型中数据检索过程的效率和有效性的参数。调整这些参数可以影响计算效率和检索信息质量之间的权衡。 LlamaIndex 对此提供了强大的支持,请查看下面的文章。

查看超参数调整的文档

# contains the parameters that need to be tuned
param_dict = {"chunk_size": [256, 512, 1024], "top_k": [1, 2, 5]}

# contains parameters remaining fixed across all runs of the tuning process
fixed_param_dict = {
    "docs": documents,
    "eval_qs": eval_qs,
    "ref_response_strs": ref_response_strs,
}

def objective_function_semantic_similarity(params_dict):
    chunk_size = params_dict["chunk_size"]
    docs = params_dict["docs"]
    top_k = params_dict["top_k"]
    eval_qs = params_dict["eval_qs"]
    ref_response_strs = params_dict["ref_response_strs"]

    # build index
    index = _build_index(chunk_size, docs)

    # query engine
    query_engine = index.as_query_engine(similarity_top_k=top_k)

    # get predicted responses
    pred_response_objs = get_responses(
        eval_qs, query_engine, show_progress=True
    )

    # run evaluator
    eval_batch_runner = _get_eval_batch_runner_semantic_similarity()
    eval_results = eval_batch_runner.evaluate_responses(
        eval_qs, responses=pred_response_objs, reference=ref_response_strs
    )

    # get semantic similarity metric
    mean_score = np.array(
        [r.score for r in eval_results["semantic_similarity"]]
    ).mean()

    return RunResult(score=mean_score, params=params_dict)


param_tuner = ParamTuner(
    param_fn=objective_function_semantic_similarity,
    param_dict=param_dict,
    fixed_param_dict=fixed_param_dict,
    show_progress=True,
)

results = param_tuner.tune()

重新排名:

在将检索结果发送到LLM之前对其进行重新排序可以显着提高 RAG 性能。这个 LlamaIndex笔记本展示了以下之间的区别:

  • 直接检索前 2 个节点而无需重新排序,导致检索不准确。
  • 通过检索前 10 个节点并使用CohereRerank重新排序并返回前 2 个节点来精确检索。
import os
from llama_index.postprocessor.cohere_rerank import CohereRerank

api_key = os.environ["COHERE_API_KEY"]
cohere_rerank = CohereRerank(api_key=api_key, top_n=2) # return top 2 nodes from reranker

query_engine = index.as_query_engine(
    similarity_top_k=10, # we can set a high top_k here to ensure maximum relevant retrieval
    node_postprocessors=[cohere_rerank], # pass the reranker to node_postprocessors
)

response = query_engine.query(
    "What did Elon Musk do?",
)

关于自定义 Reranker 的很酷博客:单击此处

重新排名后上下文丢失

该论文定义了这一点:“带有答案的文档是从数据库中检索的,但没有进入生成答案的上下文。当从数据库返回许多文档并进行合并过程以检索答案时,就会发生这种情况”。

更好的检索策略

LlamaIndex 提供了一系列从基础到高级的检索策略,帮助我们在 RAG 管道中实现准确的检索。

  • 每个索引的基本检索
  • 高级检索和搜索
  • 自动检索
  • 知识图检索器
  • 组合/分层检索器

image-20240415223408629image-20240415223416820image-20240415223429057

不同的检索策略

微调嵌入

如果即使在改变检索策略后模型仍表现不佳,我们应该根据数据微调我们的模型,从而为LLM本身提供背景。在此过程中,我们获得了嵌入模型,随后在这些自定义嵌入模型的帮助下将原始数据转换为向量数据库。

未提取上下文

系统很难从提供的上下文中提取正确的答案,尤其是在信息过载时。关键细节被遗漏,从而影响了响应的质量。该论文暗示:“当上下文中存在太多噪音或相互矛盾的信息时,就会发生这种情况”。

以下是一些建议的解决方案。

迅速压缩

LongLLMLingua 研究项目/论文中介绍了长上下文环境中的即时压缩。通过与 LlamaIndex 的集成,我们现在可以将 LongLLMLingua 实现为节点后处理器,它将在检索步骤之后压缩上下文,然后将其输入 LLM。 LongLLMLingua 压缩提示可以以更低的成本获得更高的性能。此外,整个系统运行速度更快。

from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.response_synthesizers import CompactAndRefine
from llama_index.postprocessor.longllmlingua import LongLLMLinguaPostprocessor
from llama_index.core import QueryBundle

node_postprocessor = LongLLMLinguaPostprocessor(
    instruction_str="Given the context, please answer the final question",
    target_token=300,
    rank_method="longllmlingua",
    additional_compress_kwargs={
        "condition_compare": True,
        "condition_in_question": "after",
        "context_budget": "+100",
        "reorder_context": "sort",  # enable document reorder
    },
)

retrieved_nodes = retriever.retrieve(query_str)
synthesizer = CompactAndRefine()

# outline steps in RetrieverQueryEngine for clarity:
# postprocess (compress), synthesize
new_retrieved_nodes = node_postprocessor.postprocess_nodes(
    retrieved_nodes, query_bundle=QueryBundle(query_str=query_str)
)

print("\n\n".join([n.get_content() for n in new_retrieved_nodes]))

response = synthesizer.synthesize(query_str, new_retrieved_nodes)

长上下文重排序

一项研究发现,当关键数据位于输入上下文的开头或结尾时,通常会出现最佳性能。LongContextReorder旨在通过重新排序检索到的节点来解决“迷失在中间”的问题,这在需要大的 top-k 的情况下很有帮助。

from llama_index.core.postprocessor import LongContextReorder

reorder = LongContextReorder()

reorder_engine = index.as_query_engine(
    node_postprocessors=[reorder], similarity_top_k=5
)

reorder_response = reorder_engine.query("Did the author meet Sam Altman?")"Did the author meet Sam Altman?")

image-20240415223600634

一开始就有更好的背景

image-20240415223608001

提示压缩和 LongContextReorder

这是一篇非常有趣的论文,它阐明了注意力机制是如何不统一的,并且更多地关注某些部分,因此重要的信息位于注意力的开始。

输出格式错误

许多用例需要以 JSON 格式输出答案。

  • 更好的文本提示/输出解析。
  • 使用OpenAI函数调用+JSON方式
  • 使用令牌级别提示(LMQL、指南)

LlamaIndex 支持与其他框架提供的输出解析模块集成,例如GuardrailsLangChain

请参阅下面的 LangChain 输出解析模块的示例代码片段,您可以在 LlamaIndex 中使用它。有关更多详细信息,请查看关于输出解析模块的LlamaIndex 文档。

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.core.output_parsers import LangchainOutputParser
from llama_index.llms.openai import OpenAI
from langchain.output_parsers import StructuredOutputParser, ResponseSchema

# load documents, build index
documents = SimpleDirectoryReader("../paul_graham_essay/data").load_data()
index = VectorStoreIndex.from_documents(documents)

# define output schema
response_schemas = [
    ResponseSchema(
        name="Education",
        description="Describes the author's educational experience/background.",
    ),
    ResponseSchema(
        name="Work",
        description="Describes the author's work experience/background.",
    ),
]

# define output parser
lc_output_parser = StructuredOutputParser.from_response_schemas(
    response_schemas
)
output_parser = LangchainOutputParser(lc_output_parser)

# Attach output parser to LLM
llm = OpenAI(output_parser=output_parser)

# obtain a structured response
query_engine = index.as_query_engine(llm=llm)
response = query_engine.query(
    "What are a few things the author did growing up?",
)
print(str(response))

Pydantic 为构建LLM的输出提供了大力支持。

from pydantic import BaseModel
from typing import List

from llama_index.program.openai import OpenAIPydanticProgram

# Define output schema (without docstring)
class Song(BaseModel):
    title: str
    length_seconds: int


class Album(BaseModel):
    name: str
    artist: str
    songs: List[Song]

# Define openai pydantic program
prompt_template_str = """\
Generate an example album, with an artist and a list of songs. \
Using the movie {movie_name} as inspiration.\
"""
program = OpenAIPydanticProgram.from_defaults(
    output_cls=Album, prompt_template_str=prompt_template_str, verbose=True
)

# Run program to get structured output
output = program(
    movie_name="The Shining", description="Data model for an album."
)

这会将 LLM 中的数据填充到类对象中。

  • LLM 文本完成 Pydantic 程序:这些程序利用文本完成 API 与输出解析相结合,处理输入文本并将其转换为用户定义的结构化对象。
  • LLM 函数调用 Pydantic 程序:这些程序利用 LLM 函数调用 API,获取输入文本并将其转换为用户指定的结构化对象。
  • 预打包的 Pydantic 程序:这些程序旨在将输入文本转换为预定义的结构化对象。

查看 W&B 的另一个类似内容:https://github.com/wandb/edu/tree/main/llm-structed-extraction

OpenAI JSON 模式使我们能够设置response_format{ "type": "json_object" }启用 JSON 模式的响应。启用 JSON 模式时,模型仅限于生成解析为有效 JSON 对象的字符串。虽然 JSON 模式强制执行输出格式,但它无助于针对指定模式进行验证。有关更多详细信息,请查看 LlamaIndex 关于OpenAI JSON 模式与数据提取的函数调用的文档。

输出的特异性水平不正确

答复可能缺乏必要的细节或具体性,通常需要后续询问才能澄清。答案可能过于模糊或笼统,无法有效满足用户的需求。

高级检索策略

当答案未达到您期望的正确粒度时,您可以改进检索策略。一些可能有助于解决这一痛点的主要高级检索策略包括:

输出不完整

片面的回答并没有错,而是错误的。然而,尽管信息在上下文中存在并且可以访问,但它们并没有提供所有细节。例如,如果有人问:“文件A、B、C主要讨论了哪些方面?”单独询问每份文件可能会更有效,以确保得到全面的答复。

查询转换

比较问题在朴素的 RAG 方法中尤其表现不佳。提高 RAG 推理能力的一个好方法是添加查询理解层 ——在实际查询向量存储之前添加查询转换。以下是四种不同的查询转换:

  • 路由:保留初始查询,同时查明其所属的适当工具子集。然后,将这些工具指定为合适的选项。
  • 查询重写:维护选定的工具,但以多种方式重新编写查询,以将其应用于同一组工具。
  • 子问题:将查询分解为几个较小的问题,每个问题针对由其元数据确定的不同工具。
  • ReAct Agent 工具选择:根据原始查询,确定要使用的工具并制定要在该工具上运行的特定查询。

image-20240415223713201

添加代理工具

查看 LlamaIndex 的查询转换手册了解所有详细信息。

另外,请查看 Iulia Brezeanu 撰写的这篇精彩文章“高级查询转换以改进 RAG”,了解有关查询转换技术的详细信息。

可扩展性

无法扩展到更大的数据量

处理数千/数百万文档的速度很慢。另一个问题是我们如何高效地处理文档更新?简单的摄取管道无法扩展到更大的数据量。

并行化摄取管道

● 并行文档处理
● HuggingFace TEI
● RabbitMQ 消息队列
● AWS EKS 集群

image-20240415223724281

LlamaIndex 提供摄取管道并行处理,该功能可将 LlamaIndex 中的文档处理速度提高 15 倍。

# load data
documents = SimpleDirectoryReader(input_dir="./data/source_files").load_data()

# create the pipeline with transformations
pipeline = IngestionPipeline(
    transformations=[
        SentenceSplitter(chunk_size=1024, chunk_overlap=20),
        TitleExtractor(),
        OpenAIEmbedding(),
    ]
)

# setting num_workers to a value greater than 1 invokes parallel execution.
nodes = pipeline.run(documents=documents, num_workers=4)

速率限制错误

如果 API 的服务条款允许,我们可以注册多个 API 密钥并在我们的应用程序中轮换它们。这种方法有效地增加了我们的速率限制配额。但是,请确保这符合 API 提供商的政策。

如果我们在分布式系统中工作,我们可以将请求分散到多个服务器或 IP 地址,每个服务器或 IP 地址都有其速率限制。实施负载平衡,以优化整个基础设施中速率限制使用的方式动态分配请求。

结论

我们探讨了开发 RAG 管道的 9 个痛点(7 个来自论文,另外 2 个),并针对所有这些痛点提供了相应的建议解决方案。这是我们 RAG 系列的第 1 部分,在下一篇博客中,我们将深入探讨如何处理表和其他高级事物,如何使用缓存等。


yg9538 2024年4月15日 22:50 1102 收藏文档