跳到主要内容

0.0% 的复制率,48.3% 的事实恢复率:你的 RAG 评估量错了那一头

0x7F
0x7F

· 阅读约 6 分钟

0.0%,48.3%。同一张防护评估表上并排躺着这两个数,都是真的。

把逐字复制压到 0.0% 的,是归一化 8-gram 那一档,20-token 级别。把隐藏事实捞回 48.3% 的,是同一个平台上只发三查询的自适应攻击者,模型 Qwen3-8B。

这两个数不矛盾。一个量的是“文本有没有被复制出来”,一个量的是“信息有没有转移出去”。问题在于,绝大多数 RAG 安全评估只量前一个,然后拿着 0.0% 去交差。

arXiv:2610.30193,Peiyang Liu 等,10 月 21 日挂上去的,cs.CR 主分类,ACL Rolling Review 八月 cycle 建议接收。文章本身是方法论:118 篇全文确认的 RAG 防御论文,32 个标注者双编码,kappa 0.867,按“测量端点”而不是按作者声称的目标来组织分类。我一开始把它当又一篇 survey 划过去了,直到看见里面那两个统计数字才坐直。

防守方只量能被量到的那个数

17.8% 的研究把内容披露或重建作为主要端点。在冻结的版权/所有权关系标签下,73.3% 的记录以语料使用检测为主要端点,没有一条以“被输出表达”为主要端点。

合起来读就一句:大家忙着证明“我的语料没被拿去训练/没被检索到”,没什么人在证明“就算被检索到了,输出端也吐不出东西”。

这不是研究者偷懒。内容披露这个端点在通用问答数据集上根本标不出来:TriviaQA 上没有“私有事实”这种东西,你没法在它上面测量泄漏。评估就往能标的方向漂——漂到语料使用检测、漂到逐字命中率、漂到相似度阈值。指标不会说谎,指标只会回答你问它的那个问题。

换成攻击者的思路:我不需要你的原文。我要的是你文档里的事实。

举个机制示意,别当载荷看。某份内部文档里有这么几个槽:

部署环境: 生产 / 预发 / 灰度
密钥管理: <方案名>,挂载 path <...>
轮换周期: <n> 天
审批链路: <角色A> → <角色B>

逐字复制防守得再好也没用。第一轮问“这套服务用的是什么密钥管理方案”,第二轮问“它挂在哪个 path 下”,第三轮问“轮换周期多久、谁审批”。三个问题里没有一句是你文档的原话,8-gram 重叠为零,相似度检测全部放行——拼起来,一个完整的、可以直接拿去打的攻击面地图就出来了。

链条短得可怜:定位语料 → 把文档拆成可问的事实槽 → 多轮自适应改写 → 拼装。每一轮看上一轮返回了什么,调整措辞。引信在“改写”这一侧,而改写是免费的。

防御动作和保护结果之间,没有默认映射

这篇综述最值钱的地方不是它总结了多少篇,是把分类轴从“作者声称防什么”换成了“实际测量了什么端点”。检索重排序和上下文改写这两个动作,各自横跨五到六个端点类别。

翻译成工程语言:你在 pipeline 里加了一个 rerank,你以为它防的是注入,实际上它可能只动了召回排序,对“事实能不能被拼出来”一点作用没有。防御机制和保护结果之间不是一一对应的,得你自己拿证据把这条线接上,接不上就是自我安慰。

我在这一点上态度很偏:任何以 n-gram 重叠、相似度、逐字命中率为核心的 RAG 输出过滤器,攻击面天然落在“改写”那一侧,而改写对模型来说成本为零。这跟 prompt injection 靠“让模型更会分辨指令与数据”根治不了是同一个道理——检测层的军备竞赛会一直被绕,收敛风险的是权限边界,不是更聪明的判别器。

具体到 RAG,权限边界就两处:检索这一段能碰到什么,生成这一段被允许说什么。其余都是检测。

——离题了,收。回到那两个数。

多数评估里的攻击者根本没参与设计

综述里另一条值得单独拎出来的:多数评估用通用问答数据集加非自适应攻击,而不是敏感数据上的自适应多轮抽取。

翻成人话:你跑的那套 eval,攻击者没参与设计。它固定 prompt、单轮、不看返回结果、不会因为你挡了一次就换措辞。现实里的攻击者会看返回结果,会读你输出的那句“抱歉,我无法提供该信息”,然后换一条路再问一遍。

我手上压着的几个还没到 disclosure 窗口的东西里,有一个就是靠这种“看返回、改措辞”的土办法打穿的,全程没有一句逐字复制。

这个洞我一开始判成高危,后来把触发条件摊开看:得同时满足检索能跨租户、文档里有结构化事实、攻击者能维持多轮会话——三个前提叠一起,现实里没那么容易凑齐,级别下调,但没归零。分级这件事,判重和判轻都是失职。

拆弹清单

编号,能落地的那种:

  1. 主指标换掉。 把“逐字复制率/相似度命中率”从主指标降成辅助指标。主指标换成事实恢复率:从你自己文档里抽出敏感字段做成事实槽(谁的、哪个环境、哪个 path、哪个周期),跑三查询自适应抽取,看能拼回多少。这个数字才是你要对外说的那个。

  2. 评估集从自己语料里长出来。 通用 QA 数据集只能证明你的模型会答题,证明不了你的东西没漏。

  3. 让攻击方自适应。 至少要求评估里的攻击 prompt 能看到上一轮返回、允许多轮改写。做不到这一条的 eval,结论按“未测”处理。

  4. 检索侧收权限。 分库、分租户、按文档敏感级别切检索范围。这是权限动作,不是过滤动作——一次 retrieve 不该有能力跨过敏感文档的边界。

  5. 输出侧对关键字段做结构化替换。 连接串、内网地址、key 前缀这类东西,不该交给“模型判断能不能说”,应该在生成之前就被占位符顶掉。让模型说不出它压根没拿到的东西,比让它“注意不要输出”靠得住。

  6. 审计日志记事实槽的追问形状。 同一会话里对同一批文档做语义相近的多轮改写,在日志里是有形状的:

session=... turn=1 target=doc_17 intent=key_mgmt
session=... turn=2 target=doc_17 intent=path
session=... turn=3 target=doc_17 intent=rotation

只有你记了,这种形状才检测得到。没记,就等于没发生。

别慌。不是要你把 RAG 关回箱子里——检索该干的事照样让它干。要收回来的就一处:检索这一段到底能碰到什么。这一处握在自己手里,剩下的交给模型自由发挥都行。

排雷记录:逐字复制率是给你自己看的进度条,不是给攻击者设的门槛;门槛在权限上,不在措辞上。