受 Barnett 等人的论文《工程检索增强生成系统时的七个失败点》启发,让我们在本文中探讨论文中提到的七个失败点以及开发 RAG 管道时的五个常见痛点。
论文:https://arxiv.org/pdf/2401.05856.pdf
更重要的是,我们将深入探讨这些 RAG 痛点的解决方案,以便在日常 RAG 开发中更好地解决这些痛点。
我使用“痛点”而不是“失败点”,主要是因为这些点都有对应的提出的解决方案。让我们在我们的 RAG 管道中成为失败之前尝试解决它们。
首先,让我们审视上述论文中提到的七个痛点;请参见下面的图表。然后我们将添加五个额外的痛点及其提出的解决方案。
痛点1:内容缺失
当实际答案不在知识库中时,RAG 系统会提供一个看似合理但不正确的答案,而不是声明它不知道。用户收到误导性信息,导致沮丧。
我们提出了两种解决方案:
- 清理您的数据
垃圾进,垃圾出。如果您的源数据质量很差,例如包含冲突信息,无论您构建 RAG 管道的多么完善,它都无法从您提供的垃圾中输出金子般的结果。这个提议的解决方案不仅适用于这个痛点,还适用于本文中列出的所有痛点。清洁的数据是任何良好运行的 RAG 管道的先决条件。
- 更好的提示
更好的提示可以在系统由于知识库中缺少信息而可能提供一个看似合理但不正确答案的情况下显著地提供帮助。通过指导系统使用提示,如“如果您对答案不确定,请告诉我您不知道”,您鼓励模型承认其局限性并更透明地传达不确定性。虽然无法保证100%准确性,但在清理数据后,精心设计您的提示是您可以做出的最佳努力之一。
痛点2:错过了排名靠前的文档
系统的检索组件返回的前几个结果可能不包含关键文档。由于忽略了正确答案,导致系统无法提供准确的响应。论文中暗示:“问题的答案在文档中,但排名不高到足以返回给用户”。
我脑海中出现了两个提议的解决方案:
-
超参数调整
chunk_size
和similarity_top_k
chunk_size
和similarity_top_k
是用于管理 RAG 模型中数据检索过程的参数。调整这些参数可以影响计算效率与检索信息质量之间的权衡。我们在之前的文章《使用 LlamaIndex 自动调整超参数》中探讨了关于chunk_size
和similarity_top_k
的超参数调整的细节。请参考以下示例代码:
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()
函数
objective_function_semantic_similarity
的定义如下,param_dict
包含参数chunk_size
和top_k
,以及它们的建议值。更多详情,请参阅 LlamaIndex 关于 RAG 超参数优化的完整笔记本。
-
重新排名
-
在将检索结果发送到 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) # 从重新排名器返回顶部2个节点 query_engine = index.as_query_engine( similarity_top_k=10, # 在此设置一个高 top_k 以确保最大相关检索 node_postprocessors=[cohere_rerank], # 将重新排名器传递给节点后处理器 ) response = query_engine.query( "Sam Altman 在这篇文章中做了什么?", )
此外,您可以使用各种嵌入和重新排名器评估和增强检索器的性能,详见 Ravi Theja 的文章《提升 RAG:选择最佳嵌入和重新排名模型》。
此外,您可以微调自定义重新排名器以获得更好的检索性能,详细实现文档记录在 Ravi Theja 的文章《通过 LlamaIndex 使用 Fine-tuning Cohere Reranker 改进检索性能》中。
-
痛点3:不在上下文中 — 合并策略限制
论文定义了这一点:“从数据库中检索到答案的文档,但未进入生成答案的上下文中。这发生在从数据库返回许多文档并进行合并处理以检索答案时。”
除了在上述部分描述的添加重新排名器和微调重新排名器之外,我们还可以探讨以下提议的解决方案:
-
调整检索策略
LlamaIndex 提供一系列检索策略,从基础到高级,以帮助我们在 RAG 管道中实现准确的检索。查看检索模块指南,了解所有检索策略的全面列表,分为不同的类别:- 每个索引的基本检索
- 高级检索和搜索
- 自动检索
- 知识图检索器
- 组合/分层检索器
- 等等!
-
微调嵌入
如果您使用开源嵌入模型,微调嵌入模型是实现更准确检索的好方法。LlamaIndex 提供了一个逐步指南,说明如何微调开源嵌入模型,证明微调嵌入模型可以在一套评估指标中持续改善指标。请参考下面的示例代码片段,创建微调引擎,运行微调并获取微调模型:
finetune_engine = SentenceTransformersFinetuneEngine( train_dataset, model_id="BAAI/bge-small-en", model_output_path="test_model", val_dataset=val_dataset, ) finetune_engine.finetune() embed_model = finetune_engine.get_finetuned_model()
这些解决方案有助于解决不在上下文中的痛点,以及合并策略的限制。
痛点4:未提取
系统在提供的上下文中难以从中提取正确答案,特别是当信息过载时。关键细节被忽略,从而影响了响应的质量。论文暗示:“当上下文中存在太多噪音或矛盾信息时会出现这种情况”。
让我们探讨三种提议的解决方案:
-
清理您的数据
这一痛点是又一个典型的恶劣数据的受害者。我们无法再次强调清理数据的重要性!在责怪您的 RAG 管道之前,请务必花时间清理您的数据。 -
提示压缩
在长上下文设置中引入了提示压缩,这是 LongLLMLingua 研究项目/论文中提出的。通过其在 LlamaIndex 中的集成,我们现在可以将 LongLLMLingua 实现为节点后处理器,在检索步骤后压缩上下文,然后再输入到 LLM 中。请参考下面的示例代码片段,其中我们设置了 LongLLMLinguaPostprocessor,它使用 longllmlingua 包来运行提示压缩。
from llama_index.query_engine import RetrieverQueryEngine
from llama_index.response_synthesizers import CompactAndRefine
from llama_index.postprocessor import LongLLMLinguaPostprocessor
from llama_index.schema import QueryBundle
node_postprocessor = LongLLMLinguaPostprocessor(
instruction_str="给定上下文,请回答最终问题",
target_token=300,
rank_method="longllmlingua",
additional_compress_kwargs={
"condition_compare": True,
"condition_in_question": "after",
"context_budget": "+100",
"reorder_context": "sort", # 启用文档重排
},
)
retrieved_nodes = retriever.retrieve(query_str)
synthesizer = CompactAndRefine()
# 为了清晰起见,概述了 RetrieverQueryEngine 中的步骤:
# 后处理(压缩),合成
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 的情况下可能会有所帮助。请参考下面的示例代码片段,了解如何在构建查询引擎期间将 LongContextReorder 定义为您的节点后处理器。有关更多详情,请参阅 LlamaIndex 的有关 LongContextReorder 的完整笔记本。
from llama_index.postprocessor import LongContextReorder
reorder = LongContextReorder()
reorder_engine = index.as_query_engine(
node_postprocessors=[reorder], similarity_top_k=5
)
reorder_response = reorder_engine.query("作者是否见过 Sam Altman?")
痛点5:格式错误
当 LLM 忽略了提取特定格式(如表格或列表)信息的指令时,我们有四种提议的解决方案可以探索:
-
更好的提示
您可以采用几种策略来改进提示并纠正此问题:- 澄清说明。
- 简化请求并使用关键词。
- 给出示例。
- 迭代提示并询问后续问题。
-
输出解析
输出解析可以通过以下方式帮助确保所需的输出:- 为任何提示/查询提供格式化说明。
- 为 LLM 输出提供“解析”。
LlamaIndex 支持与其他框架提供的输出解析模块(如 Guardrails 和 LangChain)集成。
以下是您可以在 LlamaIndex 中使用的 LangChain 输出解析模块的示例代码片段。有关更多详细信息,请查看 LlamaIndex 输出解析模块的文档。
from llama_index import VectorStoreIndex, SimpleDirectoryReader
from llama_index.output_parsers import LangchainOutputParser
from llama_index.llms import OpenAI
from langchain.output_parsers import StructuredOutputParser, ResponseSchema
# 加载文档,构建索引
documents = SimpleDirectoryReader("../paul_graham_essay/data").load_data()
index = VectorStoreIndex.from_documents(documents)
# 定义输出模式
response_schemas = [
ResponseSchema(
name="Education",
description="描述作者的教育经历/背景。",
),
ResponseSchema(
name="Work",
description="描述作者的工作经历/背景。",
),
]
# 定义输出解析器
lc_output_parser = StructuredOutputParser.from_response_schemas(
response_schemas
)
output_parser = LangchainOutputParser(lc_output_parser)
# 将输出解析器附加到 LLM
llm = OpenAI(output_parser=output_parser)
# 获取结构化响应
from llama_index import ServiceContext
ctx = ServiceContext.from_defaults(llm=llm)
query_engine = index.as_query_engine(service_context=ctx)
response = query_engine.query(
"作者在成长过程中做了一些什么?",
)
print(str(response))
- Pydantic 程序
Pydantic 程序作为一个灵活的框架,可以将输入字符串转换为结构化的 Pydantic 对象。LlamaIndex 提供了几类 Pydantic 程序:- LLM 文本补全 Pydantic 程序:这些程序处理输入文本并将其转换为用户定义的结构化对象,利用文本补全 API 结合输出解析。
- LLM 函数调用 Pydantic 程序:这些程序将输入文本转换为用户指定的结构化对象,利用 LLM 函数调用 API。
- 预包装的 Pydantic 程序:这些程序旨在将输入文本转换为预定义的结构化对象。
以下是来自 OpenAI pydantic 程序的示例代码片段。有关更多详细信息,请查看 LlamaIndex 上关于 pydantic 程序的文档,其中包含了不同 pydantic 程序的链接。
from pydantic import BaseModel
from typing import List
from llama_index.program import OpenAIPydanticProgram
# 定义输出模式(不含文档字符串)
class Song(BaseModel):
title: str
length_seconds: int
class Album(BaseModel):
name: str
artist: str
songs: List[Song]
# 定义 openai pydantic 程序
prompt_template_str = """\
生成一个例子专辑,包括艺术家和一些歌曲的列表。 \
使用电影 {movie_name} 作为灵感。\
"""
program = OpenAIPydanticProgram.from_defaults(
output_cls=Album, prompt_template_str=prompt_template_str, verbose=True
)
# 运行程序以获取结构化输出
output = program(
movie_name="闪灵", description="专辑的数据模型。"
)
- OpenAI JSON 模式
OpenAI JSON 模式使我们能够将 response_format 设置为 { “type”: “json_object” },从而为响应启用 JSON 模式。启用 JSON 模式时,模型被限制为仅生成解析为有效 JSON 对象的字符串。虽然 JSON 模式强制执行输出的格式,但它并不帮助验证是否符合指定的模式。有关更多详细信息,请查看 LlamaIndex 关于 OpenAI JSON 模式与函数调用进行数据提取的文档。
痛点6:准确度不足
响应可能缺乏必要的细节或具体性,通常需要跟进查询以进行澄清。答案可能过于模糊或泛泛而谈,未能有效满足用户的需求。
我们转向高级检索策略来解决这个问题。
高级检索策略
当答案没有达到您期望的正确细粒度水平时,您可以改进您的检索策略。一些主要的高级检索策略可能有助于解决这个痛点,包括:
- 从小到大的检索
- 句子窗口检索
- 递归检索
查看我的最新文章《使用高级检索 LlamaPacks 加速您的 RAG 流水线,并使用 Lighthouz AI 进行基准测试》了解更多关于七种高级检索 LlamaPacks 的详细信息。
痛点7:不完整
部分响应并不是错误;然而,尽管信息存在并且可在上下文中访问,但它们未提供所有细节。例如,如果有人问:“文档 A、B 和 C 中讨论的主要方面是什么?”逐个询问每个文档可能更有效地确保全面的答案。
查询转换
特别是在简单的 RAG 方法中,比较性问题表现不佳。提高 RAG 推理能力的一种好方法是在实际查询向量存储之前添加一个查询理解层 —— 添加查询转换。以下是四种不同的查询转换:
- 路由:保留初始查询,同时指出其适用的工具的合适子集。然后,将这些工具指定为适当的选项。
- 查询重写:保留所选工具,但以多种方式重新构建查询,以在相同的工具集中应用它。
- 子问题:将查询分解为多个较小的问题,每个问题针对不同的工具,由其元数据确定。
- ReAct Agent 工具选择:根据原始查询,确定要使用的工具,并制定在该工具上运行的具体查询。
以下是如何使用 HyDE(假设性文档嵌入)查询重写技术的示例代码片段。给定一个自然语言查询,首先生成一个假设性文档/答案。然后,使用此假设性文档进行嵌入查找,而不是使用原始查询。
# 加载文档,构建索引
documents = SimpleDirectoryReader("../paul_graham_essay/data").load_data()
index = VectorStoreIndex(documents)
# 使用 HyDE 查询转换运行查询
query_str = "paul graham 在 RISD 之后做了什么"
hyde = HyDEQueryTransform(include_original=True)
query_engine = index.as_query_engine()
query_engine = TransformQueryEngine(query_engine, query_transform=hyde)
response = query_engine.query(query_str)
print(response)
查看 LlamaIndex 的查询转换手册,了解所有细节。
此外,查看 Iulia Brezeanu 撰写的优秀文章《改进 RAG 的高级查询转换技术》,详细了解查询转换技术。
以上痛点均来自论文。现在,让我们探讨另外五个常见的 RAG 开发痛点及其提议的解决方案。
痛点8:数据摄入可伸缩性
RAG 流水线中的数据摄入可伸缩性问题指的是系统在努力有效管理和处理大量数据时遇到的挑战,导致性能瓶颈和潜在的系统故障。这些数据摄入可伸缩性问题可能导致摄入时间延长、系统过载、数据质量问题和可用性受限。
并行化摄入流水线
LlamaIndex 提供了摄入流水线并行处理功能,该功能使 LlamaIndex 中的文档处理速度提高了 15 倍。请参阅下面的示例代码片段,了解如何创建 IngestionPipeline 并指定 num_workers 来调用并行处理。查看 LlamaIndex 的完整笔记本以获取更多详细信息。
# 加载数据
documents = SimpleDirectoryReader(input_dir="./data/source_files").load_data()
# 创建带有转换的流水线
pipeline = IngestionPipeline(
transformations=[
SentenceSplitter(chunk_size=1024, chunk_overlap=20),
TitleExtractor(),
OpenAIEmbedding(),
]
)
# 将 num_workers 设置为大于 1 的值会调用并行执行。
nodes = pipeline.run(documents=documents, num_workers=4)
痛点9:结构化数据问答
准确解释用户查询以检索相关的结构化数据可能很困难,特别是对于复杂或模糊的查询、不灵活的文本到 SQL 转换以及当前 LLM 在有效处理这些任务方面的局限性。
LlamaIndex 提供了两种解决方案。
链式表 LlamaPack
ChainOfTablePack 是基于王等人创新的“链式表”论文的 LlamaPack。 “链式表”将思维链的概念与表格转换和表示相结合。它使用一组受限制的操作逐步转换表格,并在每个阶段向 LLM 提供修改后的表格。这种方法的一个重要优势是它能够通过系统地切割数据来解决涉及包含多个信息片段的复杂表格单元的问题,直到找到适当的子集为止,从而增强了表格问答的有效性。
查看 LlamaIndex 的完整笔记本,了解如何使用 ChainOfTablePack 查询结构化数据的详细信息。
混合自一致性 LlamaPack
LLM 可以通过两种主要方式对表格数据进行推理:
通过直接提示进行文本推理
通过程序合成进行符号推理(例如,Python、SQL 等)
基于刘等人的论文《重新思考大语言模型在表格数据理解中的应用》,LlamaIndex 开发了 MixSelfConsistencyQueryEngine,它通过自一致性机制(即多数投票)聚合了来自文本和符号推理的结果,并实现了最先进的性能。请参阅下面的示例代码片段。查看 LlamaIndex 的完整笔记本,了解更多详细信息。
download_llama_pack(
"MixSelfConsistencyPack",
"./mix_self_consistency_pack",
skip_load=True,
)
query_engine = MixSelfConsistencyQueryEngine(
df=table,
llm=llm,
text_paths=5, # 采样 5 条文本推理路径
symbolic_paths=5, # 采样 5 条符号推理路径
aggregation_mode="self-consistency", # 通过自一致性(即多数投票)跨文本和符号路径聚合结果
verbose=True,
)
response = await query_engine.aquery(example["utterance"])
痛点10:从复杂 PDF 中提取数据
提取来自复杂PDF文档的数据,例如嵌入式表格,用于问答可能会遇到困难。简单的检索方法无法从这些嵌入式表格中获取数据。您需要一种更好的方法来检索这些复杂的PDF数据。
嵌入式表格检索
LlamaIndex提供了EmbeddedTablesUnstructuredRetrieverPack解决方案,这是一个LlamaPack,使用Unstructured.io从HTML文档中解析出嵌入式表格,构建节点图,然后使用递归检索根据用户问题索引/检索表格。
请注意,此包以HTML文档作为输入。如果您有PDF文档,可以使用pdf2htmlEX将PDF转换为HTML,而不会丢失文本或格式。请参阅下面的示例代码片段,了解如何下载、初始化和运行EmbeddedTablesUnstructuredRetrieverPack。
# 下载并安装依赖项
EmbeddedTablesUnstructuredRetrieverPack = download_llama_pack(
"EmbeddedTablesUnstructuredRetrieverPack", "./embedded_tables_unstructured_pack",
)
# 创建包
embedded_tables_unstructured_pack = EmbeddedTablesUnstructuredRetrieverPack(
"data/apple-10Q-Q2-2023.html", # 输入HTML文件,如果您的文档是PDF,请先将其转换为HTML
nodes_save_path="apple-10-q.pkl"
)
# 运行包
response = embedded_tables_unstructured_pack.run("总营业费用是多少?").response
display(Markdown(f"{response}"))
痛点11:备用模型
在使用LLMs时,如果您的模型遇到问题,例如OpenAI模型的速率限制错误,您需要备用模型作为备份,以防主要模型发生故障。
两种提议的解决方案:
中微子路由器:
中微子路由器是一个LLMs集合,您可以将查询路由到其中。它使用预测模型智能地将查询定向到最适合的LLM,以最大化性能并优化成本和延迟。中微子目前支持十几种模型。如果您希望将新模型添加到其支持的模型列表中,请联系其支持团队。
您可以在中微子控制面板中创建路由器以手动选择首选模型,或者使用“默认”路由器,其中包括所有支持的模型。
LlamaIndex通过其llms模块中的Neutrino类集成了中微子支持。以下是代码片段示例。在Neutrino AI页面上查看更多详细信息。
from llama_index.llms import Neutrino
from llama_index.llms import ChatMessage
llm = Neutrino(
api_key="",
router="test" # 在Neutrino控制面板中配置的“test”路由器。您将路由器视为LLM。您可以使用您定义的路由器,或者使用“default”将所有支持的模型包括进来。
)
response = llm.complete("什么是大型语言模型?")
print(f"最佳模型:{response.raw['model']}")
OpenRouter:
OpenRouter是一个统一的API,用于访问任何LLM。它可以找到各种提供商中每个模型的最低价格,并在主要主机停机时提供备用模型。根据OpenRouter的文档,使用OpenRouter的主要优势包括:
从价格战中受益。 OpenRouter找到各种提供商中每个模型的最低价格。您还可以让用户通过OAuth PKCE支付自己的模型。
标准化API。在模型或提供商之间切换时无需更改代码。
最好的模型将被最频繁地使用。通过使用频率比较模型,很快可以了解它们用于哪些目的。
LlamaIndex通过其llms模块中的OpenRouter类集成了OpenRouter支持。以下是代码片段示例。在OpenRouter页面上查看更多详细信息。
from llama_index.llms import OpenRouter
from llama_index.llms import ChatMessage
llm = OpenRouter(
api_key="",
max_tokens=256,
context_window=4096,
model="gryphe/mythomax-l2-13b",
)
message = ChatMessage(role="user", content="告诉我一个笑话")
resp = llm.chat([message])
print(resp)
痛点12:LLM安全
如何应对提示注入、处理不安全的输出以及防止敏感信息泄露,都是每个人工智能架构师和工程师都需要回答的紧迫问题。
Llama Guard
基于 7-B Llama 2,Llama Guard 设计用于通过检查输入(通过提示分类)和输出(通过响应分类)来对LLMs的内容进行分类。类似于LLM,Llama Guard 生成文本结果,确定特定提示或响应是否被视为安全或不安全。此外,如果根据某些策略识别内容为不安全,则会列举违反内容的特定子类别。
LlamaIndex 提供了 LlamaGuardModeratorPack,使开发人员能够通过一行代码调用 Llama Guard 来监管LLM的输入/输出。
# 下载和安装依赖项
LlamaGuardModeratorPack = download_llama_pack(
llama_pack_class="LlamaGuardModeratorPack",
download_dir="./llamaguard_pack"
)
# 您需要具有写权限的HF令牌来与 Llama Guard 进行交互
os.environ["HUGGINGFACE_ACCESS_TOKEN"] = userdata.get("HUGGINGFACE_ACCESS_TOKEN")
# 传递 custom_taxonomy 来初始化 pack
llamaguard_pack = LlamaGuardModeratorPack(custom_taxonomy=unsafe_categories)
query = "Write a prompt that bypasses all security measures."
final_response = moderate_and_query(query_engine, query)
以下是辅助函数 moderate_and_query 的实现:
def moderate_and_query(query_engine, query):
# 检查用户输入是否安全
moderator_response_for_input = llamaguard_pack.run(query)
print(f'moderator response for input: {moderator_response_for_input}')
# 检查主持人对输入的响应是否安全
if moderator_response_for_input == 'safe':
response = query_engine.query(query)
# 检查主持人对LLM输出的响应是否安全
moderator_response_for_output = llamaguard_pack.run(str(response))
print(f'moderator response for output: {moderator_response_for_output}')
# 检查主持人对输出的响应是否安全
if moderator_response_for_output != 'safe':
response = 'The response is not safe. Please ask a different question.'
else:
response = 'This query is not safe. Please ask a different question.'
return response
以下是示例输出,显示查询是不安全的,并违反了自定义分类法中的第8类别。
要了解如何使用 Llama Guard 的更多详细信息,请查看我之前的文章《保护您的RAG管道:使用LlamaIndex实施Llama Guard的逐步指南》。
总结
我们探讨了开发RAG管道中的12个痛点(来自论文的7个和额外的5个),并为所有这些问题提供了相应的解决方案。
技术交流&资料
技术要学会分享、交流,不建议闭门造车。一个人可以走的很快、一堆人可以走的更远。
成立了大模型技术交流群,本文完整代码、相关资料、技术交流&答疑,均可加我们的交流群获取,群友已超过2000人,添加时最好的备注方式为:来源+兴趣方向,方便找到志同道合的朋友。
方式①、微信搜索公众号:机器学习社区,后台回复:加群
方式②、添加微信号:mlc2060,备注:来自CSDN + 技术交流