公司介绍 服务 Workflow 商城HOT
CASES / 案例
Agent 中台企业 AI 知识中台 · 可私有化 智能客服Telegram 7×24 自动接待获客 NEVA私人 AI 情感陪伴 Agent TG 智能监控飞单识别 · 客户资产保护 广告素材自动化ComfyUI 流水线 · 素材批量生成
AI 应用趋势NEW 交付流程 联系
首页 / AI 应用趋势 / RAG 知识库的权限控制:别让 AI 把不该看的
DOC.20260619TRENDS / 深度

RAG 知识库的权限控制:别让 AI 把不该看的文档答出来

发布 2026-06-199701 字RAG权限控制AI 安全
FIG.01 / RAG ACCESS CONTROL 用户请求 role · dept FILTER 权限过滤层 检索前置 scope=allowed 知识库检索 合规答案 authorized 越权文档拦截 BLOCKED · risk 检索层权限边界 DOC.20260619 · MACHINEER

越权答案不是答错,是把不该看的喂进了上下文

先把问题定义清楚。一个 RAG 知识库出错,通常分两类:一类是检索到的内容本身有限,模型答得不够准,这属于质量问题,可以靠召回优化、提示工程、人工评测慢慢磨。另一类完全不同——模型答得很准,准到把一份你本来无权看的文档原文复述给你。后者不是质量问题,是安全事故。把这两件事混在一起谈,是很多团队上线后才反应过来的第一个坑。

列几个具体场景,工程上都不少见。一个普通员工在内部问答里输入"今年管理层定了哪些战略调整",系统从向量库里召回了一份高管闭门会纪要,模型照着总结出来;财务以外的部门同事问"我们组明年预算大概多少",结果连隔壁部门的预算盘子一起答了出来;外包驻场人员手里只有一个普通账号,却能问出核心研发模块的设计文档;多租户的 SaaS 产品里,客户 A 的运营在后台一问,命中了客户 B 上传的合同样本。这些场景的共同点是:发起提问的人没做任何"攻击",他只是正常打字,是系统主动把不该给的内容递了过去。

这里有一个时间顺序上的关键判断,值得单独拎出来说。一旦无权内容进入了模型的上下文窗口,泄露在工程意义上就已经发生了,跟模型最后吐没吐出来无关。很多人下意识想在输出端补救——加一道敏感词过滤、让模型在回答前先"声明"自己只能讲有权限的部分、或者用另一个模型审一遍输出。这些都晚了。因为只要那段文字被拼进了 prompt,它就参与了这一轮推理:模型可能在回答里换个说法转述,可能被追问几句套出来,可能在引用列表里露出文档标题,甚至可能因为多轮对话的上下文残留而在后续轮次复现。你过滤掉的只是某一次输出的某种表述,挡不住信息本身已经在场。

所以真正的控制点只有一个位置:模型生成之前。具体说,是在检索召回和上下文拼装这两步里就把权限算清楚,让无权文档根本进不了候选集,更进不了 prompt。这是结构性的安全,而不是靠运气的过滤。把权限放在生成之后,等于先把保险柜打开让人看了一眼,再讨论要不要让他描述里面有什么——顺序错了,后面做什么都是补救。

那能不能把权限规则写进系统提示,让模型自己判断"这段你能不能看"?答案是不能,原因也在上面那条时间线里:要让模型判断,前提是这段内容已经在它的上下文里了,判断这个动作本身就发生在泄露之后。退一步讲,即便不考虑顺序,让模型执行访问控制也是把一个确定性的逻辑判断,交给一个概率性的系统去做——它今天遵循,明天可能在一句巧妙的追问下就绕过去了。权限校验需要的是"要么给、要么不给"的确定结果,这种活儿应该留给代码和数据层,不该指望模型的自觉。

