
Ravi Theja • 2024-01-15
使用 LlamaIndex 构建多租户 RAG 系统
引言
RAG(检索增强生成)系统中的多租户概念变得越来越重要,尤其是在涉及数据安全和隐私时。简单来说,多租户是指系统独立且安全地服务多个用户(“租户”)的能力。
考虑这个场景:在一个 RAG 系统中,有两个用户,用户 1 和用户 2。他们都有自己的一套文档,并已将这些文档索引到系统中。多租户在这里的关键在于,当用户 1 查询系统时,他们应该只从自己索引的文档中接收答案,而不是从用户 2 索引的文档中接收答案,反之亦然。这种分离对于维护数据机密性和安全性至关重要,因为它防止了不同用户之间意外或未经授权的私人信息交叉引用。
在 RAG 系统中的多租户背景下,这意味着设计一个不仅能有效理解和检索信息,还能严格遵守用户特定数据边界的系统。每个用户与系统的交互都是隔离的,确保 RAG 流水线中的检索器组件只访问与该特定用户相关且允许的信息。这种方法在涉及敏感或专有数据的场景中非常重要,因为它可以防止数据泄露和隐私侵犯。
在本篇博客文章中,我们将深入探讨如何使用 LlamaIndex 构建多租户 RAG 系统。
解决多租户挑战
管理多租户的关键在于元数据。在索引文档时,我们在将其添加到索引之前,会将用户特定信息包含在元数据中。这确保了每个文档都唯一地与单个用户关联。
在查询阶段,检索器使用此元数据进行过滤,只访问与查询用户关联的文档。随后,它执行语义搜索,为该用户检索最相关的信息片段或“top_k 块”。通过实施此方法,我们有效地防止了不同用户之间未经授权的私人信息交叉引用,维护了每个用户数据的完整性和机密性。
现在我们已经讨论了概念,接下来深入探讨多租户 RAG 系统的构建。如需详细的分步指南,请随时跟随我们Google Colab Notebook中的后续说明进行。
下载数据
我们将使用论文 An LLM Compiler for Parallel Function Calling
和 Dense X Retrieval: What Retrieval Granularity Should We Use?
进行演示。
!wget --user-agent "Mozilla" "https://arxiv.org/pdf/2312.04511.pdf" -O "llm_compiler.pdf"
!wget --user-agent "Mozilla" "https://arxiv.org/pdf/2312.06648.pdf" -O "dense_x_retrieval.pdf"
加载数据
我们将为用户 Jerry
加载论文 LLMCompiler
的数据,并为用户 Ravi
加载论文 Dense X Retrieval
的数据
reader = SimpleDirectoryReader(input_files=['dense_x_retrieval.pdf'])
documents_jerry = reader.load_data()
reader = SimpleDirectoryReader(input_files=['llm_compiler.pdf'])
documents_ravi = reader.load_data()
创建一个空索引
我们将首先创建一个空索引,然后向其中插入文档,每个文档都带有包含用户信息的元数据标签。
index = VectorStoreIndex.from_documents(documents=[])
摄取流水线
摄取流水线 (IngestionPipeline) 对于数据摄取和执行转换非常有用,包括分块、元数据提取等。这里我们利用它来创建节点,然后将这些节点插入到索引中。
pipeline = IngestionPipeline(
transformations=[
SentenceSplitter(chunk_size=512, chunk_overlap=20),
]
)
更新元数据并插入文档
我们将为每个用户更新文档的元数据,并将文档插入到索引中。
# For user Jerry
for document in documents_jerry:
document.metadata['user'] = 'Jerry'
nodes = pipeline.run(documents=documents_jerry)
# Insert nodes into the index
index.insert_nodes(nodes)
# For user Ravi
for document in documents_ravi:
document.metadata['user'] = 'Ravi'
nodes = pipeline.run(documents=documents_ravi)
# Insert nodes into the index
index.insert_nodes(nodes)
定义查询引擎
我们将为两个用户定义带有必要过滤器的查询引擎。
# For Jerry
jerry_query_engine = index.as_query_engine(
filters=MetadataFilters(
filters=[
ExactMatchFilter(
key="user",
value="Jerry",
)
]
),
similarity_top_k=3
)
# For Ravi
ravi_query_engine = index.as_query_engine(
filters=MetadataFilters(
filters=[
ExactMatchFilter(
key="user",
value="Ravi",
)
]
),
similarity_top_k=3
)
查询
# Jerry has Dense X Rerieval paper and should be able to answer following question.
response = jerry_query_engine.query(
"what are propositions mentioned in the paper?"
)
该论文提到命题(propositions)作为另一种检索单元选择。命题被定义为文本中意义的原子表达式,对应于文本中不同的意义片段。它们是最小的,不能再进一步分割成单独的命题。每个命题都是上下文化的和自包含的,包括从文本中解释其意义所需的所有必要上下文。论文使用比萨斜塔的例子演示了命题的概念,其中一段文字被分割成三个命题,每个命题对应于一个关于该塔的独特事实。
# Ravi has LLMCompiler paper
response = ravi_query_engine.query("what are steps involved in LLMCompiler?")
LLMCompiler 由三个关键组件组成:LLM Planner、Task Fetching Unit 和 Executor。LLM Planner 根据用户输入识别执行流程,定义不同的函数调用及其依赖关系。Task Fetching Unit 在将变量替换为前置任务的实际输出后,调度可以并行执行的函数调用。最后,Executor 使用关联的工具执行调度的函数调用任务。这些组件协同工作,以优化 LLM 的并行函数调用性能。
# This should not be answered as Jerry does not have information about LLMCompiler
response = jerry_query_engine.query("what are steps involved in LLMCompiler?")
我很抱歉,但在给定的上下文中我找不到关于 LLMCompiler 所涉及步骤的任何信息。
如演示所示,如果 Jerry 查询 Ravi 索引的文档,系统将不会从该文档中检索到任何答案。
下一步是什么?
我们在 LlamaPacks 和 Replit 模板中包含了一个 MultiTenancyRAGPack,它提供了一个 Streamlit 界面供您亲自动手体验。务必探索一下。