检索时权限正确,不代表回答发出时权限仍然有效。RAG 的一次请求会跨过身份解析、检索、重排、生成和流式传输,撤权可能发生在任何两步之间。本文讨论这些步骤之间的一致性问题;协议与示例是工程设计分析,不是实测结果,也不代表某个产品已经提供全部保证。
先给“撤权成功”一个可验证的时间边界
权限感知的企业 RAG:从检索到可核查答案已讨论身份、证据和访问范围的绑定。这里进一步追问:当用户在一次回答途中失去权限,哪个事件之后不得再使用资料?如果没有明确答案,管理员看到撤权成功,研发看到检查通过,两边的记录可能都成立。
至少区分三个承诺。提交承诺只表示权威权限库已接受变更;新检查承诺表示在约定一致性条件下、撤权之后发起的检查能够看到变更;输出承诺则要求撤权完成后,受控出口不再新放行依赖已撤权资料的内容。它们需要不同机制,不能用同一个绿色状态代替。
定义四个观测点:t_commit 是权威撤权提交,t_check 是某次授权判定使用的状态时点,t_model 是证据通过模型入口的放行时点,t_out 是答案通过用户出口的放行时点。请求在 t_check 被允许,只说明那个状态下合法;若 t_check < t_commit < t_out,旧判定不能自然延长到输出。
边界还必须写清观察位置。服务端停止放行之后,已经交给网络的字节仍可能到达手机;已经交给外部模型的正文也不能靠取消本地请求追回。因此,“撤权完成后无新的服务端放行”可以设计验收,“点击撤权后任何终端立即忘记全部内容”不能作为可兑现的工程承诺。
用一个暂停点,重现正常请求里的竞态
以下为构造场景。员工 U 原本属于项目组 G,可以读取报价附件 D。请求 Q 完成检索和授权,系统暂停在调用模型之前;管理员将 U 移出 G,权限库确认提交;随后恢复 Q。如果 Q 沿用先前判定,附件仍会进入模型。这条路径甚至不需要缓存故障,问题是检查之后状态改变。
再把暂停点放到模型已经收齐上下文、用户尚未收到答案的位置。重新检索不会清除本次模型上下文;只把最终引用链接删除,也不能证明答案未复述附件。若要求阻断本次交付,应丢弃尚未放行的答案,取消生成,并在必要时以当前合法证据重新建立请求。
第三种情况是用户已收到部分流式文本。此时可以停止剩余输出,不能撤回先前文字;后续会话还可能把已撤权材料作为历史上下文再次发送。需要记录答案与会话摘要的证据依赖,重新组织后续上下文。对无法追溯来源的综合摘要,保守处理是停用相关摘要并重新生成。
这属于检查与使用之间的竞态。MITRE 的 CWE-367 描述了状态在检查后、使用前改变导致原判断失效的问题。[1] 将这一区分用于 RAG,是本文的架构推演:把检查挪近使用可以缩小窗口,但没有协调机制就不能证明窗口已消失。
一致性令牌规定读取下界,不替代输出协调
Google Zanzibar 论文把授权变更与内容变化的因果顺序纳入设计,避免旧授权状态错误放行后续内容。[2] 这一思路对 RAG 的启发是:证据的正文版本与权限判定依据应可关联,而不是仅在请求上写一个“已授权”。具体产品仍需按自身接口与存储配置实现。
SpiceDB 的 at_least_as_fresh 要求检查使用不早于所给 ZedToken 的数据;令牌是不透明值,应交给服务解释,不能按字符串排序。其 fully_consistent 在调用开始选取快照,之后发生的写入不在这次检查中。[3] 因此,某次检查足够新鲜,仍不能保证检查完成后没有撤权。
还要核对存储后端:SpiceDB 文档明确指出,CockroachDB 上 fully_consistent 存在不能保证读到刚写入关系的情况,并建议用写入返回的 ZedToken 配合 at_least_as_fresh。[3] 工程上应验证实际后端和版本,不把参数名称理解成跨系统的即时一致保证。
OpenFGA 的 HIGHER_CONSISTENCY 明确用于绕过查询缓存、直接访问数据库;其官方说明也指出,自定义多区域数据库适配器仍需处理底层复制语义。[4][5] 这与按一致性令牌指定版本下界并非同一种接口。无论采用哪种机制,都必须先让请求取得覆盖相关撤权的可信状态;永远携带旧令牌,只能保证不早于旧时点。
把权限依赖带到缓存与模型入口
建议为请求保存租户、可信主体、证据文档与修订、授权模型版本,以及涉及的群组或策略范围。正文版本与权限版本分开:U 退出 G 时,D 的正文可能完全没变。只比较文档更新时间会漏掉成员关系变化;只记录用户 ID,也看不出同一用户的权限已经收缩。
对答案缓存保存证据依赖清单,在命中后重新确认当前主体能访问全部依赖。缓存键中的授权代次必须来自可信状态,而不是浏览器参数。共享缓存可以复用计算,但不应复用另一个用户的放行结论。不能定位依赖的旧条目,应整体失效,不能以“答案里没写文档名”作为保留理由。
代次可以按租户、群组或资源范围维护,但作用域必须覆盖继承权限。租户代次简单而失效面大;按资源精细维护更省缓存,却需要追踪上级目录、嵌套群组和策略变化。新增一条授权路径也可能影响最终判断,所以应重算有效权限,而非看到某一关系被删除就假定所有访问均已失效。
候选筛选、外部重排和模型入口分别检查自己的边界。索引里旧的访问列表不能充当权威撤权状态;授权服务不可用或版本来源不明时,相关受限证据应暂停使用。内容更新本身的完整性可结合 RAG 知识库增量更新与删除一致性中的版本清单理解,但请求固定了内容快照,也不能冻结用户后续的访问权。
严格阻断在途请求,需要可完成的撤权屏障
如果业务要求“撤权完成后不再新放行”,可以设计应用层撤权屏障。以下是有前提的方案,并非上述授权引擎自动提供的功能。前提是相关外部调用与用户输出都经过受控出口,撤权流程能够协调这些出口;只控制某个检索服务,不足以做出端到端承诺。
第一步先持久化撤权意图,并暂停受影响范围的新放行。第二步提交权限变更,把返回的一致性信息与新的受控代次登记到权威状态。跨存储无法原子提交时,用可重试的状态机保持“撤权处理中”,故障时继续阻断;不要在权限库更新成功、出口却未更新的中间状态显示全部完成。
第三步使服务节点拒绝旧代次,取消相关在途请求,丢弃待发送缓冲,处理已获准的发送操作。出口的代次比较与放行动作必须和撤权推进存在明确顺序,例如由同一协调器串行决定;不能比较完代次后又把正文放进不受检查的异步队列。旧操作尚未完成或隔离时,不确认屏障完成。
最后,确认所有仍能服务该范围的出口已进入新状态,才返回“撤权完成”。失联节点必须失去服务资格,恢复后先同步代次;否则它可能继续输出。代价是额外协调、等待和误拒绝。这里承诺的是受控出口的放行顺序,不是已经传出字节的到达顺序,也不保证外部服务删除既有副本。
流式输出与重试,分别限定损失和重复使用
并非所有系统都值得承担严格屏障成本。可以选择明确的有界撤权策略:周期性更新授权状态,对每批输出检查代次,失联时停发;但应把同步、调度和缓冲延迟纳入上界。如果任一阶段在故障下无界,就只能给出正常条件下的目标值,不能承诺固定秒数内必然停止。
缓冲完整答案、在交付前检查,可以减少逐字输出时的中途撤权暴露,也会增加首字延迟;单独一次终检仍有检查与发送的间隙。逐批检查允许更早发现变更,但批次越小,协调开销越高。采用哪一种,应根据资料敏感度和交互体验决定,并说明批次已放行后不再可追回。
撤权导致请求重试时,应重新取得主体与当前有效权限,重新构建证据,不能复用旧上下文继续生成。队列任务携带原主体和授权依赖,出队时再判断;不要悄悄用服务账号把失败任务“补跑成功”。对于仅剩公开资料足以回答的问题,可以生成新的受限范围答案;不能回答时给出不暴露隐藏资料名称的说明。
请求取消也要与数据清理区分。取消生成是减少后续处理与输出,清理缓存、日志或服务方留存是另外的流程。审计记录需要保存决定所依据的版本和放行事件,通常不必复制整份敏感正文。身份凭证、原始令牌和附件内容不应为了排查竞态而无边界写入日志。
验收必须控制事件顺序,而不是反复点击碰运气
为上述 U、G、D 构造独立测试夹具,使用隔离的虚拟附件,并在授权后、模型入口前、首批输出后设置可控制暂停点。验收方先规定事件顺序与预期结果,再执行,不依赖随机等待。外部重排器、模型入口和用户出口都记录证据标识及代次,才能区分“没有泄漏到最终答案”和“资料从未越过边界”。
至少回放五类反例:授权后撤权再恢复请求;缓存未过期但成员资格已撤销;文档权限不变、仅上级群组关系改变;出口失联或重启后尝试恢复旧请求;首批流式文字发出后撤权。另保留仍有另一条合法授权路径的正例,防止系统以全部拒绝获得表面上的零违规。
分别统计撤权提交到屏障完成的耗时、完成后旧代次新放行次数、取消后仍继续输出的批次,以及合法请求的误拒绝率。对有界策略,另测各故障条件下最后一次旧权限放行距离提交的延迟。报告样本量、最大值和未完成撤权数量;未完成不能被排除后只汇报成功请求的漂亮分位数。
上线前应能拿出一条完整证据链:谁在什么范围撤销了什么关系,哪一权限状态覆盖该变更,哪些在途请求被处理,哪个出口在何时停止放行。测试中零违规是必要证据,却不是未来绝不失败的证明。把承诺限定到可控制的边界,并让失败保持可见,才有条件把“撤权成功”作为可依赖的业务状态。
