微信扫码
与创始人交个朋友
我要投稿
简单地说,RAG 是一种为我们的 LLM 提供额外上下文以生成更好、更具体响应的技术。LLMs是根据公开可用的数据进行训练的,它们是真正的智能系统,但它们无法回答我们的具体问题,因为它们缺乏回答这些查询的上下文。通过 RAG,我们提供了必要的上下文,以便我们可以优化我们出色的 LLM 的使用。
RAG 是一种将新知识或能力插入我们的 LLM 的方法,尽管这种知识插入不是永久性的。向 LLM 添加新知识或功能的另一种方法是通过微调 LLM 到我们的特定数据。
通过微调来增加新知识是相当棘手、艰难、昂贵和永久的。通过微调添加新功能甚至会影响它以前拥有的知识。在微调过程中,我们无法控制哪些权重将被改变,从而无法控制哪些能力将增加或减少。
现在,我们是进行微调、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 管道与特定的痛点进行分类,并单独解决它们。让我们在下一节中看一下具体问题及其解决方案。
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 = {
documents, :
eval_qs, :
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(
query_engine, show_progress=True
)
# run evaluator
eval_batch_runner = _get_eval_batch_runner_semantic_similarity()
eval_results = eval_batch_runner.evaluate_responses(
responses=pred_response_objs, reference=ref_response_strs
)
# get semantic similarity metric
mean_score = np.array(
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 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?",
)
该论文定义了这一点:“带有答案的文档是从数据库中检索的,但没有进入生成答案的上下文中。当从数据库中返回许多文档,并进行合并过程以检索答案时,就会发生这种情况。
更好的检索策略
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/
53AI,企业落地应用大模型首选服务商
产品:大模型应用平台+智能体定制开发+落地咨询服务
承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2024-05-14
2024-04-26
2024-03-30
2024-04-12
2024-05-10
2024-07-18
2024-05-22
2024-05-28
2024-04-25
2024-04-26
2024-11-14
2024-11-13
2024-11-13
2024-11-13
2024-11-12
2024-11-11
2024-11-08
2024-11-07