Мой блог
LLM зацикливается в продакшене: как локализовать петлю повтора без слепой смены параметров
Диагностика зацикливания LLM в продакшене без слепой смены параметров
Представьте ситуацию: сервис на базе языковой модели три недели работает без нареканий, но неожиданно начинает возвращать пользователям отчёты, где с середины текста раз за разом дублируется один и тот же абзац, после чего генерация обрывается. В логах бэкенда нет ни единой ошибки, однако метрика p99 уходит в небеса, GPU полностью загружен фазой декодирования (decode), а затронутые запросы генерируют в разы больше выходных токенов, чем обычно.
В инженерной практике и специализированной литературе подобное поведение называют деградацией текста (text degeneration) или петлёй повтора (repetition loop). Когда происходит такой инцидент, у команды инженеров возникает естественный соблазн сразу изменить параметры сэмплинга — например, повысить штраф за повторы. Однако такой подход часто не только не решает проблему, но и ухудшает работу системы. Давайте подробно разберём, как локализовать причину деградации с позиции эксплуатации продакшен-систем.



Разбор видов зацикливания: почему важно отличать типы повторов
Термин «зацикливание» объединяет три совершенно разных сбоя, каждый из которых требует своего метода устранения:
- Токеновый повтор: модель «залипает» на единственном токене или слове и начинает бесконечно воспроизводить его. Этот сбой обнаруживается и задерживается простыми проверками.
- Повтор фрагментов (текстовый): модель начинает циклично генерировать целые фразы, списки или абзацы. Именно этот тип аномалии приводит к характерному бимодальному распределению длины ответов и перерасходу GPU-ресурсов.
- Семантическая петля: генератор ходит по кругу смыслов, но переформулирует мысли разными словами. Детекторы n-грамм здесь бессильны. Подобное поведение наиболее характерно для рассуждающих (reasoning) моделей.
В этом материале я сосредоточусь на диагностике и нейтрализации второго типа — циклических повторов текстовых фрагментов.
Исходные условия: что видит дежурный инженер
Для наглядности рассмотрим типичный сервис генерации отчётов. Бэкенд написан на Kotlin, взаимодействие наружу идет по синхронному REST, а внутри организован стриминг. Модель открытого класса 30B развёрнута локально с помощью движка vLLM и доступна через OpenAI-совместимый API. Нагрузка составляет около 8 RPS в пике, а целевая длина нормального ответа лежит в диапазоне 600–900 токенов.

Запросы отправляются со следующими базовыми настройками сэмплинга:
SamplingParams(
temperature=0.7,
top_p=0.9,
repetition_penalty=1.0,
presence_penalty=0.0,
frequency_penalty=0.0,
max_tokens=4096,
stop=None,
)
При анализе 41 200 запросов за сутки инженер видит аномальные сигналы:
- finish_reason: 6,6% запросов завершились по причиненному лимиту
length. - Длина аномальных ответов: ровно 4096 токенов без исключений. Это свидетельствует об упоре в жесткий потолок, а не о естественном завершении мысли.
- Распределение длин: четко бимодальное с двумя пиками — около 740 токенов для здоровых ответов и ровно 4096 токенов для сбойных. Пространство между ними практически пустое.
- Хвост ответа: многократно повторяющийся один и тот же абзац.
- Задержка (latency): p50 находится на уровне 4,1 секунды, тогда как p99 вырастает до 38 секунд.
Системный промпт при этом содержит 112 строк с нумерованными списками, вложенными маркерами и таблицей вариантов ответов. Меняться модель или провайдер инференса не могут — проблему необходимо решить на стороне текущей инфраструктуры.

