庫(kù)權(quán)限隔離全鏈路實(shí)戰(zhàn):從架構(gòu)設(shè)計(jì)到代碼實(shí)現(xiàn))
1. 為什么權(quán)限隔離是RAG知識(shí)庫(kù)落地的生死線做過(guò)RAG項(xiàng)目的人都有一個(gè)共識(shí)Demo跑通只要一天但要讓這套東西真正在企業(yè)內(nèi)部跑起來(lái)權(quán)限問(wèn)題能卡你一個(gè)月。我見(jiàn)過(guò)太多團(tuán)隊(duì)興沖沖搭好了向量庫(kù)、接上了大模型、問(wèn)答效果也不錯(cuò)結(jié)果一上生產(chǎn)就傻眼——銷(xiāo)售部門(mén)的人問(wèn)出了HR的薪酬制度普通員工檢索到了管理層的戰(zhàn)略會(huì)議紀(jì)要。這不是技術(shù)bug這是權(quán)限設(shè)計(jì)的缺失。RAG知識(shí)庫(kù)的權(quán)限隔離本質(zhì)上要解決一個(gè)核心矛盾檢索增強(qiáng)生成依賴(lài)的是“全量語(yǔ)義空間”的相似度匹配而企業(yè)信息管理要求的是“最小權(quán)限原則”下的精確訪問(wèn)控制。這兩者天然打架。向量檢索可不管你是誰(shuí)它只算余弦相似度誰(shuí)的內(nèi)容跟query最接近就返回誰(shuí)。所以權(quán)限隔離不是加個(gè)登錄框就完事了它必須貫穿從文檔入庫(kù)、向量化、檢索、重排到最終生成的全鏈路。這篇文章適合正在做或準(zhǔn)備做企業(yè)級(jí)RAG知識(shí)庫(kù)的開(kāi)發(fā)和產(chǎn)品同學(xué)。不管你是用Dify、FastGPT這類(lèi)低代碼平臺(tái)還是自己基于LangChain、LlamaIndex手搓權(quán)限隔離的思路是通用的。我會(huì)從架構(gòu)設(shè)計(jì)講到代碼級(jí)實(shí)現(xiàn)把每個(gè)環(huán)節(jié)的坑和取舍都攤開(kāi)說(shuō)。全文基于我在多個(gè)企業(yè)知識(shí)庫(kù)項(xiàng)目中的實(shí)際經(jīng)驗(yàn)涉及具體參數(shù)和方案選型時(shí)會(huì)給出計(jì)算依據(jù)和對(duì)比分析。先給一個(gè)全局認(rèn)知RAG權(quán)限隔離分三個(gè)層次。第一層是文檔級(jí)權(quán)限控制誰(shuí)能看到哪些文檔第二層是塊級(jí)權(quán)限同一篇文檔里不同段落可能有不同密級(jí)第三層是檢索時(shí)過(guò)濾在向量搜索階段就把無(wú)權(quán)內(nèi)容排除掉而不是檢索完再過(guò)濾。三層缺一不可只做第一層等于沒(méi)做因?yàn)橄蛄繋?kù)里存的是chunk不是文檔。2. 全鏈路權(quán)限隔離的架構(gòu)設(shè)計(jì)與核心思路2.1 從文檔入庫(kù)到答案生成的完整鏈路拆解一條完整的RAG鏈路是這樣的文檔上傳 → 解析分塊 → 向量化 → 存入向量庫(kù) → 用戶(hù)提問(wèn) → query向量化 → 相似度檢索 → 重排 → 組裝上下文 → 大模型生成 → 返回答案。權(quán)限隔離要在每個(gè)環(huán)節(jié)都埋入控制點(diǎn)。文檔上傳階段需要記錄文檔的元數(shù)據(jù)包括所屬部門(mén)、密級(jí)、可訪問(wèn)角色列表。解析分塊階段每個(gè)chunk要繼承文檔的權(quán)限屬性同時(shí)支持chunk級(jí)別的權(quán)限覆蓋。向量化階段本身不涉及權(quán)限但存入向量庫(kù)時(shí)權(quán)限字段必須作為metadata一起寫(xiě)入。檢索階段是最關(guān)鍵的用戶(hù)發(fā)起查詢(xún)時(shí)系統(tǒng)要根據(jù)用戶(hù)身份生成一個(gè)權(quán)限過(guò)濾條件在向量搜索時(shí)作為pre-filter傳入。重排和生成階段做二次校驗(yàn)防止過(guò)濾遺漏。我選擇在向量庫(kù)層面做pre-filter而不是后過(guò)濾原因很直接后過(guò)濾會(huì)導(dǎo)致召回?cái)?shù)量不可控。假設(shè)你設(shè)top_k10檢索出10個(gè)chunk后再過(guò)濾掉8個(gè)無(wú)權(quán)限的實(shí)際只剩2個(gè)上下文嚴(yán)重不足答案質(zhì)量斷崖式下降。而pre-filter是在搜索時(shí)就只在該用戶(hù)有權(quán)限的子集里找最相似的top_k10就是實(shí)打?qū)嵉?0個(gè)有效結(jié)果。2.2 權(quán)限模型選型RBAC還是ABAC企業(yè)級(jí)權(quán)限模型主流兩種RBAC基于角色的訪問(wèn)控制和ABAC基于屬性的訪問(wèn)控制。RBAC簡(jiǎn)單直觀用戶(hù)關(guān)聯(lián)角色角色關(guān)聯(lián)權(quán)限。ABAC更靈活根據(jù)用戶(hù)屬性、資源屬性、環(huán)境屬性動(dòng)態(tài)計(jì)算權(quán)限。對(duì)于RAG知識(shí)庫(kù)我的建議是RBAC為主、ABAC為輔。為什么因?yàn)橄蛄繋?kù)的metadata過(guò)濾能力有限大多數(shù)向量庫(kù)Milvus、Qdrant、Weaviate支持的是標(biāo)量字段的等值或范圍過(guò)濾你很難在檢索時(shí)執(zhí)行復(fù)雜的策略計(jì)算。RBAC可以把權(quán)限簡(jiǎn)化為“角色標(biāo)簽列表”存成數(shù)組字段檢索時(shí)做ARRAY_CONTAINS判斷效率高且兼容性好。具體做法每個(gè)chunk的metadata里存一個(gè)access_roles字段值是角色I(xiàn)D的數(shù)組比如[role_hr, role_admin]。用戶(hù)登錄后系統(tǒng)拿到他的角色列表檢索時(shí)構(gòu)造過(guò)濾條件access_roles IN user_roles。這樣一次向量搜索就能完成權(quán)限隔離不需要額外的策略引擎介入。如果企業(yè)有更細(xì)粒度的需求比如“同一篇文檔A部門(mén)只能看前兩章B部門(mén)能看全部”那就需要chunk級(jí)權(quán)限覆蓋。實(shí)現(xiàn)方式是在分塊時(shí)給每個(gè)chunk單獨(dú)打權(quán)限標(biāo)簽而不是簡(jiǎn)單繼承文檔權(quán)限。這增加了入庫(kù)時(shí)的復(fù)雜度但換來(lái)了靈活性。2.3 向量庫(kù)metadata設(shè)計(jì)的關(guān)鍵字段metadata設(shè)計(jì)直接決定了權(quán)限過(guò)濾能不能做、好不好做。以下是我在實(shí)際項(xiàng)目中沉淀的字段規(guī)范字段名類(lèi)型說(shuō)明是否必填doc_idstring文檔唯一標(biāo)識(shí)是chunk_idstring塊唯一標(biāo)識(shí)是access_rolesarray[string]可訪問(wèn)角色列表是security_levelint密級(jí)1-4數(shù)字越大越機(jī)密是departmentstring所屬部門(mén)否source_typestring來(lái)源類(lèi)型wiki/郵件/文檔否created_atint64創(chuàng)建時(shí)間戳否is_publicbool是否公開(kāi)是access_roles是核心過(guò)濾字段security_level用于ABAC場(chǎng)景下的輔助判斷is_public用于快速放行公開(kāi)內(nèi)容。這幾個(gè)字段配合使用能覆蓋絕大多數(shù)企業(yè)場(chǎng)景。注意不同向量庫(kù)對(duì)數(shù)組類(lèi)型metadata的支持程度不同。Milvus支持ARRAY類(lèi)型且能做contains過(guò)濾Qdrant支持?jǐn)?shù)組但過(guò)濾語(yǔ)法有差異Weaviate的where過(guò)濾器對(duì)數(shù)組支持較弱。選型時(shí)務(wù)必確認(rèn)你的向量庫(kù)版本支持?jǐn)?shù)組字段過(guò)濾否則只能退化成用字符串拼接like匹配性能和準(zhǔn)確性都會(huì)打折。3. 核心環(huán)節(jié)實(shí)操?gòu)娜霂?kù)到檢索的權(quán)限落地3.1 文檔入庫(kù)時(shí)的權(quán)限標(biāo)注與分塊策略入庫(kù)是整個(gè)鏈路的起點(diǎn)權(quán)限標(biāo)注必須在這一步完成。我的做法是設(shè)計(jì)一個(gè)DocumentProcessor類(lèi)在解析文檔時(shí)同步提取權(quán)限信息。權(quán)限信息來(lái)源通常有三個(gè)文檔管理系統(tǒng)自帶的權(quán)限、上傳時(shí)人工指定、根據(jù)文檔路徑自動(dòng)推斷。自動(dòng)推斷的規(guī)則可以這樣設(shè)計(jì)如果文檔路徑包含/hr/自動(dòng)打上role_hr標(biāo)簽如果文件名包含confidentialsecurity_level設(shè)為3。人工指定作為補(bǔ)充允許上傳者覆蓋自動(dòng)推斷結(jié)果。這種“自動(dòng)為主、人工為輔”的策略在保證準(zhǔn)確性的同時(shí)降低了操作成本。分塊策略對(duì)權(quán)限隔離有直接影響。如果按固定長(zhǎng)度切分一個(gè)chunk可能跨越兩個(gè)不同密級(jí)的段落。我的建議是按語(yǔ)義邊界分塊同時(shí)以權(quán)限邊界為硬切分點(diǎn)。具體來(lái)說(shuō)先用語(yǔ)義分塊器如基于句嵌入的TextSplitter做初步切分然后檢查每個(gè)chunk是否跨越了權(quán)限邊界如果跨越就強(qiáng)制切開(kāi)。這樣保證每個(gè)chunk的權(quán)限標(biāo)簽是單一且明確的。from langchain.text_splitter import RecursiveCharacterTextSplitter def split_with_permission_boundary(text, permission_markers): permission_markers: [(start_pos, end_pos, roles), ...] 按權(quán)限邊界切分文檔確保每個(gè)chunk權(quán)限單一 splitter RecursiveCharacterTextSplitter( chunk_size512, chunk_overlap50, separators[\n\n, \n, 。, , ] ) chunks splitter.split_text(text) result [] for chunk in chunks: chunk_start text.find(chunk) chunk_end chunk_start len(chunk) # 找到覆蓋該chunk的權(quán)限標(biāo)記 covering_roles None for start, end, roles in permission_markers: if chunk_start start and chunk_end end: covering_roles roles break if covering_roles is None: # chunk跨越了權(quán)限邊界需要進(jìn)一步切分 covering_roles [role_public] # 降級(jí)為公開(kāi) result.append({text: chunk, access_roles: covering_roles}) return result這段代碼的核心邏輯是先做語(yǔ)義分塊再檢查每個(gè)chunk是否落在單一權(quán)限區(qū)間內(nèi)。如果跨越了邊界保守起見(jiàn)降級(jí)為公開(kāi)權(quán)限或者進(jìn)一步切分。實(shí)際項(xiàng)目中我傾向于進(jìn)一步切分而不是降級(jí)因?yàn)榻导?jí)會(huì)導(dǎo)致機(jī)密內(nèi)容泄露。3.2 向量化與metadata寫(xiě)入的注意事項(xiàng)向量化本身不涉及權(quán)限但metadata寫(xiě)入有幾個(gè)坑要避開(kāi)。第一access_roles數(shù)組不能為空空數(shù)組在過(guò)濾時(shí)行為不確定有的向量庫(kù)會(huì)當(dāng)成“無(wú)權(quán)限”有的會(huì)當(dāng)成“所有人可訪問(wèn)”。我的做法是強(qiáng)制要求至少有一個(gè)角色公開(kāi)內(nèi)容用[role_public]表示。第二metadata字段名要統(tǒng)一且簡(jiǎn)潔。我見(jiàn)過(guò)項(xiàng)目里同時(shí)存在access_role、allowed_roles、permission三個(gè)字段維護(hù)起來(lái)極其痛苦。建議在項(xiàng)目初期就定好schema所有入庫(kù)代碼走同一個(gè)封裝函數(shù)。第三批量寫(xiě)入時(shí)注意向量庫(kù)對(duì)metadata大小的限制。Milvus單個(gè)entity的metadata有大小上限通常64KB如果access_roles列表特別長(zhǎng)比如一個(gè)chunk允許200個(gè)角色訪問(wèn)可能會(huì)超限。解決方案是用角色組代替角色列表比如定義group_all_employees代表所有員工角色metadata里只存組ID。def build_metadata(doc_id, chunk_id, roles, security_level, department): 構(gòu)建標(biāo)準(zhǔn)化的chunk metadata if not roles: roles [role_public] # 角色組壓縮如果roles包含所有已知角色替換為group_all if set(roles) set(ALL_KNOWN_ROLES): roles [group_all] return { doc_id: doc_id, chunk_id: chunk_id, access_roles: roles, security_level: security_level, department: department or unknown, is_public: role_public in roles, created_at: int(time.time()) }3.3 檢索階段的pre-filter實(shí)現(xiàn)與性能優(yōu)化檢索階段的權(quán)限過(guò)濾是整個(gè)鏈路的核心。以Milvus為例搜索時(shí)可以傳入expr參數(shù)做標(biāo)量過(guò)濾from pymilvus import Collection def search_with_permission(collection, query_vector, user_roles, top_k10): 帶權(quán)限過(guò)濾的向量檢索 # 構(gòu)造權(quán)限過(guò)濾表達(dá)式 roles_str , .join([f{r} for r in user_roles]) expr fARRAY_CONTAINS_ANY(access_roles, [{roles_str}]) results collection.search( data[query_vector], anns_fieldembedding, param{metric_type: COSINE, params: {nprobe: 16}}, limittop_k, exprexpr, output_fields[doc_id, chunk_id, access_roles, text] ) return resultsARRAY_CONTAINS_ANY是Milvus 2.3支持的數(shù)組過(guò)濾函數(shù)判斷數(shù)組中是否有任意元素在給定列表里。這個(gè)表達(dá)式的性能取決于兩個(gè)因素?cái)?shù)組字段的基數(shù)每個(gè)chunk有多少個(gè)角色和用戶(hù)角色列表的長(zhǎng)度。實(shí)測(cè)下來(lái)當(dāng)access_roles平均長(zhǎng)度在5以?xún)?nèi)、用戶(hù)角色數(shù)在10以?xún)?nèi)時(shí)過(guò)濾對(duì)檢索延遲的影響在10%以?xún)?nèi)。如果向量庫(kù)不支持?jǐn)?shù)組過(guò)濾退而求其次的方案是用位圖bitmap。把角色映射為位位置access_roles存成一個(gè)整數(shù)的位掩碼。用戶(hù)角色也轉(zhuǎn)成位掩碼過(guò)濾時(shí)做位與運(yùn)算。這種方案性能極好但角色數(shù)量不能超過(guò)64個(gè)用int64且可讀性差。適合角色體系穩(wěn)定的場(chǎng)景。實(shí)操心得pre-filter的性能瓶頸往往不在過(guò)濾本身而在過(guò)濾后的候選集太小導(dǎo)致ANN索引退化。如果某個(gè)用戶(hù)的權(quán)限范圍很窄過(guò)濾后只剩幾百個(gè)向量近似最近鄰搜索可能退化成暴力搜索。解決辦法是給ANN索引設(shè)置合適的nprobe參數(shù)或者在權(quán)限極窄時(shí)直接走暴力搜索路徑。我在項(xiàng)目中會(huì)監(jiān)控每次檢索的候選集大小低于1000時(shí)自動(dòng)切換到暴力搜索。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄4.1 權(quán)限泄露的典型場(chǎng)景與修復(fù)場(chǎng)景一重排階段泄露。檢索時(shí)做了權(quán)限過(guò)濾但重排模型如Cohere Rerank、BGE Reranker的輸入包含了無(wú)權(quán)內(nèi)容。這種情況通常是因?yàn)闄z索和重排之間有一層緩存緩存里存了全量結(jié)果。修復(fù)方法是確保緩存key包含用戶(hù)角色或者干脆在重排前再做一次權(quán)限校驗(yàn)。場(chǎng)景二生成階段泄露。大模型在生成答案時(shí)可能從上下文中“推理”出無(wú)權(quán)信息。比如上下文里只有公開(kāi)的財(cái)務(wù)數(shù)據(jù)但模型根據(jù)這些數(shù)據(jù)推測(cè)出了未公開(kāi)的結(jié)論。這不是權(quán)限過(guò)濾能解決的需要在prompt層面加約束明確告知模型“只基于提供的上下文回答不要做推斷”。場(chǎng)景三日志泄露。調(diào)試日志里打印了完整的檢索結(jié)果包括無(wú)權(quán)chunk。這是最容易被忽視的泄露渠道。我的做法是在日志輸出前統(tǒng)一做一次脫敏把a(bǔ)ccess_roles不包含當(dāng)前用戶(hù)角色的chunk文本替換為[REDACTED]。4.2 性能瓶頸排查速查表現(xiàn)象可能原因排查方法解決方案檢索延遲突增權(quán)限過(guò)濾導(dǎo)致候選集過(guò)小查看檢索返回的候選集大小調(diào)整nprobe或切換暴力搜索召回率下降權(quán)限標(biāo)簽錯(cuò)誤抽樣檢查chunk的access_roles修復(fù)入庫(kù)時(shí)的權(quán)限標(biāo)注邏輯部分用戶(hù)搜不到內(nèi)容角色映射缺失對(duì)比用戶(hù)角色列表和chunk角色列表補(bǔ)充角色映射或默認(rèn)角色過(guò)濾表達(dá)式報(bào)錯(cuò)向量庫(kù)版本不支持?jǐn)?shù)組過(guò)濾查看向量庫(kù)文檔和版本號(hào)升級(jí)版本或改用位圖方案內(nèi)存占用過(guò)高metadata過(guò)大統(tǒng)計(jì)metadata平均大小壓縮角色列表或使用角色組4.3 幾個(gè)容易踩的坑坑一忘了處理“公開(kāi)”內(nèi)容。公開(kāi)內(nèi)容應(yīng)該對(duì)所有用戶(hù)可見(jiàn)但如果access_roles里只寫(xiě)了[role_public]而用戶(hù)角色列表里沒(méi)有role_public就會(huì)搜不到。解決方案是在構(gòu)造過(guò)濾條件時(shí)自動(dòng)把role_public加入用戶(hù)角色列表。坑二角色繼承沒(méi)考慮。如果角色有繼承關(guān)系比如role_manager繼承role_employee過(guò)濾時(shí)要把繼承鏈上的所有角色都展開(kāi)。我見(jiàn)過(guò)項(xiàng)目里只判斷直接角色導(dǎo)致經(jīng)理看不到員工能看的內(nèi)容。坑三向量庫(kù)的filter和limit順序。有些向量庫(kù)是先取top_k再過(guò)濾有些是先過(guò)濾再取top_k。這個(gè)差異會(huì)導(dǎo)致完全不同的結(jié)果。Milvus是pre-filter先過(guò)濾再搜索Qdrant默認(rèn)也是pre-filter但Weaviate在某些版本是post-filter。選型時(shí)務(wù)必確認(rèn)這一點(diǎn)。坑四多租戶(hù)場(chǎng)景下的collection隔離。如果一套系統(tǒng)服務(wù)多個(gè)租戶(hù)最安全的做法是每個(gè)租戶(hù)一個(gè)collection而不是靠metadata過(guò)濾。metadata過(guò)濾在極端情況下比如過(guò)濾表達(dá)式寫(xiě)錯(cuò)會(huì)導(dǎo)致跨租戶(hù)泄露。collection隔離雖然資源開(kāi)銷(xiāo)大但安全性高一個(gè)數(shù)量級(jí)。5. 不同RAG框架下的權(quán)限隔離實(shí)現(xiàn)差異5.1 Dify知識(shí)庫(kù)的權(quán)限隔離能力邊界Dify是目前最流行的低代碼RAG平臺(tái)之一但它的權(quán)限隔離能力有限。Dify的知識(shí)庫(kù)權(quán)限是應(yīng)用級(jí)的也就是說(shuō)你可以控制哪些用戶(hù)能訪問(wèn)哪個(gè)應(yīng)用但沒(méi)法在同一個(gè)應(yīng)用內(nèi)做文檔級(jí)或chunk級(jí)的權(quán)限隔離。如果你的場(chǎng)景是“每個(gè)部門(mén)一個(gè)獨(dú)立知識(shí)庫(kù)”Dify夠用但如果需要“一個(gè)知識(shí)庫(kù)內(nèi)不同用戶(hù)看到不同內(nèi)容”Dify原生做不到。變通方案是在Dify前面加一層網(wǎng)關(guān)根據(jù)用戶(hù)身份路由到不同的知識(shí)庫(kù)。或者用Dify的API模式自己實(shí)現(xiàn)檢索層把Dify只當(dāng)作生成層。我實(shí)際項(xiàng)目中采用后者自己用Milvus做檢索和權(quán)限過(guò)濾把過(guò)濾后的上下文傳給Dify的completion API做生成。這樣既利用了Dify的編排能力又實(shí)現(xiàn)了細(xì)粒度權(quán)限控制。5.2 自建方案LangChain Milvus的權(quán)限隔離實(shí)踐自建方案靈活性最高但工作量也最大。核心是要實(shí)現(xiàn)一個(gè)PermissionAwareRetriever繼承LangChain的BaseRetriever在_get_relevant_documents方法里注入權(quán)限過(guò)濾邏輯。from langchain.schema import BaseRetriever, Document from typing import List class PermissionAwareRetriever(BaseRetriever): collection: object embed_model: object user_roles: List[str] def _get_relevant_documents(self, query: str) - List[Document]: query_vector self.embed_model.embed_query(query) roles list(set(self.user_roles [role_public])) roles_str , .join([f{r} for r in roles]) expr fARRAY_CONTAINS_ANY(access_roles, [{roles_str}]) results self.collection.search( data[query_vector], anns_fieldembedding, param{metric_type: COSINE, params: {nprobe: 16}}, limit10, exprexpr, output_fields[text, doc_id, chunk_id] ) docs [] for hits in results: for hit in hits: docs.append(Document( page_contenthit.entity.get(text), metadata{ doc_id: hit.entity.get(doc_id), chunk_id: hit.entity.get(chunk_id), score: hit.score } )) return docs這個(gè)Retriever可以直接接入LangChain的QA鏈替換默認(rèn)的向量檢索器。關(guān)鍵點(diǎn)是user_roles在初始化時(shí)傳入每次檢索自動(dòng)帶上權(quán)限過(guò)濾。5.3 權(quán)限緩存與刷新策略用戶(hù)角色不是一成不變的員工轉(zhuǎn)崗、離職、權(quán)限調(diào)整都會(huì)影響檢索結(jié)果。如果每次檢索都去查數(shù)據(jù)庫(kù)拿角色延遲會(huì)增加如果緩存角色又面臨一致性問(wèn)題。我的策略是短TTL緩存 主動(dòng)失效。角色信息緩存在Redis里TTL設(shè)為5分鐘。同時(shí)監(jiān)聽(tīng)角色變更事件變更時(shí)主動(dòng)刪除對(duì)應(yīng)用戶(hù)的緩存。這樣正常情況下檢索延遲不受影響角色變更后最多5分鐘生效。對(duì)于安全要求極高的場(chǎng)景可以把TTL降到1分鐘或者干脆不緩存每次實(shí)時(shí)查詢(xún)。注意角色緩存key要包含租戶(hù)ID多租戶(hù)場(chǎng)景下不同租戶(hù)的同名用戶(hù)角色不能混用。我見(jiàn)過(guò)因?yàn)榫彺鎘ey沒(méi)加租戶(hù)前綴導(dǎo)致跨租戶(hù)權(quán)限串號(hào)的案例排查了兩天才定位到。6. 權(quán)限隔離的測(cè)試與驗(yàn)證方法6.1 構(gòu)造權(quán)限測(cè)試用例集權(quán)限隔離做完了怎么驗(yàn)證它真的有效靠人工點(diǎn)幾個(gè)用戶(hù)試試是不夠的需要系統(tǒng)化的測(cè)試用例。我的做法是構(gòu)造一個(gè)權(quán)限矩陣行是用戶(hù)角色列是文檔密級(jí)每個(gè)單元格期望的結(jié)果是“可見(jiàn)”或“不可見(jiàn)”。然后寫(xiě)自動(dòng)化測(cè)試腳本用不同角色的賬號(hào)去檢索同一組query斷言返回結(jié)果是否符合預(yù)期。測(cè)試用例要覆蓋邊界情況公開(kāi)文檔對(duì)所有角色可見(jiàn)、機(jī)密文檔只對(duì)特定角色可見(jiàn)、跨部門(mén)文檔的隔離、角色繼承鏈的傳遞、角色變更后的即時(shí)生效。每個(gè)用例都要有明確的預(yù)期結(jié)果不能模棱兩可。6.2 滲透測(cè)試思路模擬越權(quán)檢索除了正向測(cè)試還要做反向測(cè)試——模擬越權(quán)檢索。具體做法是用一個(gè)低權(quán)限賬號(hào)構(gòu)造一些“誘導(dǎo)性query”看能不能檢索出高權(quán)限內(nèi)容。比如用HR賬號(hào)問(wèn)“公司薪酬體系”正常應(yīng)該返回HR文檔然后用普通員工賬號(hào)問(wèn)同樣的問(wèn)題應(yīng)該返回空或者公開(kāi)的薪酬政策絕不能返回HR內(nèi)部文檔。我還會(huì)做“拼接攻擊”測(cè)試把多個(gè)低權(quán)限文檔的內(nèi)容拼在一起看大模型能不能推理出高權(quán)限信息。這種攻擊很難完全防御但可以通過(guò)prompt約束和輸出過(guò)濾來(lái)降低風(fēng)險(xiǎn)。6.3 監(jiān)控與告警指標(biāo)上線后需要持續(xù)監(jiān)控權(quán)限相關(guān)的指標(biāo)。核心指標(biāo)包括權(quán)限過(guò)濾后的平均召回?cái)?shù)量太低說(shuō)明過(guò)濾過(guò)嚴(yán)、權(quán)限過(guò)濾的耗時(shí)占比太高說(shuō)明過(guò)濾邏輯需要優(yōu)化、越權(quán)檢索告警次數(shù)任何一次都值得排查、角色緩存命中率太低說(shuō)明緩存策略需要調(diào)整。我通常會(huì)在檢索層埋點(diǎn)記錄每次檢索的用戶(hù)角色、過(guò)濾條件、候選集大小、返回結(jié)果數(shù)量。這些數(shù)據(jù)不僅能用于監(jiān)控還能幫助優(yōu)化權(quán)限模型——比如發(fā)現(xiàn)某個(gè)角色的候選集總是很小可能是角色權(quán)限配置有問(wèn)題。7. 一些實(shí)戰(zhàn)中的取舍與體會(huì)權(quán)限隔離沒(méi)有銀彈每個(gè)方案都有取舍。pre-filter性能好但依賴(lài)向量庫(kù)能力post-filter靈活但召回不可控。RBAC簡(jiǎn)單但不夠細(xì)ABAC靈活但實(shí)現(xiàn)復(fù)雜。collection隔離最安全但資源開(kāi)銷(xiāo)大metadata過(guò)濾省資源但有泄露風(fēng)險(xiǎn)。我的經(jīng)驗(yàn)是先明確安全底線再在底線之上做性能優(yōu)化。如果數(shù)據(jù)密級(jí)很高比如財(cái)務(wù)、法務(wù)寧可犧牲性能也要用collection隔離。如果數(shù)據(jù)主要是內(nèi)部公開(kāi)信息metadata過(guò)濾就夠了。不要為了追求架構(gòu)優(yōu)雅而犧牲安全性也不要為了絕對(duì)安全而把系統(tǒng)做得無(wú)法使用。另一個(gè)體會(huì)是權(quán)限設(shè)計(jì)要盡早介入不要等系統(tǒng)跑通了再補(bǔ)。我見(jiàn)過(guò)太多項(xiàng)目在后期加權(quán)限結(jié)果發(fā)現(xiàn)向量庫(kù)的metadata結(jié)構(gòu)不支持、檢索鏈路要重構(gòu)、已有的chunk要全部重新入庫(kù)。前期多花兩天設(shè)計(jì)權(quán)限模型后期能省兩周的返工時(shí)間。最后分享一個(gè)實(shí)用技巧在開(kāi)發(fā)階段可以加一個(gè)“上帝模式”開(kāi)關(guān)允許開(kāi)發(fā)者以全權(quán)限檢索方便調(diào)試。但這個(gè)開(kāi)關(guān)必須硬編碼在配置文件里且在生產(chǎn)環(huán)境強(qiáng)制關(guān)閉。我通常會(huì)在代碼里加一個(gè)斷言如果檢測(cè)到生產(chǎn)環(huán)境且上帝模式開(kāi)啟直接拋異常阻止啟動(dòng)。這個(gè)技巧幫我避免了好幾次潛在的生產(chǎn)事故。