微信扫码
与创始人交个朋友
我要投稿
LLMs非常出色,但能否利用它们处理我们的私有数据查询呢?这就是检索增强生成(Retrieval Augmented Generation,RAG)发挥作用的地方。由于大多数公司拥有大量专有数据,他们希望聊天机器人或其他基于文本的AI能针对其公司定制,因此RAG的使用正迅速增长。RAG是LLMs的一个非常有趣的用例,它们与LLMs的上下文长度增加形成了直接竞争,我不知道两者中哪个会占上风。但我相信,为改进RAG开发的许多技术将被应用于未来的系统中。RAG可能在几年后消失,也可能不会,但一些有趣的技术可能会启发下一代系统。所以,让我们直接深入探讨创建下一代AI系统的细节吧。
简单来说,RAG是一种技术,用于为我们的大型语言模型(LLMs)提供额外的上下文,以便生成更准确、更具体的回答。LLMs是基于公开可用数据训练的,它们是非常智能的系统,但无法回答特定问题,因为它们缺乏回答这些查询所需的上下文。通过RAG,我们可以提供必要的上下文,从而更好地利用我们出色的LLMs。
RAG是一种向LLMs插入新知识或能力的方法,尽管这种知识插入不是永久性的。另一种向LLMs添加新知识或能力的方法是通过针对特定数据微调LLMs。
通过微调添加新知识相当复杂、困难、昂贵,并且是永久性的。通过微调添加新能力甚至会影响它之前拥有的知识。在微调过程中,我们无法控制哪些权重会改变,因此哪些能力会增加或减少。
现在,我们选择微调、RAG还是两者的结合,完全取决于手头的任务。没有放之四海而皆准的解决方案。
流程:
将文档分割成相等的片段。
每个片段是原始文本的一部分。
为每个片段生成嵌入向量(如OpenAl嵌入、sentence_transformer)
将每个片段存储在向量数据库中。
从向量数据库集合中找到最相似的Top-k片段。
将这些片段接入LLM响应合成模块。
简单的RAG
!pip install llama-index
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
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)
documents = SimpleDirectoryReader(input_files=['pg_essay.txt']).load_data()
index = VectorStoreIndex.from_documents(documents)
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库创建一个简单的RAG管道。
对于简单问题和小规模简单文档集,朴素的RAG方法通常效果良好。
- “特斯拉的主要风险因素是什么?”(基于特斯拉2021年10K报告)
- “作者在YC期间做了什么?”(保罗·格雷厄姆的文章)
但现实生活往往没这么简单。因此,下一部分我们将探讨挑战和可能的解决方案。稍后,我们将定义此类系统的未来。
在分析每个问题之前,我们先定义一下整体挑战。人工智能系统与当前的软件系统有很大不同。
由AI驱动的软件是由一组黑盒参数定义的。很难理解其函数空间是什么样子。模型参数经过调整,而周围的参数(提示模板)则没有。
如果系统的一个组件是黑盒,那么整个系统的组件都变成了黑盒。组件越多,需要调整的参数就越多。每个参数都会影响RAG管道的整体性能。用户应该调整哪些参数呢?选择太多了!
精度低: 检索集中并非所有片段都相关
— 幻觉 + 中间迷失问题
召回率低: 并非所有相关片段都被检索到。
— 缺乏足够的上下文供LLM综合答案
信息过时: 数据冗余或过时。
幻觉:模型给出了与上下文不符的答案。
不相关:模型给出的答案未回答问题。
毒性/偏见:模型给出了有害或冒犯性的答案。
因此,最佳做法是将我们的RAG管道分类为特定问题,并分别解决。让我们在下一部分看看具体问题及其解决方案。
1. 知识库中缺少上下文
2. 初始检索阶段缺少上下文
3. 重新排序后仍缺少上下文
4. 上下文未被提取
5. 输出格式错误
6. 输出的精确度不正确
7. 输出不完整
无法扩展到更大数据量
速率限制错误
今天,我们正审视这些工程挑战,并试图提出一种新的创新方法。
这个问题很容易理解,你提出的问题需要一些上下文才能得到回答。如果RAG系统没有正确抓取文档片段,或者源数据本身缺少上下文,它只会给出一个泛泛的答案,不足以解决用户查询。
我们有几项提议的解决方案:
清理数据:
如果你的源数据质量差,比如包含冲突信息,那么无论我们的RAG管道构建得多么好,也无法从输入的垃圾中产出黄金。
清理数据的一些常见策略包括:
移除噪声和无关信息: 这包括删除特殊字符、停用词(如“the”和“a”)和HTML标签。
识别并修正错误: 包括拼写错误、打字错误和语法错误。拼写检查器和语言模型等工具可以帮助完成这项工作。
去重: 删除重复记录或可能影响检索过程的相似记录。
更好的提示:
通过使用如“如果你不确定答案,请告诉我你不知道”的提示,可以引导系统承认其局限性,并更透明地传达不确定性。不能保证100%的准确性,但在清理数据后,精心设计提示是你可以做的最佳努力之一。
添加元数据:
为每个片段注入全局上下文
关键文档可能不会出现在系统检索组件返回的最顶部结果中。因此,正确答案被忽略,导致系统无法提供准确的响应。论文指出,“问题的答案在文档中,但排名不够高,无法返回给用户”。
针对这一问题,有两个解决方案:
超参数调整:块大小(chunk_size)和前k个(top-k)
chunk_size
和 similarity_top_k
是用于管理RAG模型中数据检索过程效率和效果的参数。调整这些参数会影响计算效率与检索信息质量之间的权衡。LlamaIndex提供了对此的支持,详情请参阅以下文章。
查看文档以进行超参数调整。
param_dict = {"chunk_size": [256, 512, 1024], "top_k": [1, 2, 5]}
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"]
index = _build_index(chunk_size, docs)
query_engine = index.as_query_engine(similarity_top_k=top_k)
pred_response_objs = get_responses(
eval_qs, query_engine, show_progress=True
)
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
)
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()
重新排名(Reranking)
在将检索结果发送给LLM之前重新排名,显著提高了RAG的性能。LlamaIndex的笔记本展示了以下差异:
不使用重新排名直接检索前2个节点的不准确检索。
使用CohereRerank
重新排名并返回前2个节点的准确检索(首先检索前10个节点)。
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)
query_engine = index.as_query_engine(
similarity_top_k=10,
node_postprocessors=[cohere_rerank],
)
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="请根据上下文回答最后的问题",
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()
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
一项研究[2307.03172]发现,最佳性能通常出现在关键数据位于输入上下文的开始或结束时。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("作者是否见过Sam Altman?")
更好的上下文放在开头提示压缩和LongContextReorder
许多应用场景需要以JSON格式输出答案。
更好的文本提示/输出解析。
使用OpenAI函数调用+ JSON模式
使用令牌级提示(LMQL,Guidance)
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
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 = OpenAI(output_parser=output_parser)
query_engine = index.as_query_engine(llm=llm)
response = query_engine.query(
"作者成长过程中做了哪些事情?"
)
print(str(response))
Pydantic 提供了对LLMs输出结构化的良好支持。
from pydantic import BaseModel
from typing import List
from llama_index.program.openai import OpenAIPydanticProgram
class Song(BaseModel):
title: str
length_seconds: int
class Album(BaseModel):
name: str
artist: str
songs: List[Song]
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="专辑数据模型。"
)
这将把LLM的数据填充到类对象中。
LLM文本完成Pydantic程序:这些程序处理输入文本,并将其转换为用户定义的结构化对象,利用文本完成API结合输出解析。
LLM函数调用Pydantic程序:这些程序接受输入文本,并将其转换为用户指定的结构化对象,通过利用LLM函数调用API。
预包装的Pydantic程序:设计用于将输入文本转换为预定义的结构化对象。
通过设置 response_format
为 { "type": "json_object" }
,OpenAI JSON模式允许启用JSON模式响应。启用JSON模式后,模型被限制仅生成可解析为有效JSON对象的字符串。虽然JSON模式强制输出格式,但不帮助验证指定的模式。更多详情,请参阅LlamaIndex关于OpenAI JSON模式与数据提取的函数调用的文档。
回答可能缺乏必要的详细信息或具体性,通常需要后续查询以进行澄清。答案可能过于模糊或笼统,无法有效地满足用户的需求。
高级检索策略
当答案的粒度不是你期望的级别时,你可以改进检索策略。以下一些主要的高级检索策略可能有助于解决这一问题:
从小到大检索
句子窗口检索
递归检索
部分回答虽然没有错误,但没有提供所有细节,尽管在上下文中信息是完整且可获取的。例如,如果问“文档A、B和C中讨论的主要方面是什么?”可能更有效的方式是分别针对每个文档提问,以确保答案全面。
查询转换
尤其是比较型问题,在朴素的RAG方法中表现不佳。提高RAG推理能力的一个好方法是在实际查询向量存储之前添加一个查询理解层——进行查询转换。以下是四种不同的查询转换:
路由:保留原始查询,同时确定与之相关的适当工具子集。然后,将这些工具指定为合适的选择。
查询重写:保持选定的工具,但以多种方式重新构建查询,以便在相同工具集上应用。
子问题:将查询分解为几个较小的问题,每个问题针对由其元数据确定的不同工具。
ReAct 代理工具选择:根据原始查询,确定要使用的工具并为该工具制定具体查询。
添加代理工具
请查看LlamaIndex的查询转换食谱以获取所有详细信息。
另外,可以阅读Iulia Brezeanu的精彩文章改进RAG的高级查询转换,了解更多关于查询转换技术的详情。
处理成千上万或数百万份文档的速度很慢。另一个问题是,我们如何高效地处理文档更新?简单的摄入管道无法扩展到更大的数据量。
并行处理文档
HuggingFace TEI
RabbitMQ 消息队列
AWS EKS 集群
LlamaIndex 提供了并行处理的摄入管道功能,使 LlamaIndex 的文档处理速度提高高达 15 倍。
documents = SimpleDirectoryReader(input_dir="./data/source_files").load_data()
pipeline = IngestionPipeline(
transformations=[
SentenceSplitter(chunk_size=1024, chunk_overlap=20),
TitleExtractor(),
OpenAIEmbedding(),
]
)
nodes = pipeline.run(documents=documents, num_workers=4)
如果API的服务条款允许,我们可以在应用程序中注册多个API密钥并进行轮换。这种方法实际上会增加我们的速率限制配额。但请确保这符合API提供者的政策。
如果我们在分布式系统中工作,可以将请求分散到具有各自速率限制的多台服务器或IP地址上。实现负载均衡,以动态地分布请求,优化整个基础设施的速率限制使用。
53AI,企业落地应用大模型首选服务商
产品:大模型应用平台+智能体定制开发+落地咨询服务
承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2025-01-11
蚂蚁集团基于 Ray 构建的分布式 AI Agent 框架
2025-01-10
我们即将进入 Agentic AI 时代 ,而第一个落地就是 Coding Agent
2025-01-10
2025 AI Agent迷局:谁在玩真的,谁在演戏?
2025-01-10
AGI 通用人工智能模型:基础理论与实现路径
2025-01-09
杨芳贤|AI 2.0时代,如何拥抱与驾驭大模型?
2025-01-09
字节为AI埋下了三条主线
2025-01-09
深度长文|AI的“巴别塔”:多Agent协同为何如此之难?
2025-01-08
独家对话阿里云刘伟光:什么是真正的AI云
2024-08-13
2024-05-28
2024-04-26
2024-08-21
2024-06-13
2024-08-04
2024-07-09
2024-09-23
2024-07-18
2024-04-11