AI · 8 мин чтения

Нейросеть для генерации текста: почему она молчит

Нейросеть для генерации текста в любом моём проекте, будь то Telegram-бот на aiogram, сценарий в n8n или чат-бот с базой знаний на Claude, работает по одной и той же схеме: токенизирует промпт, предсказывает следующий токен, добавляет его к контексту и повторяет цикл, пока не встретит стоп-условие. Самый частый баг, с которым приходит клиент, звучит одной фразой: «бот ничего не ответил». В девяти случаях из десяти причина не в самой модели, а в том, как настроены max_tokens, стоп-последовательности или обработка finish_reason на стороне бэкенда. Разберу механику генерации по шагам и покажу, где именно рвётся цепочка.

Что происходит внутри нейросети для генерации текста при каждом слове

Модель не «знает» ответ целиком и не пишет его сразу. Она получает промпт, разбитый токенизатором на подслова (токен не равен слову: «генерация» может распасться на два-три токена), и на каждом шаге считает распределение вероятностей по всему словарю: какой токен логичнее всего поставить следующим. Дальше один токен выбирается по этому распределению, добавляется к уже сгенерированному тексту, и весь процесс запускается заново с учётом нового контекста. Так по одному токену собирается абзац.

Контекстное окно у актуальных моделей Claude - миллион токенов, и 200 тысяч осталось только у Haiku 4.5. В него входит не только сам вопрос пользователя, но и системный промпт, история переписки и куски документов, которые вы подмешиваете для RAG. Если сумма всего этого упирается в лимит, часть контекста просто обрезается до того, как модель увидит запрос, и на выходе может оказаться пустая строка вместо содержательного ответа: модели физически нечего было отвечать.

Отдельная головная боль, характерная именно для потоковой (streaming) генерации: клиент читает ответ по частям через Server-Sent Events, и если соединение обрывается или фронтенд закрывает поток раньше времени, пользователь видит пустое сообщение, хотя на стороне модели генерация прошла нормально и просто не долетела до экрана. В таких случаях проблема не в нейросети и не в промпте, а в обработке потока на клиенте, и лечится это буферизацией чанков и таймаутом на стороне бэкенда, а не сменой модели.

Токены и effort: почему один и тот же промпт даёт разный текст

Распределение вероятностей на каждом шаге редко выглядит как один явный лидер: обычно есть три-пять токенов с близкими шансами. Раньше этим разбросом управляли параметром temperature, а пул кандидатов сужали через top_p и top_k. На актуальных моделях Claude этих ручек больше нет: temperature, top_p и top_k, выставленные в недефолтное значение, возвращают ошибку 400, и запрос не доходит до генерации. Вместо них глубину рассуждения и общий расход токенов задаёт output_config.effort со значениями low, medium, high, xhigh и max.

По умолчанию effort равен high, и это ровно то же самое, что не передавать параметр вовсе. На практике для саппорт-бота я ставлю low или medium: ответ собирается быстрее и дешевле, модель не уходит в длинные рассуждения там, где нужен короткий ответ по базе знаний. Для разбора сложных многошаговых задач поднимаю до xhigh или max. Одинаковый промпт при этом всё равно может дать разный текст: параметров, которые жёстко фиксировали выбор токена, в API больше нет, а effort задаёт не конкретный токен, а объём работы модели над ответом. И отдельная ловушка при переезде со старых моделей: если temperature или top_p остались в коде от прошлой версии интеграции, запрос вернёт 400 и в логах бота это выглядит как молчание, а не как ошибка модели.

Пять причин, из-за которых генерация обрывается пустой строкой

За несколько лет работы с Claude API и OpenAI API в чат-ботах и веб-сервисах на практике встречаются одни и те же причины:

  • Стоп-последовательность совпала с промптом на первом же токене. Если в system-промпте или в примере диалога встречается та же строка, что задана в stop_sequences, модель может остановиться сразу после её появления.
  • max_tokens скопирован из примера в документации и не изменён под задачу. Значение 16 или 50 годится для демо-запроса, но для развёрнутого ответа бота обрезает текст на середине предложения, а на потоковой генерации это выглядит как внезапная тишина.
  • Контентный фильтр отклонил тему, и API вернул пустой content или отдельный finish_reason вместо текста. Это не ошибка сети, поэтому такие случаи легко пропустить, если код проверяет только код ответа 200, а не содержимое.
  • Модель работает в режиме tool use или structured output и вызвала инструмент вместо того, чтобы отдать текст. Код ждёт content[0].text, а получает блок с вызовом функции, и в переменной для текста оказывается пустая строка.
  • RAG-контекст пришёл пустым: поиск по базе знаний не нашёл релевантных кусков, промпт собрался без данных, и модель честно отвечает «не знаю» или обрывается, потому что инструкция требовала отвечать только на основе контекста.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Как я ловлю пустые ответы в продакшене на aiogram, n8n и RAG

В Telegram-ботах на aiogram я всегда логирую не только текст ответа, но и stop_reason из ответа Claude или finish_reason из ответа OpenAI: если бот молчит, в логах сразу видно, дело в max_tokens, в фильтре или в пустом контексте. Без этого приходится гадать по одному отчёту пользователя «бот завис».

