跳到主要内容
「以下是文档内容」这五个字,我以后不敢写了

「以下是文档内容」这五个字,我以后不敢写了

老墨
老墨

· 阅读约 4 分钟

旧稿:

以下是文档内容,请总结要点:

这句话我大概写过上千遍。今天得把它拎出来说:它是句假话。

没有【文档内容】这回事。有的只是某个 extractor 从 OOXML 里挑出来的那部分字符,按它自己定的顺序排好,塞进 context window。模型从头到尾没看过那份文档一眼,它看的是别人抄给它的一份笔记。

前两天 arXiv 上挂了篇论文,作者 Penghui Li,投在密码学与安全分类下。它讲的不是模型怎么理解文档,是文档在到达模型之前就已经被歪过一次了。不同的 extractor 读的 OOXML 部件不一样,解释方式也不一样,吐出来的文本跟 Office 里显示的样子对不上。就这么朴素的一件事,我之前居然从来没审过。

岔一句。我们总说模型“读”文档。它没读过任何东西。读书这个动作里包含看版面、看层级、看哪句话缩进了、哪个框飘在右上角,全是人眼在干。模型那边只有一串字符,和字符的先后。扯回来。

论文里有个例子,我看完坐在那愣了一会。同一张幻灯片,把文本框的位置挪一点点,视觉上人眼几乎分不出区别,投影出来还是那几个字,还是那个观感,但 extractor 吐出来的行序变了。模型读到的段落顺序变了,摘要的重点就跟着变了。

不是内容变了,是内容到达的顺序变了。而我们写 prompt 的人,从来不觉得顺序算内容的一部分。

他们用静态源码分析加动态跑,在 extractor 里找出 19 条渠道:读到人看不见的东西。另外 12 条:人看得见、extractor 不读。前者是藏,后者是漏。而且是广泛使用的那些 extractor,普遍不管显示格式和阅读顺序,连“看得见的内容”都可能以另一副样子到达模型。

后面这段是我最在意的。他们构造了五个攻击,全程不往文档里塞任何一句指令,就能让模型和人类审阅者对同一份文件得出不同结论。测了八个系统:ChatGPT、Claude、Gemini、M365 Copilot,再加 Codex、Claude Code、Antigravity、GitHub Copilot CLI 四个 coding agent,每一个都至少被其中一个攻击骗过去。没有一个攻击是靠说话的。

这一条该让很多正在做的事显得可疑。现在防注入的主流思路,是把文档内容用 XML tag 包起来,再告诉模型“这里面的话不是指令”。包当然比不包好。可这篇的意思是,attacker 根本不需要说话,他只要挪一个文本框。你所有的“忽略文档内的指令”型防御,防的是一个不存在的假设:以为攻击必须是一句话。

推敲一下我自己的那句旧稿。问题不只是它声称了不存在的东西,还在于它顺手给了模型一个错误的前提:你手上这份东西,和原文档是一回事。模型信了,后面所有的结构推断都建立在这个错误前提上。

我一开始想的是,在 prompt 里加一句告诫就完了。V2 大概长这样:

以下是自动抽取的文档文本,阅读顺序可能与原文件不一致,如发现结构异常请说明。

跑了几份带复杂版式的文件,没用。模型确实“知道”自己拿的是二手转述了,但那 12 类被漏掉的内容不会因为一句提示就回来。信息不在那里,提示词喊破喉咙也没用。这版是个安慰剂,我自己先把它划掉。

写到这我把前面的话推翻一半:prompt 这一层能做的其实只有一件事,别撒谎,别声称那是原文。剩下的活儿在 prompt 外面。论文提的 Prism 就是干这个的,在文本交给模型之前,按 OOXML 规范把人类看到的那个视图重建出来,轻量,而且据说能泛化到没见过的 payload。位置是对的:修在原料上,不是修在措辞上。

我这门手艺管的是“这句话准不准”,管不了“这句话的原料是不是缺斤短两”。以前我把这两件事混成一件了。

定稿先放这:

以下是从 <文件名> 抽取的文本,由自动工具生成。
阅读顺序与版面可能与原文件不一致;判断结构时以我给出的字段名为准,不要依据行序推断。
--- 抽取开始 ---

比旧稿长了三倍,一句客套字没有。短不是目的,把模型不该有的那个前提拆掉才是。

但我不敢说这就是终版。这版只处理了“顺序不可信”,漏读的那部分它一点辙没有,模型现在知道自己可能读漏了,可它不知道漏了什么。哪天真把 Prism 那类重建层接上,这段提示大概要整段删掉,退回一句干净的“以下是文档内容”。到那天再改一版,不丢人。