RAG系统开发中的关键挑战与有效解决方案

受Barnett等人所著的《Seven Failure Points When Engineering a Retrieval Augmented Generation System》一文的启发,本文将深入探讨RAG(检索增强生成)系统开发中遇到的若干关键问题,及其相应的解决策略,以期在日常开发中更有效地应对这些挑战。

格式问题

当我们要求计算机以特定格式(如表格或JSON)整理信息时,常常会发现大型语言模型(LLM)未能如预期输出结果。为了更好地指导计算机理解需求,可以通过以下方法确保输出格式的准确性:

更精确的提示

  • 使指令更加明确。
  • 简化问题并突出关键词。
  • 提供具体示例。
  • 通过不断提问逐步细化问题。

解析输出

  • 为每个查询提供格式化指南。
  • 对计算机回答进行解析。

LlamaIndex支持与其他框架(如Guardrails和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程序文档:链接

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="The Shining",   
    description="专辑的数据模型。"  
)  

OpenAI JSON模式

OpenAI的JSON模式允许设置response_format为{ "type": "json_object" },以便激活响应的JSON模式。启用此模式后,计算机将仅生成能够解析为有效JSON对象的字符串。尽管JSON模式规定了输出格式,但并不保证内容符合特定规范。更多信息请参阅LlamaIndex关于OpenAI JSON模式与数据提取功能调用的文档。

内容缺失问题

当知识库中缺乏实际答案时,RAG系统往往会给出似乎合理但错误的答案,而不是承认无法回答。这会导致用户接收到误导性信息,产生错误引导。如何解决内容缺失问题?

1. 优化数据源

“输入什么,输出什么。”如果源数据质量较差,充斥着冲突信息,那么无论如何构建RAG流程,都无法从杂乱无序的数据中获得有价值的结果。

2. 改进提示方式

在知识库信息不足的情况下,改进提示方式可以显著提升效果。例如,可以设置提示“如果你无法确定答案,请表明你不知道”,鼓励模型认识到自身的局限性并更透明地表达不确定性。虽然无法保证完全准确,但在优化数据源之后,改进提示方式是我们可以采取的最佳措施之一。

错过排名靠前的文档

有时系统在检索信息时,最重要的文档未能出现在结果的顶部。这导致正确答案被忽略,系统因此无法提供准确的回答。

即:“问题的答案其实在某个文档里,只是它没有获得足够高的排名以至于没能呈现给用户

为了解决这个问题,我们可以考虑两种思路:

  1. 重新排名检索结果

在将检索到的结果发送给大型语言模型(LLM)之前,对这些结果进行重新排名可以显著提升RAG的性能。LlamaIndex的一个笔记本展示了两种不同方法的效果对比:

  • 直接检索前两个节点而不进行重新排名,可能导致不准确的检索结果。
  • 先检索前十个节点,然后使用CohereRerank进行重新排名,最后返回前两个节点,这种方法可以提高检索准确性。
  1. 调整chunk_size和相似度排名similarity_top_k

chunk_size和similarity_top_k是用来调节RAG(检索增强生成)模型数据检索过程中效率与效果的参数。改动这些参数能够影响计算效率与信息检索质量之间的平衡。

脱离上下文 — 整合策略的限制

论文提到:“虽然数据库检索到了含有答案的文档,但这些文档并没有被用来生成答案。这种情况往往出现在数据库返回大量文档后,需要通过一个整合过程来找出答案。”我们的解决方案有哪些呢?

  1. 优化检索策略

以LlamaIndex为例,该平台提供了一系列从基础到高级的检索策略,以帮助我们在RAG流程中实现精准检索。欲了解所有检索策略的详细分类,请查阅retrievers模块的指南。

  • 从每个索引进行基础检索
  • 进行高级检索和搜索
  • 自动检索
  • 知识图谱检索器
  • 组合/分层检索器
  • 更多其他选项!
  1. 微调嵌入模型