把这层认识立住,后面的工程设计才有地基。权限控制在 RAG 里不是某个可选的增强功能,而是检索流水线的一等公民,它决定的是哪些文档有资格成为候选、能不能进入排序、最终能不能被模型看到。从这一节往后,我们讨论的所有方案——检索层过滤的顺序、物理与逻辑隔离的取舍、多租户的身份来源、缓存策略、侧信道封堵——本质上都是在回答同一个问题:怎么保证在模型动笔之前,它面前摊开的每一份材料,提问的人都确实有权看。这个判断标准会贯穿全文,遇到任何方案,都先拿它去量一量是否成立。

把权限钉死在检索层:为什么"拿到结果再过滤"不等于安全

先说一个工程上容易被混淆的等式:很多团队默认"系统有登录、分了管理员和普通用户,那权限就做完了"。这是把两层不同的东西画上了等号。登录鉴权解决的是"你是谁、能不能进这个功能",它是一道访问门;而知识库的权限要回答的是"这条具体的文档片段,当前这个人能不能被它影响到答案"。前者是功能粒度,后者是数据粒度,两者之间隔着整个检索语义。门开了不代表门后每一份资料都对你开放。

真正的分歧出现在过滤的时机上。一种做法是先正常召回,把命中的片段拿到手,再在返回前按权限筛一遍——听起来结果一样,对外只吐出有权看的那部分。但这条路在工程上是有裂缝的。问题不在最终展示,而在中间那一步:被筛掉的片段已经从向量库里取出来了,它的存在、它的相关性得分、甚至它的标题,都已经进入了系统的处理链路。一旦后续有任何环节(重排序、日志、调试输出、缓存键)触碰到这批未过滤的候选,无权内容就有了泄露的入口。"事后筛掉"保护的是出口,没保护过程。

所以过滤必须前移到召回和上下文注入这两个动作之前发生。换个角度看会更清楚:模型最终能说出什么,完全取决于上下文里被喂进去了什么。要让无权文档绝不可能出现在答案里,唯一可靠的办法是让它压根没机会进入上下文——也就是在构造检索条件时就把权限约束当成查询的一部分,没权限的片段连被取出的资格都没有。这样保护的边界从"输出层"挪到了"数据获取层",越靠前,可被绕过的环节就越少。

结构上,比较稳的做法是把权限抽成一个独立的层,让它横在业务逻辑之上,而不是散落在各个调用点里。无论是 RAG 的检索请求,还是 Agent 对话发起的工具调用、知识查询,入口都先过这一层校验,拿到的是已经被权限收窄过的可见范围,再往下走召回。把权限做成共享的前置关卡而非每个功能各写一遍,好处有两个:一是判定逻辑只有一份,不会出现"检索接口校验了、Agent 那条链路忘了校验"的不一致;二是审计有统一的落点,谁在什么权限下取了哪些片段,能集中记录、能回放。

需要强调的是,应用层那套"管理员/普通用户"的角色隔离不是多余的,它是必要的第一道关,负责功能可见性和操作权限的粗粒度划分。但它处理的是"能不能用这个功能",管不到"这次检索召回的几十条片段里哪几条该被你看见"。后者是细到单条数据、还要结合查询语义动态判定的事,必须由检索层自己承担。把这两层分清楚,才不会出现"角色配对了,可知识库照样答出了别的部门的合同条款"这类问题。

落到判断上:评估一个 RAG 系统的权限是否可信,不要只看它有没有登录和角色,要看过滤动作发生在流水线的哪一步。如果答案是"召回之后、返回之前",那它防的是展示,不是数据;如果答案是"召回条件里就带着权限约束、无权片段从未被取出",才算把权限钉在了检索层。这条线,是"看起来安全"和"真的安全"的分界。

检索流水线的正确顺序:召回 → ACL 过滤 → Rerank → top5

