文档已经撤权,为什么 Agent 的旧回答仍可能把它带回来?
历史机制说明(2026-10-20): 本文复盘的是 2026 年 9 月的撤权与发布竞态。之后架构已调整授权与发布职责;文中的当时方案和 RED/GREEN 记录,不代表当前系统仍采用相同的发布拒绝合同。请结合最新架构导读理解现行 Owner 边界。
内容主线:模块精讲;兼线:系统架构与工程观点。本文只讨论受保护知识的披露时点,不讨论一般向量检索召回率。
给 RAG 接入资料许可时,一个很自然的设计是:检索前先检查权限。通过了,读出片段;模型写完答案,再把引用交给用户。
但 2026 年 9 月我们对 ETF Agent 做撤权测试时,发现一个更实际的漏洞:历史回放已经会隐藏失效材料,首次流式回答却仍可能发出同一段原文。
这意味着一个文档从“允许读”到“不允许披露”的变化,不能只在检索入口处理。模型可能早就取得了片段;旧快照也可能已经保存在数据库中。真正需要重新核对的,是每一次受保护内容变成用户可见结果的时刻。
一、读到资料,与现在还能发表资料,是两个问题
想象一份仅对获准账户开放的研究报告。Agent 在上午读取了 V1,生成了一段包含报告摘录的分析。下午报告被撤权或被替换,用户重新打开历史对话。
如果系统只记得“上午检索合法”,下午仍然可能从多个地方泄漏旧内容:尚未发出的流式增量、已经生成的回答块、断线恢复时的快照、历史聚合或导出的附件。
我们必须拆出三个时间坐标:
| 时刻 | 必须回答的问题 | 不能拿什么代替 |
|---|---|---|
| 检索、阅读时 | 当前账户允许取得哪份资料、哪一版本? | 搜索命中的标题 |
| 回答提交时 | 依赖资料此刻仍有发布资格吗? | 数分钟前的检索许可 |
| 历史再次显示时 | 当前用户是否仍可以看到这个已保存片段? | 旧版成功提交回执 |
这不是“每次重新搜索一遍”的要求。资料身份、版本、内容 hash、policy/auth revision 与适用性期限是不同的东西;真正的资格复核应尽量沿它们的权威记录完成。
二、真实 RED:回放能挡住,第一次发送却漏了
早期治理已经能在某些历史读取场景隐藏撤权内容,看起来似乎有了正确保护。
但 9 月 12 日的一次验证把撤权插入生成与发布之间:模型已经形成知识相关段落,许可状态在提交前变化。旧路径仍可能把带受保护摘录的首次 SSE 或已提交正文送出;之后的回放再隐藏,已经太晚了。
修复没有依赖模型识别“这是一段机密信息”,而是要求知识相关的回答段在提交前再查一次当前的来源绑定。resolver 返回拒绝、未知、错误或版本不一致,受保护原文都不能进入首发消息或 Answer Segment;系统保留来源资格变化的可诊断原因,并让正常允许的独立内容继续按自己的资格处理。
这里有一条很现实的边界:挡住未来披露,不等于能从用户已经收到的浏览器字节里把旧内容收回。 因此,补救不能仅仅依靠下次历史回放正确。
三、为什么发布前检查一次仍然不够?
第一轮修复解决了“生成时允许、提交时已经不允许”的部分问题,但还没有完全消除竞态。
假设 sidecar 文件记载当前许可,数据库里的批准索引也记着一份许可。发布线程刚从 sidecar 看到了允许,另一线程马上撤权并更新数据库,正文随后提交。
这时候谁的“当前状态”才算数?
我们最终没有让所有文件和数据库同时成为独立权威。方案改成:以已经提交的 approved knowledge index 为当前可发布身份的依据,文件 sidecar 只作为等待验证、等待同步的输入。
对于依赖受保护资料的发布事务,检查和提交要与同一索引记录建立明确的锁与提交顺序。提交前核对当前 policy/auth revision、文档 version/hash、assessment 的有效时限;并发撤权如果先完成,发布就拒绝;若发布先完成,随后撤权只能禁止后续披露,不能追溯“收回”已发送字节。
这样设计确实增加了发布时的校验成本,但避免了把两个相互不同步的状态各自当作“最新”。
关键不是增加多少次权限检查,而是检查的结果与真正产生披露效果的提交处于同一个可解释的时间秩序。
四、另一个容易做错的地方:暂时不能看,不代表应该永久删除
撤权之后,还要决定历史里保存过的正文怎么办。
这里至少存在两种不同状态。
一种是明确的 retention revoke。它具有清理效果:需要处理可展示快照、受影响的绑定段和聚合视图,避免今后从另一条路径重新读出旧内容。
另一种是当前资料暂时不可达、作用范围被拒绝、assessment 过期,或者来源阅读资格未知。这些状态都不能拿来推断“已经发生了永久撤销”,也不能擅自删除原始历史事实。
如果把所有 unknown 都转换为永久删除,不但损害可恢复性,还会破坏审计;如果一律只隐藏、不处理明确撤权的持久副本,旧摘要又可能在未来路径复活。
所以我们区分:当前不允许显示和依明确保留撤销合同清理受影响副本,由不同的明确事实驱动。
这尤其适用于同时读取公开资料、获准内部研究和账户特定数据的 Agent。一条私人来源失效,不应该让无关的公开解释跟着消失;反过来,公开引用有效也不允许它替私人来源赋权。
五、我们怎样知道这不是只在架构图上成立?
这项治理保留了有针对性的失败与成功对照:
- 首发 SSE 的生成后撤权曾经被放行,补上提交前资格复核后,受保护正文不能继续送出。
- 并发批准索引变更与发布使用同一逻辑索引的提交次序;撤权先行与发布先行,分别得到可以解释的结果。
- 明确 retention revoke 与普通 scope denied/unknown 分支分开检查;不得把“当前不可看”偷换成“永久删除”。
- 同一阶段的 provider-free 离线检查记录 371 个文件、2860 个测试通过,隔离 PostgreSQL 7 个文件、39 个测试通过。这些是原日期的工程证据,不是本轮重新执行。
不过,RAG 后来更大的完整性与质量验收并没有因为这些数字就完成。当时真实来源、端到端质量和最终正式关闭仍有开放项;9 月 14 日的后续真实试验也留下了未通过的筛选结果。权限发布合同通过,不意味着金融知识问答的召回质量已经达标。
这一点很重要,因为同一套系统完全可能做到“绝不越权显示旧内容”,却仍然检索不出足够好的资料。两个目标应该分别验证。
六、这件事为什么值得单独写?
我们已经写过资料引用、Publication 和运行时授权。但这里有一个不属于它们任何一篇的独立问题:
一份知识从允许到撤权的状态变化,怎样跨过检索、发布与历史,仍然在每一次受控披露时生效?
答案不在“模型需要更谨慎”,也不在“重新加一次来源白名单”。它在于让检索所得、批准索引、发表时点和历史回放共享真正的当前资格,同时分清隐藏与删除两种后果。
如果系统只是对永远公开、不更新的静态文档做问答,未必需要投入这么多工程复杂度。但只要你的 Agent 会读取可撤权、可修订、分账户授权的资料,检索成功只能说明当时允许读,不能代表以后每次都允许发。
取材与验证边界
- 2026-09-12 RAG 撤权与发布线性化回顾
- Financial Knowledge Evidence 原始记录
- 原变更的完整验收状态与未完成项
公开正文只说明已观察到的工程机制;不能据此声明整个 RAG 产品质量、真实金融研究收益或生产部署均已验收。
公开版本说明: 原稿引用的私有项目测试、Spec 与工程日志未随文章公开;本文保留案例及其取材时间、局部验证范围,不将内部验收作为读者可直接访问的外部证据。