Привет!

Эпоха дикой генерации слопотекста, кажется, заканчивается. Недавно от Anthropic вышла статья How Claude's Text Watermark Works, которая посвящена текстовому водяному знаку и его детекции. Один из главных научпоп-исследователей LLM — Себастьян Рашка — подготовил детальный разбор того, как вообще в LLM может быть устроен такой вотермаркинг: разбор понятный, глубокий, с картиночками — все как мы любим.

Я сделал частичный перевод этого разбора — Себастьян очень крутой, но часто долго разгоняется, слишком много извиняется за свои предположения, порой чрезмерно рекламит книгу, а иногда просто нудит или тяжело пишет.

Поэтому публикую чистенькую, сильно переписанную и адаптированную под русский язык версию. Тема очень крутая и горячая, приятного чтения!

В оригинале в статье идет долгое вступление о том, как LLM вообще генерит токены. Я убрал эту часть: если вы это знаете (а в 2026 надо бы!), то будет скучно, если нет, то приглашаю к оригиналу. Здесь это вступление заменено одним абзацем, а дальше только суть про водяной знак (который я иногда буду называть вотермарком, потому что англицизм вполне настоялся сам по себе).

Модель генерирует ответ по одному токену за раз и на каждом шаге она выдает оценки (скоры) по всему словарю токенайзера, softmax превращает их в вероятности — и дальше наступает сэмплирование, то есть непосредственно выбор следующего токена. Вариантов у него два: жадное декодирование берет самый вероятный токен, всегда и без исключений, а вероятностное сэмплирование бросает нечестный кубик: шансы токена пропорциональны его вероятности, потому чаще всего выбор все равно падает на один из верхних по скорам токенов.

Жадный вариант выглядит логичнее, но на длинных текстах вырождается: модель скатывается в повторы и зацикливания. Дальше всё зависит от того, насколько модель уверена на конкретном шаге: при остром распределении выбор предрешён и кубик ничего не решает. Но при равноценности двух-трех кандидатов, например,— «холодно и пасмурно» (overcast) против «холодно и серо» (gray), — решает именно случайность, и потому один и тот же промпт даёт в итоге слегка разные тексты.

Этот момент запоминаем: водяной знак Claude встроен ровно сюда — не в модель и не в готовый текст, а в источник случайности внутри сэмплирования.

Оба варианта («пасмурно» и «серо») — разумные следующие токены для задачи генерации продолжения текста и выбор между ними равноценен: нельзя объективно сказать, что один из них хуже другого. Поэтому при случайном сэмплировании — поскольку у обоих токенов относительно высокие и притом близкие по величине оценки — мы можем получить как один, так и другой.

Так что при многократном повторении сэмплирования почти в половине случаев мы получили бы «overcast», и почти в половине — «gray». Именно поэтому LLM часто выдают разные ответы на один и тот же промпт.

А выбранный токен затем влияет на все последующие токены, и так далее.

Сиды (random seeds) и детерминированное сэмплирование

Теперь я хотел бы кратко поговорить о генерации случайных чисел: если мы используем генератор случайных чисел вот так, он выдаст случайную последовательность чисел. Если запустить его ещё раз — последовательность будет уже другой и каждый раз при новом запуске мы будем получать разную последовательность.

Но если я задам сид, например 123, и запущу код несколько раз, мы по-прежнему получим случайные числа, но каждый запуск будет приводить к одной и той же последовательности.

То есть они всё ещё случайны: с фиксированным сидом мы по-прежнему получаем случайные числа, отличающиеся друг от друга внутри последовательности, — но теперь они воспроизводимы. Иными словами, с сидом или без него мы в любом случае получаем случайные числа, но с сидом — воспроизводимую последовательность. Запоминаем: эта небольшая вводная поможет лучше понять, как оно устроено.

Например, я уже говорил, что при случайном сэмплировании мы можем получить либо «gray», либо «overcast». Но если мы используем конкретный сид, скажем 42, то будем всегда выбирать, например, «overcast». То есть выбор по-прежнему случайный, но мы делаем его детерминированным. В этом случае для данного промпта модель всегда будет выбирать «overcast».

Если использовать другой сид, модель может выбрать «gray» — и при каждом сэмплировании она будет неизменно выбирать «gray» в качестве следующего токена. То есть это по-прежнему случайное сэмплирование, но мы делаем его детерминированным за счёт сида.