检索链路里有四个动作:向量召回、权限过滤、重排、截断取前几条。它们的相对顺序不是风格问题,而是会直接决定有没有越权泄露。把顺序定下来,比调任何一个模型参数都重要。一个能用的默认配置是:召回阶段放宽到几十条候选(取 50 是个稳妥的起点),交给权限服务逐条精确校验,过滤掉无权访问的,剩下的再做重排,最后截取前五条进上下文。下面拆开讲每一步为什么是这个位置。

召回的口子要开得够大

很多人凭直觉把初始召回的 top_k 设成 5,理由是「反正最后也只用五条」。这个直觉在加了权限过滤之后就会出问题。召回是按向量相似度排的,跟当前用户有没有权限毫无关系——相似度最高的那几条,完全可能正好是这个用户看不到的文档。如果初始只捞 5 条,过滤完可能只剩 1 条甚至 0 条有效结果,模型拿着残缺的上下文去答,要么答得很虚,要么干脆说不知道。用户的体感是「这破系统什么都查不到」,但根因不在检索质量,在召回口子开太小,被权限过滤一刀切空了。

所以召回阶段要按「过滤后还能剩够用的量」来倒推。如果一个部门的可见文档占比偏低,召回数就得相应往上抬。50 是经验值,不是铁律——可以观察过滤通过率,动态调整:通过率低的租户多召回,通过率高的可以收一点省算力。关键是别让过滤把候选池抽干。

过滤必须卡在重排前面

把权限过滤放在重排之后,是个看起来无害、实则危险的顺序错误。重排(Rerank)的本质是让一个模型读进候选文档的正文,再按与问题的相关性重新打分。注意「读进正文」这件事——只要先重排再过滤,那些无权访问的文档内容就已经被送进了重排模型。即便它们最后被过滤掉、没进最终上下文,泄露在重排那一刻就已经发生了。

如果用的是自建、跑在内网的重排服务,这层风险还算可控,至少数据没出墙。但很多团队为了省事直接调外部 Rerank API,性质就完全不同了:无权文档的正文被打包发到了第三方服务商的服务器上。这已经不只是逻辑越权,而是把租户的敏感数据外传出了自己的边界。合规审计里这是要出大事的一类,跟「答案里多说了一句」完全不在一个量级。

反过来,先过滤再重排,逻辑就干净了:进入重排模型的每一条候选,都是当前用户确认有权看的。重排只在合法候选里排序,无权内容根本没机会被任何模型——无论内部还是外部——接触到。顺带还省了算力:重排是按候选条数收费、按条数耗时的,先把无权的砍掉,等于让重排只处理真正会用到的那部分。

把这条链路当成一条防线看

这四步连起来,是一条单向收窄的漏斗:50 条候选 → 权限过滤后剩下合法的若干条 → 重排 → 取前五。每一步都只会让数据范围变小或重排,绝不会把已经过滤掉的东西再放回来。这个单调收窄的性质,是你能对外承诺「越权内容不会进上下文」的工程依据——只要过滤这一环的判定是可靠的、卡位是在重排之前的,后面无论重排怎么排、截断取几条,都不可能凭空冒出无权文档。

落到实现上,权限过滤这一步建议走集中的权限服务,对召回回来的每条文档逐一校验,而不是在检索层里塞一堆零散的 if 判断。这样过滤逻辑只有一处、可测、可审计,哪天权限规则变了也只改一个地方。校验的入参里,用户身份和租户标识必须来自服务端可信来源,这一点在多租户场景下尤其要钉死——但那是另一节要展开的事了。

三种隔离方案怎么选:从独立索引到行级权限

权限隔离不是一个开关,而是一条强度连续的光谱。从工程实现上看,可落地的做法大致分三档:每租户独立的索引或库、共享存储里靠 tenant_id 与 acl_tags 强制过滤、再到把权限下沉进数据源本身的行级控制。强度从高到低,成本与灵活性恰好相反。选哪一档,取决于你的数据敏感度、租户数量级,以及团队愿意为隔离付出多少运维代价。

