vLLM 把文本水印塞进采样器后,真正变难的是工程问题
同一段模型回复,复制进文档、改掉几个句子、再让另一个模型润色一次。几分钟后,它还算不算原模型生成的内容?
这不是文字游戏。对提供模型能力的团队,它关系到滥用追踪、内容申诉和数据污染。对接入 LLM 的应用团队,它关系到你能不能回答一件很具体的事,某段争议文本是否来自自己的服务。
9 月 24 日,vLLM 把 Gumbel-max 文本水印接进了 Model Runner v2。新闻标题看起来很平,像推理引擎又多了一个开关。但把官方文章和实现细节读完,我反而觉得它最有价值的地方不在「水印」二字,而在于它没有把水印当作一个后处理功能。
它被放进了采样路径最里面。
这件事很符合生产系统的脾气。只要你真的打算上线,理论上漂亮的方案很快就要面对三个不太讲道理的问题,吞吐会不会掉,Speculative Decoding 会不会被弄坏,模型会不会突然开始复读。
vLLM 这次给出的答案也不完美。但它把问题摆在台面上了,这比许多只展示检测准确率的水印方案有用得多。
先把一个误解放下,文本水印不是往字里塞记号
图片和音频可以在像素或波形中加一点人耳、人眼不敏感的扰动。文本不行。一个 token 换成另一个 token,意思、语气、代码可运行性都可能变了。
所以文本水印不是给输出附一层不可见的 Unicode 字符,也不是在文件 metadata 里写一个来源标签。前者复制粘贴或格式清理就没了,后者离开原文件更是立即失效。靠句长、用词习惯去猜某段文字像不像某个模型,听起来省事,误判却很难收场。
真正有分量的路线,是在模型每一步选择下一个 token 时动手。
语言模型给出候选 token 的概率分布,然后采样一个 token。假设「部署」有 50% 概率,「发布」有 30%,「上线」有 20%。常规随机采样会从这个分布抽一次,随机数用完即丢。水印方案则让这份随机性可复现,检测方拿到密钥和同样的文本上下文,能够重建当时每个 token 对应的随机值。
这里有个很容易被忽略的边界。可复现不等于可预测。密钥不公开时,外部的人仍然看到一次普通采样。只有持有密钥的检测器,才能发现最终被选中的 token 总在统计上偏向某一类随机值。
这是一种累积信号,不是逐字盖章。短回复、格式固定的 JSON、重复性很强的代码,都比一篇长的开放式回答难测得多。
Gumbel-max 的巧妙之处,是不改概率分布
传统的 green list 水印会对一组 token 加分,逼模型更倾向选它们。检测很直接,副作用也直接。模型可能会偏向某些字词、句法,输出质量和分布被悄悄推歪。
vLLM 选的是 Gumbel-max trick。它把每个候选 token 的 logit 加上 Gumbel 噪声,再取最大值。数学上,这个「加噪声再取最大」和从原始 categorical distribution 采样是等价的。
听上去有点反直觉。明明加了东西,怎么会没改结果分布?关键在于加的不是固定偏置,而是服从特定分布的随机噪声。重复足够多次,候选 token 被挑中的频率仍会回到模型原来的概率。
vLLM 没有每次临时抽随机数,而是用 PRF 根据密钥、最近上下文和候选 token ID 生成伪随机值。官方实现的默认上下文长度是前 4 个 token。相同的密钥、相同的上下文、相同的 token,会得到同样的噪声。
生成端据此选 token。检测端拿着密钥,把文本重新 tokenizer 一遍,就能按上下文把那些值再算出来。
如果一段文字没有水印,实际 token 和这些带密钥的随机值没有关联。若有水印,被选 token 会更常落在高值区域。检测器把每个 token 的分数累加,再用 Gamma distribution 算单侧 p-value。p-value 小,不是在宣判「它肯定由某模型生成」,而是在说「无水印文本碰巧得到这么高分的概率很低」。
这个措辞差别很大。
水印能给出处置线索,不能替代证据链。对外部申诉、账号处罚、版权争议这类高风险动作,模型供应商若把一次 p-value 检测当作最终裁决,迟早会撞墙。
一旦检测多个密钥,统计账本就开始变复杂
官方文章里有一个细节很诚实。设定 1% false positive rate,不代表实际系统里就固定有 1% 的误判。
真实服务往往不知道文本究竟来自哪个模型、哪个 tokenizer、哪个水印版本。检测系统可能要依次尝试多个密钥、多个配置。候选测试一多,总会有一个偶然分数很高。要控制总体误报,就得做 multiple-testing correction。
代价是阈值更严格,检测能力下降。
官方图表给出一个相当具体的对比。在 1% FPR 下,创意写作大约 100 个 token 就能接近 100% TPR。MBPP 代码任务难得多,拿到 400 个 token 时,分别在测试 1、10、100 个候选配置的条件下,TPR 约为 69%、49%、43%。
这组数字比「水印可检测」更接近生产事实。高熵、长文本有很多可供信号累积的分叉。代码、模板和短回答可选空间很小,水印没那么多地方发力。
如果你在做一个面向用户的 Agent,应该把这个限制写进产品设计,而不是放进脚注。比如对一条 40 token 的客服答复说「检测不到水印」,它只说明证据不足,不能推导出它不是模型生成的。对一段 2,000 token 的技术文档,检测结果才更有讨论价值。
别把「没有检出」做成一个假阴性承诺。
最容易把论文方案推翻的,是推理性能
模型服务不喜欢在 [batch, vocabulary] 上额外开大 tensor。词表通常是几万到十几万,batch 一上来,临时噪声矩阵带来的显存和 memory traffic 会比算法描述吓人得多。
vLLM 的工程选择很干脆,把伪随机数生成、Gumbel 变换和 argmax reduction 融成一个 GPU kernel。Philox 每次生成四个随机值,kernel 以连续四个 token ID 为一组处理。水印请求和普通请求仍能共用采样器,一张按行的 mask 决定该行取带密钥噪声还是普通随机数。
这不是锦上添花。一个经常被忽略的现实是,水印不会只跑在超大模型上。小模型对 scheduler 延迟很敏感,大词表对额外计算很敏感。任何只在单一离线 benchmark 里好看的实现,进到 batch inference 后都可能变成尾延迟制造机。
vLLM 用 Qwen3.5-27B、单张 H100、MTP-3、每次 512 个输出 token 做了吞吐测试。batch 从 1 到 256,8 个密钥的配对平均变化在 -1.1% 到 +2.0% 之间,官方判断没有显著降速。
我会把这个结果理解为一个很好的工程信号,而不是普遍性能结论。它验证的是特定模型、硬件、MTP 配置和输出长度。换到不同 GPU、不同词表、不同 sampling 参数,还是要压自己的压测数据。
但方向没有问题。文本水印要成为服务能力,必须寄生在已有的高速采样链路里,而不是在请求完成后另起一套计算。
Speculative Decoding 给水印出了道难题
现在不少推理服务依靠 Speculative Decoding 提速。draft model 先提一串 token,target model 再验证并接受其中一部分。两者分布越接近,接受率越高,提速就越明显。
直觉上,把同一套水印噪声同时加给 draft 和 target 似乎很自然。问题是两边虽然各自仍保持原始分布,水印后的分布相互重叠却可能变少,接受率掉了。
这种退化非常别扭。你为了可归因加了水印,却拿推理加速作为代价。对于高并发 API,这基本等于没有上线价值。
vLLM 的办法是 dual-key。被接受的 draft token 用一个 key,target residual token 和 bonus token 用另一个 key。这样 target 的采样保持独立,不被被拒 draft proposal 干扰,常规接受率可以保住。
但这笔账没有白嫖。最终文本里混着两个 key 的信号,检测时也要同时计算并合并分数。每个 key 的信号被稀释,检测更弱。官方直接承认这一点。
这正是我觉得这次实现值得写的地方。它没有假装可以同时得到最强检测和零性能损失,而是把 trade-off 放在算法接口里。对使用者来说,应该暴露的是策略,而不是一句「已启用水印」。你的业务究竟更在意吞吐、检测功效还是密钥管理成本,答案不会一样。
更险的一坑,单 token 无失真不等于整段文本正常
Gumbel-max 有单 token 分布不变的保证,但完整序列还有相关性问题。
假设上下文重复了,PRF 又由上下文和密钥决定,同一个上下文会得到同一组噪声。如果模型在相似上下文下的 logits 也相近,之前赢过一次的 token 可能再次赢出。极端情况下,生成会跌进重复循环。
官方文章用 1 + 1 + 的例子说明了这个现象。上下文 (1, +, 1, +) 第二次出现后,固定 key 又让 1 获得同样的随机优势。接下来又回到相同上下文,循环就有机会自我加强。
这不是玩具问题。它会表现为更长的输出、重复内容、退化行为,最后由用户感知成「这个模型今天怎么变笨了」。
vLLM 的解法叫 generation-time context deduplication。生成时如果发现前置上下文已经出现过,就跳过这一步的水印,回到普通采样。这样保存了完整序列层面的非失真特性。官方在 Qwen3.5-27B 上报告,这项检查带来的端到端吞吐下降最多 0.19%。
这一段很值得所有 Agent 开发者记住。线上模型事故经常不是某个大功能突然失败,而是两个各自正确的局部优化交叉后,出现了一条重复、超时、耗尽 token 的路径。
水印和 decoding 都是局部正确。把它们组合起来,才需要系统工程。
对 Java 和 Agent 团队,先做来源策略,再接 API
如果你的服务端是 Spring Boot,或者 Agent 运行时用 LangGraph、LangChain 一类框架编排,看到 vLLM 支持水印后最容易犯的错,是立刻给所有请求开开关。
我不建议这么做。先回答三个业务问题。
第一,谁是检测者。水印检测需要密钥和 matching tokenizer。若把检测能力给到外部用户,密钥轮换、权限隔离、审计日志和误用防护都要一起设计。检测 API 不是一个无状态的小工具,它本身是归因基础设施。
第二,检测结果服务什么动作。内部调试、内容溯源提示、训练数据清洗、自动封禁,风险等级完全不同。越接近自动处罚,越需要把水印结果和请求日志、账号行为、人工复核拼成证据链。
第三,哪些响应值得标记。长篇自由文本适合,短 JSON、tool call 参数、结构化抽取结果通常不适合。对 Agent 来说,工具调用更应该依赖 trace ID、审计事件和权限记录,而不是希望在几十个 token 里塞进来源信号。
一个比较务实的接入方式,是把水印看成模型网关的一项 policy,而不是业务代码里的 if else。
- 在模型路由层按 tenant、模型版本、场景和数据等级决定是否启用
- 把 watermark key ID、tokenizer 版本、模型版本记录到不可篡改的请求审计事件中
- 检测服务只给可信后台调用,并返回 p-value、候选配置数量、有效 token 数,而不是只返回真或假
- 对低 token 数或多配置扫描的结果明确标注低置信度
- 任何处罚或对外认定都要求额外证据,绝不只看一次检测
这看起来比一个开关麻烦。可一旦进入合规、纠纷或安全事件,麻烦会从「当初多写一点设计」变成「谁能证明我们当时用的是哪把 key、哪个 tokenizer、什么阈值」。
日志不是附属品。它和水印共同构成了可追溯性。
水印不是反洗稿神器,也不是 AI 身份证
还有两层期待需要降温。
一层是对改写的期待。vLLM 的信号写在 token 选择中,原文完整复制时能保留。局部修改会损伤修改点附近的上下文,但在超过最近上下文窗口后,后续信号仍可能保存。大幅改写、翻译、摘要、把多段文字混在一起,检测功效会明显变差。这不是实现不好,是文本语义可变性带来的边界。
另一层是对身份的期待。检测器需要密钥和 tokenizer,检测到的是某种配置下的统计一致性,并不自动回答作者是谁、用户是否恶意、内容是否真实。尤其当服务端要测试多个版本、多个模型时,multiple-testing correction 会再压低检测功效。
所以我不认为文本水印会解决互联网来源危机。它更像 TLS 日志、邮件 DKIM、代码签名那样的一个基础件。单独拿出来不够,放进一套身份、审计、申诉和治理系统里才有价值。
这也解释了为什么 vLLM 的实现路线比新闻标题更重要。它把抽象的 provenance 问题落到了最具体的地方,GPU kernel、batch、decode 接受率、重复上下文和统计阈值。每一个都不性感,却决定功能是否活得过第一个生产环境。
现在能做什么
vLLM 已提供启动时启用 Gumbel-max watermarking 的配置,以及一个最小 HTTP 检测服务示例。真正要落地的团队,别把首个迭代定成全量上线。
挑一个低风险、文本较长、可回查请求日志的场景做 shadow mode。保留普通输出路径,采集有效 token 数、检测分数、P99 latency、Speculative Decoding 接受率和重复率。再用真实业务里的改写、摘录、拼接文本测一遍。
你会很快知道自己的场景需要的不是「有没有水印」,而是「在哪些输出上,这个信号足以帮助人做判断」。
水印最终不是印章。
它是一条需要被整个工程系统认真保护的证据线。