Как работает ключ вотермаркинга

Так вот, в вотермаркинге Claude идея примерно та же: задаётся сид. Но этот сид — не просто число, которое кто-то взял и зафиксировал. Вместо этого используется секретный ключ — по сути, что-то вроде API-ключа, — и из этого ключа вместе с четырьмя предыдущими словами, собственно, и выводится сид.

Суть в том, что как и в генерации чисел, здесь есть фиксированный сид, и этот сид всегда выбирает один и тот же следующий токен. То есть, вместо простого статичного сида у них есть секретный ключ, и вдобавок для вывода сида используется информация о предыдущих токенах. Но об этом позже.

Идея в том, что вотермаркинг делает генерацию текста более детерминированной на определённых позициях. Рассмотрим, например, вот эти правдоподобные тексты слева. Пусть у нас есть текст:

The weather today is cold and ..... (Погода сегодня холодная и...)

Я могу выбрать либо «overcast», либо «gray». Дальше в предложении будет развилка: «light» или «gentle».

Оба варианта взаимозаменяемы.

А затем еще развилки:

the breeze is moving / blowing through the trees (бриз движется / дует в кронах деревьев)

the streets feel quiet / still («улицы кажутся тихими / замершими»)

Это, по сути, означает, что я мог бы сказать как «moving», так и «blowing», как «quiet», так и «still». То есть в тексте есть много ситуаций, где варианты токенов почти равновероятны — как мы уже видели ранее. А значит, прогоняя промпт через LLM мы получаем то один вариант, то другой. Вотермаркинга здесь пока еще нет.

В зависимости от числа таких позиций возможных ответов здесь может быть, например, 128. Разумеется, чем длиннее текст, тем больше в нём позиций со взаимозаменяемыми словами — и тем больше комбинаций, то есть возможных выходных текстов.

Итак, ещё раз: одним из возможных выходных текстов мог бы быть...

The weather today is cold and overcast. A light breeze is moving through the trees, and the streets seem quiet. I think I'll stay home and read a book with a cup of tea

(Погода сегодня холодная и пасмурная. Лёгкий бриз движется сквозь деревья, и улицы кажутся тихими. Думаю, я останусь дома и почитаю книгу за чашкой чая.)

Это один из возможных текстов. Другой возможный текст:

The weather today is cold and gray. A gentle breeze is blowing through the trees, and the streets seem still. I think I'll stay inside and read a novel with a mug of tea

(Погода сегодня холодная и серая. Мягкий бриз дует сквозь деревья, и улицы кажутся безмолвными. Думаю, я побуду дома и почитаю роман за кружкой чая.)

Как видите, сгенерированы два вполне разумных текста, а их комбинаций существует и больше. И все они разумны — нет такого, который был бы объективно лучше другого. Они просто равнозначны, а какой вариант конкретно мы получим — чистая случайность.

Теперь, если зафиксировать сид — например, взять сид 99, — мы можем всегда получать вот этот текст. То есть с помощью сида мы фиксируем, какой ответ получим: случайное сэмплирование остаётся случайным, но становится детерминированным в том смысле, что оно воспроизводимо — результат всегда будет одним и тем же. И это всё ещё генерация без вотермаркинга, теперь просто с сидом.

И вотермаркинг делает по сути то же самое. Только вместо простого сида у них есть так называемый случайный ключ, и этот ключ, собственно, участвует в выборе текста. Но кое-что мы уже можем сказать: в блог-посте про Claude утверждается, что вотермаркинг не должен ухудшать текст. И если посмотреть на этот механизм — да, понятно, почему он не должен его ухудшать.

«К слову, я здесь не защищаю водяные знаки — я просто пытаюсь объяснить, как они работают. Так что, пожалуйста, не убивайте гонца. Я лишь хочу сказать: с точки зрения конечного пользователя вотермаркинг — это не что иное, как фиксация сида, которая делает сэмплирование в некотором смысле детерминированным» (цитата из оригинала).

Строго говоря, «фиксация сида» объясняющая метафора: настоящая случайность из сэмплирования никуда не девается, ключ лишь подкручивает шансы. Поэтому на практике вотермарк проявляется не как одинаковые ответы на один промпт, а как чуть менее разнообразные.

