водяной знакстатистический след, встраиваемый в процесс выбора токенов, а не в метаданные в сгенерированном тексте — это статистический след, который встраивается в сам процесс выбора токеновэлемент словаря модели, из которого по одному собирается текст, а не в метаданные. Разбираем, чем отличаются подходы OpenAI и Anthropic, как устроены генерация и детекция в открытых реализациях, какие пороги влияют на баланс между качеством текста и надёжностью обнаружения и почему простые правки вроде замены тире или перефразирования ослабляют, но не всегда убивают сигнал.
Два подхода: OpenAI и Anthropic
OpenAI даёт клиентам API включать водяные знаки по желанию. Текстовое маркирование остаётся выключенным по умолчанию в API, и это объясняется тем, что клиенты сами решают, как водяные знаки соотносятся с их обязательствами по прозрачности и опытом пользователей. Включение происходит на уровне проекта или организациинастройка, общая для группы запросов: проект объединяет запросы одной задачи, организация — все проекты аккаунта, поэтому выбор действует сразу для всех входящих в них запросов, при этом клиенты выбирают, какие поддерживаемые модели должны его использовать; отдельных изменений в запросах не требуется.
Anthropic применяет водяные знаки на уровне модели, поэтому они присутствуют независимо от того, из какого продукта или поверхности Claude приходит текст. Эквивалентного отказа для разработчиков API Anthropic не описывает, давая им меньше контроля над тем, несёт ли вывод модели знак. Глобальность объяснялась отсутствием надёжного способа ограничить технологию по региону.
Как работает водяной знак: генерация
Сидирование и «зелёный список»
Идея простая: на каждом шаге генерации словарь токенов делится на зелёныетокены, которые при генерации получают бонус к логитам и красныетокены, которые бонуса не получают, и логитынеобработанные оценки модели для каждого токена словаря до преобразования в вероятности зелёных получают бонус. Детектор потом считает долю зелёных токенов в тексте и проверяет, не слишком ли она велика для случайного текста.
Разделение должно быть воспроизводимым: детектор, зная префикс, обязан получить тот же зелёный списокнабор токенов, получающих бонус на данном шаге, что и генератор. Поэтому список выводится из хеша предыдущих токенов. Схема сидирования задаётся параметром seeding_scheme; если он не указан, подставляется "simple_1". Из схемы извлекаются и сохраняются четыре атрибута: тип псевдослучайной функции, ширина контекста, флаг self-saltрежим, при котором зелёный список строится с учётом самого кандидата на следующий токен, а не только префикса и ключ хеширования.
Перед сидированием проверяется, что длина входных токенов не меньше ширины контекста — иначе выбрасывается ошибка. Затем из последних context_width токенов и ключа хеширования вычисляется ключ PRFпсевдослучайная функция, детерминированно превращающая префикс в число-зерно для генератора случайных чисел, и генератор инициализируется этим ключом по модулю 2⁶⁴−1 (защита от переполнения).
Размер зелёного списка — это доля словаря, задаваемая параметром gamma. Словарь случайно переставляется, и в зависимости от флага выбора зелёных токенов берутся либо первые элементы перестановки, либо последние. В расширенной версии процессора значения по умолчанию иные: gamma=0.25 и схема "selfhash".
Смещение логитов и переполнение
Маска зелёных токенов строится той же формы, что и логиты, и отмечает True те позиции, которые входят в зелёный список для каждой последовательности батча. К логитам этих позиций прибавляется delta — это и есть бонус зелёным токенам. Само сэмплирование в разобранных источниках не показано: видно только изменение логитов перед сэмплером.
Отдельный сюжет — переполнение при «бесконечном» смещении. При вычислении spike-энтропий коэффициент alpha получается как экспонента от delta, и если он равен бесконечности, срабатывает обработка переполнения: внутренние величины z_value и expected_gl_coef устанавливаются в 1.0. Эти величины используются при перенормировке softmax-вероятностей: вероятности делятся на 1 + z_value * probs, и возвращается сумма перенормированных вероятностей.
Self-salting и rejection sampling
Обычный зелёный список зависит только от префикса. Self-salting расширяет контекст на текущий токен: список строится с учётом самого кандидата на следующий токен. Реализовано это через rejection sampling — логиты сортируются по убыванию, и для каждого кандидата он добавляется к префиксу, после чего для расширенного контекста запрашивается зелёный список. Кандидат попадает в итоговый список только если он сам входит в список, построенный с его добавлением. Такой знак устойчивее, потому что зависит от предсказанного токена.
Перебор кандидатов прерывается досрочно по одному из правил: fixed_score — когда разрыв между лучшим и следующим скором превышает delta; fixed_list_length — когда набрано 10 зелёных токенов; fixed_compute — после 40 итераций. В основном вызове этот метод используется только при включённом self-salt, иначе берётся обычный список.
Две реализации генератора
В базовом генераторе выбор следующего токена — обычная выборка с температурой и top-p: логиты делятся на температуру, берётся softmax, сортировка по убыванию, кумулятивная сумма, маскируются токены с накопленной вероятностью выше порога, оставшиеся нормируются, затем multinomial выбирает токен, а gather переводит его в исходный порядок словаря. При температуре не больше нуля берётся argmax логитов.
Хеширование n-граммного контекста реализовано в функции получения seed и поддерживает несколько режимов. В режиме hash начальное значение умножается на ключ и складывается с каждым id токена по модулю 2⁶⁴−1. В режиме additive seed равен ключу, умноженному на сумму id, затем пропускается через хеш. В режиме skip берётся ключ, умноженный на первый id, и хешируется. В режиме min берётся минимум из хешированных произведений ключа на каждый id.
OpenAI-генератор после top-p фильтрации для каждого текста получает seed из n-граммных токенов, устанавливает его в генератор, создаёт вектор случайных чисел размера словаря, сдвигает его на -payload, переставляет по индексам вероятностей и заменяет вероятности на rs^(1/p), после чего выбирает argmax и делает gather. Maryland-генератор сначала пропускает логиты через процессор: для каждого текста получается seed, генерируется перестановка словаря, первые gamma * vocab_size токенов становятся зелёным списком, к их логитам прибавляется delta, а смещение сдвигается на -payload. Затем выполняется та же top-p и multinomial-выборка, что и в базовом генераторе. Ключевое отличие: Maryland модифицирует логиты и использует обычную multinomial-выборку, а OpenAI заменяет вероятности на rs^(1/p) и берёт argmax.
Как работает водяной знак: детекция
Z-оценка и p-value
Детектор наследует ту же базу, что и процессор, поэтому получает те же тип PRF, ширину контекста, флаг self-salt и ключ хеширования. z-оценкачисло стандартных отклонений, на которое наблюдённое значение отличается от ожидаемого считается как отклонение наблюдённого числа зелёных токенов от ожидаемого при доле gamma:
z = (observed_count - gamma * T) / sqrt(T * gamma * (1 - gamma))где T — общее число токенов. p-valueвероятность получить столь же или более экстремальный результат, если водяного знака нет получается как функция выживания нормального распределения от z. Ширина контекста и флаг self-salt влияют на разбиение на n-граммы: длина n-граммы равна context_width + 1 - self_salt. Флаг self-salt определяет, берётся ли весь n-грамм как префикс или только его часть без последнего токена. Тип PRF и ключ хеширования используются при сидировании — результат ограничивается по модулю 2⁶⁴−1 и подаётся в инициализацию генератора. Результат построения зелёного списка кэшируется, и кэш следует сбрасывать при изменении типа PRF, ширины контекста, флага self-salt или ключа хеширования.
Скользящее окно
Для коротких фрагментов детектор ищет окно, в котором сигнал наиболее силён. Сначала для каждого n-грамма определяется, зелёный он или красный, и строится бинарный вектор попаданий. Число зелёных попаданий в каждом окне считается через префиксные суммы: первое окно — это частичная сумма до size - 1, остальные — разность двух срезов префиксной суммы. Для каждого окна z-оценка считается по той же формуле с заменой T на размер окна. Для каждого размера окна берётся максимальная z-оценка среди всех положений, а оптимальным размером окна объявляется тот, у которого эта максимальная оценка наибольшая. Затем число зелёных токенов восстанавливается из оптимальной z-оценки и размера окна.
Если размер окна задан как "max", перебираются все размеры от 1 до длины полного контекста; иначе берётся список размеров, перечисленных через запятую.
Разные детекторы и критерий Неймана — Пирсона
Maryland-детектор возвращает вектор длины словаря, где зелёные токены равны 1, остальные 0, и сдвигает его на -token_id. P-value считается через бета-распределение. Z-версия того же детектора использует z-оценку и нормальную функцию выживания.
OpenAI-детектор в скоринге генерирует случайные числа, берёт логарифм и сдвигает результат; p-value считается через гамма-распределение. Z-версия использует z-оценку с параметрами mu0=1 и sigma0=pi/sqrt(6).
критерий Неймана — Пирсонастатистический критерий, выбирающий порог так, чтобы при заданной вероятности ложной тревоги максимизировать вероятность обнаружения реализован в отдельном детекторе: он принимает на вход вероятность токена и умножает логарифмический скор на (1/prob - 1). Его p-value реализует границу Чернова: если суммарный скор больше ожидаемого, p-value сразу равно 1.0; иначе решается уравнение относительно параметра c* через минимизацию квадратичной невязки методом fminbound, и p-value вычисляется как экспонента от суммы логарифмов плюс произведение c*S. Разница между z-подходом и критерием Неймана — Пирсона в том, что первый предполагает нормальность распределения скора, а второй использует границу Чернова и нормальности не требует.
Границы Чернова
В методе с пометкой «Chernoff bound» вероятности преобразуются в массив, вычисляется их количество k и отношение ratio = ps / (1 - ps + eps). Затем считается величина E = (1/(ratio+eps)).sum(), и если входной скор S больше E, p-value сразу устанавливается в 1.0. Иначе решается уравнение (1/(c* + ps/(1-ps))).sum() = S относительно c*: задаётся функция квадратичной невязки, верхняя граница поиска равна k/S - min(ratio), и минимизация выполняется через fminbound(func, 0, c1). Найденное c подставляется в верхнюю границу p-value, после чего возвращается максимум из p-value и eps.
Гиперпараметры и пороги
Рекомендации по умолчанию
Для базовой генерации рекомендуются gamma=0.25 и delta=2.0, для ширины контекста — умеренное значение h=4. В качестве PRF по умолчанию рекомендуется selfhash, но можно использовать minhash. Эти значения выбираются через seeding_scheme="selfhash" — сокращение для полной записи с указанием типа PRF, ширины контекста и ключа.
Баланс настраивается так: если качество текста страдает, уменьшают delta; если нужна большая устойчивость к правкам, уменьшают h. Выбор PRF имеет значение только при h>1. При детекции всегда нужно запускать с флагом игнорирования повторяющихся n-грамм.
Self-hashing: что даёт и чего стоит
Self-hashing расширяет контекст водяного знака на текущий токен, фактически бесплатно добавляя один токен к контексту. При генерации это реализовано через rejection sampling: кандидат добавляется к префиксу, зелёный список пересчитывается, и кандидат принимается только если он сам входит в построенный с его участием список.
Цена — скорость. Приходится хешировать все возможные следующие токены и применять смещение только к тем, чьё включение в контекст даёт хеш с этим токеном в зелёном списке. Реализация медленная из-за использования CUDA-генератора псевдослучайных чисел и простого внутреннего цикла. Метод не батчится и предполагает жадную генерацию; для других типов сэмплинга он работает менее эффективно. При детекции self-salt уменьшает длину n-грамм на единицу и добавляется к числу оценённых токенов в проверке соответствия.
Пограничные случаи и устойчивость
Правки, перефразирование, перевод
Сигнал ослабевает, но не обязательно исчезает. В тестах OpenAI замена 10% слов в 400-токенном фрагменте синонимами снижала детекцию с примерно 92% до 66%, а замена 25% — до 17%. При этом после сильного человеческого перефразирования знак всё ещё обнаруживается в среднем после наблюдения 800 токенов при частоте ложных срабатываний 1e-5. Причина в том, что перефразирование статистически склонно сохранять n-граммы или более длинные фрагменты оригинала, что даёт уверенную детекцию при достаточном числе токенов.
Ограничения: короткие фрагменты могут содержать слишком мало материала для надёжного обнаружения, а сильные правки могут сделать знак необнаружимым. Для уверенного обнаружения нужно наблюдать достаточно длинный фрагмент.
Почему код сложнее маркировать
Знак создаётся через выбор модели между подходящими словами, поэтому при меньшем числе вариантов обнаружение затрудняется. В коде на каждый следующий токен приходится меньше правдоподобных альтернатив, чем в обычной прозе, поэтому у системы меньше возможностей встроить сигнал, не меняя поведение программы. Anthropic столкнулась с тем же: код даёт меньше возможностей встроить сигнал, не меняя поведение программы, хотя естественно-языковой текст внутри кода, например комментарии, маркировать легче.
OpenAI тестировала Astra с водяными знаками и без на бенчмарках DeepSWE, AutomationBench и Terminal-Bench и не нашла значимой разницы в производительности. Но это не говорит о том, насколько надёжно такой код затем распознаётся как помеченный.
Обход водяных знаков
Приёмы из практики
Описаны три приёма: правило в промпте, второй проход через модель и детерминированная замена тире регуляркой. Правило в промпте — это константа, которая дописывается в конец каждого промпта и содержит чек-лист с примерами «было/стало»; главное правило вынесено первым заголовком в контекст автора. Второй проход — отдельный вызов модели с температурой 0.2, который чистит текст, не переписывая и не сокращая его, сохраняя структуру и разметку. Регулярка заменяет все длинные тире (em, en, horizontal bar, minus, figure dash) на обычный дефис, а также заменяет двойной дефис с пробелами на одиночный.
Функция обработки сначала детерминированно вызывает удаление маркеров, затем, если включён флаг второго прохода, делает вызов модели с соответствующим промптом, и если после этого остаются антитезы, делает одну прицельную повторную попытку с дополнительным указанием переписать их напрямую. Механизм детекции антитез проверяет наличие конструкций «не X, а Y» и «это не X, это Y».
Что именно удаляется
Удаление маркеров заменяет двойной дефис с пробелами на одиночный, а затем все символы из набора тире — на обычный дефис. Антитезы проверяются отдельно: ищутся шаблоны «это не X, это Y», «не X, а Y», «дело не в X, а в Y» и «X, а не Y». Если после первого прохода модели антитеза осталась, делается ровно одна повторная попытка с инструкцией переписать антитезы напрямую, без противопоставления. Длинные тире и антитезы названы двумя главными маркерами ИИ, поэтому их удаление и переписывание может делать текст менее характерным для ИИ.
Ограничения регулярок
Набор тире состоит из пяти символов, и каждый заменяется на дефис; из набора намеренно исключён символ ━, потому что на нём держатся разделители секций и нарезка длинных сообщений. Регулярки для антитез только ищут их и ничего не удаляют: ограничение на длину удерживает совпадение внутри одного предложения и не даёт склеить «не» из начала абзаца с «а» через три строки. Ложные срабатывания всё равно бывают, например на «это не его задача, это задача монтажёра», поэтому детектор лишь запускает одну прицельную попытку переписать текст, а вслепую вырезанная фраза сломала бы смысл.
С антитезами регуляркой так не получится: «не X, а Y» нельзя механически превратить во что-то осмысленное, предложение нужно переписать. Тире продолжали проскакивать, потому что языковая модель работает с токенами и иногда меняла тире на запятую или ставила новое в переписанном предложении.
Компромиссы и практические выводы
Качество против силы маркировки
Компромисс задаётся величиной delta. Достаточно большое значение даёт «жёсткий» водяной знак, кодирующий 1 бит информации на каждом токене, но это не рекомендуемая настройка, так как она сильно влияет на качество модели. Умеренный диапазон [0.5, 2.0] подходит для обычного использования.
Скорость против устойчивости
Скорость генерации зависит от режима self-salt: при его включении перебираются кандидаты с досрочной остановкой по правилам fixed_score, fixed_list_length или fixed_compute. Надёжность детекции регулируется порогом z-оценки — изменение порога меняет чувствительность детектора. Нормализаторы (unicode, homoglyphs, truecase) могут смягчать модификации сгенерированного текста, которые иначе сбили бы детекцию. Опция игнорирования повторяющихся n-грамм меняет правила так, что каждый уникальный n-грамм считается только один раз. Для устойчивости к правкам рекомендуется меньшая ширина контекста, а при детекции — всегда включать игнорирование повторяющихся n-грамм. Итоговое решение детектора — сравнение z-оценки с порогом.
Что из этого следует на практике
Водяной знак — это статистический сигнал, а не метка в метаданных, поэтому его нельзя «стереть» одной операцией. Он живёт в распределении выбранных токенов, и любое вмешательство в текст — замена слов, перефразирование, перевод — размывает этот сигнал пропорционально глубине правок.
Из механики следуют конкретные практические выводы:
- Длина важнее всего. Для уверенного обнаружения нужно наблюдать достаточно длинный фрагмент. Короткие тексты могут содержать слишком мало материала для надёжной детекции, и это ограничение не обходится настройками.
- Код и формализованный текст маркируются хуже. Меньше правдоподобных вариантов следующего токена — меньше возможностей встроить сигнал. Комментарии и естественно-языковые вставки маркируются легче.
- Сила знака покупается качеством. Большой
deltaдаёт жёсткий знак, но сильно влияет на качество модели; умеренный диапазон — компромисс. - Self-salting повышает устойчивость ценой скорости. Перебор кандидатов и хеширование всех возможных следующих токенов замедляют генерацию, а метод предполагает жадную генерацию и плохо батчится.
- Простые косметические правки не гарантируют обхода. Замена тире и переписывание антитез меняют поверхностные признаки, но не трогают распределение токенов, в котором живёт знак. Регулярки к тому же ограничены: они ищут, но не переписывают, а ложные срабатывания не позволяют удалять найденное вслепую.
- Порог детекции — регулятор чувствительности. Меняя его, можно смещать баланс между ложными срабатываниями и пропусками, но не отменить фундаментальную зависимость от длины текста и глубины правок.
Где смотреть в коде
- detector.py: score_tok
- watermark_processor.py: __init__
- watermark_processor.py: WatermarkLogitsProcessor
- extended_watermark_processor.py: WatermarkLogitsProcessor
- generator.py: sample_next
- watermark_processor.py: _score_rejection_sampling
- watermark_processor.py: _score_sequence_window
- extended_watermark_processor.py: __call__