最强的一档是物理隔离:每个租户一套独立的向量索引、独立的库,甚至独立的 namespace。它的好处是边界清晰到几乎不需要解释——查询根本碰不到别人的数据,因为那些数据压根不在当前连接的索引里。出了问题,排查范围天然收敛在单租户内。代价也直白:租户数一上规模,索引碎片化严重,资源利用率掉下来,热点租户和长尾租户混在一套调度里,运维要为成千上万个小库做版本管理、备份、迁移。这条路适合租户少、单租户数据量大、合规要求硬的场景,比如几家大客户各自独占一套;不适合 SaaS 那种动辄上万小租户的形态。

中间一档是逻辑隔离加强约束:所有租户的数据放在同一套存储里,每条切片带上 tenant_id 和 acl_tags 这类标签,查询时把这些标签作为强制条件压进过滤器。它在资源效率和隔离性之间取了个平衡,是大多数多租户系统的现实选择。但这一档的安全完全押在"过滤永远不被绕过"上——只要有一条查询路径忘了拼 tenant_id,跨租户泄露就发生了。所以它对工程纪律的要求最高:过滤条件必须收口到一个统一的检索入口,不允许任何业务代码自己手写裸查询,标签的写入和校验都要有测试兜底。它给你省下的机器成本,会以代码审查和回归测试的形式还回来。

最贴近数据的一档是行级权限,典型实现是 PostgreSQL 的 Row Level Security。它把过滤规则写进数据库策略,无论谁发起查询、走哪条业务路径,数据库都会按当前会话身份自动追加行级条件。它的价值在于把"别忘了过滤"这件事从应用层移走了——应用就算写了一条不带租户条件的查询,数据库也会替你挡住越权行。对那些怕业务代码越来越多、过滤点越来越散的团队,RLS 是一道兜底的护栏。前提是你的检索栈确实经过这层数据库,而且会话身份的设置本身可信;如果向量检索绕开了关系库直接打索引,RLS 就管不到那部分了。

这三档不是互斥的。常见的稳妥组合是:用逻辑隔离承载主体,再叠一层 RLS 做兜底,对极少数最敏感的租户单独切出物理隔离。关系库这一侧的权限模型也要跟上。一个朴素但有效的做法,是在用户表里放一个 role 字段,在数据库层面把 admin 和普通 user 区分开,让"谁能看管理范围、谁只能看自己范围"在数据结构里就成立,而不是散落在各处的 if 判断里。再往上,单独建一张 user_file 之类的关联表,把每一份上传进知识库的文档和它的归属、可见范围登记清楚。这张表的意义不止于权限校验:它让管理员能统一维护文档清单,出问题时能溯源到"这份文档是谁传的、授权给了谁"。权限和资产台账放在一起管,审计和回收才有抓手。

有一个诱惑值得专门提醒:不要试图用知识图谱来替代权限系统。图谱擅长表达实体之间的关系,看起来天然能描述"谁能访问什么",于是有人想把访问控制也建模成图里的边,靠图查询来判定权限。这条路通常会走进新的权限地狱——关系一旦复杂起来,边的语义会膨胀,继承、传递、例外规则互相缠绕,最后没人说得清某个用户到底为什么能看到某份文档。权限判定要的是确定、可枚举、可审计,而图查询给的是灵活但难以穷尽的推理。把这两件事混在一层,调试时你会同时丢掉图谱的清晰和权限系统的可控。让知识图谱去做它擅长的语义关联,权限的最终裁决交给一套独立、规则明确的机制,这个边界划清楚,后面省下的麻烦远比当初多写的几张表要多。

选型上给个可操作的判断:租户少、合规硬,优先物理隔离;多租户 SaaS,逻辑隔离打底加 RLS 兜底;无论哪种,关系库里的 role 与 user_file 这类台账都不能省,它们是权限校验和事后审计共同依赖的地基。强度越高的方案越贵,但泄露一次的代价通常比这些成本高得多,往强的一侧多留一点余量,一般不会后悔。