Где применяется водяной знак

Промежуточный итог: без вотермаркинга мы, как правило, сэмплируем без сида — думаю, большинство людей его вообще не используют и не факт, что его в принципе можно задать у вашего провайдера.

Так или иначе: без вотермаркинга у нас случайное сэмплирование. С вотермаркингом — справа на слайде — сэмплирование тоже случайное. Но вдобавок к чисто случайному сэмплированию появляется ключ вотермаркинга.

Этот ключ вотермаркинга передаётся генератору случайных чисел для установки конкретного сида — этот сид не фиксируется один раз на всю генерацию — иначе это была бы просто воспроизводимость, а не метка. Он работает на каждом шаге: ключ плюс несколько предыдущих токенов дают псевдослучайное число для этого конкретного выбора. Логиты модели при этом не меняются — подменяется только способ бросить кубик.

Поэтому водяной знак живет на этапе сэмплирования, а не внутри LLM. А значит, не нужно обучать или переобучать модели — нужно надстроить логику поверх декодера.

Как работает детекция водяного знака

Но мы ещё не закончили. Хочется также поговорить о том, как понять или проверить, есть ли у текста водяной знак. Детекция водяного знака возможна только при наличии доступа к ключу.

Поэтому если у нас есть несколько текстов, то узнать про водяной знак просто так невозможно — потому что для этого нужен ключ вотермаркинга. Нужна скоринговая функция, которой вы, по сути, оцениваете текст. И идея такая: если оценка выше определённого порога, значит, текст промаркирован; иначе — нет. Но мы как конечные пользователи сделать этого не можем, поскольку ключа у нас нет и никто его не даст.

Ключ будет только у Anthropic, хотя в блог-посте упоминается, что они, конечно, предоставят доступ — точнее, собираются разработать для этого API. Деталей нет, но, возможно, они дадут такой доступ по API для компаний, которым необходимо помечать сгенерированный контент, например, для X или Substack.

А может, такой доступ откроют и для конечных пользователей. Но суть в следующем: детекция водяного знака возможна только при наличии ключа вотермаркинга — ну или того самого API, который они собираются разработать.

Как удалить водяной знак

А вот удаление водяного знака — это интересно. Теперь, когда мы знаем, как работает вотермаркинг, мы понимаем и его слабые места: всё критически зависит от конкретных токенов на определённых позициях. Например, если в тексте ниже выделенные цветом слова (токены) — это позиции вотермаркинга, то мы знаем, что могли бы удалить водяной знак, отредактировав их, верно? Заменив все слова на этих позициях, мы бы со стопроцентной гарантией сломали водяной знак.

Но проблема в том, что мы этих позиций не знаем.

Мы не знаем, где находятся эти слова, потому что не мы генерировали водяной знак — а значит, не знаем, на какие позиции смотреть. Практический сценарий здесь такой: просто редактировать текст наугад. Мы случайным образом меняем несколько слов и надеемся, что затронули достаточно позиций, чтобы повредить водяной знак и фактически его удалить. И поскольку мы также не знаем, какие токены имеют наивысшие оценки — для этого понадобился бы доступ к той самой LLM, чтобы заново прогнать через неё промпт и выяснить это, — нам остаётся только гадать.

Например, мы можем заменить «overcast» на «cloudy» («облачная») — ведь мы не знаем, что высокую оценку имело именно «gray». В данном случае вариант «gray» может показаться интуитивно очевидным, но бывают случаи, где всё далеко не так очевидно. Главная идея: это просто обычное редактирование текста, при котором мы меняем какие-то позиции, но это по-прежнему лишь гадание, где находятся позиции вотермаркинга.

Поскольку мы их не знаем, мы просто должны заменить несколько слов тут и там. И если этих измененных слов достаточно много, это тоже сломает водяной знак.

Как работает скоринговая функция водяного знака

Это был вотермаркинг в двух словах. Выше упоминалась скоринговая функция, позволяющая выяснить, есть ли в тексте водяной знак, — давайте посмотрим на нее детальнее. Она немного сложнее, и для общей картины понимать её устройство не обязательно.

Причина, по которой она устроена именно так, — удешевление детекции. Потому что иначе пришлось бы для каждой проверки снова прогонять весь промпт, получать оценки, затем применять сид вотермаркинга и сравнивать еще раз. Это дорого.

