Appearance
上一篇把文档解析干净了。这一篇讲一个企业几乎必然会提、教程却几乎不讲的需求:权限。同一个知识库,销售只能查到销售的资料、财务只能看财务的、普通员工看不到管理层文档。做 SaaS 还要更进一步——多租户:A 公司的数据绝不能被 B 公司检索到。这块做不好,轻则闹笑话,重则是数据泄露事故。面试时能把这块讲清楚,含金量很高。
先说一个最常见、最危险的错误做法
很多人第一反应是:正常检索出前 5 条,再把用户没权限的删掉。 这是错的,而且危险,原因有三:
一是会"泄露给了模型"。 就算你最后不显示无权限的内容,它也已经被检索出来、拼进了提示词喂给模型——数据其实已经离开了权限边界,出现在了日志、缓存、甚至模型的上下文里。这在合规审计上是过不了关的。
二是结果会"缺斤少两"。 检索出的前 5 条里如果 3 条没权限被删了,用户实际只拿到 2 条,答案质量无端变差,你还不知道为啥。
三是排序信息也算泄露。 "系统检索到了某条但没给你看"——这个存在性本身有时就是敏感信息。
正确的心法只有一句:权限必须是检索的"前置过滤条件",而不是"事后删除"。 让检索从一开始就只在"这个用户有权看的文档"范围里进行。
正确做法:把权限"焊进"检索
分两步。
第一步:入库时,给每个文档块打上权限标签。 在 metadata 里记清楚这块内容属于谁、谁能看——部门、角色、租户 ID 等:
python
from langchain_core.documents import Document
docs = [
Document(page_content="财务报销制度……",
metadata={"tenant_id": "acme", "dept": "finance", "level": "normal"}),
Document(page_content="高管薪酬方案……",
metadata={"tenant_id": "acme", "dept": "hr", "level": "confidential"}),
Document(page_content="销售话术手册……",
metadata={"tenant_id": "acme", "dept": "sales", "level": "normal"}),
]第二步:检索时,带上当前用户的权限作为过滤条件。 用户的权限从可信的身份系统取(登录态、RBAC 服务),绝不能由前端/用户自己传——否则等于把门钥匙交给了访客。然后把它作为检索的 filter:
python
import os
from langchain_openai import OpenAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
embeddings = OpenAIEmbeddings(model="text-embedding-v3", api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")
vectorstore = InMemoryVectorStore.from_documents(docs, embeddings)
# 当前登录用户的权限(从可信的身份系统取,不是用户自己传的)
current_user = {"tenant_id": "acme", "depts": {"finance", "sales"}, "can_see_confidential": False}
def secure_search(question: str, user: dict):
# 权限作为「前置过滤」:只在该用户有权看的文档里检索
def allowed(doc) -> bool:
m = doc.metadata
if m.get("tenant_id") != user["tenant_id"]:
return False # 租户隔离:跨租户一律不可见
if m.get("dept") not in user["depts"]:
return False # 部门权限
if m.get("level") == "confidential" and not user["can_see_confidential"]:
return False # 密级
return True
return vectorstore.similarity_search(question, k=3, filter=allowed)
for d in secure_search("有什么制度", current_user):
print(d.metadata["dept"], "→", d.page_content[:20])
# 只会返回 finance / sales 的普通文档;hr 的机密文档从一开始就检索不到关键在于 filter 是传给检索本身的——无权限的内容根本不会被检索出来,也就绝不会进入提示词、日志或模型。这才是安全的边界。
上面用内存向量库演示了
filter用法。上生产换成 Chroma、Milvus、pgvector 等专业向量库时,它们都原生支持带元数据过滤的检索(写法通常是类似{"dept": {"$in": ["finance","sales"]}}的条件)。思路一样:把权限翻译成过滤条件,随查询一起下推到向量库。
多租户:两种隔离策略
给多个客户(租户)提供服务时,隔离有两种常见做法,各有取舍:
共享库 + 租户 ID 过滤:所有租户数据放一个库,每条打 tenant_id,查询强制带租户过滤。省资源、好维护,适合租户多、单个数据量小的场景。风险是一旦某处过滤条件写漏,就会串租户——所以要把"必带租户过滤"做成不可绕过的统一入口。
每租户独立库/集合:每个租户单独一个向量库或集合,物理隔离,最安全,适合大客户、强合规要求。代价是资源开销和管理复杂度都更高。
现实中常是混合:中小租户共享库、按 ID 隔离,大客户单独开库。没有绝对最优,取决于你的合规要求和成本。
一个必须守住的底线
无论哪种方案,记住两条铁律:① 权限一定在检索层前置过滤,不在回答层事后删;② 用户权限从可信身份系统取,永不信任客户端传来的身份。 这两条守住了,才谈得上"企业级"。
小结
权限 RAG 的核心,是把"谁能看什么"变成检索的前置过滤条件:入库时给文档打权限标签(租户/部门/密级),检索时带上当前用户(可信来源)的权限做过滤,让无权限内容从源头就检索不到。多租户则在"共享库+过滤"和"独立库"之间按合规与成本权衡。
下一篇换个体裁——把真实项目里踩过的坑做成故障复盘合集:幻觉、召回、成本、注入,一个个"现象→定位→修复",这也是面试里最像实战经验的部分。