多租户强隔离:tenant_id 只能来自服务端,绝不让前端传

多租户的事故往往不是因为过滤逻辑写错了,而是因为过滤所依赖的那个身份字段本身就可被伪造。你可以把召回、ACL、Rerank 的顺序排得一丝不苟,但只要 tenant_id 是从请求体里读出来的,整条流水线就建在沙地上。攻击者不需要绕过任何过滤规则,只要把 JSON 里的 tenant_id 改成别人的值,过滤器就会忠实地帮他召回另一个租户的全部文档。这是把判官的笔交到了被告手里。

所以这一节其实只讲一件事:身份字段的来源。tenant_id、user_id、department_id 这类决定"你能看什么"的字段,必须从服务端的认证上下文里取,由网关或鉴权中间件在校验完会话之后注入,下游服务不接受、也不读取请求里携带的同名字段。换句话说,前端可以告诉你它想查什么,但不能告诉你它是谁。"想查什么"是业务参数,"我是谁"是身份事实,这两者的信任级别天差地别,混在同一个请求体里传是工程上的大忌。

为什么"前端能传"约等于"一定会被伪造"

有人会说,前端传 tenant_id 只是为了方便,正常用户不会去改它。这个假设在内部系统里都站不住脚,在对外服务里更是灾难。请求体对客户端完全透明,改一个字段的成本是打开开发者工具按几下回车。一旦 tenant_id 可由前端控制,攻击面就从"突破鉴权"退化成"猜一个合法的租户 ID",而租户 ID 经常是自增整数或可枚举的短字符串。这等于把强隔离降级成了一道形同虚设的提示。多租户场景下,tenant_id 应当作为强制附加的隔离字段绑定在每一次检索请求上,并且这个值只能源自服务端的认证结果——这一点在相关工程实践中被反复强调,原因正是伪造 tenant_id 或 user_id 会直接导致跨租户的越权读取。

落到实现上,可操作的判断有几条。第一,鉴权中间件解析完 token 后,把租户与用户身份写进一个请求级的上下文对象,检索服务只从这个上下文里取值,代码层面不提供"从 body 读 tenant_id"的入口,从源头上消灭误用。第二,把 tenant_id 作为查询的硬性条件下推到向量库或元数据存储,而不是查完再在应用层比对——前者是数据库帮你保证边界,后者是你自己记得过滤,可靠性不在一个量级。第三,对每一条入库的文档,租户归属在写入时就固化为不可变的元数据字段,避免后续靠路径、命名空间等"软约定"来推断归属。

登录鉴权是隔离生效的前提

tenant_id 来自服务端的前提,是先有一个可信的服务端身份。如果系统允许匿名访问,那认证上下文里根本没有租户信息,强隔离也就无从谈起。所以知识库管理页和对话页都应当强制登录,只有通过账号鉴权的合法会话才能进入系统,不留匿名入口。这不只是为了挡住未授权的人,更是为了保证每一次检索请求背后都有一个明确、可追溯的身份,过滤逻辑才有依据可循。一个没有登录态的请求,应当在进入检索流程之前就被拒绝,而不是带着空身份走到过滤这一步再做特判——fail-closed 比 fail-open 在这里是不可妥协的默认值。

权限的管理面同样要收紧。新增用户、删除用户、调整归属与权限这类操作,应当限定在专门的管理员角色手里,普通账号既看不到也调不动。把用户与租户的映射关系、权限变更记录统一持久化到可靠的存储里,一来保证服务重启或扩容后身份关系不丢,二来让每一次权限调整都有据可查。这部分听起来像基础的后台管理,但它恰恰是 tenant_id 可信度的根基:服务端注入的那个值之所以可信,是因为它背后有一套被严格管控的身份数据在支撑。

几个容易被忽略的边角

