Image adapted from Seven Failure Points When Engineering a Retrieval Augmented Generation System
· 痛点1:内容缺失
· 痛点2:错过了排名靠前的文件
· 痛点3:不在上下文中 — 合并策略的局限性
· 痛点4:未提取
· 痛点 5: Wrong Format
· 痛点6: 不正确的特异性
· 痛点7:不完整
· 痛点8:数据摄入可扩展性
· 痛点9: 结构化数据质量保证
· 痛点10:从复杂的PDF中提取数据
· 痛点11:备用模型(或模型)
· 痛点12: LLM 安全
受Barnett等人的论文《工程检索增强生成系统的七个失败点》的启发,让我们在本文中探讨论文中提到的七个失败点以及开发RAG管道中的五个常见痛点。
更重要的是,我们将深入探讨这些RAG痛点的解决方案,这样我们就能更好地应对我们日常RAG开发中的这些痛点。
我使用“痛点”而不是“失败点”,主要是因为这些点都有相应的解决方案。在我们的RAG管道中,让我们试着修复它们,避免它们变成失败。
请问这段文字是要翻译成哪种语言呢?
图像来源:工程检索增强生成系统时的七个失败点
RAG系统在知识库中没有实际答案时会提供一个似是而非的答案,而不是声明它不知道。用户会收到误导性信息,导致沮丧。
我们有两个提议的解决方案:
垃圾进,垃圾出。如果您的源数据质量很差,比如包含矛盾信息,无论您如何构建RAG管道,它也无法将您输入的垃圾变成金子。
本文提出的解决方案不仅可以解决这一痛点,还可以解决本文列出的所有痛点。干净的数据是任何运行良好的 RAG 管道的先决条件。
更好的提示可以在系统可能因知识库中信息不足而提供一个似是而非的答案的情况下,显著地帮助。
通过这样的提示来指导系统,比如“告诉我你不知道,如果你对答案不确定”,你鼓励模型承认自己的局限性,并更透明地表达不确定性。
虽然不能保证 100% 的准确性,但制作提示语是您在清理数据后所能做的最佳努力之一。
重要文件可能不会出现在系统检索组件返回的顶部结果中。正确答案被忽略,导致系统无法提供准确的回复。
这篇论文暗示:“问题的答案在文件中,但排名不够高,无法返回给用户”。
两个提议的解决方案浮现在我脑海中:
similarity_top_k
chunk_size
and 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
及其对应的建议值:
# 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)
请参考LlamaIndex关于RAG超参数优化的完整笔记本获取更多详细信息。
在将检索结果发送给 LLM 之前对其进行Reranking大大提高了 RAG 的性能。这本 LlamaIndex notebook 展示了这两者之间的区别:
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 Sam Altman do in this essay?",
)
此外,您可以微调自定义的重新排序器,以获得更好的检索性能,详细的实现已在《通过与LlamaIndex微调Cohere Reranker来改善检索性能》中记录
这篇论文定义了这一点:“从数据库中检索到了带有答案的文档,但并未用于生成答案的上下文中。
这种情况发生在从数据库返回许多文档时,然后进行合并处理以获取答案。
除了按照上述部分所述添加重新排序器并对重新排序器进行微调之外,我们还可以探索以下提出的解决方案:
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()
系统在处理提供的上下文时很难提取出正确的答案,特别是在信息过载的情况下。关键细节被忽略,影响了回答的质量。
这篇论文暗示:“当环境中存在太多噪音或矛盾信息时,就会发生这种情况”。
让我们探讨三种提议的解决方案:
这一痛点是又一个典型的受到糟糕数据影响的受害者。我们无法再次强调数据清洁的重要性!在责怪你的RAG管道之前,请花时间清理你的数据。
LongLLMLingua 研究项目/论文引入了长语境设置中的提示压缩。通过将其集成到 LlamaIndex 中,我们现在可以将 LongLLMLingua 作为节点后处理器来实现,它将在检索步骤后压缩上下文,然后再将其输入 LLM。
请参见下面的示例代码片段,其中我们设置了 LongLLMLinguaPostprocessor
,它使用 longllmlingua
包来运行提示压缩。
请查看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="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 的情况下可能会有所帮助。
请参见以下示例代码片段,了解在查询引擎构建过程中如何将 LongContextReorder
定义为您的 node_postprocessor
。有关更多详细信息,请参阅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("Did the author meet Sam Altman?")
当LLM忽略提取特定格式(如表格或列表)信息的指令时,我们有四个建议的解决方案可供探讨:
有几种策略可以采用来改善您的提示并纠正这个问题:
Output parsing可以通过以下方式来帮助确保所需的输出:
LlamaIndex支持与其他框架提供的输出解析模块集成,例如Guardrails和LangChain。
请参阅LangChain输出解析模块的示例代码片段,您可以在LlamaIndex中使用。有关更多详细信息,请查看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
# 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
from llama_index import ServiceContext
ctx = ServiceContext.from_defaults(llm=llm)
query_engine = index.as_query_engine(service_context=ctx)
response = query_engine.query(
"What are a few things the author did growing up?",
)
print(str(response))
Pydantic 程序是一个多功能框架,可将输入字符串转换为结构化的 Pydantic 对象。LlamaIndex 提供几类 Pydantic 程序:
请参见OpenAI pydantic程序中的示例代码片段。有关更多详细信息,请查看LlamaIndex关于pydantic程序的文档,以获取不同pydantic程序的笔记本/指南链接。
from pydantic import BaseModel
from typing import List
from llama_index.program 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."
)
OpenAI JSON模式使我们能够设置 response_format
为 { "type": "json_object" }
,以启用响应的JSON模式。启用JSON模式时,模型受限于仅生成解析为有效JSON对象的字符串。虽然JSON模式强制输出的格式,但它并不帮助验证指定的模式。
请查看LlamaIndex关于OpenAI JSON模式与函数调用进行数据提取的文档,以获取更多详细信息。
这些回答可能缺乏必要的细节或具体性,通常需要后续的查询来进行澄清。答案可能过于模糊或泛泛,未能有效满足用户的需求。
我们转向高级检索策略寻求解决方案。
当答案的粒度不符合您的期望时,您可以改进检索策略。一些可能有助于解决这一痛点的主要高级检索策略包括:
请查看我的最新文章《使用高级检索LlamaPacks来启动您的RAG管道,并使用Lighthouz AI进行基准测试》,了解更多关于七种高级检索LlamaPacks的详细信息。
部分回答并不是错误;然而,尽管信息已经存在并且在上下文中可以获取,但它们并没有提供所有的细节。
例如,如果有人问:“文件A、B和C中讨论的主要方面是什么?”单独询问每个文件可能更有效,以确保得到全面的答案。
比较问题在最原始的 RAG 方法中表现尤为糟糕。提高 RAG 推理能力的一个好方法是添加查询理解层--在实际查询向量存储之前添加查询转换。以下是四种不同的查询转换:
请参见以下示例代码片段,演示如何使用HyDE(假设性文档嵌入),一种查询重写技术。给定自然语言查询,首先生成一个假设性文档/答案。
这个假设性文件随后用于嵌入查找,而不是原始查询。
# load documents, build index
documents = SimpleDirectoryReader("../paul_graham_essay/data").load_data()
index = VectorStoreIndex(documents)
# run query with HyDE query transform
query_str = "what did paul graham do after going to 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的查询转换手册,了解所有细节。
此外,请查看这篇很棒的文章 Advanced Query Transformations to Improve RAG ,作者是
Iulia Brezeanu
以上痛点均来自于该论文。现在,让我们探讨在RAG开发中常见的另外五个痛点及其提出的解决方案。
数据摄入可伸缩性问题指的是系统在努力高效管理和处理大量数据时出现的挑战,导致性能瓶颈和潜在的系统故障。
这种数据摄入可扩展性问题可能导致摄入时间延长、系统超载、数据质量问题和可用性受限。
LlamaIndex提供摄取管道并行处理功能,该功能可在LlamaIndex中实现高达15倍的更快文档处理速度。请查看下面的示例代码片段,了解如何创建 IngestionPipeline
并指定 num_workers
以调用并行处理。查看LlamaIndex的完整笔记本以获取更多详情。
# 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)
准确解释用户查询以检索相关的结构化数据可能会很困难,特别是对于复杂或含糊不清的查询、不灵活的文本到SQL转换以及当前LLMs在有效处理这些任务方面的限制。
LlamaIndex提供两种解决方案。
ChainOfTablePack
是基于Wang et al.创新的“链表”论文的LlamaPack。“链表”将思维链的概念与表格转换和表示相结合。它使用一组受限制的操作逐步转换表格,并在每个阶段向LLM呈现修改后的表格。
这种方法的一个重要优势是它能够通过系统地切分和分析数据来解决涉及包含多个信息片段的复杂表格单元的问题,从而提高表格问答的效果。
请查看LlamaIndex的完整笔记本,了解如何使用 ChainOfTablePack
来查询您的结构化数据的详细信息。
LLMs可以通过两种主要方式推理表格数据:
基于刘等人的论文《重新思考大型语言模型对表格数据的理解》,LlamaIndex开发了 MixSelfConsistencyQueryEngine
,该模型利用自洽机制(即多数投票)汇总了文本和符号推理的结果,并取得了最先进的性能。请查看下面的示例代码片段。查看LlamaIndex的完整笔记本以获取更多详情。
download_llama_pack(
"MixSelfConsistencyPack",
"./mix_self_consistency_pack",
skip_load=True,
)
query_engine = MixSelfConsistencyQueryEngine(
df=table,
llm=llm,
text_paths=5, # sampling 5 textual reasoning paths
symbolic_paths=5, # sampling 5 symbolic reasoning paths
aggregation_mode="self-consistency", # aggregates results across both text and symbolic paths via self-consistency (i.e. majority voting)
verbose=True,
)
response = await query_engine.aquery(example["utterance"])
您可能需要从复杂的PDF文档中提取数据,例如嵌入的表格,用于问答。天真的检索方式无法从这些嵌入的表格中获取数据。您需要一种更好的方式来检索这样复杂的PDF数据。
LlamaIndex提供了一个解决方案EmbeddedTablesUnstructuredRetrieverPack,即使用Unstructured.io解析HTML文档中的嵌入表格,构建节点图,然后使用递归检索来根据用户问题索引/检索表格的LlamaPack。
请注意,此包将HTML文档作为输入。如果您有PDF文档,可以使用pdf2htmlEX将PDF转换为HTML,而不会丢失文本或格式。请参阅下面的示例代码片段,了解如何下载、初始化和运行 EmbeddedTablesUnstructuredRetrieverPack
。
# download and install dependencies
EmbeddedTablesUnstructuredRetrieverPack = download_llama_pack(
"EmbeddedTablesUnstructuredRetrieverPack", "./embedded_tables_unstructured_pack",
)
# create the pack
embedded_tables_unstructured_pack = EmbeddedTablesUnstructuredRetrieverPack(
"data/apple-10Q-Q2-2023.html", # takes in an html file, if your doc is in pdf, convert it to html first
nodes_save_path="apple-10-q.pkl"
)
# run the pack
response = embedded_tables_unstructured_pack.run("What's the total operating expenses?").response
display(Markdown(f"{response}"))
在使用 LLMs 时,您可能会想知道,如果您的模型遇到问题,比如与OpenAI的模型发生速率限制错误,您需要一个备用模型作为主要模型发生故障的替代方案。
2个建设的优化方式:
Neutrino 路由器是 LLM 的集合,您可以将查询路由到它。它使用预测模型将查询智能地路由到最适合提示的 LLM,在优化成本和延迟的同时最大限度地提高性能。Neutrino 目前支持十几种模型。如果您希望在支持的机型列表中添加新的机型,请联系他们的支持人员。
您可以在Neutrino仪表板中创建一个路由器来手动选择您喜欢的模型,或者使用“default”路由器,其中包括所有支持的模型。
LlamaIndex已通过其 Neutrino
类在 llms
模块中集成了Neutrino支持。请查看下面的代码片段。在Neutrino AI页面上查看更多详情。
from llama_index.llms import Neutrino
from llama_index.llms import ChatMessage
llm = Neutrino(
api_key="",
router="test" # A "test" router configured in Neutrino dashboard. You treat a router as a LLM. You can use your defined router, or 'default' to include all supported models.
)
response = llm.complete("What is large language model?")
print(f"Optimal model: {response.raw['model']}")
OpenRouter是访问任何LLM的统一API。它为任何型号找到最低价格,并在主机宕机时提供备用方案。根据OpenRouter的文档,使用OpenRouter的主要好处包括:
请问您需要将 "Benefit from the race to the bottom. OpenRouter finds the lowest price for each model across dozens of providers. You can also let users pay for their own models via OAuth PKCE." 翻译成哪种语言?
Standardized API. 切换模型或供应商时无需更改您的代码。
最好的模型将被最多地使用。通过它们的使用频率进行比较,很快就会知道它们被用于什么目的。
LlamaIndex已经通过其 OpenRouter
类在 llms
模块中集成了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="Tell me a joke")
resp = llm.chat([message])
print(resp)
如何对抗提示注入,处理不安全的输出,并防止敏感信息泄露,都是每个AI架构师和工程师都需要回答的紧迫问题。
基于7-B Llama 2,Llama Guard旨在通过检查输入(通过提示分类)和输出(通过响应分类)来对LLMs的内容进行分类。
Llama Guard 的功能与 LLM 相似,它能生成文本结果,以确定特定提示或回复是安全的还是不安全的。
另外,如果根据某些政策将内容识别为不安全,它将列举出内容违反的具体子类别。
LlamaIndex提供 LlamaGuardModeratorPack
,使开发人员可以在下载和初始化包之后通过一行代码调用Llama Guard来调节LLM的输入/输出。
# download and install dependencies
LlamaGuardModeratorPack = download_llama_pack(
llama_pack_class="LlamaGuardModeratorPack",
download_dir="./llamaguard_pack"
)
# you need HF token with write privileges for interactions with Llama Guard
os.environ["HUGGINGFACE_ACCESS_TOKEN"] = userdata.get("HUGGINGFACE_ACCESS_TOKEN")
# pass in custom_taxonomy to initialize the 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):
# Moderate the user input
moderator_response_for_input = llamaguard_pack.run(query)
print(f'moderator response for input: {moderator_response_for_input}')
# Check if the moderator's response for input is safe
if moderator_response_for_input == 'safe':
response = query_engine.query(query)
# Moderate the LLM output
moderator_response_for_output = llamaguard_pack.run(str(response))
print(f'moderator response for output: {moderator_response_for_output}')
# Check if the moderator's response for output is safe
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。
请参阅我的上一篇文章《保护您的RAG管道:使用Llama Guard和LlamaIndex的逐步实施指南》,了解如何使用Llama Guard的更多细节。
我们探讨了开发RAG管道中的12个痛点(其中7个来自论文,另外5个是额外的),并为所有这些问题提供了相应的解决方案。请参见下面的图表,该图表改编自论文《构建检索增强生成系统时的七个失败点》中的原始图表。
Image adapted from Seven Failure Points When Engineering a Retrieval Augmented Generation System
将所有12个RAG痛点及其提出的解决方案并列在一张表中,我们现在有:
标有星号的痛点摘自论文《设计检索增强生成系统时的七个失败点》
这份清单并非详尽无遗,但旨在阐明RAG系统设计和实施的多方面挑战。我的目标是促进更深入的理解,并鼓励开发更健壮、适用于生产的RAG应用程序。
Happy coding!