К тому же нужно знать, какая именно LLM использовалась, — а это делает всю затею практически нереализуемой, ведь зачастую мы даже не знаем промпта. Так что у них есть трюк: они, скажем так, модифицируют сэмплирование таким образом, чтобы на этапе скоринга LLM была уже не нужна. В блог-посте упоминается, что метод они позаимствовали из научной статьи в Nature от DeepMind.

Метод называется SynthID-Text, и Anthropic прямо говорит: их вотермаркинг — версия SynthID-Text, но под капотом все детали настройки ожидаемо неизвестны.

SynthID-Text и турнирное сэмплирование

Всё предыдущее про генерацию по-прежнему верно, но появляются новые нюансы в том, как сэмплируется токен.

Они используют не просто, условно, random.choice из NumPy — здесь применяется кое-что более изощрённое.

Итак, снова предположим, что наш контекст — «the weather today is cold», и мы хотим сгенерировать следующий токен. Возьмём, например, варианты: «gray», «overcast», «gloomy» («мрачная»), «cloudy». Вероятность «gray» — 50%, «overcast» — 30%, «gloomy» — 15%. Пусть «cloudy» — 5%, а всё остальное — условно 0. На рисунке всё выглядит немного иначе — переиспользуется старая иллюстрация. Но представьте, что это самые вероятные токены — «gray» и «overcast», — а всё остальное пренебрежимо мало, кроме разве что «gloomy» и «cloudy».

Для понимания примера лучше всего представить маленький словарь из четырёх токенов вместо всех возможных. И мы могли бы использовать random.choice из NumPy с этими вероятностями, чтобы сэмплировать следующий токен.

И мы могли бы использовать ключ вотермаркинга с генератором случайных чисел, чтобы сделать сэмплирование детерминированным и получить нужный нам водяной знак. Но это было бы дорого — не само сэмплирование — с ним всё в порядке, оно довольно дешёвое. Дорогой была бы последующая детекция, если мы захотим проверять случайные тексты из интернета.

Вместо этого у них есть механизм, который применяется прямо при сэмплировании, то есть во время генерации, — так, чтобы его можно было повторно использовать потом, при детекции. Этот механизм называется турнирным сэмплированием. То есть вместо чего-то вроде random.choice они используют концепцию турнирного сэмплирования.

Как это работает? На первый взгляд может показаться сложным, но, честно говоря, выглядит это куда сложнее, чем есть на самом деле — стоит один раз уловить суть — и дальше всё довольно прямолинейно.

У нас по-прежнему есть контекст, а к нему — вероятные (правдоподобные) следующие токены с их вероятностями. Теперь — у них есть то, что называется случайными функциями вотермаркинга.

Случайные функции вотермаркинга

У нас три функции вотермаркинга: G1, G2 и G3. В реальности их может быть 30, 50 и даже больше — здесь три просто потому, что так нагляднее на слайде: компактнее и лучше помещается. Теперь посмотрим на слово «gray» — оно может дать нам сигнатуру 101. Что я имею в виду: мы используем ключ вотермаркинга для генерации сида, и у нас есть три функции — G1, G2, G3.

Я подаю на вход слово «gray». Здесь опускается одна деталь: обычно на вход подаётся не одно слово, а вместе с тремя-четырьмя предыдущими словами контекста — то есть «cold» и «gray». Так вот, если подать это в G1 вместе с ключом вотермаркинга, получим значение 1. Почему? Просто так работает эта функция — она случайная. Случайная функция возвращает либо 0, либо 1. В данном случае, с этим ключом и этим токеном, она возвращает 1.

С тем же ключом, но другой функцией, получаем 0. А вот здесь — снова 1. И если функций больше — скажем, все 30, — сигнатура превратится в очень длинную строку из нулей и единиц.

По сути, это битовая строка — набор битов из нулей и единиц, и для «gray» через функции вотермаркинга получилась сигнатура 101. Теперь то же самое можно проделать и для всех остальных токенов: для «gray» уже сделали, дальше — «overcast», «gloomy» и «cloudy». У каждого получается своя сигнатура.

Например, у «overcast» — 010, у «gloomy» — 001, у «cloudy» — 100. Следующий шаг — собственно турнирное сэмплирование, где мы просто разбиваем токены на пары.