多租户隔离做到位之后,仍有几处缝隙值得盯紧。其一是后台批处理与异步任务,这类调用往往没有用户会话,开发者容易图省事用一个超级权限去跑全量数据,结果绕过了租户边界,建议为系统任务也分配明确的租户范围或显式的服务身份,而不是默认放行。其二是跨服务调用时身份的透传,A 服务带着用户身份调 B 服务,如果中间环节把上下文丢了,B 服务就可能用默认或空租户执行检索,这种"中途掉身份"的问题在链路长的系统里很常见,需要在服务间约定好身份的传递契约。其三是缓存键,多租户下任何缓存都必须把 tenant_id 纳入键的组成,否则一个租户的查询结果可能被另一个租户命中,隔离在缓存层悄悄失效。

这一节的核心立场可以收成一句:身份是事实,不是参数。让服务端独占 tenant_id 的写入权,配合强制登录与受控的权限管理,多租户隔离才有一个不可伪造的支点。其余的过滤、排序、审计都建立在这个支点之上——支点松了,上面盖得再精巧也是空中楼阁。

性能与缓存:批量校验、TTL 与高敏不缓存的取舍

把 ACL 过滤插进检索路径,绕不开一个现实问题:每多一道权限判断,就多一段同步等待。检索本身已经要扛向量召回的延迟,如果过滤环节再拖几百毫秒,整条链路的体感就崩了。这一节谈的是怎么让权限校验既准又快,以及哪些地方为了快而省的步骤其实省不得。

最容易踩的坑是逐条校验。召回阶段一次拿回几十个候选 chunk 很正常,假如对每个 chunk 都单独发一次权限查询,那就是几十次往返。权限服务通常是独立部署的,单次调用看着不贵,乘上候选数量再叠加网络抖动,尾延迟会被拉得很难看。更糟的是这种调用模式对权限服务自身是放大攻击——一个检索请求扇出成几十个下游请求,并发一上来下游先被打垮。

正确的做法是把判断合并成一次。拿到候选集后,先按文档维度去重——同一篇文档常常命中多个 chunk,真正需要确认权限的是文档而不是每个片段。去重后把这批文档 ID 一次性丢给权限服务,让它批量返回每个文档对当前主体是否可见。几十个候选去重后往往只剩十几篇文档,一次批量查询就能覆盖,往返次数从几十降到一次。前提是权限服务得提供批量接口;如果只有单查接口,这是优先要补的工程缺口,而不是用并发循环硬凑。

批量之后还想再压延迟,就轮到缓存。检索场景里,短时间内对相近问题的召回结果高度重叠,同一个用户对同一批文档的可见性在几十秒内基本不会变,这部分判断结果是值得缓存的。缓存键要带上主体身份和文档标识,绝不能只按文档缓存——按文档缓存等于把 A 的权限判断结果发给了 B,这就不是性能优化是越权了。

TTL 怎么定,本质是在新鲜度和命中率之间找平衡。把过期时间控制在 60 秒或 300 秒这个量级是比较稳妥的区间:足够覆盖一次连续问答里的重复召回,又短到让权限变更能在可接受的窗口内生效。TTL 拉得越长命中率越高,但权限收回后的"放行窗口"也越长——用户已经被移出某个项目组,缓存却还认为他能看,这段时间里他依然问得出内容。所以 TTL 不是越大越好,它是一个可以解释给安全团队听的风险参数。

真正的分界线在高敏数据上。对涉密文档、合规受限内容这类高敏权限,我的判断是不缓存。原因很直接:缓存意味着接受一个过期窗口,而高敏场景对"权限已收回但仍被放行"几乎零容忍。这里宁可每次都走一遍实时校验,把那点延迟吃下来,也不要为了快留一个数分钟的暴露口子。普通文档可以缓存换性能,高敏文档用性能换确定性,这是按数据等级分档处理,不是一刀切。

