AI 水印军备竞赛
本文是英文原文的机器翻译,部分细节可能有所损失,请以英文版本为准。 阅读英文原文
要点速览
我读了 Anthropic 的公告,然后打开文本编辑器去找零宽字符。一个都没有。水印并不藏在字母之间,而是在模型选了哪些词里,这一点我接受得比应该的更慢。这也意味着“水印清除器”根本没有东西可清除。
人们称之为水印的三种东西
有三种不同的机制都被叫作水印。它们几乎没有共同点。
| 机制 | 信号所在 | 可被什么去除 | 第三方能否检测 |
|---|---|---|---|
| 隐藏的 Unicode | 零宽字符、Tag 区块 (U+E0000-E007F) | 任何查找替换 | 能,轻而易举 |
| C2PA 元数据 | 文件容器中的签名清单 | 截图、重新保存、格式转换 | 能,可以检查是否存在 |
| 统计水印 | 模型的 token 选择 | 大幅改写或翻译 | 不能,需要厂商的密钥 |
Anthropic 部署了其中两种。文件会得到 C2PA 签名的来源元数据,也就是影像行业使用的同一套 Content Credentials 标准,适用于 .svg、.png 和 .jpg。文本得到第三行。Anthropic 的文档把它称为“直接嵌入文本本身的不可察觉水印”,它“不会改变 Claude 回复的含义、质量或可读性”。
推出范围比标题所暗示的更窄。按 Anthropic 的说法,“2026 年 8 月 2 日或之后发布的 Claude 模型将在发布时支持机器可读标记”,对现有模型的支持则被描述为正在进行中。划分依据是模型,而不是国家。同一页面还说,无论 Claude 在何处提供,受支持模型的输出都适用标记,范围覆盖全球。驱动力是欧盟 AI 法案第 50(2) 条下的 AI 生成内容透明度行为守则,Anthropic 以模型提供者和生成式 AI 系统提供者的双重身份签署了它。所以它是逐个模型到来的,一旦某个模型有了,所有人都会得到。
Google 在 Gemini 上运行 SynthID-Text 已有一段时间,方法发表在 Nature 上。它只改变采样这一步。在每个 token 处,它用前面 k 个 token 为一个伪随机函数设定种子,把词表划分成一个有 m 层的锦标赛对阵表,并提高那些不断赢下对局的 token 的采样概率。把这一点铺到足够多的层和足够多的 token 上,就会得到可测量的统计偏置,而读者看不出任何单个词有问题。
检测不需要原始模型。它需要带密钥的伪随机函数,以及对候选文本的一次打分。带水印的文本在 g 值上系统性地高于不带水印的文本,这个差距就是一次假设检验。
我就是在这里卡住的。
Signal Verifiable in a browser? Why
------ ------------------------ ---
Zero-width chars Yes Codepoint scan
Unicode Tag block Yes Codepoint scan, decodes to ASCII
C2PA manifest Presence, not signature JUMBF box / PNG caBX chunk markers
Claude watermark NO Keyed detector, method unpublished
SynthID-Text NO Keyed detector, Google-side API
Anthropic 的帮助页对此说得很直接。他们说自己正在“努力让用户和其他第三方能够检测 Claude 嵌入的水印和来源元数据”,并且“将在即将发布的技术文档中分享检测机制的细节”。即将发布意味着现在还不存在。在它出现之前,Anthropic 之外没有人能检查文本水印。任何声称能做到的工具,都是换了个名字在运行通用的 AI 文本分类器,而这类分类器的错误表现要糟糕得多,尤其是对用第二语言写作的人。
构建检查器
所以我做的版本,只声称自己能检查的那部分。
C2PA 这一侧花的代码最多。Content Credentials 清单位于一个 JUMBF 盒子里,这个盒子落在哪里取决于容器。PNG 把它放在 caBX 块里,JPEG 把它放在 APP11 段里,SVG 把它放在 XML 元数据里。找到它意味着在原始字节中扫描这些标记:
const scanC2PA = (buf: ArrayBuffer) => {
const bytes = new Uint8Array(buf);
const find = (needle: string): boolean => {
const n = needle.length;
const limit = bytes.length - n;
for (let i = 0; i <= limit; i++) {
let ok = true;
for (let j = 0; j < n; j++) {
if (bytes[i + j] !== needle.charCodeAt(j)) { ok = false; break; }
}
if (ok) return true;
}
return false;
};
const isPng = bytes[0] === 0x89 &&
String.fromCharCode(bytes[1], bytes[2], bytes[3]) === 'PNG';
const hasJumbf = find('jumb') || find('jumd');
const hasC2pa = find('c2pa');
const hasPngBox = isPng && find('caBX');
return { found: hasC2pa || hasPngBox, hasJumbf, isPng };
};
这能找到清单,但不能验证它。验证意味着检查证书链和密码学签名,我不会在浏览器标签页里手写这些,再把它当作权威结果呈现。工具会这样说明,并指向官方验证器。
这个扫描有一种失败模式,我应该点明而不是掩盖。在原始字节中搜索四个字符的标记可能产生误报,因为没有什么能阻止字节 c2pa 出现在压缩的图像数据里。我还是保留了它。要在三种格式上正确解析容器结构,对于一个整个主张只是“这里有东西,请到别处去验证”的工具来说,工作量太大。它是烟雾报警器,标签上也是这么写的。如果哪天它不够用了,解决办法是做真正的盒子解析。
文本这一侧复用了我的隐藏文本检查器里的码点扫描器:零宽字符、能不可见地夹带整条 ASCII 消息的 Unicode Tag 区块、双向控制符、变体选择符。这些都能检测到,也值得标出。
最难的部分不是代码,而是一句判定的措辞。工具里有一个信号叫“统计水印(Claude、SynthID-Text)”,它的永久状态是 not checkable,并附有说明。我在发布前纠结了很久,因为一个回答“我不知道”的工具看起来像是坏了。
这很重要,因为这些结果会被怎么用。有人把一段文字放进检查器,然后把输出带进纪律会议。所以工具一开始就给出四条提醒。前两条是 Anthropic 自己的立场,后两条是我对自己所构建工具的局限的说明:
- 没有标记不能证明是人类所写。Anthropic 把大幅编辑、改写、翻译以及与其他文字合并,列为让标记无法检测的情形。
- 有标记同样不能证明作者身份。它只表明内容可能被该模型处理过,而不能说明是谁写的,或人类改动了多少。
- 存在 C2PA 不等于通过了 C2PA 验证。这个工具能找到清单,但不会验证签名或签发者。
- 元数据很脆弱。截图、重新保存和大多数平台上传都会把它剥掉,所以对一个下载的文件得到干净的结果,对原件几乎说明不了什么。
当一份文档有两位作者时会发生什么
上面所有内容都假设一份文档只有一位作者。几乎没有哪份文档只有一位。人们在一个聊天机器人里起草,在另一个里重写某一节,通过 OpenRouter 路由到那一周最便宜的模型,中间还手工编辑。这不是什么特别的规避手段,而是现在人们的工作方式。
Anthropic 已经指出了这一点。它列出的标记无法检测的条件里,包括被“大幅编辑、改写、翻译或与其他文字合并”的文本。它把这当作一个注意事项,说到这里就停了。机制在这里,因为这个效应的大小让我吃惊。
绿名单检测是对 token 计数做的 z 检验。这里用的是已发表的绿名单构造,参数仅作示意。它不是 Anthropic 的方案,那个方案没有公开。结果的形状可以类推,具体数字不能。
z = (greens - γT) / sqrt(T · γ(1-γ))
假设一份文档有 T 个 token,其中只有 W 个由机器撰写,每个把绿色率提高 ε。人类 token 以基线率 γ 贡献绿色,所以预期的绿色计数是 Tγ + Wε,整份文档的统计量变成:
z_whole = W·ε / sqrt(T · γ(1-γ))
现在只给机器撰写的片段打分:
z_span = ε·sqrt(W) / sqrt(γ(1-γ))
把一个除以另一个,一切都约掉,只剩一个数:
z_whole / z_span = sqrt(W/T) = sqrt(f)
给整份文档打分,只保留你检测能力中机器撰写比例的平方根。在 f = 0.1 时,这是 32%。取 γ = 0.5 和绿色率提升 ε = 0.25 作为示意值,一段 300 个 token 的机器撰写段落单独打分是 z = 8.66,会以极高的置信度被标出。把同一段落放进一份 3,000 个 token 的文档,整份文档的分数降到 z = 2.74,低于任何合理的阈值。水印没有变,是检测器量错了单位。
所以混用模型并不是在攻击水印,而是在攻击检测器对窗口的选择。
解决办法是让一个窗口滑过文本并取最大分数,大致就是 SynthID-Text 论文里的 WinMax 思路。它有效,但要付出代价。给数百个重叠窗口打分,意味着对一份文档做数百次假设检验,所以需要多重比较校正,这会抬高阈值,又把短片段推回阈值以下。你是在用稀释问题换误报问题,这个旋钮无论怎么调都避不开两者。
窗口法也只能定位某一家厂商的标记,因为每个提供者都用自己的密钥运行自己的伪随机函数:
for each vendor key K: # Anthropic, Google, OpenAI, ...
for each window w in text: # all of these need FPR correction together
score(w, K)
三家厂商意味着三个检测器、三次 API 往返、三种信任关系。没有人提供联合版本,而 Anthropic 的检测器还没有公开。
然后是让上面大部分内容都变得纯属学术的那个漏洞。水印要求运行推理的一方对采样这一步施加偏置。这是服务栈的属性,不是文本的属性。所以:
| 路径 | 有水印吗? | 原因 |
|---|---|---|
| OpenRouter → Claude / Gemini / GPT | 有 | 在提供者的栈内、返回 token 之前就已施加。路由器只是转发成品文本。 |
| OpenRouter → Llama / Qwen / Mistral | 没有 | 开放权重的推理提供者既没有义务,也没有动机去实现带偏置的采样。 |
| 你自己笔记本上的 llama.cpp | 没有 | 同样的原因,更甚。 |
OpenRouter 并不是靠路由击败了水印。它提供了一条从一开始就没有施加标记的通道,而这条通道只隔着一个配置字符串。所以这套机制只覆盖专有的前沿 API,别无其他。开放权重对大多数文本工作已经足够好,并且在机制之外。想绕过的人只需要改一个模型名。
我把稀释效应作为第三个标签页做进了检查器。它不分析任何文本,也不检测任何东西。它运行 z 检验的算术,并画出随着你改变机器撰写比例,两个分数如何分离。
我哪里想错了
我原以为水印的耐久性是要能扛住编辑。其实是要扛住改写,这难得多,因为改写会重新采样 token 的选择,而信号恰恰就在那里。我最初的计划是做一个带置信百分比的检查器,因为人们期待这个,也是它能排得上名次。我否决了。我给出的任何数字都会是换了标签的通用 AI 文本分类器,而这类分类器有文献记载的误报问题,尤其是针对非英语母语的写作者。
更难的决定是水印清除器,那个真正能带来流量的相邻产品。我放弃了。它会是一个用来绕过披露承诺的工具,而且反正也行不通。没有字符可剥,所以清除意味着足以破坏文字质量的激进改写,而浏览器工具不带上一个模型就无法改写。事后让我想起的是,我随这个一起发布的标点修正器确实会剥掉零宽字符,那是我给自己草稿做的清理。同一个正则,不同的产品。分界线在页面如何说明这个工具的用途,而不在代码里。
这会走向何处
只有厂商才能验证的来源信号,还不是公开的来源证明。但 sqrt(f) 这个结果指向的东西比“军备竞赛仍在继续”更具体。仅靠内容的取证无法回答人们真正在问的问题。不是文本是否由模型生成,而是哪些部分、由谁、人类改了多少。我们现有的每种机制,恰好都在真实文档所处的地方退化,也就是有多位作者、有长长编辑历史的文件。所以有用的做法大概是停止审问文本,转而开始记录环境。编辑器的修订历史、提交时间线和签名的编辑链,对作者身份的记录比对成品做的任何统计检验都可靠得多,而 C2PA 的断言模型针对的正是这种分层的、多参与者的来源证明。水印真正的任务比标题所暗示的要窄。它是对没人想隐藏的机器文本默认开启的披露,而对抗性的情形则放在它们一直应该在的地方处理,也就是那些记录文档是如何制作出来的工具里。