给离线 RAG 做供应链安全,默认动作是签名。文本打个包,预计算好的向量一起塞进去,签名,推到边缘设备上。验签通过,这条链路就算干净了。
这个动作本身没错。错的是它给出的安心感覆盖的范围。
签名保证的只有一件事:这堆字节是某个持有私钥的人发出来的,中途没被换过。它不保证这堆字节内部的两样东西彼此对得上。文本是文本,向量是向量,签名把两者一起封进信封,然后就不管它们之间是什么关系。这篇讲完,你会拿到一个区分:字节完整性和语义完整性,是两件事,签名只碰得到前一件。
这里最容易被搞混的是,大家把“知识包的完整性”当成一个属性,而它其实是两个。字节完整性,签名管这个。语义完整性——这段向量是不是真的编码了这段文本——签名完全管不着,因为签名算法不认识 embedding。
最近有篇论文专门盯这条缝。作者是 Georgios Feretzakis 和 Alexandros Papaspyridis,11 月 4 号挂到 arXiv,编号 2611.06498,主分类密码学与安全,交叉信息检索和机器学习。这个分类组合我看了两眼,签名的语言和检索的语言本来就不在一个坐标系里,被硬塞进同一个信封,麻烦就出在这。
论文给它起了个名字,叫 text-vector integrity gap,文本-向量完整性缺口。我不太喜欢“缺口”这个词,听着像拿点腻子能补上。它更像一条结构性的接缝:能被签名覆盖的部分,和被检索实际使用的部分,不是同一部分。
检索器不读文本
往下拆一层。检索这一步,模型真正看见的只有向量。文本是给人看的,也是给最后生成用的,检索器不读它。所以一个包“验签通过”之后,真正决定模型看到什么的那个东西,从头到尾没被验过一次。
这不是谁疏忽了。签名算法不认语义,只认字节。
那攻击者要做什么?几乎什么都不用做。他不用改文本,只要让向量歪一点点。歪多少不用多,检索取的是余弦相似度最大的那个,只要在这条查询上比正牌文档高出一点,就赢了。文本一字不动,语义甚至完全正确,最后生成出来的答案照样是错的:进上下文的是攻击者那段文本,正牌那段压根没被检索到。
这个动作的名字叫查询劫持。
容差不是偷懒,是物理事实
直觉答案是:重算一遍向量,跟包里的比一下,对不上就拒。
往下拆一层就撞墙。重算不一定重得出来。论文报了一条很朴素的事实:常见浮点推理和动态 INT8 推理流程,换个批大小、换个架构,逐位复现都保证不了。同一段文本,同一份权重,换台机器算出来的向量末几位就是会飘。
所以校验器必须给容差。说人话就是,不要求“一模一样”,只要求“足够近”:重算向量和包里的向量余弦相似度落在某个容差 τ 之内,就算通过。
这句是全篇的枢纽。容差不是工程上偷懒,它是为了吸收“算不出完全一样”这个物理事实。但容差一旦存在,攻击面就存在了。攻击者要做的事情变得非常简单:把自己的向量摆进这个容差球里。
劫持成功率有多高?这个问题看着该由攻击者的水平决定,其实不是。论文的结论是,它由查询本身的检索边距分布决定。
边距说白了就是第一名和第二名之间的余弦差。一条查询,正牌文档相似度 0.81,第二名 0.79,边距就是 0.02。攻击向量只要能挤进正牌文档上方 τ 以内,这条查询就归他了。
于是有个很干脆的关系:
margin(q) = cos(q, v_gold) - max_{d ≠ gold} cos(q, v_d)
容差 τ 下,查询 q 可被劫持 ⟺ margin(q) < τ
劫持成功率 ≈ P(margin < τ)
式子本身没什么可推的,它就是定义。真正要命的是代进去的那个分布。
论文在五个语料库、两个嵌入模型上量了边距。结论是普遍偏小。小到什么程度:把容差压到浮点噪声量级,仍然有最多约 5.5% 的查询暴露。排除掉完全重复的文档之后,任一语料库的暴露比例不超过约 1.2%。
1.2% 听起来很小。翻译成场景:每一百次查询里有一次会拿到攻击者的文档,而且这一次可预测、可重复、稳定出现。做定向投放,这个量级足够了。
但更麻烦的地方在容差那一端,不在边距这一端。
两头堵死
上面那个 5.5% 的前提是容差能压到浮点噪声量级。这个前提工程上不成立,因为容差还有另一个方向的约束:诚实的量化向量得能过。
论文在 CPU 上拿小型嵌入模型做了 INT8 和 FP32 的对比,阈值按诚实余弦相似度分布的 0.1% 分位来校准。0.1% 分位的意思是,九成九九的诚实向量都得放过。这个方向其实没得选,你总不能把自家正常发布的向量拦掉一大片。
在那个阈值下,攻击暴露比例是 98% 到 100%。
我一开始把这条读成“参数没调好”。这么讲其实不严谨,应该反过来说:是两条约束的可行域根本不相交。容差的下界由“诚实向量要能过”定死,上界由“边距要装得下”定死,量化误差把下界推到了上界外面。中间没有一段同时满足两头。这不是调参问题,是结构问题。
机制讲到这儿都还停在相似度层面。它到底有多要紧,得看最后生成出来什么。
论文在树莓派 5 上跑本地语言模型,做了一组对照:这类攻击把正确答案比例从 0.75 打到 0.39。我盯着这两个数看了一会儿。不是“回答质量下降了一点”,是掉到比掷硬币略好。边缘设备上跑的往往就是这种小模型,本来余量就薄,检索这一层再被劫持,剩下的容错空间基本没有了。
压缩向量存储也没救。论文对覆盖 57,638 篇文档的 FAISS 乘积量化索引做完整性审计,篡改过的向量全部通过审计,而这些向量能让攻击者文档在 25% 的目标查询里排在首位。
这里有个额外的坑:审计是在压缩后的码上做的。压缩本身有损,你在一个有损表示上做完整性检查,等于先天把“差不多”写进了判定标准。PQ 索引的存在意义就是省内存,它省下来的那部分精度,正好是审计需要的那部分精度。
应用层兜不住
看到这儿会有一个自然的想法:那在应用层兜一下,让模型对可疑检索结果弃答,或者做语义筛查,或者干脆剪掉低置信的上下文。
论文对这几条都报了预注册的阴性结果。我看到“预注册”三个字的时候反而更信一点。阴性结果比阳性结果值钱,因为它明确告诉你这条路堵死了,别再往这儿投时间。
弃答、上下文剪枝、语义筛查,三条路都没走通。原因其实挺直接:这几条机制判断的都是“这段检索结果跟问题像不像”,而攻击向量的特点恰恰是“像”。它就是在相似度空间里挪的,任何以相似度为输入的筛查,看到的都是一个合格的、甚至更合格的向量。
校验对象换掉
论文给的方案是把校验对象换掉:不校验向量本身,校验量化编码,把歧义交给校验器处理,再加校准保护带和规范化。三个平台上,这个方案接受了样本里全部诚实向量。
然后是那句我觉得整篇最该抄下来的话:发布方没有选择余地。
一个校验机制如果给发布方留了裁量空间——某些情况可以放行,某些情况可以解释——那它就并不是校验,只是走个形式。能通过的集合必须是被人预先钉死的,发布方只能往里塞,不能商量。
这条原则跟具体算法无关,换任何方案都成立。我觉得它比论文主推的那套编码校验更值得记。
签名管的是“谁发的”,检索靠的是“哪个更近”,这两件事中间隔着一次 embedding 和一道容差,而那道容差本身就是个语义判断,它决定了什么算“对得上”。
供应链安全那套语汇是从代码和权重那边搬过来的。那边,这堆字节和它的行为是同一个东西,封住字节就封住了一切。RAG 里封不住,中间那层是几何,不是字节。
至于你手上那条链路要不要为这件事改设计,我不替你做判断。但下次再看到“知识包已签名”这句话,你大概会先问一句:那个 τ 是谁定的,定给谁看的。