落地时把这几条串起来:校验路径上先去重再批量,缓存键绑定主体,普通权限设短 TTL,高敏权限标记为不可缓存并强制实时查。另外留一个口子——当权限系统发生变更(成员调整、文档密级升级)时,最好能主动失效相关缓存,而不是干等 TTL 自然过期。能做到主动失效,普通文档的 TTL 就可以适当放宽,整体延迟和新鲜度都更好看。这套机制不复杂,但它决定了权限过滤是"能用"还是"能上生产"。

堵住侧信道与引用泄露:答案没漏,标题和命中数也可能漏

前面几节解决的是"内容不进上下文"。但权限泄露不止内容这一条路。一个把正文过滤得很干净的系统,照样可能从边缝里漏信息出去——而且这些边缝平时没人盯,上线后才被人摸出规律。

最容易被忽视的是引用本身。RAG 答案后面挂的那串出处,工程上往往被当成"已经过滤过的安全副产品"直接渲染出去。问题在于,标题这一行就可能是敏感信息。一篇叫《裁员名单及补偿方案(2025Q1)》的文档,哪怕模型一个字正文都没引用,光把这个标题显示在引用列表里,就已经把"公司在做裁员、且方案已成文"这件事告诉了不该知道的人。文件名、文档摘要、所属空间名、最后修改人,这些元数据都属于要做权限判断的对象,不能因为它不是"正文"就放行。

引用链接的处理要比展示更狠一层。展示时做了过滤,不代表点开时还安全。常见的坑是:列表渲染走了一遍 ACL,但点击跳转直接拿文档 ID 去对象存储取原文,这一跳没有再校验。结果就是越权用户虽然看不到引用条目,却能拿到一个泄漏出去的链接、或者猜到 ID 规律,绕过列表直接拉全文。正确做法是把点击打开当成一次独立的资源访问请求,在服务端重新执行一遍文档级权限校验,和检索那次是两套独立的检查点,谁都不替谁背书。ID 用不可枚举的随机串,也是顺手该做的。

真正阴险的是统计型侧信道。系统经常会在界面上顺手暴露一些"无害"的数字:召回命中数、相似度分数分布、"为你找到 N 篇相关文档"这类提示。攻击者不需要看到内容,他用低权限账号反复试探不同关键词,观察命中数从 0 跳到 1,就能推断出某个名字、某个项目、某份合同在库里存在。这就是经典的存在性泄露——你确认了"有这么个东西",本身就是情报。错误提示的差异同样致命:"无权访问"和"未找到"如果返回得不一样,等于明明白白告诉对方"东西在,只是你看不了"。这两种情况的对外表现必须做成一致的:要么都返回空,要么都返回同一个泛化提示,不让响应的形状随权限状态变化。

处理侧信道的思路和处理正文不同。正文是"过滤掉就行",侧信道是"要让有权和无权的两次请求在可观测层面长得一样"。所以命中数、分数这类辅助信息,要么不展示,要么基于过滤后的结果集再算一遍,绝不能拿过滤前的原始数字。这里有个容易踩的反例:为了性能,先算好总命中数缓存起来,再做权限过滤——那个缓存的数字就是过滤前的,照样漏。统计口径必须跟在权限边界后面走。

最后是可审计这件事,它不是合规摆设,是排查泄露的唯一抓手。侧信道泄露的特点是隐蔽、滞后、难复现,等你发现异常往往已经过去很久。所以每次检索要把判断依据落到日志里:请求方的身份和权限上下文从哪来、参与过滤的策略命中了哪几条、原始召回多少、过滤后剩多少、最终引用了哪些文档 ID。这几个数字凑齐,事后任何一条"某某看到了不该看的"的投诉,都能拉出当时的现场重新跑一遍,确认到底是过滤逻辑出错、缓存读脏了,还是权限数据本身配错。没有这条证据链,你连"有没有漏"都说不清,更别提定位。把策略和过滤过程做成可回放,等于给整套权限控制上了一道事后保险。

落地与验收:审计字段、交付目标与可回放的过滤证据