Анализ типовых гипотез и ошибочных решений
Когда на разборе инцидента звучат варианты действий, большинство из них оказываются ошибочными:
- Гипотеза A: Поднять repetition_penalty до 1.2 и presence_penalty до 1.5. В движке vLLM штрафы
presence_penaltyиfrequency_penaltyприменяются к сгенерированным токенам, аrepetition_penaltyнаказывает модель в том числе за использование слов из входящего промпта. Кроме того, в структурированных текстах (JSON, таблицы) легитимные повторы ключей и заголовков попадают под штраф, ухудшая итоговое качество. - Гипотеза B: Снизить температуру до 0.2. Низкая температура делает выбор детерминированным и прижимает генерацию к наиболе вероятному продолжению. Однако для многих современных моделей (например, Qwen3 в режиме мышления) сжатие распределения или greedy decoding только усиливает устойчивость петли.
- Гипотеза C: Увеличить max_tokens до 8192. Так как сбойные запросы упираются ровно в потолок 4096 токенов, удвоение лимита не поможет модели «дописать мысль», а лишь вдвое увеличит затраты времени GPU на зацикленные запросы.
- Гипотеза D: Проверить инфраструктуру. Версия vLLM, параметры квантизации KV-кэша и шаблоны чата действительно могут провоцировать деградацию вывода. Если инцидент совпал с обновлением стека, эту гипотезу проверяют через анализ полного diff конфигурации.
- Гипотеза E: Зафиксировать артефакты, воспроизвести поведение и снять замер logprobs. Единственно верный первый шаг. Прежде чем меняться параметры, необходимо собрать профиль инцидента.
Механика процесса: как возникает устойчивая петля генерации
Трансформерная модель не имеет внутреннего правила «чем чаще встречается токен, тем выше его вероятность». Вероятность каждого последующего токена вычисляется авторегрессионно на основе текущего состояния и всего накопленного контекста.
Если в процессе генерации контекст попадает в область распределения, где продолжение повторяющегося фрагмента становится математически наиболее вероятным, формируется самоподдерживающийся режим. Каждый новый сгенерированный повтор добавляется в контекст, усиливая вероятность следующего такого же повтора.
В результате параметр max_tokens из защитного предохранителя превращается в единственный реальный фактор остановки генерации. Сбойный запрос производит в разы больше токенов на фазе декодирования, надолго занимая вычислительные мощности и блокируя ресурсы KV-кэша для других пользователей.
Пошаговый алгоритм проверки гипотез
Чтобы найти истинную причину деградации, я рекомендую соблюдать строгую последовательность действий:
- Шаг 1. Фиксация окружения и отпечатка инцидента. Сохраните ревизию модели, токенизатора, шаблон чата, стоп-токены, точные параметры сэмплинга, конфигурацию KV-кэша и версию vLLM. Ошибки в шаблоне чата часто приводят к тому, что модель пропускает токен завершения (EOS).
- Шаг 2. Воспроизведение аномального поведения. Проведите несколько прогонов сохраненного запроса с фиксированным seed. Обратите внимание, что онлайн-инференс в vLLM не дает стопроцентной побитовой воспроизводимости без режима
batch invariance, поэтому целью является фиксация устойчиво повторяющегося характера ошибки. - Шаг 3. Снятие logprobs на границе начала повтора. Сохраните распределение вероятностей top-N токенов непосредственно перед точкой входа в петлю:
from vllm import LLM, SamplingParams
llm = LLM(model="")
params = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=4096,
logprobs=5,
seed=42,
)
outputs = llm.generate([problem_prompt], params)[0].outputs[0]
for idx, step in enumerate(outputs.logprobs):
top_tokens = sorted(step.items(), key=lambda item: item[1].logprob, reverse=True)
print(idx, [(token.decoded_token, round(token.logprob, 3)) for _, token in top_tokens])
О деградации свидетельствует резкая концентрация вероятностей на лидирующем токене повторяющегося фрагмента при одновременном падении вероятности токена EOS.

