Перейти к содержимому

Повторы и зацикливание в генерации

Вырожденная генерация — модель повторяет одну строку десятки раз или застревает в цикле «… и так далее, и так далее …» — классический симптом кривой конфигурации (temperature=0 + repetition_penalty не выставлен), битого fine-tune или деградации самой модели. Ответ технически «успешен», но бесполезен, и без детектора это заметит только пользователь.

Считаем долю повторяющихся n-грамм в тексте. Высокая доля = вырождение.

import os, requests
def repetition_ratio(text: str, n: int = 3) -> float:
"""Доля не-уникальных n-грамм. 0.0 = всё уникально, ~1.0 = вырождение."""
tokens = text.split()
if len(tokens) < n * 2:
return 0.0
grams = [tuple(tokens[i:i + n]) for i in range(len(tokens) - n + 1)]
unique = len(set(grams))
return 1.0 - unique / len(grams)
def longest_repeat_run(text: str) -> int:
"""Максимальное число подряд идущих одинаковых строк."""
lines = [l.strip() for l in text.splitlines() if l.strip()]
best = cur = 1
for a, b in zip(lines, lines[1:]):
cur = cur + 1 if a == b else 1
best = max(best, cur)
return best
def check_output(answer: str):
ratio = repetition_ratio(answer)
run = longest_repeat_run(answer)
if ratio > 0.5 or run >= 4:
notify("🔁 Зацикленная генерация",
f"Повтор n-грамм: {int(ratio*100)}%, "
f"подряд одинаковых строк: {run}.\n"
f"Фрагмент: {answer[:200]}\n\n"
"Проверьте temperature / repetition_penalty и версию модели.",
priority=7)
return True
return False
def notify(title, message, priority):
requests.post(f"{os.environ['NOTIFLY_URL']}/message",
params={"token": os.environ["NOTIFLY_TOKEN"]},
json={"title": title, "message": message, "priority": priority},
timeout=5)

Порог 0.5 подобран под естественный язык; для кода поднимите его — код законно повторяется (импорты, скобки).

Одно вырождение — шум. Считаем долю «плохих» ответов в окне, как в всплеске галлюцинаций:

import redis
R = redis.from_url(os.environ["REDIS_URL"])
def observe(answer: str):
bad = 1 if (repetition_ratio(answer) > 0.5 or longest_repeat_run(answer) >= 4) else 0
R.lpush("llm-degenerate", bad)
R.ltrim("llm-degenerate", 0, 199)
win = [int(x) for x in R.lrange("llm-degenerate", 0, 199)]
if len(win) >= 50 and sum(win) / len(win) > 0.15:
if R.set("alert:degenerate", "1", nx=True, ex=600):
notify("🔁 Всплеск зацикливаний",
f"Вырожденных ответов: {int(sum(win)/len(win)*100)}% за {len(win)}",
priority=8)
  • метрики: доля повторных n-грамм и длина серии;
  • фрагмент вырожденного вывода (первые 200 символов);
  • параметры генерации (temperature, top_p, repetition_penalty);
  • модель и версия.