langchain+qwen实现RAG数据之间的隔离
关于RAG(检索增强生成)应用中的数据隔离,其实是一个绕不开的痛点。说白了,你在系统里上传的那些资料,总不能让大家随便翻吧?你上传的阿里的财报,别人一搜就出来了,这肯定不行。

所以,在开发这类应用时,数据隔离是硬需求。核心原则很简单:每个用户只能看到自己被授权访问的那部分数据,不能越界。这篇文章就来聊一聊,如何用LangChain、通义千问(Qwen)和Qdrant向量数据库,把这个功能落地。
主要的实现步骤
如果熟悉RAG的朋友,可以直接看核心思路。大致流程是这样的:
- 先加载文档
- 对文档数据进行分块
- 分块之后,给每一块数据打上
role权限标签,明确谁可以看 - 当用户发起检索时,会根据
role字段把数据过滤一遍 - 过滤后的相关数据,连同问题一起发给大模型(这里用的是通义千问)
- LLM处理后,返回最终答案
整个功能的核心,其实就是这个role字段的过滤逻辑。当用户提问时,我们就已经确定了他能查哪一类的文档。
向量数据库的选型
要存大量文档的数据,肯定得用向量数据库。这里选了Qdrant。原因挺直接:开源、跨平台、在Windows下也能快速部署和玩玩。很多人也会考虑Milvus,文档做得确实不错,但Windows下的开发环境配置起来相对麻烦一点,就暂时没选。
使用到的技术栈
本次用到的技术组合是:langchain + qwen + qdrant。
开发前的准备
首先,你需要准备一个通义千问的API-KEY,没有这个,后面就没法玩。
开始教程
安装依赖
如果之前有过LangChain环境的,可以跳过部分依赖。主要安装Qdrant客户端:
pip install --upgrade --quiet qdrant-client
引入必要的库
from langchain_community.chat_models.tongyi import ChatTongyi from langchain_community.vectorstores import Qdrant from langchain_community.document_loaders import PyPDFLoader from langchain_community.embeddings import DashScopeEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter from qdrant_client.http import models as rest from langchain.prompts import PromptTemplate from langchain.chains.retrieval_qa.base import RetrievalQA import os qwen_api_key = "你的通义千问API-KEY" os.environ["DASHSCOPE_API_KEY"] = qwen_api_key
构建向量化对象
embeddings = DashScopeEmbeddings(
model="text-embedding-v1", dashscope_api_key=qwen_api_key
)
定义读取文档的函数,并进行分块
这里我们定义了一个方法,读取PDF文件。在处理过程中,会为每一块文档的元数据(metadata)添加一个roles字段,这个字段就是一个权限列表,标识哪些角色可以访问这块数据。
def readPdfData(documents):
page = []
for document_path, roles in documents.items():
# 加载文件
pdf_loader = PyPDFLoader(document_path, extract_images=True)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=200, add_start_index=True
)
loaded_documents = pdf_loader.load_and_split()
for doc in loaded_documents:
doc.metadata["roles"] = roles
# 对文档进行分块
split_documents = text_splitter.split_documents(loaded_documents)
page.extend(split_documents)
return page
在调用时,比如我们加载一份阿里的财报PDF,就为它分配一个ali的权限标记:
split_documents = readPdfData({"test.pdf": ["ali"]})
构建Qdrant向量数据库对象
我们将数据存入Qdrant。这里为了演示方便,直接将数据加载到内存中。集合名称定义为my_documents。
qdrant = Qdrant.from_documents(
split_documents,
embeddings,
prefer_grpc=True,
location=":memory:",
collection_name="my_documents",
)
往已有的集合里追加新文档
假设我们又拿到了一份茅台的财报PDF,同样为其分配一个maotai权限标记:
documents = {
"maotai.pdf": ["maotai"],
}
split_documents = readPdfData(documents)
qdrant.add_documents(split_documents, batch_size=20)
定义检索对象
现在,我们在检索时,会利用filter参数,只返回那些metadata.roles字段中包含我们指定角色的数据。这里假设用户有maotai和ali两个角色的权限:
user_roles = ["maotai", "ali"]
qdrant_retriever = qdrant.as_retriever(
search_kwargs={
"filter": rest.Filter(
must=[
rest.FieldCondition(
key="metadata.roles",
match=rest.MatchAny(any=user_roles)
)
]
)
}
)
构建大语言模型对象
llm = ChatTongyi(model="qwen-plus")
向Qwen模型提问
当用户提问“贵州茅台?”时,系统会拿着问题去Qdrant里检索,先通过权限过滤,再找出最相关的文档内容,最后将这些内容作为上下文,连同用户的原始问题一起发送给Qwen模型。
prompt_template = """
Question: {question}
使用源回答问题。如果没有答案,请说"文本中没有答案".
Source: {context}
### Response:
"""
prompt = PromptTemplate(
template=prompt_template, input_variables=["context", "question"]
)
retrieval_qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=qdrant_retriever,
return_source_documents=True,
chain_type_kwargs={"prompt": prompt},
)
response = retrieval_qa.invoke({"query": "贵州茅台?"})
print(response["result"])
输出结果如下:
在2023年第三季度报告中,贵州茅台的主要财务数据显示: 1. 营业收入为103,268,354,688.44元,同比增长18.48%。 2. 归属于上市公司股东的净利润为52,876,217,064.12元,同比增长19.09%。 3. ...
看起来很不错,因为我们正好有权限访问maotai.pdf。
现在换个场景。我们把用户的权限修改一下,只给a和ali权限,不再包含maotai。
user_roles = ["a", "ali"]
然后重新执行上面的提问代码,你会发现,模型给出的回答变成了:
贵州茅台在上述文本中没有被提及。
这就对了。由于我们去掉了maotai权限,即使数据就存在库里,用户也搜不到它。
最后总结
至此,我们实现了一个基于权限的数据隔离方案。核心思路很简单:在将文档存入Qdrant向量数据库时,通过metadata.roles字段定义好数据的分组和权限归属;当用户发起检索时,检索器根据用户自身携带的角色权限进行过滤,确保他只能看到自己有权访问的数据。文件归文件,用户归用户,这才是企业级RAG应用该有的样子。
-
- 关于宇宙的好的网名有哪些
- 角色扮演 | 1
- 网名