到这一步,权限设计的成败不再取决于你画的架构图,而取决于一件事:出了问题,你能不能把当时的过滤过程原样调出来看。验收一个 RAG 权限方案,本质是验收它的可观测性——每一次检索都得留下能复盘的痕迹,而不是只看最终答案对不对。下面四个问题,是交付评审里最常被追问的。

我已经在 Prompt 里写了"不要泄露机密内容",还需要做检索层权限吗?

需要,而且这两件事不在一个层面上。先撇开模型听不听话的争论,只看一个工程指标:你怎么向验收方证明它生效了?Prompt 里的约束没有任何可记录的产物——没有字段、没有计数、没有可回放的判定记录,事后只能靠重新提问去碰运气复现,这种东西过不了审计。

检索层的权限则天然产出证据。一次请求走完,日志里应当落下三个递减的数:召回阶段拿到多少条、按访问控制清单筛过之后剩多少条、最终拼进上下文多少条。比如原始召回 50、过滤后 12、入上下文 5,再附上这次校验了几篇文档、放行了几篇。这串数字才是验收方真正要的东西。它把"权限有没有起作用"从一个信念问题,变成了一个可以逐条核对的事实问题。Prompt 约束可以作为最末端的兜底话术,但它替代不了能留痕的检索层管控。

权限校验到底放在 Rerank 之前还是之后?

之前,没有例外。这里换个角度从验收倒推:交付目标是"越权证据进入上下文 0",要证明这一点,你必须能拿出一个在重排序发生之前就已经确定的过滤后计数。如果校验排在 Rerank 后面,那意味着越权文档已经先一步进了打分环节——哪怕最后没被选中,它的内容也已经被外部重排序服务读取过,这就已经构成泄露,而你的日志根本记不下"它本不该出现在这里"。

所以顺序不只是性能考量,它直接决定了 after_acl_count 这个字段有没有意义。只有过滤先行,这个数才代表"经过授权的候选集大小",重排序和截断都只在这个集合内部进行,证据链才闭合。

逐条调权限服务太慢,能不能加缓存?

能加,但缓存命中不能让审计链断掉。常见做法是把一批文档 ID 的访问判定合并成一次批量校验,再给结果配一个较短的过期时间,避免每条都去敲权限服务。这部分优化在性能那节已经讲透,这里只补一条验收红线:即便命中缓存,本次请求仍要照常记录校验文档数与通过数,过滤原因照常可回放。否则缓存就成了审计盲区——你查一次越权事件,发现日志里写着"命中缓存",却调不出当时依据的是哪个版本的权限快照,这种日志等于没记。

另外,高敏感等级的内容判定不进缓存,每次实算。缓存换来的是吞吐,代价是判定的时效性会滞后于权限变更,对普通文档可以接受,对高敏文档不能拿这点延迟去赌。

答案正文没泄露内容,引用标题和命中数也算泄露吗?

算。这正是为什么 final_count 要单独记录、日志要按维度拆开。一份用户无权访问的文档,哪怕正文没被复述,只要它的标题出现在引用列表里,或者命中数从平时的 3 条悄悄变成 4 条,对方就推断出了"这里存在一份我看不到的东西"。这类侧信道泄露不会体现在答案文本上,只能靠把管理员操作、RAG 检索、Agent 对话运行这几类日志分开审计才查得出来。

验收方法很直接:拿一个低权限账号,专门去问那些你明知存在但它无权看到的内容,然后核对返回的引用标题、命中计数有没有任何抖动。三个计数字段加上分类日志,让这种探测留下可对比的基线。

把上面四点收口成一张验收清单:权限必须在检索层强制生效,越权证据零进入上下文,每次过滤都附带可回放的判定原因。落地节奏上,权限版本建议放在 MVP 的第二到第四周完成,先把审计字段和这道越权防线补齐,再往上叠功能。顺序反了,后面每加一个检索入口,都是在没有留痕能力的地基上盖楼。

© 2026 MACHINEER · 万机智能machineer.atsop.io · 把 AI 装进你的业务流程