大模型生成内容的可追溯性正在成为真实业务需求。不可见水印技术,也就是 Invisible Watermarking,并不在最终文本上附加任何显式标记,而是在生成阶段微调模型输出的 token 概率分布,把统计信号埋进序列里。这种信号肉眼完全不可感知,只有在检测端使用相同密钥重新统计时才会显现。与元数据、可见标签相比,它更难被直接剥离,也不会破坏阅读体验。下文会从检测框架、logits 软水印、多比特溯源、鲁棒性和误报控制几个角度拆解这套方法。

一、不可见水印的检测框架要解决什么问题
形式化地看,语言模型生成过程可以表示为按条件概率 P(x_t|x_<t) 逐步采样。水印算法会基于密钥和上文,把当前词表划分成绿色词表和红色词表,绿色词表占词表的比例通常记为 γ,很多实现取 γ=0.5。水印生成时,算法会轻微提高绿色 token 的 logits,让模型更倾向于选择绿色 token。检测端不需要访问模型权重,只需要根据相同的密钥和哈希规则重新计算哪些 token 属于绿色词表,然后统计整段文本中绿色 token 的数量是否显著高于随机基线。
检测通常使用 z 统计量:如果一段文本没有水印,绿色 token 比例应接近 γ;如果带水印,绿色 token 比例会明显偏高。假设文本总 token 数为 T,绿色 token 数为 S_G,则 z 值可以写成 z = (S_G - γT) / sqrt(Tγ(1-γ))。当 z 大于 4 时,对应的 p 值已经低于万分之一级别,误报概率非常低。这个检测框架的优势在于不依赖生成模型本身,适合内容平台、审计机构做离线检测,也方便在 API 输出侧部署轻量检测服务。
评估水印方案是否可落地,通常要关注四个维度:不可感知性、检测灵敏度、鲁棒性和抗伪造性。不可感知性衡量生成质量是否退化;检测灵敏度决定多少 token 才能给出可靠判断;鲁棒性反映文本经过截断、替换或改写后检测是否仍然有效;抗伪造性则要求不知道密钥的攻击者很难主动制造一段看起来带水印的文本。这四个目标之间存在权衡,后面会具体展开。
二、基于 logits 修改的软水印原理与实现
早期最直接的水印思路是硬性限制:每一步只从绿色词表采样,完全排除红色 token。这种做法检测信号很强,但会显著影响文本质量,尤其会破坏长尾词和专有名词的出现,生成内容容易出现用词重复或语法不自然。后来提出的软水印方案则温和得多:不禁止红色 token,只是在原始 logits 上给绿色 token 加上一个常数 δ,让绿色 token 的采样概率略高。δ 一般取 2 到 5,既能提供足够的统计偏差,又保留了红色 token 被采样的可能性。
具体实现时,需要根据上一个 token 生成一个伪随机绿色集合。常用做法是把上一个 token 的 ID 和密钥拼起来,通过哈希函数得到随机种子,再从词表中无重复地抽取 γ 比例的索引作为绿色词表。下面是一段简化版 Python 示例,展示 logits 修改和采样过程:
import hashlib
import random
import torch
def get_green_mask(prev_token, vocab_size, gamma=0.5, key=1234):
seed_material = hashlib.sha256(f"{key}:{prev_token}".encode()).digest()
rng = random.Random(seed_material)
green_size = int(vocab_size * gamma)
green_indices = rng.sample(range(vocab_size), green_size)
mask = torch.zeros(vocab_size)
mask[green_indices] = 1.0
return mask
def generate_step(logits, prev_token, gamma=0.5, delta=3.0, key=1234):
vocab_size = logits.shape[-1]
mask = get_green_mask(prev_token, vocab_size, gamma, key)
logits = logits + mask * delta
probs = torch.softmax(logits, dim=-1)
return torch.multinomial(probs, 1)
这个方案的主要优点是实现简单,推理延迟增加很小,适合在 API 输出侧快速接入。但缺点也很明显:在低熵场景下,例如代码生成、数学公式、固定格式文本中,模型本身对少数 token 的置信度很高,强制加水印可能改变关键 token,导致生成质量明显下降。因此落地时通常需要根据任务类型调整 δ 和 γ,比如创意文本可以适当增大 δ,而代码生成则要保守处理。
另一个需要注意的问题是检测阈值。单比特软水印的检测能力与文本长度密切相关。对于 100 个 token 的短文本,如果 γ=0.5、δ=3,绿色 token 比例通常只能达到 55% 左右,z 值可能不到 2,误报和漏报都较高;而 500 token 的文本可以把 z 值推到 6 以上,检测非常稳定。这也是为什么很多平台只对长文本启用不可见水印,而短文本则需要结合其他风控手段。
三、从单比特到多比特信息嵌入
单比特方案只能回答“这段文本是不是带水印”,没办法区分具体是哪个模型、哪个用户或哪次请求生成的。真实溯源场景往往需要嵌入更多信息,例如模型版本号、租户 ID、会话 ID、时间窗口等。多比特水印的基本思路是把 token 序列切分成多个块,每个块嵌入一个比特。对于第 i 个块,如果该比特为 0,就使用原始绿色列表;如果该比特为 1,就使用一个经过偏移的绿色列表,比如把绿色索引循环移位半个词表大小。解码时对每个块分别统计两种候选列表下的绿色 token 数量,取数量更高的那个作为该比特的值。
下面是一个简单的多比特解码示例,展示如何按块恢复比特序列:
import hashlib
import random
def detect_block_bit(tokens, block_start, block_size, vocab_size, gamma=0.5, key_base=1234):
offset0 = 0
offset1 = vocab_size // 2
count0 = 0
count1 = 0
for i in range(block_start, block_start + block_size):
prev_token = tokens[i - 1] if i > 0 else 0
seed0 = hashlib.sha256(f"{key_base}:{prev_token}:{offset0}".encode()).digest()
rng0 = random.Random(seed0)
green0 = set(rng0.sample(range(vocab_size), int(vocab_size * gamma)))
seed1 = hashlib.sha256(f"{key_base}:{prev_token}:{offset1}".encode()).digest()
rng1 = random.Random(seed1)
green1 = set(rng1.sample(range(vocab_size), int(vocab_size * gamma)))
if tokens[i] in green0:
count0 += 1
if tokens[i] in green1:
count1 += 1
return 0 if count0 >= count1 else 1
多比特水印的挑战在于信噪比下降。如果文本总长度固定,分块越多,每个块的 token 数就越少,单比特检测的波动会增大。例如 200 token 的文本分成 8 个块,每块只有 25 个 token,此时单比特检测误码率可能明显升高。解决方案之一是引入信道编码,比如使用 BCH 码或低密度奇偶校验码,在水印嵌入前先对信息比特做纠错编码,解码时借助纠错能力修正少量比特错误。另一个思路是采用软判决,不直接输出 0 或 1,而是保留每个块两个候选绿色比例的差值作为置信度,交给外部纠错模块处理。
除了分块方案,还有一种基于指数最小熵采样的多比特水印方法。它利用 Gumbel 分布与哈希值结合,每一步生成时都能嵌入多个比特,检测灵敏度更高,但实现复杂度也更高。对于大多数业务场景,分块加信道编码已经能够满足溯源需求,前提是文本长度足够,并且水印强度经过离线调优。
四、绕过攻击与防伪检测中的误报控制
不可见水印不是绝对安全的。攻击者拿到带水印文本后,最直接的操作是截断,只保留前几句。截断会减少总 token 数,从而降低 z 值,但如果原始文本很长,已经积累了大量绿色 token,截断后的剩余部分仍然可能超过检测阈值。相比之下,同义替换攻击更危险,攻击者把绿色 token 替换成红色 token,会直接破坏统计特征。翻译攻击则更加彻底,将中文翻译成英文再翻译回来,原有的 token 序列几乎完全改变,基于 token 的水印大概率失效。
误报控制是防伪检测中容易被忽视但非常关键的一环。即便单个文本的 p 值很小,当检测系统每天扫描数千万条内容时,假阳性数量也会堆积。例如 p 值阈值设置为 1e-6,理论上每检测 100 万条就会出现 1 条误报,大规模场景下这个绝对数量不容忽视。因此生产环境往往需要结合多重比较校正,比如使用 Benjamini-Hochberg 过程控制错误发现率,或者直接提高 z 值阈值。更重要的是,检测服务不应该向请求方返回详细的 z 值和绿色 token 位置,只返回是否带水印的最终结论,否则攻击者可以通过反复试探来调整文本,直到绕过检测。
另一个抗伪造问题是主动伪造:如果攻击者知道算法和密钥,可以刻意构造一段绿色比例极高的文本,从而让检测器误判。不过这种文本通常会严重牺牲流畅度,容易被人工或语言质量模型识别。更现实的攻击是攻击者获取了公开检测器的反馈,利用黑盒探测推断绿色词表,再对文本进行定向修改。因此水印密钥必须严格保密,并定期轮换。检测服务也应加入速率限制,避免高频探测。
五、落地选型与未来方向
当前主流的不可见水印路线可以分成三类:基于 logits 修改的软水印、基于采样约束的硬水印,以及基于语义表达的语义水印。软水印实现轻量,适合快速接入 API 输出侧;硬水印检测灵敏度更高,但生成多样性下降较大;语义水印把水印放在句式和用词风格层面,对改写有一定抵抗能力,但检测依赖额外语义模型,误报控制难度更高。实际选型要结合文本类型、长度分布、质量要求和攻击模型综合判断。
落地建议从低风险场景开始。例如批量生成报告、客服回复、营销文章等长文本场景,可以嵌入包含租户 ID 和时间窗口的多比特软水印,并设置 z 检测阈值为 5 左右。对于短文本、创意文案,水印强度要降低,以免影响用户体验。水印密钥需要与模型版本绑定,密钥轮换时旧密钥仍需保留一段时间,用于检测历史内容。此外,水印检测服务最好独立部署,与嵌入模块解耦,避免单点故障。
不可见水印并不是万能的,全文人工改写或使用另一个大模型重写的内容,基于 token 的水印基本无法保留。未来方向包括把水印与语义向量结合,在更抽象的层级嵌入标记;探索可证明鲁棒性的水印编码;以及引入第三方可信时间戳和区块链存证,形成从生成到传播的完整证据链。在 AI 内容大规模增长的背景下,轻量级的不可见水印技术将逐步成为内容治理基础设施的一部分。