Площадки перестали пускать машинный текст самотёком. Habr проверяет публикации автоматически, а Кевин Индиг пишет о том, что платформы строят anti‑slop‑системы — механизмы подавления массового низкокачественного ИИ‑контента. Дальше обычно следует совет писать хорошо, и на этом разговор заканчивается.

Мне нужно было проверяемое условие вместо совета, поэтому я собрал детектор на правилах и прогнал через него 59 собственных статей. Главный результат оказался не в том, сколько текстов он забраковал, а в том, что при агрегатном подсчёте он не забраковал ни одного, хотя дефекты были в сорока.

Что вообще ловится правилами

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

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

Категория с фиксированным словарём — самая простая:

def score_v4(text):
    """Повторяющиеся связки."""
    phrases = ["также", "кроме того", "помимо этого", "при этом",
               "в свою очередь", "более того", "вместе с тем"]
    tl = text.lower()
    found = {p: tl.count(p) for p in phrases if tl.count(p) > 0}
    max_count = max(found.values(), default=0)
    return 10 if max_count >= 5 else (3 if max_count >= 3 else 0)

Категория со статистикой по тексту — про ритм:

def score_v2(text):
    """Монотонность ритма: разброс длин предложений."""
    sentences = split_sentences(text)
    if len(sentences) < 3:
        return 0
    sd = stdev(len(s) for s in sentences)
    return 15 if sd < 3 else (7 if sd < 5 else 0)

Порог 3 подобран не из теории. Я посчитал стандартное отклонение длин предложений на своих текстах и на нескольких, написанных моделью без правки: у человеческих оно почти всегда выше пяти, у машинных сползает к двум‑трём. Это разделяющий признак на моём корпусе, а не универсальная константа.

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

LITERAL_CTX = re.compile(
    r'(запрос|ключ|фраз|объявлен|поиск|вбива|ищут|семантик|выдач|'
    r'сниппет|подсказк|промпт)', re.IGNORECASE)

def in_literal_context(paragraph: str) -> bool:
    """В абзаце сказано, что внутри кавычек — запрос или объявление?"""
    return bool(LITERAL_CTX.search(paragraph))

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

Прогон по 59 статьям

Дальше я прогнал детектор по всем своим финальным текстам — 59 файлов, разные форматы и площадки, писались на протяжении нескольких месяцев.

import glob, json, subprocess, statistics

rows = []
for path in sorted(glob.glob("articles/*/final.md")):
    out = subprocess.run(
        ["python3", "detector.py", path, "--json"],
        capture_output=True, text=True,
    )
    rows.append(json.loads(out.stdout))

scores = [r["auto_score"] for r in rows]
print(len(rows), statistics.median(scores), max(scores))
print(sum(1 for r in rows if r["hard_flags"]))

Результат: медиана суммарного балла — 3, среднее — 3,5, максимум по всему корпусу — 19. Порог, при котором текст считается требующим доработки, у меня стоял на 40. То есть по агрегатной сумме не забраковалась ни одна статья из 59.

При этом хотя бы одна категория была выбита у 40 текстов из 59.

Почему сумма врёт

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

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

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

hard_flags = []
if v9_score >= V9_MAX:
    hard_flags.append(f"В9: {v9_count} кавычки на обычных словах")
if v11_score >= V11_MAX:
    hard_flags.append("В11: слово под полным запретом")
...
if hard_flags and exit_code == 0:
    exit_code = 1        # сумма низкая, но категория в потолке

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

Что выбито в сорока текстах

Разбивка по категориям оказалась неожиданно узкой. Четыре категории дают почти все срабатывания: кавычки на обычных словах — 28 текстов, запрещённые слова и жаргонные англицизмы — 27, подзаголовки‑ярлыки без темы — 25, абзац, начинающийся с анонса вместо сути, — 13. Остальные девять категорий вместе дали четыре случая.

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

Второй срез — по времени. Тексты, написанные до 5 августа 2026 года, дают хотя бы один флаг в 100% случаев (19 файлов). Написанные после — в 53% (40 файлов).

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

Чего этот детектор не делает

  • Он не отличает машинный текст от человеческого. Он находит признаки невычитанного текста, а таких хватает и у людей.

  • Корпус мал и однороден: 59 текстов одной редакции на одну тему. Пороги подобраны на нём и на другом корпусе поедут.

  • Правила ловят русскую стилистику. На английском половина категорий бессмысленна.

  • Обойти детектор, зная правила, тривиально. Это инструмент вычитки, а не защита от злого умысла.

Что я забрал из этого

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

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

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

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