Одно важное упрощение: на самом деле в турнире играют не «все кандидаты по разу», а пул токенов, насэмплированных из распределения модели — частые токены попадают туда по несколько раз. Так вероятности модели продолжают управлять результатом, и текст не портится.

Турнирное сэмплирование

Это как футбольный турнир на стадии плей-офф (на вылет): всегда играют две команды друг против друга. Здесь та же идея: у нас пара токенов, и они, по сути, соревнуются между собой. А очки берутся из тех самых функций. В первом раунде начинаем с первой функции.

Итак, пара «cloudy» и «gray».

Смотрим в таблицу: у «gray» — 1, у «cloudy» — 1, получается счёт 1:1. Пара «overcast» и «gray»: у «overcast» — 0, у «gray» — 1. Получаем 0:1. Пара «gloomy» и «overcast»: у «gloomy» — 0, у «overcast» — 0. Счёт 0:0. И затем «gray» против «gray» — дубликаты тут норма: частые токены попадают в пул по несколько раз.

Пары формируются случайно. Итак, здесь у нас 1:1 — ничья. При ничьей победитель тоже выбирается случайно — вот тут ключ уже ни при чём. Здесь выживает «cloudy». А в этой паре, согласно G1, побеждает «gray» — у него единица. «Gray» проходит дальше. Дальше у «gloomy» и «overcast» ничья — победитель выбран случайно [«overcast»], и последний «gray» тоже выбран случайно.

Теперь у нас «cloudy» и «gray», «overcast» и «gray» — играем следующий раунд турнира. В этом раунде используется G2. Согласно G2, у «cloudy» — 0, у «gray» — тоже 0. У «overcast» — 1, и у «gray» — тоже 0. И далее — следующая стадия турнира.

Итак, ничья — случайно выбираем «gray». А здесь победителем выходит «overcast». В финале — «gray» против «overcast». Снова смотрим на очки: у «gray» — 1, у «overcast» — 0. Побеждает «gray». Вот так и сэмплируется токен «gray». А что здесь делает ключ вотермаркинга? Если вернуться на несколько картинок назад: ключ участвует в генерации этих оценок через случайные функции вотермаркинга.

То есть ключ вотермаркинга, по сути, определяет, какие значения мы получаем на всех этих стадиях. Так что ключ по-прежнему критически важен — с другим ключом все сигнатуры выглядели бы иначе.

Детекция водяных знаков без повторного запуска LLM

Мы сэмплировали следующий токен — именно так и работает эта модифицированная процедура сэмплирования. Мы могли бы обойтись random.choice из NumPy, но у этого подхода есть недостаток: если мы захотим заскорить случайный текст из интернета, придётся заново запускать LLM.

С этой же техникой — не придётся. Да, техника звучит очень странно и громоздко, но её преимущество в том, что теперь мы можем гораздо проще скорить случайные тексты без повторного дорогого запуска языковой модели.

По сути, всё это сделано лишь для того, чтобы детекция была проще и дешевле. Честности ради: без прогона LLM детектируются и другие схемы вотермаркинга, но фича именно турнира в другом — каждый слой добавляет сигнала, и знак ловится надёжнее при том же качестве текста.

Давайте представим, что наш пример «the weather today is cold and gray» — это новый текст, найденный в интернете. Для этого у нас — точнее, у Claude/Anthropic — есть ключ вотермаркинга и те самые функции G1, G2 и G3.

Текст прогоняется через эти функции. Для одной позиции — для «gray» — мы получим 101, так же, как получали в процессе генерации. То есть всё как раньше. И если сложить эти биты, получится два бита: единица и единица. Скажем для простоты, что в этой позиции два бита информации. Это, конечно, очень упрощённая иллюстрация.

Предположим, что для «gray» в этой позиции мы получаем оценку 2. Если бы на этой позиции стояло другое слово — «overcast», — мы получили бы 1; для «gloomy» — тоже 1, и для «cloudy» — 1. То есть я просто суммирую значения по каждой строке. Это и есть оценка, которую мы получаем на каждой позиции.

Если проделать то же самое на других позициях, оценки на них будут другими. Например, предположим, что на первой позиции я получаю 2. Здесь — 2. Для «today» — 3. Для «is» — 2. «Cold» — 2. И «gray» — 3. То есть я применяю функции вотермаркинга так, как показал на предыдущем слайде.