- Шаг 4. Локализация источника методом бинарного поиска. Поочередно сокращайте блоки системного промпта наполовину и измеряйте долю аномальных генераций. Если промпт не влияет на появление петли, переходите к сравнению BF16 и квантизованных сборок модели или настроек инференс-движка.
- Шаг 5. Корректировка параметров и внедрение защиты. Корректируйте сэмплинг только после анализа промпта и структуры ответа.
Сторонний практический кейс: почему параметры не помогли
В одном из опубликованных практических кейсов команда столкнулась с 15% деградацией генерации при использовании Qwen3 под управлением vLLM. Первоначально инженеры попытались устранить проблему повышением presence_penalty до 1.5, но это не дало результата, а дальнейшее увеличение штрафов привело к росту доли деградаций до 15% обратно.
Причиной зацикливания оказался сам системный промпт, перенасыщенный нумерованными списками, вложенными буллетами и таблицами. После переработки инструкции: сокращения её объёма почти вдвое (на 46%) и перевода списков в связный текстовый формат — доля деградаций упала до нуля.
Это говорит о том, что избыточно четкие структурные шаблоны в промпте способны смещать вероятностное распределение модели в сторону бесконечных повторов аналогичных структур.
Практические рекомендации для внедрения в продакшен
Для предотвращения и ликвидации зацикливаний в боевых системах следует использовать следующие механизмы:
1. Встроенная детекция на уровне vLLM (начиная с версии 0.17.0)
Движок поддерживает досрочное завершение генерации при обнаружении повторяющихся N-грамм в выходе. Запрос получает статус FINISHED_REPETITION:
from vllm import SamplingParams
from vllm.sampling_params import RepetitionDetectionParams
params = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=4096,
repetition_detection=RepetitionDetectionParams(
min_pattern_size=3,
max_pattern_size=20,
min_count=4,
),
)
Обратите внимание: пороги нужно калибровать под ваш домен, иначе в коде, Markdown-таблицах или JSON-ответах валидные структуры будут ложно считываться как петля.
2. Легковесный детектор на уровне приложения/стриминга

Если требуется кастомная логика остановки, можно обрабатывать скользящее окно токенов в процессе стриминга:
from collections import Counter
def check_repetition_degeneration(tokens, window_size=256, ngram_size=16, threshold=0.65):
tail = tokens[-window_size:]
if len(tail) < ngram_size * 3:
return False
ngrams = [tuple(tail[i:i + ngram_size]) for i in range(len(tail) - ngram_size + 1)]
counts = Counter(ngrams)
repeated_count = sum(count - 1 for count in counts.values() if count > 1)
return (repeated_count / len(ngrams)) >= threshold
3. Единая система мониторинга деградации
После внедрения автообрезок метрика finish_reason = length перестанет отражать реальную картину. Формируйте целевую метрику degeneration_rate объединенно по всем факторам (упор в max_tokens с повтором, срабатывание repetition_detected и обрыв детектором приложения). Отдельно отслеживайте показатель wasted_output_tokens — объем бесполезно сгенерированных токенов, сожравших GPU-время.
4. Защита от шторма повторных запросов (retry storm)
Автоматический ретрай сбойного запроса без изменений параметров или промпта приведет к тому, что система начнет зацикливаться снова и снова. Используйте экспоненциальный backoff с джиттером, ограничения по дедлайну и паттерн Circuit Breaker.
Ограничения подхода и краевые случаи
Предложенные методы имеют свои границы применимости:
- N-граммные детекторы бессильны против семантических петель, когда модель ходит по кругу смыслов без точного повторения токенов.
- Для кодогенерации и структурированного вывода пороги срабатывания детектора должны быть ощутимо ослаблены.
- Если модель начинает зацикливаться даже на пустом промпте, проблема кроется в повреждении весов, неверной квантизации или сбое слоя инференса.
Главный вывод для инженера
Системный промпт — такой же полноценный инженерный артефакт, как и программный код. Он должен храниться в системе контроля версий, проходить код-ревью и тестироваться на долю деградации вывода. При возникновении аварий крайне важно сопротивляться импульсивному кручению параметров и начинать с фиксации окружения, замера распределения probabilities и системной проверки гипотез.
Источник: habr.com
