解决现代RAG系统中的生产问题
作者: IT大咖说 来源: IT大咖说
LLM 很棒,但是我们可以使用它们来回答我们对私人数据的查询吗?这就是检索增强生成或 RAG 的用武之地。RAG的使用一直在迅速增长,因为大多数公司都拥有大量的专有数据,他们希望他们的聊天机器人或其他基于文本的人工智能特定于他们的公司。RAG 是 LLM 的一个非常有趣的用例,它们与 LLM 上下文长度的增加直接竞争,我不知道这两者中的哪一个会占上风。但我确信,许多为创建更好的 RAG 而开发的技术将用于未来的系统,RAG 可能会在几年内消失,也可能不会消失,但一些有趣的技术可能会激发下一代系统的灵感。因此,事不宜迟,让我们来看看创建下一代人工智能系统的细节。
◆什么是RAG?
简单地说,RAG 是一种为我们的 LLM 提供额外上下文以生成更好、更具体响应的技术。LLMs是根据公开可用的数据进行训练的,它们是真正的智能系统,但它们无法回答我们的具体问题,因为它们缺乏回答这些查询的上下文。通过 RAG,我们提供了必要的上下文,以便我们可以优化我们出色的 LLM 的使用。
RAG 是一种将新知识或能力插入我们的 LLM 的方法,尽管这种知识插入不是永久性的。向 LLM 添加新知识或功能的另一种方法是通过微调 LLM 到我们的特定数据。
通过微调来增加新知识是相当棘手、艰难、昂贵和永久的。通过微调添加新功能甚至会影响它以前拥有的知识。在微调过程中,我们无法控制哪些权重将被改变,从而无法控制哪些能力将增加或减少。
现在,我们是进行微调、RAG 还是两者兼而有之,完全取决于手头的任务。没有适合所有人的人。
构建基本的 RAG 管道
◆过程:
-
将文档拆分为偶数块。
-
每个块都是一段原始文本。
-
为每个区块生成嵌入(例如 OpenAl 嵌入、sentence_transformer)
-
将每个块存储在向量数据库中。
-
从矢量数据库集合中查找 Top-k 最相似的块
-
插入 LLM 响应综合模块。
!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 方法往往适用于简单的问题,而不是简单的一小部分文档。
-
“特斯拉的主要风险因素是什么?”(超过特斯拉 2021 10K)
-
“作者在YC期间做了什么?”(保罗·格雷厄姆随笔)
但现实生活很少这么简单。因此,在下一节中,让我们看看挑战和可能的补救措施。然后,定义此类系统的未来。
◆总体挑战
但在我们研究每个痛点之前,让我们先定义一下整体挑战。人工智能系统与当前的软件系统有很大不同。
人工智能驱动的软件由一组黑匣子参数定义。真的很难推理功能空间是什么样子的。对模型参数进行调整,而对周围的参数(提示模板)
进行调整。
如果系统的一个组件是黑匣子,则系统的所有组件都将成为黑匣子。组件越多,我们需要调整的参数
就越多。每个参数都会影响整个 RAG 管道的性能。用户应该调整哪些参数?选择太多了!
◆检索错误
-
低精度: 并非检索到的集合中的所有块都是相关的——幻觉 + 迷失在中间问题
-
低召回率: 现在,所有相关的块都已检索。— 缺乏足够的上下文供 LLM 合成答案
-
过时的信息:数据冗余或已过期。
◆生成不良响应
-
幻觉:模型编造了一个不在上下文中的答案。
-
无关紧要:模型编造了一个不回答问题的答案。
-
毒性/偏见:该模型构成了一个有害/令人反感的答案。
因此,最佳做法是将我们的 RAG 管道与特定的痛点进行分类,并单独解决它们。让我们在下一节中看一下具体问题及其解决方案。
◆现代 RAG 管道的 9 个挑战和解决方案
响应质量相关
1.知识库中缺少上下文
2.初始检索传递中缺少上下文
3.重新排名后缺少上下文
4.未提取上下文
5.输出格式错误
6.输出具有不正确的特异性级别
7。输出不完整
可扩展性
8.无法扩展到更大的数据量
9.速率限制错误
◆知识库中缺少上下文
这很容易理解,你提出的问题需要一些上下文来回答,如果你的 RAG 系统没有选取正确的文档块,或者源数据本身缺少上下文,它只会给出一个通用的答案,不够具体,无法解决用户查询。
我们提出了一些解决方案:
清理数据:
如果你的源数据质量很差,比如包含相互冲突的信息,无论我们构建的RAG管道有多好,它都无法从我们提供给它的垃圾中输出黄金。
有一些常见的策略可以清理数据,仅举几例:
-
消除噪音和不相关的信息: 这包括删除特殊字符、停用词(常用词,如“the”和“a”)和 HTML 标记。
-
识别并更正错误: 这包括拼写错误、拼写错误和语法错误。拼写检查器和语言模型等工具可以帮助解决这个问题。
-
重复数据删除: 删除可能影响检索过程的重复记录或类似记录。
更好的提示:
通过使用诸如“告诉我你不确定答案,请告诉我你不知道”之类的提示来指示系统,您可以鼓励模型承认其局限性并更透明地传达不确定性。不能保证 100% 的准确性,但制作提示是您在清理数据后可以做出的最大努力之一。
添加元数据:
将全局上下文注入每个区块
初始检索传递中缺少上下文
基本文档可能不会出现在系统检索组件返回的顶部结果中。正确答案被忽略,导致系统无法提供准确的响应。该论文暗示,“问题的答案在文档中,但排名不够高,无法返回给用户”。
对于这个痛点,有两种解决方案:
块大小和 top-k 的超参数调整:
和chunk_size都是similarity_top_k用于管理 RAG 模型中数据检索过程的效率和有效性的参数。调整这些参数可以影响计算效率和检索信息质量之间的权衡
# 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 个节点而不使用 reranker 来检索不准确。
-
通过检索前 10 个节点并使用 CohereRerank 重新排名和返回前 2 个节点来准确检索。
import os
from llama_index.postprocessor.cohere_rerank import CohereRerankapi_key = os.environ[“COHERE_API_KEY”]
cohere_rerank = CohereRerank(api_key=api_key, top_n=2) # return top 2 nodes from rerankerquery_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?”,
)
◆重新排名后缺少上下文
该论文定义了这一点:“带有答案的文档是从数据库中检索的,但没有进入生成答案的上下文中。当从数据库中返回许多文档,并进行合并过程以检索答案时,就会发生这种情况。
更好的检索策略
LlamaIndex 提供了一系列从基本到高级的检索策略,以帮助我们在 RAG 管道中实现准确的检索。
-
从每个索引进行基本检索
-
高级检索和搜索
-
自动检索
-
知识图谱检索器
-
组合/分层检索器
◆微调嵌入
如果模型在更改检索策略后仍表现不佳,我们应该根据数据微调模型,从而为 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)
◆LongContext 重新排序
一项研究发现,当关键数据位于输入上下文的开头或结尾时,通常会出现最佳性能。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?")
◆输出格式错误
许多用例需要以 JSON 格式输出答案。
-
更好的文本提示/输出解析。
-
使用 OpenAI 函数调用 + JSON 模式
-
使用令牌级提示(LMQL、指导)
LlamaIndex 支持与其他框架(如 Guardrails 和 LangChain)提供的输出解析模块集成。
请参阅下面的 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 程序:这些程序通过利用调用 API 的 LLM 函数,获取输入文本并将其转换为用户指定的结构化对象。
-
预打包的 Pydantic 程序:这些程序旨在将输入文本转换为预定义的结构化对象。
OpenAI JSON 模式使我们能够设置为为响应启用 JSON 模式。启用 JSON 模式后,模型被限制为仅生成分析为有效 JSON 对象的字符串。虽然 JSON 模式强制执行输出的格式,但它无助于针对指定架构进行验证。有关更多详细信息,请查看 LlamaIndex 关于 OpenAI JSON 模式与函数调用数据提取的文档。
◆输出具有不正确的特异性级别
答复可能缺乏必要的细节或具体性,通常需要后续查询才能澄清。答案可能过于模糊或笼统,无法有效满足用户的需求。
◆高级检索策略
当答案未达到预期的正确粒度级别时,可以改进检索策略。一些可能有助于解决这一痛点的主要高级检索策略包括:
-
从小到大的检索
-
句子窗口检索
-
递归检索
◆输出不完整
部分回应没有错;但是,它们并未提供所有详细信息,尽管信息在上下文中存在且可访问。例如,如果有人问,“文件A、B和C中讨论的主要方面是什么?”,那么单独询问每个文件以确保得到全面的答案可能会更有效。
◆查询转换
比较问题在幼稚的 RAG 方法中尤其表现不佳。提高 RAG 推理能力的一个好方法是添加查询理解层,即在实际查询向量存储之前添加查询转换。下面是四种不同的查询转换:
-
路由:保留初始查询,同时精确定位与其相关的相应工具子集。然后,将这些工具指定为合适的选项。
-
查询重写:保留所选工具,但以多种方式重新表述查询,以便将其应用于同一组工具。
-
子问题:将查询分解为几个较小的问题,每个问题都针对由其元数据确定的不同工具。
-
ReAct 代理工具选择:根据原始查询,确定要使用的工具,并制定要在该工具上运行的特定查询。
添加代理工具
◆案可扩展性
◆无法扩展到更大的数据量
处理数千/数百万个文档很慢。另一个问题是我们如何有效地处理文档更新?简单的引入管道无法扩展到更大的数据量。
◆并行化引入管道
-
并行化文档处理
-
HuggingFace TEI
-
RabbitMQ 消息队列
-
AWS EKS 集群
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 部分,在下一篇博客中,我们将深入探讨如何处理表和其他高级内容,如何使用缓存等。
◆资源:
Hyperparameter Optimization for RAG:https://docs.llamaindex.ai/en/stable/examples/param_optimizer/param_optimizer/
Cleaning:https://unstructured-io.github.io/unstructured/core/cleaning.html
设计检索增强生成系统时的七个故障点:https://arxiv.org/pdf/2401.05856.pdf
Boosting RAG: Picking the Best Embedding & Reranker models:https://www.llamaindex.ai/blog/boosting-rag-picking-the-best-embedding-reranker-models-42d079022e83
Cohere Rerank:https://docs.llamaindex.ai/en/stable/examples/node_postprocessor/CohereRerank/
LongLLMLingua 研究项目:https://arxiv.org/abs/2310.06839
Output Parsing Modules:https://docs.llamaindex.ai/en/stable/module_guides/querying/structured_outputs/output_parser/
来源:https://www.toutiao.com/article/7353822815373558311/?log_from=8ba50a65dc279_1712453217671
“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:[email protected]
来都来了,走啥走,留个言呗~
** IT大咖说 |**** 关于版权******
**由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。**投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!
感谢您对IT大咖说的热心支持!
相关推荐
推荐文章
更多AI工具,参考Github-AiBard123,国内AiBard123