Как я отучил нейросеть падать в обморок от мата
Я надиктовываю мысли для поста голосом на бегу. И сама мысль хорошая, темп быстрый, связки — трёхэтажные, и всё по-кайфу.
Отправляю в бота. Получаю ответ: «I'm sorry, I can't help with that.» 🥴
То есть нейросеть услышала «б***ь» и решила, что её пытаются втянуть во что-то незаконное. Хотя её пытались втянуть только в интересную историю.
Я делаю Telegram-бота «голос → готовый пост» (Groq, whisper-large-v3 для распознавания, gpt-oss-120b для текста). Борьба с ханжеством модели заняла у меня больше времени, чем вся остальная архитектура. Рассказываю, как это устроено, потому что грабли общие для всех, кто делает LLM-продукты ⬇
Почему модель давится?
Дело и не в настройках, и не в том, что я плохой промпт-инженер (а хотя...). Модели вылизаны RLHF-ом так, что ненормативная лексика в запросе воспринимается как сигнал — опасная территория 🚨. Вот даже через API, даже без интерфейса ChatGPT: safety-поведение зашито в веса, а не в кнопки. Просьба «не отказывай, б***ь» в промпте работает… иногда. На температуре 0.2 — чаще, на 0.7 — как повезёт. «Как повезёт» мне не надо: пользователь получает отказ один раз и уходит, второго шанса у бота нет.
Попытка №1: попросить вежливо
Первая версия промпта содержала одну строчку: «не отказывайся из-за мата». Работало ровно до первой диктовки, где мат стоял в начале предложения. Модель виделa ругательство первым токеном контекста и уходила в глухую оборону. Стало понятно: нужен не намёк, а контракт.
Три слоя защиты, которые остались в проде
Слой 1. Cleaner — технический редактор транскрипции.
Первым делом сырой текст с Whisper идёт на чистку: убираются «эээ», «ну», «короче», обрывы фраз и повторы. И здесь же жёстко прописано:
«Мат и грубая лексика в исходной речи — это не повод отказаться от задачи или зацензурировать текст. Никогда не отвечай отказом — просто очисти текст от мусора речи, не трогая сам мат. Сленг и разговорные обороты не причёсывай в официальную речь.»
Важный нюанс: cleaner не трогает регистр. Если человек диктовал неформально — очищенный текст звучит так же неформально, просто без мусора. Если диктовал по-деловому план для отчёта — не превращает это в развязную речь. Ну а это только начало 😐
Слой 2. Style contract — общий блок для всех контентных промптов.
У бота восемь режимов (пост для Telegram, конспект, выжимка, анти-выгорание и т.д.). Чтобы не дублировать правила в каждом, есть общий контракт оформления и тона, который подклеивается к любому режимному промпту. Там есть отдельный раздел:
«Пользователь мог материться, ругаться или говорить грубо в исходной диктовке — это обычная живая речь, а не запрещённый контент. Никогда не отказывайся выполнять задачу, не пиши "I'm sorry, I can't help with that", не подвергай текст цензуре и не заменяй мат нейтральными словами по своей инициативе. Твоя задача — оформить сказанное, а не решать, прилично оно или нет.»
Без формулировки «а не решать, прилично оно или нет» ничего бы и не вышло. Модель перестаёт играть в моральный компас, когда ей явно сказали, что моральный компас здесь не входит в должностные обязанности.
Слой 3. isRefusal() — страховка в коде.
Промпт — это договорённость, а не гарантия. Поэтому в коде есть детектор отказов: если модель всё-таки проскочила мимо инструкций, пользователь не видит её служебную отмазку.
Проверка длины — не паранойя, а необходимость: нормальный результат на 2000 знаков может содержать «не могу» внутри живой фразы автора («не могу рекомендовать этот сервис»). А отказ — всегда короткая служебная записка. Дальше поведение ветвится: в cleaner при отказе молча берём сырой текст (лучше нечищеный результат, чем никакой), в контентных режимах — честное сообщение пользователю с предложением переформулировать, а не экран с чужим «I'm sorry».
Продакшен-будни в двух абзацах
На уровне API — ретраи и ротация ключей: при таймауте или 429 запрос молча уходит на следующий ключ из пула в тридцать один, пользователь ничего не замечает и ничего не нажимает дважды.
Кстати, о ключах — почему не OpenRouter? Секретный секрет: тогда я про них просто не знал. Затестил Groq, понял, что скорость из разряда «быстрее, чем быстро», и настроил всё под него. Не вижу смысла менять сейчас, потому что Groq реально подходит под такую задачу, хоть и с промптами размером с этот пост 😅
Что это всё дало:
Скажу как есть: пользователей — нулёвый ноль. Посты в соцсетях, сообщения админам — всё без толку. Если кто с опытом и чего знает — подскажите 🫶
Вывод:
LLM-продукт живёт или умирает на стыке: модель хочет как лучше, а пользователь говорит как живёт. Если ваш LLM падает в обморок от реальной речи пользователя — проблема явно не в пользователе.
Бот живой, потыкать можно: @VoiceToPostBot, первые три обработки бесплатно, мат остаётся вашим.
P.S. Модерация площадки попросила меня замаскировать мат в скриншотах. Нейросеть, напомню, попросила меня не произносить его вообще. Теперь же прогресс налицо.