В сценариях n8n перед узлом отправки сообщения в Telegram или в CRM ставлю проверку на пустой content и на finish_reason: если он не равен «stop», сценарий не отправляет пользователю пустое сообщение, а либо повторяет запрос с более высоким max_tokens, либо уводит диалог на оператора. Для чат-бота с базой знаний отдельно логирую количество найденных чанков перед вызовом модели: если поиск вернул ноль совпадений, это отдельная ветка, а не повод звать нейросеть с пустым контекстом.

Вот упрощённый пример проверки на Python, который вставляю в обработчик ответа Claude API:

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": prompt}]
)

if not response.content or response.stop_reason == "max_tokens":
    logger.warning("Пустой или обрезанный ответ: %s", response.stop_reason)
else:
    answer = response.content[0].text

Такая проверка занимает пять строк, но убирает большинство жалоб на «бот не отвечает» ещё до того, как о них узнает клиент.

Настройки, которые чаще всего режут ответ раньше времени

Параметр Что делает Как приводит к пустому или обрезанному ответу
max_tokens Ограничивает длину ответа в токенах Слишком маленькое значение обрезает текст на середине фразы, на стриминге это выглядит как внезапная тишина
stop_sequences Останавливает генерацию при совпадении со строкой Если похожая строка уже есть в промпте, модель обрывает ответ на первом токене
output_config.effort Задаёт глубину рассуждения и общий расход токенов, значения low, medium, high, xhigh, max На low модель отвечает заметно короче и без пояснений, и такой ответ легко принять за обрезанный
temperature, top_p, top_k Управляли случайностью выбора токена на прежних моделях На актуальных моделях Claude недефолтное значение возвращает ошибку 400: генерации не происходит вообще, и без разбора тела ответа это выглядит как пустой ответ
Жёсткие рамки в system-промпте Задаёт формат и ограничения ответа Противоречивые инструкции («отвечай только в JSON, но не длиннее пяти слов») модель иногда решает через пустой ответ

Как выбрать и внедрить нейросеть для генерации текста в проект

Настройки зависят от задачи, а не от того, что «Claude лучше GPT» или наоборот. Для саппорт-бота с базой знаний важнее умеренный effort и жёсткая проверка finish_reason, для генерации рекламных описаний товаров, наоборот, нужно меньше ограничений в системном промпте. Для структурированного извлечения данных из документов беру structured output со схемой, потому что там куда важнее предсказуемость формата, чем разнообразие формулировок.

На практике нейросеть для генерации текста редко работает изолированно: в сценариях n8n она обычно стоит рядом с узлами СДЭК или Т‑Банка (бывший Тинькофф) и собирает ответ клиенту из данных заказа, статуса доставки или платежа, а не просто отвечает на абстрактный вопрос. Если в таком сценарии модель вернула пустой ответ, а проверки нет, клиент получит пустое сообщение вместо, например, трек-номера, и это куда заметнее для бизнеса, чем пустой ответ в тестовом чате.

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

Из готовых примеров у меня в библиотеке уже есть ИИ-чатбот на Claude, собранный с обработкой пустых и обрезанных ответов из коробки, можно посмотреть логику проверок на реальном коде, а не только в описании.

Если нужна нейросеть для генерации текста под конкретную задачу бизнеса, от точечной интеграции Claude API или OpenAI в существующий сайт до чат-бота с базой знаний на RAG, Telegram-бота с диалоговой логикой или сценария в n8n, который связывает модель с CRM и мессенджерами, закладывайте в бюджет не только сам вызов API, но и обработку пустых, обрезанных и отфильтрованных ответов: без неё бот молчит именно в тот момент, когда важен ответ клиенту.

Частые вопросы

Почему нейросеть иногда возвращает пустой ответ без ошибки в статусе запроса?

Потому что API технически отработал корректно: запрос принят, модель отдала результат, просто этим результатом оказалась пустая строка или content_filter вместо текста. Код 200 говорит только о том, что сервер обработал запрос, а не о том, что в ответе есть текст. Проверять нужно stop_reason или finish_reason, а не только статус HTTP-ответа.

Как отличить пустой ответ от ответа, обрезанного по max_tokens?

По значению finish_reason (в OpenAI) или stop_reason (в Claude). Если там «length» или «max_tokens», текст просто закончился по лимиту и его можно продолжить повторным запросом с остатком контекста. Если content вообще пустой при finish_reason «stop», модель сама решила, что ответа не будет, и здесь разбираться нужно с промптом или контекстом, а не увеличивать лимит токенов.

Можно ли починить пустые ответы, покрутив параметры генерации?

На актуальных моделях Claude крутить нечего: temperature, top_p и top_k удалены, и в недефолтном значении такой запрос возвращает ошибку 400. Осталась одна ручка, output_config.effort: поднять её имеет смысл, если модель отвечает слишком коротко на сложном промпте. Но если пустой ответ вызван обрезанным контекстом, сработавшим контентным фильтром или вызовом инструмента вместо текста, effort тут ни при чём, и без проверки finish_reason проблема просто станет реже, а не исчезнет.

Сколько стоит внедрить нейросеть для генерации текста в бота или CRM?

Точечная интеграция Claude API или OpenAI в существующий сайт или сервис стоит от 50 000 ₽, чат-бот с базой знаний на RAG тоже от 50 000 ₽, Telegram-бот с диалоговой логикой от 30 000 ₽, а сценарий в n8n, который связывает модель с CRM или мессенджерами, от 25 000 ₽. Итоговая цена зависит от того, сколько источников данных нужно подключить и насколько сложная логика проверки ответов требуется.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно