
Длинное тире не выдаёт нейросеть. Я проверил на корпусе
Почти всё, что считается признаком машинного текста, я померил на 7500 текстах. Половина признаков не работает, часть работает наоборот, а мой собственный детектор поставил текстам агента баллы выше, чем моим.
Все знают, что длинное тире выдаёт нейросеть
Это первое, что говорят про машинный текст. Em dash, длинное тире, — вместо запятой. Оно стоит первым пунктом в каждом списке признаков, его вычищают все гуманизаторы, у меня оно тоже было жёстким запретом номер один.
Я взял корпус LLMTrace - 2708 человеческих и 1455 машинных текстов на русском, генераторы gpt-4, o3, gigachat, Qwen2.5 - и посчитал частоту.
длинное тире, русский:
человек 794.7 на 100 тысяч слов
машина 749.6У человека чаще. Отношение 0.94, то есть признак работает против нас: текст с тире чуть вероятнее написан живым человеком.
А в английском то же самое тире даёт 1.73, и там сигнал есть. Один символ, два языка, противоположный вывод.
Что ещё оказалось наоборот
Дальше стало интереснее. Я прогнал по корпусу весь свой набор правил, и вот те, у которых отношение ниже единицы:
| Правило | отношение | |
|---|---|---|
| in order to | 0.10 | у человека в десять раз чаще |
| feel free to | 0.12 | |
| we will | 0.21 | |
| «As an AI» и прочие артефакты чат-бота | 0.30 | модели давно так не пишут, это люди их цитируют |
| английский канцелярит целиком | 0.31 | оказался человеческим |
| латиница внутри кириллического слова | 0.28 | |
| tutorial voice, «as you can see» | 0.79 |
Про латиницу отдельно. Считается, что подмена букв - это попытка обмануть детектор: кириллическая «о» меняется на латинскую. У человека такое встречается втрое чаще, чем у машины, потому что это обычная опечатка при переключении раскладки. Люди промахиваются, модели нет.
Треть моего английского набора работала в обратную сторону. Я оставил эти правила, но с проставленным числом: веса они больше не получают и признаком генерации не считаются. Как совет по стилю остаются - «in order to» всё равно стоит сокращать до «to», просто это не след машины.
Что работает
Не всё оказалось мусором. Есть признаки с огромным разрывом.
Эмодзи. В 2708 человеческих текстах - ноль. У машины 19 на сто тысяч слов. Единственный признак, который разделяет идеально.
Отдельные слова. Тут мне повезло: у Wikipedia есть страница «Signs of AI writing», которую правят люди, и на ней держится половина открытых гуманизаторов. Взял оттуда слова, которых у меня не было, померил:
| Слово | отношение |
|---|---|
| fostering | 75.7 |
| pivotal | 50.5 |
| underscore | у человека ноль |
| meticulous | 47.1 |
| intricate | 27.8 |
| showcase | 26.1 |
| vibrant | 18.1 |
До этого мой лучший результат был seamlessly 21.0. Одна страница Wikipedia оказалась сильнее всего, что я собрал руками.
Номинальность. Плотность отглагольных существительных: «осуществление», «внедрение», «обеспечение». Об этом ниже, потому что с ней вышла отдельная история.
Мой детектор поставил текстам агента высшие баллы
Инструмент заработал, и я прогнал его по своему блогу. Одиннадцать постов, три последних написаны агентом целиком, остальные писал я.
010_spec_driven 99/100 [чисто] агент
011_proxy_bench 88/100 [чисто] агент
012_local_llm_bench 100/100 [чисто] агент
007_brain_switch 60/100 [правка] я
001_mermaid_ru 72/100 [правка] я
004_death_or_alive 75/100 [правка] яРазделение обратное. Инструмент, написанный чтобы ловить следы нейросети, поставил нейросети высшие баллы, а автору - «требуется правка».
Мои посты полны оборотов вроде «универсалов в современном мире становится все меньше», «Давайте посмотрим как это работает», «убивает не только твою продуктивность но и тебя». Это не следы машины. Это штампы, и они мои.
А агент писал под моим CLAUDE.md, где запрещены длинное тире, «не просто X, а Y», канцелярит и многословие.
Разрыв создаёт не нейросеть, а инструкции
Механизм объясняет работа в PNAS про 66 признаков Байбера. Параллельные корпуса в 66 тысяч фрагментов, шесть моделей, попарно человек против модели - точность 93-98 процентов. Но интересна не точность, а один столбец:
| Признак | GPT-4o | Llama3-70B-Instruct | Llama3-70B base |
|---|---|---|---|
| Причастные обороты | 527% | 261% | 102% |
| Средняя длина слова | 116% | 103% | 100% |
| Номинализации | 214% | 151% | 91% |
Число - частота у модели к частоте у человека. У базовой Llama всё около ста процентов. База пишет как человек.
Разрыв создаёт instruction tuning, а не нейросеть как таковая. Отсюда два следствия сразу.
Первое: гуманизаторы работают. Не потому что обманывают детектор хитростью, а потому что отменяют механизм, который детектор ловит. Мой агент не обходил проверку - он писал по инструкции, которая давит ровно те признаки.
Второе: детектор на словах и оборотах обречён. Любая инструкция вида «пиши короче и глаголами» стирает сигнал целиком, и это делается одной строчкой в промпте.
Это регистр, а не авторство
Оставался вопрос: может, инструмент вообще ничего не меряет. Взял вторую выборку - 45 SEO-статей про управление проектами, написанных агентом целиком.
| агент, 45 статей | мой блог, 11 постов | |
|---|---|---|
| медиана балла | 84 | 98 |
| маркеров на 100 слов | 0.71 | 0.28 |
| номинальность | 5.30 | 2.13 |
Разделение появилось, и приличное: в два с половиной раза. Но оно не про авторство.
Доказательство лежит внутри тех же данных. Три агентских поста в моём блоге дали номинальность 1.0-2.4, то есть ниже моей. Один и тот же автор-агент, два регистра, противоположный результат.
Корпоративный текст про планирование и внедрение набит отглагольными существительными независимо от того, кто его писал. Инструмент меряет канцелярит - честно меряет, цифры сходятся. Вопрос «кто писал» он не решает и решать не будет.
Так что если вам нужен детектор ИИ, регулярками его не сделать. А вот измеритель канцелярита - вполне, и он полезнее: канцелярит стоит убирать независимо от того, кто его написал.
Полмиллиона комментариев
Вторая половина инструмента - комментарии в коде. Те же правила плюс проверки, которых регуляркой не выразить: пересказ кода, закомментированный код, баннеры из равно, TODO без владельца.
Первый прогон на нашей платформе выдал 502 тысячи комментариев на девять тысяч файлов. Число абсурдное: полсотни комментариев на файл не бывает.
Дальше по убыванию. 62 тысячи блоков пришли из собранного JS в lib/ и bundle/ - я перестал ходить по дереву сам и стал спрашивать список у git ls-files, который соблюдает .gitignore точно. Ещё 4474 срабатывания пришлись на лицензионные шапки, которые никто никогда не редактирует.
Осталось 24 тысячи комментариев и 12 секунд вместо 82.
Отдельно вышло с закомментированным кодом. Я ловил его регуляркой, потом переписал по образцу go-critic: тело комментария подставляется в функцию и отдаётся go/parser. Разобралось - значит код. Счётчик прыгнул с 52 до 168, и это выглядело как успех, пока я не открыл выборку:
pg := newFakePostgres() // очередь пуста, воркер должен просто ждать
const maxMsgSize = 1024 * 1024 * 50 // 50 MBБаг был не в детекторе. Блоки резались по строкам целиком, поэтому у хвостового комментария в текст попадал код перед //. Парсер честно видел код и честно срабатывал. Я переделал сбор на байтовые смещения, и стало 13 срабатываний вместо 168 - почти все настоящие.
А для TypeScript парсера в stdlib нет
С Go повезло: go/parser лежит в стандартной библиотеке. Для TypeScript и Svelte оставалась регулярка, и она пропускала очевидное - // export let attributeKey: string | undefined за код не считала.
Напрашивается typescript-go, нативный порт компилятора. Его парсер живёт под internal/, так что подключить его можно только вендорингом: положить исходник внутрь своего дерева и ходить в него через адаптер. Это работает, но стоит полутора мегабайт чужого кода и сопровождения форка архивированного репозитория - при том что весь мой инструмент занимает сто килобайт.
Взял esbuild: публичный API, MIT, одна строка go get. api.Transform разбирает тело комментария, ошибок нет - значит код.
закомментированный код в TS и Svelte:
регулярка 76
парсер 187Из ста одиннадцати новых я проверил двадцать два самых похожих на прозу. Ложных - ноль. Бинарь вырос с 3.8 до 8.9 мегабайта, скорость не изменилась.
Агент пролез в дыру моего правила
Когда всё заработало, я натравил агента на код самого инструмента: вот список замечаний, вот правила правки, чини. Он починил восемь комментариев из двенадцати. Два получились такими:
// commentedOutCode: Go разбираем настоящим парсером, как go-critic; для остальных языков - эвристика по символам, раз в stdlib нет парсера.Сто сорок символов в одну строку. Правило требовало не больше двух строк - правило выполнено. Читать стало хуже, чем было.
Он не жульничал. Он делал ровно то, что написано: считались строки, а не текст, и склейка трёх строк в одну формально решала задачу. Дыра была моя.
Добавил предел длины строки. Новая проверка немедленно поймала оба комментария, которые агент только что и написал.
Заодно он предъявил две претензии к правилам, и обе оказались верными. Правило про TODO срабатывало на строку, которая цитирует "// TODO: fix" как пример внутри самого чекера. А блок с обязательной пометкой о потолке эвристики физически не влезал в две строки, потому что сама пометка занимала две.
Что я забрал себе
Правило без замера - это вкусовщина. Длинное тире казалось мне очевидным признаком. Отношение 0.94. Латиница внутри слова казалась подменой гомоглифов. Отношение 0.28, это опечатки живых людей. Я был уверен в обоих.
Чужие цифры проверяйте на своих данных. Я перенёс корпусные веса из чужого скилла и считал, что этого достаточно. Свой замер поменял половину из них, а четыре правила отправил в минус.
Абсурдное число в отчёте - это ваш баг, а не свойство данных. Полмиллиона комментариев, 168 кусков закомментированного кода. Каждое разваливалось, стоило открыть выборку и прочитать десяток строк глазами. Соблазн не в том, чтобы поверить, а в том, чтобы объявить это свойством большого репозитория и накрутить порог.
Счётчик, выросший после починки, стоит перепроверить отдельно. Замена регулярки на настоящий парсер утроила срабатывания. Выглядело как успех, а на деле парсер подсветил баг в моём же сборщике.
Метрика, выполнимая буквально, будет выполнена буквально. Лимит в две строки агент выполнил, склеив текст в одну строку на сто сорок символов. Любое правило, которое считает форму, а не содержание, кто-нибудь однажды удовлетворит формально - и это будет не злой умысел, а прямое следование инструкции.
Инструмент делает то, что меряет, а не то, что написано на коробке. Я писал детектор нейротекста, получил измеритель канцелярита. Штука полезная, но называть её надо своим именем, иначе первый же честный тест выставит вас дураком.
Инструмент лежит в haiodo/hum1izer. Правила в YAML, дополняются, свой набор подсовывается флагом. Балл там - правила инструмента, а не вероятность ИИ и не вердикт об авторстве. Нужен он для одного: сравнить «было» и «стало» на одном и том же тексте.
Этот пост тоже через него прогнан. 54 из 100, полоса «рерайт».
Четыре жёстких запрета, и все четыре - в цитатах: я цитирую свои же грехи из старых постов и названия правил. Отличать цитату от собственной речи инструмент не умеет.
Настоящих замечаний было два, оба мои, оба исправлены. Переписать примеры так, чтобы счётчик обнулился, можно за десять минут. Только тогда пост потеряет ровно то, ради чего написан.