
Площадки перестали пускать машинный текст самотёком. 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 текстов одной редакции на одну тему. Пороги подобраны на нём и на другом корпусе поедут.
Правила ловят русскую стилистику. На английском половина категорий бессмысленна.
Обойти детектор, зная правила, тривиально. Это инструмент вычитки, а не защита от злого умысла.
Что я забрал из этого

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

