
引言:一段"看不见的水印",逼着整个 AI 内容生态回答:这行字是谁写的?2026 年 10 月的第一周,OpenAI 做了一个看似低调、实则分量极重的技术动作:推出了名为 textGrain 的隐形文本水印技术。从欧盟开始,未来数周内,ChatGPT 和 Codex 给欧盟用户的输出会在后台被悄悄"烙"上一层水印;同时,全球 API 开发者从当日起就可以为部分模型主动开启带水印输出——默认保持关闭,不设成全球默认。这则消息的技术含量,远超一则"新功能上线"的公告。因为它真正触及的是一个 AI 时代最尖锐、也最无解的问题:当大模型能以人类的水平、几乎零成本地"写出"任何文字时,我们到底还有没有办法判断——眼前这行字,究竟是人的思考,还是机器的生成?textGrain 的野心,正是因为承认"这个问题不可能靠肉眼解决",才把答案交到了"统计指纹"手里。它不给文字加特殊符号,不在结尾标注"由 AI 生成",而是通过秘密密钥,在模型每一次"选下一个词"的瞬间,偷偷改变它的偏好,让成百上千次微小的选择累积成一个"人类察觉不到、但检测器一眼认得出"的统计规律。这篇文章我想把它彻底拆透:第一,讲清 textGrain 到底把"水印"藏在了哪里,它的本质为什么是"给一段马尔可夫式的选择序列编上密码";第二,用 Python 亲手实现一遍"水印嵌入 + 检测"的最小闭环,让你看得见它到底做了什么;第三,也是我最想强调的——认真解剖它的三道裂缝(同义词替换、短文本、数学/翻译内容),因为这决定了"有没有水印"离"是不是 AI 写的"之间,还隔着多远的距离。一、先理解一件事:为什么要"选词偏置",而不是"加水印符号"很多人的第一反应是:给 AI 输出的文字加个水印,不就是开头/结尾标一句"本文由 AI 生成"吗?为什么 OpenAI 要大费周章搞一套"统计指纹"?因为那条路根本不成立。我们来推理一下为什么:方案A:加可见标注("由AI生成") ✗ 无效 · 用户复制粘贴 → 标注消失 · 大模型微调/改写 → 标注丢失 · 这也根本挡不住"去标注"的恶意用途 方案B:加特殊符号/隐藏Unicode字符 ✗ 脆弱 · 平台清洗特殊字符 → 水印蒸发 · 翻译、转码、换行 → 位置错乱 方案C:藏在"词的选择"里(textGrain) ✓ 最强 · 水印即文字本身,随复制粘贴迁移 · 不改变意义、不改变质量 · 用统计规律实现,而非显式标记看到了吗?前两种方案的本质,都是"往文字外面贴标签"——而标签是可以被剥掉的。textGrain 的高明之处,在于它把水印"熔进"了文字本身:不是"在结果上加个记号",而是"在生成的每一步,把记号编进选词里"。这样,即使你把这段话复制到任何网站、任何文档,水印都跟着文字走,因为"这段文字长这样"这件事本身,就是水印。这套思想的哲学基础,其实和生物界的"DNA"很像:你不必在生物体表面贴个"我是谁"的标签,因为"我是谁"的信息,就写在它每一段的遗传密码里。textGrain 同样如此——它在模型"选词"这个最底层的决策里,埋下了可被识别的"遗传密码"。二、textGrain 的核心机制:把"选词"变成一段可解码的密码现在,我们进到技术内核。要理解 textGrain,先得理解大模型生成文本时的"选词"环节。当大模型要生成下一个词(token)时,它其实不是铁板一块只认一个答案,而是会给出一簇概率相近的候选词——比如"高兴"“愉快”“开心”“雀跃"在某个上下文里可能都是合理的选择,只是概率高低稍有差别。正常生成时,模型按概率采样,随机挑一个;但如果每次都"随机”,那就没有规律可言了。textGrain 的巧妙之处,就在这个"近乎随机的选择"里注入秩序:┌────────────────────────────────────────────────────────────────┐ │ textGrain 水印嵌入(生成侧)原理图 │ ├────────────────────────────────────────────────────────────────┤ │ GPT 生成下一个 token │ │ │ │ │ ▼ │ │ ┌─────────────────────────┐ │ │ │ 模型输出候选词及其概率 │ "高兴"0.31 "愉快"0.28 │ │ │ (top-k 或全概率分布) │ "开心"0.25 "雀跃"0.16 │ │ └──────────┬──────────────┘ │ │ ▼ │ │ ┌─────────────────────────┐ │ │ │ 用秘密密钥对候选词"分类" │ key哈希→在候选词上划"绿区/红区" │ │ │ (secret key) │ (对同一key,分类结果固定可重现) │ │ └──────────┬──────────────┘ │ │ ▼ │ │ ┌─────────────────────────┐ │ │ │ 轻微偏置:偏好"绿区"词 │ 不挑"最优"词,而是"绿区里较优" │ │ │ (watermark bias) │ 单次几乎无感,数百次形成统计特征 │ │ └──────────┬──────────────┘ │ │ ▼ │ │ ┌─────────────────────────┐ 全文 │ │ │ 输出文本 │──────────→│ 水印即"绿区选词频率偏高" │ │ └─────────────────────────┘ │ 就是那段可解码的统计指纹 │ │ │ │ 检测侧: │ │ · 拿到文本 + 同一 secret key │ │ · 重算每个词该在"绿区"还是"红区" │ │ · 统计"绿区词"占比:显著高于随机预期 → 命中水印 │ └────────────────────────────────────────────────────────────────┘这个机制的精髓,我用人话再翻译一遍:textGrain 用一把"秘密钥匙",在每一次"选下一个词"时,把候选词偷偷分成"该选"和"不该选"两组,然后轻微地偏向"该选"的那组。单独看任何一次选词,你几乎感觉不到差别(因为候选词本来概率就相近);但一整段话下来,所有"该选"的词会表现出一种统计上的"偏高频率"——这个频率,就是那道隐形水印。检测器只需要用同一把钥匙,重算一遍"每个词在哪组",再看"该选组的占比"是否显著偏高,就能认出它来。而且这里有个关键点:这把钥匙是"秘密"的。没有钥匙的人,不知道"哪个组该选",自然看不出、也算不出任何规律——这就把水印变成了"只有持钥者能验证"的私密信号,别人想伪造或绕过都会更难。我再用 Python 把"水印嵌入 + 检测"的最小闭环完整实现一遍——这绝不是玩具,它把 textGrain 的统计本质暴露得一清二楚:importhashlib,math,random# ---------- 工具:用秘密key把候选词分成"绿区/红区" ----------defzone(word:str,secret_key:str)-int:"""返回 1=绿区(该选) 0=红区(不该选);对同一key结果固定可重现"""h=hashlib.sha256(f"