尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

RAG知识库权限隔离全链路设计:从入库标定到检索下推的实战指南

RAG知识库权限隔离全链路设计:从入库标定到检索下推的实战指南 1. 应用层权限过滤RAG知识库权限隔离最大的坑1.1 事故现场一次看似正确的越权先讲一个我亲身经历的事故这件事直接改变了我对整个RAG知识库权限隔离方案的设计思路。当时我们团队给一家企业内部搭建了一套RAG知识库用来做制度文档问答。初期方案非常简单粗暴所有文档统一入库用户提问的时候应用层根据当前登录用户的角色把检索出来的知识片段再做一轮权限过滤权限不够的片段直接丢弃然后再把过滤后的上下文发给大模型。演示demo跑得很顺畅评审也过了逻辑闭环看起来无懈可击。结果上线第三天一个普通员工问了一句我们部门上半年的绩效考核方案是什么系统返回的答案有模有样甚至引用了绩效评级S级占比不超过15%这样的具体条款。但问题在于这份文档只有部门总监及以上角色才有权限查看。普通员工在应用层的角色过滤明明已经把这个片段剔除了可大模型在生成回答时还是在上下文里看到了这段内容。查了半天最终定位到根因Embedding阶段出了问题。这份绩效方案PDF在入库时被解析成了多个chunk其中有一个chunk恰好不包含任何敏感词权限标记字段在切分时被覆盖成了空值于是这个chunk被当成了公共文档进入了检索候选集。应用层过滤的是文档级权限但这个chunk的文档归属已经丢失了自然无从过滤。这次事故让我意识到RAG知识库的权限隔离不是检索接口后挂一个过滤器那么简单。权限状态必须从文档进入知识库的第一秒开始跟着数据一起流动走完整条链路——切分、向量化、存储、索引、召回、重排、拼接、生成——任何一个环节断了权限就漏了。1.2 为什么只在应用层过滤必然失效我知道很多人会想应用层过滤多方便用户权限从SSO拿chunk上带个文档ID召回后暴力过滤一下不就完了。这个思路在demo阶段没毛病但放到生产环境它有四个绕不过去的问题。第一个问题语义相关性优先于权限约束。向量检索的核心逻辑是找语义最接近的Top-K个片段而不是找我有权限且语义最接近的Top-K个片段。当权限约束被推迟到召回之后处理时往往会出现两种情况要么高权限用户召回结果里塞满了低价值的公共文档真正有权限的高价值片段被挤出了Top-K要么低权限用户召回结果里全是无权限片段过滤之后上下文空了大半大模型只能靠幻觉硬编答案。这两种情况都损害了问答质量根源在于检索阶段没有考虑权限条件检索空间被污染了。第二个问题召回后过滤的算力浪费。假设你的知识库有100万个chunk用户有权限的只有5万个。检索阶段明明可以用权限条件把搜索空间缩到5万再算相似度你偏要全量搜100万然后丢弃95万。这不仅仅是计算资源的问题——更大的隐患是如果Top-K设置为20无权限片段占15个过滤完只剩下5个有权限片段上下文长度不够回答质量断崖式下跌。你为了修这个问题只能把Top-K调大调大之后算力消耗更猛延迟也上去了。第三个问题权限元数据与chunk的绑定关系经常断裂。就像我开头讲的事故PDF解析、文本清洗、切分增强、父子分块这些环节都会改变chunk的边界。如果入库管道的每一步没有显式地传递权限字段某些chunk就会变成无身份数据。这类数据在后续审计里最可怕因为你根本说不清它该归谁管。第四个问题应用层过滤做不了行级和字段级的细粒度权限。很多知识库的权限模型不是简单的谁能看这篇文档而是谁能看这篇文档的薪酬部分谁能看这个季度数据谁能看华东区但不能看数据明细。这些条件翻译成过滤逻辑本质上已经是一个查询表达式的计算问题放在应用层用for循环去匹配既不优雅也不高效。所以我的结论是权限隔离必须下沉到检索链路内部从数据入库时给数据打身份标签开始让权限条件成为检索表达式的一部分。1.3 全链路设计的基本理念全链路权限隔离的核心理念一句话总结权限状态跟随数据流动并在每个流转节点强制执行。具体来说一条知识从原始文档到用户看到答案会经历五个关键节点文档接入与解析识别文档归属、密级、可见范围切分与向量化权限元数据在chunk间正确继承存储与索引权限字段可被数据库索引和查询检索召回权限条件参与候选集筛选生成与输出上下文拼装、引文溯源、审计日志。这五个节点任何一个环节没有权限状态整个链路就等于有了一个洞。下文我会按这五个节点的顺序把我实际落地的方案、踩过的坑、最终选型逻辑逐一拆开讲。2. 入库阶段的权限标定先让文档带着身份进来2.1 权限元数据的归一化设计很多团队做权限隔离上来就纠结用RBAC还是ABAC其实在企业知识库场景里纠结早了。权限模型是上层业务的事底层存储和检索关心的是这个chunk能被哪些用户、哪些角色、在什么条件下看到。所以你首先要做的是把五花八门的权限模型归一化成一套统一的元数据Schema。我在项目中采用的是一套最小但够用的元数据模板核心字段如下{ chunk_id: chunk_8f3a2e91c6, doc_id: doc_2024_0712, doc_name: 华东区Q2营收分析报告.pdf, owner: user_zhangsan, owner_dept: BI, read_roles: [BI_analyst, Finance_manager, VP], read_users: [user_lisi, user_wangwu], read_depts: [BI, Finance], security_level: internal, effective_start: 2024-01-01T00:00:00Z, effective_end: 2024-12-31T23:59:59Z, data_region: east_china, tags: [quarterly_report, revenue, q2] }这套字段设计有几个容易被忽略的细节我说一下我的考虑。第一角色、用户、部门三种可见性必须分开存。后端的SQL过滤和向量库的metadata filter都需要用这些字段拼查询条件。如果你把它们合成一个字符串字段意味着每次权限判断都要做字符串解析后果就是检索性能差且逻辑难以调试。第二有效期字段一定要有。企业知识库里大量文档是限时可见的比如薪酬方案在发布前只有HRD可见发布后全体可见。没有时间字段权限隔离就是一张静态快照过期权限、预发布权限都搞不定。第三data_region这种业务维度字段比你想的更重要。很多权限模型用组织架构拉平但实际业务里权限经常挂在区域产品线项目组上。所以入库前最好跟业务方一起把常见的权限维度枚举出来不要等上线后才发现缺字段那时候已经入库的历史数据都要重新清洗。2.2 物理隔离与逻辑隔离怎么选全链路权限隔离听起来很硬核但真正要拍板的时候你首先面临的是一个架构选择题用物理隔离还是逻辑隔离。物理隔离就是权限级别不同的文档放进不同的Collection/索引分区/表比如机密库内部库公共库分开逻辑隔离就是所有文档在一个Collection里通过metadata字段区分权限。我实践中倾向于折中方案先把核心指标摆出来对比一下维度纯物理隔离按密级分库纯逻辑隔离单库权限元数据折中方案按密级分库业务域标签隔离强度最强结构上杜绝越权依赖过滤条件的正确性强密级不可混存召回精度检索范围小精度高范围大需过滤条件收紧兼顾索引成本需管理多套向量索引一套索引资源省中等权限变更跨库迁移文档成本高改metadata即可密级不变时只需改标签运维复杂度分库多备份恢复繁琐简单中等审计难度天然分层但跨库追踪难全量日志即可需同时处理库与标签我为什么最终选了折中方案原因很实在纯物理隔离在权限模型复杂的场景里文档经常要升密降密跨库迁移不是改个字段那么简单你要重新切分、重新向量化、重新索引稍微一忙就会出数据不同步的问题纯逻辑隔离在极限情况下检索压力大且一旦某个用户没有正确过滤整库数据都暴露了。折中方案按密级分成2~4个Collection密级内再用角色标签控制既控制住了爆炸半径又不会把所有权限变更都变成跨库迁移的体力活。2.3 父子分块中的权限继承与异常处理RAG里处理长文档时父子分块parent-child chunking几乎是标配父chunk保留完整段落语义子chunk用于精确检索召回子chunk后回溯父chunk喂给大模型。但这里有个权限隔离的隐藏雷区——子chunk的权限必须继承自父chunk而不是由解析器自行生成。早期我们用的是开源的文本切分器直接按固定token数切每个子chunk的metadata是空的入库时默认给了public权限。结果就是前文说的那种事故。后来我把切分逻辑改成了这样def create_child_chunks(parent_chunk): children split_text(parent_chunk[content]) for child in children: # 权限字段强制继承父chunk不允许子chunk自定义 child[read_roles] parent_chunk[read_roles] child[read_users] parent_chunk[read_users] child[read_depts] parent_chunk[read_depts] child[security_level] parent_chunk[security_level] child[doc_id] parent_chunk[doc_id] child[parent_chunk_id] parent_chunk[chunk_id] return children这里有一个需要拍板的边界情况当一个父chunk合并了来自多个不同权限来源的内容时继承谁的权限比如一份会议纪要正文内容由公共部分和一段仅限参会人员的讨论结论组成切分程序识别出两个来源后合并成了一个chunk。我的策略是权限取严格交集并集也就是只有同时具备所有来源权限的用户才可见。这个策略宁可误伤一些可见性也绝不能把高权限内容泄露给低权限用户。在权限隔离里宁可错杀不可漏放是底线这个底线不能退让。3. 检索阶段的权限下推从召回后过滤改为召回前过滤3.1 Pre-filter与Post-filter不只是性能差异我在跟同行交流时发现很多人对Pre-filter的理解停留在性能更好这个层面。但实际上在RAG场景里Pre-filter更大的价值在于它保证了检索结果的有效性。Post-filter的问题在于它是在向量检索完成后再过滤的。向量检索只关心语义相似度不关心权限于是经常出现这种情况一个用户搜公司裁员补偿方案召回结果里80%是HR内部版本的文档他没权限剩下20%是公共的离职流程文档。过滤之后剩下两个chunk上下文严重不足大模型被迫自由发挥生成质量不可控。Pre-filter则正好相反。它先把检索空间裁剪成当前用户可访问的集合再在这个集合内算相似度。这样Top-K召回的结果天然就是用户能看的上下文密度高生成质量稳定。用一个类比Post-filter是先在大海里捞鱼再挑能吃的Pre-filter是先去有鱼的鱼塘再下网。不过Pre-filter也有一个性能前提过滤字段必须要建索引。这个我也踩过坑后面会单独讲。3.2 让向量数据库自己完成权限过滤现在主流的向量数据库或具备向量能力的检索引擎都支持带metadata条件的混合检索。你需要做的是把用户的权限集合翻译成数据库的filter语法。这里我给出四个常见的示例读者可以直接参考自己用的引擎。Milvus的filter表达式from pymilvus import Collection # 用户从SSO拿到角色、部门、用户ID # 拼出filter表达式 filter_expr ( fsecurity_level in [internal, public] fand (read_roles contains [BI_analyst] for read_users contains user_lisi for read_depts contains BI) fand effective_start 2024-06-01 fand effective_end 2024-06-01 ) collection Collection(enterprise_kb) results collection.search( data[query_vector], anns_fieldembedding, param{metric_type: COSINE, params: {offset: 0}}, limit20, exprfilter_expr, # 权限条件下推 output_fields[doc_id, chunk_id, security_level] )Elasticsearch的knn query filter{ knn: { field: embedding, query_vector: [0.1, 0.2, 0.3], k: 20, num_candidates: 200, filter: { bool: { must: [ { terms: { read_roles: [BI_analyst, VP] } }, { range: { effective_start: { lte: 2024-06-01 } } }, { range: { effective_end: { gte: 2024-06-01 } } } ] } } } }Qdrant的query filterfrom qdrant_client import QdrantClient from qdrant_client.http.models import Filter, FieldCondition, MatchAny, Range query_filter Filter( must[ FieldCondition(keysecurity_level, matchMatchAny(any[internal, public])), FieldCondition( keyread_roles, matchMatchAny(any[BI_analyst, Finance_manager]) ), FieldCondition( keyeffective_start, rangeRange(lte2024-06-01) ), FieldCondition( keyeffective_end, rangeRange(gte2024-06-01) ) ] ) client.search( collection_nameenterprise_kb, query_vectorquery_vector, query_filterquery_filter, limit20 )pgvector PostgreSQL的SQL级过滤SELECT doc_id, chunk_id, 1 - (embedding :query_embedding) AS cosine_similarity FROM knowledge_chunks WHERE security_level IN (internal, public) AND (read_roles ARRAY[BI_analyst]::varchar[] -- 数组交集运算符 OR read_users ARRAY[user_lisi]::varchar[] OR read_depts ARRAY[BI]::varchar[]) AND effective_start NOW() AND effective_end NOW() ORDER BY embedding :query_embedding LIMIT 20;这些filter表达式的共同点是权限条件放在了检索的计算路径里而不是检索完成后的过滤路径里。评审代码的时候我一般只看一点检索接口的入参是不是包含用户权限token而非过滤后的文档列表。3.3 权限token的生成与缓存策略权限条件下推的前提是服务端能拿到当前用户的完整权限集合。我们从SSO或内部权限中心拿到用户的角色、部门、直属领导等信息后会组装一个权限token这个token不是JWT那种签名串而是服务端内存里的一份权限集合快照通常长这样{ user_id: user_lisi, roles: [BI_analyst, project_member], depts: [BI, DigitalTransformation], security_levels: [internal, public], effective_time: 2024-06-01T00:00:00Z }这个权限token在网关层统一生成注入到检索请求的header里检索服务解析后翻译成数据库filter。要注意缓存策略权限变更了token不能长期不失效。我们的做法是短TTL比如5分钟 基于用户主动刷新。权限频繁变更的场景比如项目组解散、人员转岗5分钟的滞后是可以接受的但在审计要求严格的场景可以缩短到1分钟。4. 生成阶段的安全兜底Rerank、上下文拼装与响应审计4.1 不要把Rerank变成第二道越权窗口很多团队在检索之后接了一个Rerank模型用交叉编码器对候选chunk做精排。这是提升RAG效果的好手段但这里藏着一个权限隔离的隐患如果Rerank的输入是未过滤的候选集模型可能把高权限内容的分数排得很高导致低权限用户在最终上下文里看到不该看的片段。我见过一个项目检索阶段做了Pre-filter结果排完序后又接了一个RerankRerank直接把初始候选集含无权限片段拉回来重排了一遍。这就把前面辛苦做的Pre-filter架空了。正确做法是Rerank的输入必须是权限过滤后的候选集。权限过滤永远在Rerank之前。这里没有商量的余地。如果客观上Rerank要单独跑一个向量索引比如多路召回那至少要在进Rerank前先做一轮粗粒度的权限裁剪再在Rerank之后做一轮精确过滤。4.2 上下文拼装的授权边界控制检索完成之后RAG系统要把选中的若干chunk拼成上下文喂给大模型。这里很多人的做法是直接把chunk文本拼接起来然后用Prompt告诉模型只根据上下文回答。这里有个非常容易被忽略的细节chunk文本里自带的内容可能包含超出该用户权限范围的表述。比如一段会议纪要里写了本次会议决议已同步给王总监后续薪酬方案调整由HRBP跟进如果这个chunk的权限只授权给了会议参与者但文本里提到了王总监和薪酬方案那么低权限用户在上下文里看到薪酬方案这个词客观上就构成了信息泄露。我目前的解决方案是两层第一层在入库切分时对包含明显超范围信息的片段做段落级裁剪把敏感提及单独切出去第二层在生成前拼接上下文时对chunk文本做一次字段级脱敏替换比如把薪酬方案调整替换成相关方案调整。这两层都做不到100%完美但能显著降低泄露面。另一个值得做的设计是给大模型的Prompt标注可见性约束你只能基于以下授权可见的文档内容回答。这些内容来自企业内部知识库已根据提问者权限过滤。 如果文档内容与提问无关请明确回答未找到相关授权内容不得尝试猜测。这条Prompt本身并不能彻底防住模型脑补但配合上下文脱敏实际效果会好很多。4.3 引文溯源与审计日志权限隔离不只是防泄露还要可追溯。如果发现一次越权你得能在十分钟内定位到是哪个用户、在什么时间、通过哪条链路、检索到了哪些内容、最终模型输出了什么。我的做法是给每条召回片段绑定一个权限指纹它是对chunk的权限元数据做hash得到的短字符串比如internal|BI|user_lisi|2024Q2。在最终生成的响应里把用到的片段对应的指纹附加到引文里。一旦有权限投诉可以直接逆推。同时全链路日志至少要记录以下字段字段示例用途query_id唯一请求ID串联全链路日志user_id / rolesuser_lisi / BI_analyst, project_member定位权限集合filter_exprsecurity_level in [...]检查过滤条件正确性recall_docsdoc_2024_0712, chunk_8f3a2e91c6检查候选集来源rerank_scores0.87, 0.82排查排序异常final_context_idschunk_xxx, chunk_yyy核对最终上下文model_output回答正文审计生成结果latency_ms486性能排查这套日志设计好之后权限事故排查会从大海捞针变成按图索骥。5. 全链路数据流设计与落地案例5.1 一个查询请求的完整数据流为了让你对全链路有个整体感我把一条真实查询请求的数据流完整写出来。假设用户是李四角色是BI_analyst他问华东区Q2的营收结构怎么样第1步身份解析。应用层从SSO拿到李四的uid调用权限中心接口拿到他的角色BI_analyst、部门BI、可达密级internal, public组装成权限token。第2步检索请求下发。应用层把权限token作为header或参数传到检索服务检索服务把权限token翻译成数据库filter表达式。第3步向量检索Pre-filter。检索服务执行带filter的向量检索在李四可见的chunk集合内取Top-20。比如最终的filter条件是security_level in [internal,public]且read_roles包含BI_analyst或read_users包含user_lisi或read_depts包含BI。第4步Rerank精排。检索服务把20条候选chunk都是李四有权限的送入Rerank模型得到精排后的Top-8。第5步上下文脱敏与拼装。对Top-8的chunk做字段级脱敏替换再拼装成上下文连同引文指纹一起传给大模型生成服务。第6步模型生成。模型结合Prompt约束和上下文生成回答。服务把最终回答、引用片段ID、权限指纹、全文日志落库。第7步响应回传。答案返回给前端前端展示回答及引文来源。这条全链路里权限隔离不是某一个节点的事而是从第1步身份解析开始一路贯穿到第6步日志落库。任何一个节点独立来看都有漏洞但连成链路之后层层兜底整体安全性才能真正立住。5.2 一个微服务化的模块划分参考如果你所在团队是微服务架构我建议把上述链路拆成下面几个服务模块权限中心负责用户权限集合的维护与查询。它对接企业SSO、HR系统、项目管理系统提供用户uid - 权限集合的标准化接口。文档入库管道负责文档解析、切分、权限元数据继承、向量化、入库。这个管道需要读取权限中心的数据字典保证元数据字段命名一致。检索服务只做一件事——接收query 权限token输出候选chunk列表 相关分数。它的内部不关心用户是谁只关心权限token翻译成的filter表达式。生成服务负责上下文拼装、脱敏、调大模型、审计日志。它对接检索服务和权限中心做最终输出。审计服务接收检索服务和生成服务的全链路日志提供检索查询、权限投诉追踪、异常告警。5个服务之间用内部API或消息队列通信。每个服务的接口都要显式传递query_id便于全链路追踪。5.3 按团队规模分级的选型建议不同规模的团队硬套同一套架构会非常痛苦。我按团队规模给三档选型建议。小团队13人维护数据量100万chunk直接上PostgreSQL pgvector。权限过滤写在SQL里通过、这样的数组运算符搞定。优点是运维简单一套数据库搞定缺点是并发和扩展性有限。100万chunk以内pgvector性能完全扛得住没必要上分布式向量库。中规模团队数据量100万1000万chunk并发要求高建议Qdrant或Milvus。Qdrant的filter表达式很直观Milvus的过滤能力更强。此时你需要额外的权限服务来管理权限token的生成和缓存把SQL式的过滤逻辑从业务代码里抽出来。大规模团队数据量1000万多BU、强审计要求物理逻辑双隔离。按密级分Collection每个Collection按业务域打标签。权限中心独立成服务对接所有上游系统和下游检索服务。审计日志进专门的日志平台做实时告警和定期复盘。6. 实测踩坑记录与排查链路6.1 过滤条件没建索引查询从50ms变成5秒这是我第一次把filter下推到向量库时踩的坑。当时用的是Milvus我信心满满地把权限过滤条件加到了expr里可是测试的时候发现加过滤后检索耗时从50ms直接飙到了5秒。排查链路是这样的先怀疑是权限token解析太慢检查发现解析耗时不到1ms排除。再怀疑是Filter表达式写得太复杂简化后耗时依然高排除。最后打开Milvus的监控面板发现read_roles、read_depts这些字段没有被索引filter触发的是全量扫描。问题定位后我重新给权限字段建了倒排索引和标量索引检索耗时回到60ms左右。这件事的教训是Pre-filter的性能上限取决于filter字段有没有索引。权限字段的基数通常不高角色就几十个、部门几十个非常适合建倒排索引开销小收益大。所以在上线前务必确认向量数据库的标量索引配置别等压测才发现。6.2 权限元数据不一致引发的漂移丢权限还有一次事故特别隐蔽——同一个用户在A环境能看到某文档在B环境却看不到。排查时发现两个环境的Embedding模型版本不一致新版模型的向量维度变了导致文档重新向量化入库。按理说向量化了metadata应该保留。结果发现入库管道的某个历史版本在写库时漏掉了read_depts字段导致新库里的chunk只有read_roles和read_users用户李四的权限是挂在部门BI上的新库过滤时自然匹配不到。这个问题的根因是管道版本升级时metadata映射逻辑没有做兼容性测试。从那以后我制定了一个硬性规范每次升级Embedding模型或切分器都要用同一批文档跑一遍全链路回归比对权限字段的完整性和一致性。因此在你的真实项目里要特别重视这个环节任何一次RAG底层组件的升级都要把历史文档重新清洗入库并以权限元数据完整性检查作为发布的前置条件。这绝不是可有可无的流程而是防止权限静默丢失的关键手段。我见过很多团队忽视了这一点导致某次升级后大量文档的权限标签丢失系统裸奔了数周才发现后果非常严重。6.3 共享文档的权限并集与交集之争企业里大量存在跨部门共享文档比如一份市场活动总结市场部全员可看但其中涉及预算的部分仅市场总监和财务总监可看。这种文档在入库时如果合并成一个chunk权限怎么标我刚开始的方案是权限取并集即任何人都能看到整个chunk。结果上线没多久就被财务同事投诉了说预算数据不当泄露。后来改成权限取交集结果市场部普通员工连这份文档的核心内容都看不到了因为chunk里有一段财务敏感文字拉低了整个chunk的可见性。最终我的解法是在同一份文档内按敏感段落进行二次切分。敏感段落单独切成一个chunk权限范围缩小非敏感段落保持普通权限。这样既保住了文档主体可见性又对敏感部分做了隔离。所以在做RAG知识库切分时不要只按字数或语义切分要把权限边界作为切分的一个约束条件。权限边界和语义边界都是决定chunk划分的重要依据缺一不可。6.4 缓存导致的权限穿透最后这个坑非常隐蔽它跟RAG本身无关但却是知识库系统常见的隐形杀手。我们的知识库服务做了结果缓存query的embedding和Top-K结果会缓存一段时间减少重复计算。有一次一个管理员用户搜了某份高管任命文档结果被缓存了下来。几分钟后一个普通员工搜了同一个问题直接命中了缓存返回了完整的答案。权限隔离在缓存层毫无作用。排查链路是检索服务明明带了filter为什么还会命中缓存后来发现缓存key的设计只包含了query文本和向量完全没有包含权限token。修复方案很简单缓存key必须包含权限指纹即把权限token做hash塞进缓存key。这样权限集合不同的用户永远不会命中彼此的缓存。踩完这个坑后我把缓存key设计列入了权限隔离评审清单里的必查项。哪怕你的检索链路做得再完美只要缓存层开了口子一切白搭。任何一层的不设防都会让前面的努力前功尽弃。写到这里我把RAG知识库权限隔离的全链路设计从问题根源、入库标定、检索下推、生成兜底、落地案例到踩坑记录都过了一遍。最后分享一个我每次做权限隔离评审时都会用的检查清单第一权限字段在入库切分时有没有正确继承第二filter条件有没有下推到检索计算路径而非事后过滤第三缓存key有没有包含权限指纹第四Rerank的输入是不是权限过滤后的候选集第五审计日志能不能支撑越权追溯。这五条检查完不敢说万无一失至少能挡住我在实战中遇到过的所有越权路径。
返回列表