如果使用开源嵌入模型,对其进行微调是提高检索准确性的有效方法。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()  

未能提取答案

当系统需要从提供的上下文中提取正确答案时,尤其是在信息量庞大的情况下,系统常常会遇到困难。关键信息被遗漏,从而影响回答的质量。论文中提到:“这种情况通常是由于上下文中存在太多干扰信息或相互矛盾的信息”。如何解决未能提取答案的问题呢?

  1. 清理数据

这一痛点再次凸显了数据质量的重要性。再次强调,干净整洁的数据至关重要!在质疑RAG流程之前,确保先清理数据。

  1. 上下文压缩

LongLLMLingua研究项目/论文中提出了长上下文设置中的提示压缩技术。通过将其集成到LlamaIndex中,我们可以将LongLLMLingua作为节点后处理步骤,在检索步骤后压缩上下文,然后再将其输入大语言模型。以下是一个设置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="鉴于上下文,请回答最后一个问题",  
     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)  
  1. LongContextReorder重排

一项研究(链接)发现,当关键信息位于输入上下文的开始或结束时,通常能得到最好的性能。为了解决信息“丢失在中间”的问题,LongContextReorder被设计用来重新排序检索到的节点,这种方法在需要大量top-k结果时特别有效。以下是如何定义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("作者见过山姆·奥尔特曼吗?")  

回答过于宽泛

有时候,我们得到的回答可能缺乏必要的细节或特定性,这通常需要我们进一步提问以获取清晰的信息。部分答案可能过于模糊,无法有效满足用户的实际需求。为此,我们需要采用更高级的检索技巧。

当答案未达到期望的详细程度时,可以通过提升检索技巧来改善这一状况。以下是一些有助于解决这个问题的先进检索方法:

  1. 从细节到全局的检索:链接
  2. 围绕特定句子的检索:链接
  3. 逐步深入的检索:链接

回答不全面

有时,我们得到的只是部分答案,这并不是说它们是错误的,而是没有提供所有必要的细节,尽管这些信息实际上是存在并且可以获取的。例如,如果有人问:“文档A、B和C中都讨论了哪些主要内容?”针对每份文档分别提问可能会得到更为全面的答案。那么如何解决回答不全面的问题呢?

在简单的RAG模型中,比较性问题往往处理得不够好。一个提升RAG推理能力的有效方法是加入查询理解层——即在实际进行向量存储查询之前进行查询优化。以下是四种不同的查询优化方式:

  • 路由优化:保留原始查询内容,并明确它所涉及的特定工具子集。然后,将这些工具指定为合适的选择。
  • 查询改写:保持选定工具不变,但重新构思多种查询方式,以适应同一组工具。
  • 细分问题:将大问题拆分成几个小问题,每个小问题都针对根据元数据确定的不同工具。
  • ReAct Agent工具选择:根据原始查询内容,确定使用哪个工具,并构造针对该工具的特定查询。

关于如何使用假设性文档嵌入(HyDE)这一查询改写技术,您可以参考下方示例代码。在这种方法中,我们首先根据自然语言查询生成一个假设性文档或答案。然后,我们使用这个假设性文档进行嵌入式查找,而不是直接使用原始查询。具体可以参考:链接

数据处理能力的挑战

在RAG技术流程中,处理大量数据时常遇到一个难题:如果系统无法高效管理和加工这些数据,就可能导致性能瓶颈甚至系统崩溃。这种处理能力上的挑战可能导致数据处理时间大幅延长、系统超负荷运转、数据质量下降和服务可用性降低。

LlamaIndex推出了一种数据处理的并行技术,能够使文档处理速度最高提升15倍。以下代码示例展示了如何创建数据处理流程并设置num_workers,以实现并行处理。

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)  

总结

在开发RAG应用时,我遇到了许多痛点。由于篇幅有限,我这里只提到其中的八个问题。然而,实际上这些问题仅仅是冰山一角,RAG领域的挑战远不止于此。每一个痛点都值得深入探讨和解决。