И я просто складываю эти числа по трём функциям. Функции вотермаркинга очень дешёвые — их можно быстро прогнать по всему тексту и получить все оценки. А затем на их основе вычислить среднее число бит. Усреднив все эти значения, я получу, скажем, 2.23.

Теперь возьмём немного другой текст: я заменил «today» на «now» и «gray» на «overcast». Эти позиции теперь получают оценки 1 и 1. И если усреднить по всей строке, выходит 1.71. И для всего этого мне не нужна LLM. Всё, что нужно, — ключ вотермаркинга, генератор случайных чисел и функции G1, G2, G3. И всё.

LLM не нужна — а оценку получить можно, остается только подобрать порог.

Условно, если оценка больше 2, текст вотермаркирован; если меньше 2 — нет.

Вот так и можно детектировать, есть ли водяной знак в случайном тексте из интернета. По сути, это просто применение функций вотермаркинга, усреднение оценок и сравнение с порогом.

Итоги и ограничения вотермаркинга

Разница между генерацией без вотермаркинга и с ним в том, что мы управляем сэмплированием с помощью ключа вотермаркинга. А внутри этого процесса работает турнирное сэмплирование. И для этой детекции водяных знаков нужны секретный ключ и функции вотермаркинга G1…Gn.

И снова про удаление водяного знака — как самую интересную часть. В идеале нужно было бы отредактировать все позиции вотермаркинга для его гарантированной поломки, но мы не знаем, какие именно позиции вотермаркированы. Внутри же позиции выбираются так, чтобы на них были почти равновероятные токены — а есть позиции, где это условие не выполняется. Например, для «trees» может вообще не найтись альтернативного слова с высокой оценкой — поэтому такую позицию они не вотермаркируют.

То есть вотермаркинг применяется, по сути, только на определённых позициях. А поскольку мы не знаем, какие позиции нужно «атаковать», чтобы сломать или удалить водяной знак, нам пришлось бы редактировать текст сразу в нескольких местах.

Что это значит для сгенерированного текста

Для нашего прекрасного будущего вотермаркинг может привести к ухудшению качества сгенерированных ИИ текстов. Потому что генеративный слоп заполонил весь интернет, где-то на нем даже зарабатывают деньги, и потому вотермаркинг такие фабрики не остановит.

Поэтому они просто возьмут еще одну модель.

Они просто будут использовать вторую модель для редактирования текста и получать, так сказать, отредактированный сгенерированный ИИ текст. То есть пайплайн усложняется: вместо того чтобы брать текст напрямую из Claude, теперь Claude генерирует текст, этот текст прогоняется через локальную модель — и на выходе получается отредактированный сгенерированный текст, в котором водяного знака, скорее всего, уже нет.

Почему вероятна именно локальная модель? Потому что Google ведь написали ту статью, верно? Поэтому с огромной вероятностью Gemini тоже вотермаркирует контент. И OpenAI, вероятно, уже делает это или будет делать, как и все остальные ведущие лабы мира. Летом 2026 года около 190 компаний подписали кодекс прозрачности (приложение к европейскому AI Act), поэтому вотермарки так или иначе будут у всех.

А если коррекцию будет делать локальная модель, то с огромной вероятностью она будет меньше фронтирной. Технически, конечно, можно было бы генерировать текст сразу локальной моделью, но фронтирные модели гораздо точнее работают с поиском, делают фактчекинг и производят контент на порядок лучше. Поэтому первая генерация будет фронтирной моделью и затем финальное доведение моделью локальной.

И это доведение будет нацелено на обход вотермарка, а, как уже знаем, это лишь попытка угадать провотермаченные позиции, что в случае с локальными моделями и грозит ухудшением текста. То есть, у нас по-прежнему генеративный слоп, но к тому же неуклюже отредактированный, чтобы обходить слоп-фильтры.

Заключительные мысли

Целью этой статьи было познакомить с тем, что слоп-маркировка на текстовый контент это не идеи поехавших, а наша реальность и у задачи есть вполне красивое инженерное решение. Оно под капотом не такое сложное, но и совсем тривиальным его не назовешь.

Надеюсь, вам было полезно и интересно!

Мой канал про агентов, LLM, продукты и людей: Agentic World и другие статьи: