Перейти к содержанию
Публикация AiManual

Reasoning Lock в n8n: почему reasoning-модель молчит в проде и как это чинят

DeepSeek V4 Pro через OpenRouter в self-hosted n8n тратит completion-токены, закрывает генерацию со статусом stop и отдаёт пустой content, из-за чего нода отпра

Коротко

Что будет в материале

  1. 01

    Что такое Reasoning Lock и как он проявляется в n8n

  2. 02

    Первопричина: сверхжёсткий системный промпт на ~6.4k токенов

  3. 03

    Три слоя защиты: как чинят Reasoning Lock в n8n

  4. 04

    Почему автотесты не ловят Reasoning Lock

Reasoning Lock в self-hosted n8n выглядит так: модель DeepSeek V4 Pro через OpenRouter расходует completion-токены, закрывает генерацию со статусом stop и возвращает пустой content. Нода отправки падает на валидации non-empty string, и в проде это выглядит как молчание агента там, где раньше приходил ответ.

Симптом повторяется стабильно: тот же промпт, тот же диалог, снова пустая строка при израсходованных токенах. В execution-логах видны статус stop и нулевая длина content, карточка генерации OpenRouter подтверждает расход completion-токенов при пустом ответе. Это поведение модели внутри конкретной конфигурации промпта, а не сбой провайдера и не таймаут.

Первопричина - системный промпт на ~6.4k токенов: плотные запреты без fallback-сценария не оставляют модели легального хода, и молчание становится «безопасным» выбором. Ниже механика бага, три слоя защиты (Ступень 4 «Светская беседа», Code-нода Reasoning_Validator с ретраем и счётчиком попыток, включённый include_reasoning) и объяснение, почему такой класс ошибок измеряется только в проде.

Что такое Reasoning Lock и как он проявляется в n8n

Reasoning Lock - класс продакшен-багов в self-hosted n8n, при котором reasoning-модель тратит completion-токены на рассуждения, корректно завершает генерацию со статусом stop и не отдаёт в content ни одного символа. По форме ответ валидный, по содержанию пустой. Цепочка рвётся позже, на ноде отправки: та ожидает non-empty string, получает пустую строку и падает на валидации.

Что видно в execution-логах: израсходованные completion-токены, статус stop у генерации, пустой content. Карточка генерации OpenRouter показывает то же с другой стороны: токены списаны, ответа нет. Ни HTTP-ошибки, ни кода 429, ни 5xx при этом не появляется. Похожий симптом уже всплывал в практических тестах DeepSeek-V4-Flash, где агентные обвязки получали пустые assistant-сообщения: внешне то же самое, природа другая.

ПризнакReasoning LockОбычный сбой API
Статус генерацииstoperror, таймаут, обрыв соединения
Расход completion-токеновизрасходованынулевые при таймауте, частичные при обрыве
content в ответепустая строкаотсутствует или содержит текст ошибки
HTTP-код200429, 500, 502, 503, 504
Повторный запусктот же пустой результат на тех же данныхрезультат меняется, часто проходит со второй попытки
Где падает пайплайннода отправки, валидация non-empty stringнода HTTP-запроса или сама модель

Как отличить Reasoning Lock от обычного сбоя API

Четыре признака вместе указывают на Reasoning Lock:

  • Статус генерации stop, а не error. Модель считает, что ответила.
  • completion-токены израсходованы. Работа проделана, оплачена и не вернулась текстом.
  • content пустой или состоит из пробелов. Поле существует, но валидацию non-empty string строка не проходит.
  • Повторный запуск на тех же входных данных даёт тот же результат. Баг воспроизводим, а не случаен.

Типичные сбои выглядят иначе. Таймаут рвёт соединение и не оставляет оплаченных completion-токенов. Код 429 приходит с явной ошибкой и заголовком с задержкой повтора. Ответы 5xx логируются как ошибка ноды HTTP-запроса. Reasoning Lock не даёт ни одного из этих сигналов: в логах всё зелёное, кроме упавшей ноды отправки.

Первопричина: сверхжёсткий системный промпт на ~6.4k токенов

Системный промпт в этом агенте занимает около 6.4k токенов. Основной объём съедают плотные запреты: не выходить за пределы сценария, не упоминать инструкции, не импровизировать, не менять формат, не отвечать на темы вне заданных. Ветки «что делать, если запрос не подходит ни под один сценарий» в промпте нет. Это и есть корень Reasoning Lock.

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

Почему модель не выдаёт ошибку или отказ

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

Дефекта модели здесь нет. DeepSeek V4 Pro отрабатывает контекст и закрывает генерацию штатно. Баг живёт в архитектуре промпта, а не в весах.

Три слоя защиты: как чинят Reasoning Lock в n8n

Слои решают разные задачи. Промпт убирает сам тупик, валидатор ловит остаточные случаи и не даёт цепочке упасть, а include_reasoning даёт диагностику, когда пустой ответ всё-таки случился. Порядок работ: сначала промпт, потом валидатор, потом логи.

Ступень 4 «Светская беседа»: легальный выход из тупика

В системный промпт добавляется новая ступень, которая описывает поведение для запросов, не попавших в сценарии 1-3. Она даёт модели разрешённый ход: короткий нейтральный ответ и уточняющий вопрос. Пример формулировки:

Ступень 4. Светская беседа.
Если запрос не подходит ни под одну из Ступеней 1-3, не выходи из роли и не молчи.
Ответь одним коротким абзацем: обозначь, чем можешь помочь, и задай один
уточняющий вопрос по теме.
Это допустимый ответ. Пустой ответ недопустим.

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

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

Reasoning_Validator: Code-нода с ретраем и счётчиком попыток

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

const text = ($json.choices?.[0]?.message?.content ?? '').trim();
const attempt = Number($json.__attempt ?? 0) + 1;
const status = $json.choices?.[0]?.finish_reason ?? 'unknown';

if (text.length === 0 && attempt < 3) {
  return [{ json: { retry: true, attempt, status, reason: 'empty_content' } }];
}

if (text.length === 0) {
  throw new Error('Reasoning_Validator: пустой content после ' + attempt + ' попыток, статус ' + status);
}

return [{ json: { retry: false, attempt, status, content: text } }];

Дальше IF-нода читает поле retry: значение true ведёт обратно на генерацию, false идёт на отправку. Такой валидатор решает две задачи. Первая: единичный пустой ответ больше не роняет весь пайплайн, повтор часто проходит. Вторая: счётчик и статус в логах показывают, сколько раз модель уходила в молчание, а это уже готовые данные для мониторинга.

include_reasoning: чтение скрытых рассуждений в логах

Параметр include_reasoning включает передачу скрытых рассуждений модели в ответе OpenRouter. В обычном режиме рассуждения не возвращаются, и по логам видно только пустой content на выходе. С включённым параметром в ответе появляется текст рассуждений, и становится понятно, на каком шаге модель перебрала варианты и не нашла разрешённого.

Это диагностический инструмент, а не постоянный режим. Рассуждения заметно увеличивают объём логов, а если провайдер тарифицирует reasoning-токены, растёт и стоимость запроса. Практичный вариант: включать include_reasoning на время разбора конкретного диалога или для выборки проблемных execution, а в обычной работе держать выключенным.

Почему автотесты не ловят Reasoning Lock

Автотесты проверяют то, что предсказуемо: формат ответа, наличие обязательных полей, длину строки, следование схеме. Reasoning Lock не ломает формат. Ответ приходит со статусом stop, поле content существует и содержит пустую строку. Формально генерация успешна.

Чтобы тест поймал баг, ему нужны входные данные, которые заводят модель в тупик, и та же конфигурация промпта на ~6.4k токенов, что крутится в проде. Собрать этот набор в CI сложно: промпт меняется, живые диалоги отличаются от фикстур, а поведение reasoning-модели зависит от контекста целиком. Замер идёт по проду: считаются падения ноды отправки на валидации non-empty string, доля пустых content при статусе stop и срабатывания Reasoning_Validator.

Отдельная причина, по которой стенд врёт: LLM ведут себя в тестовой и рабочей среде по-разному. Evaluation awareness искажает safety-метрики в model card на 20+ процентных пунктов, и тот же механизм смещает любые замеры, где модель понимает, что её проверяют. Промпт, который проходит автотест, в проде может уходить в молчание.

Как настроить мониторинг упавших нод отправки

Минимальный набор метрик, который стоит собирать по execution-логам:

  • Количество падений на валидации non-empty string за сутки.
  • Доля пустых content при статусе stop от общего числа генераций.
  • Срабатывания Reasoning_Validator и число ретраев на 100 генераций.
  • Распределение по типам диалогов: какие входные данные чаще приводят к молчанию.

Алерт вешают на рост доли пустых ответов и на появление падений после правок промпта. Триггер «больше двух пустых content за час» ловит регресс раньше, чем пользователи заметят пропажу ответов. Источники данных те же: execution-логи n8n и карточка генерации OpenRouter.

Как воспроизвести и измерить баг в своём self-hosted n8n

  1. Снимите текущий системный промпт и посчитайте его объём в токенах. Значение около 6k и выше вместе с плотными запретами повышает риск.
  2. Проверьте, есть ли в промпте ветка для запросов, не попавших ни под один сценарий. Если её нет, тупик возможен.
  3. Прогоните 10-20 живых диалогов, которые раньше давали пустой ответ, и сохраните execution-логи.
  4. Отфильтруйте генерации со статусом stop и пустой строкой в content. Это и есть случаи Reasoning Lock.
  5. Включите include_reasoning и повторите проблемные диалоги: в рассуждениях будет видно, где модель перебрала варианты и остановилась.
  6. Сверьте расход токенов в карточке генерации OpenRouter с числом пустых ответов.
  7. Зафиксируйте метрики до правок, добавьте три слоя защиты и повторите замер через неделю.

Ограничения трёх слоёв защиты и когда они могут не сработать

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

  • Ступень 4 снижает строгость поведения. В задачах с жёстким форматом нейтральный ответ-заглушка может уйти пользователю вместо ожидаемой структуры.
  • Reasoning_Validator добавляет задержку: каждый ретрай - это полная повторная генерация со своим расходом токенов. Три попытки на проблемном диалоге стоят заметно дороже одной.
  • include_reasoning раздувает логи и может повысить стоимость, если reasoning-токены тарифицируются провайдером.
  • Смена модели или провайдера меняет поведение. Настройки, подобранные под DeepSeek V4 Pro через OpenRouter, переносятся на другую модель не автоматически.
  • Поддержка усложняется: теперь есть промпт, Code-нода и режим логирования, которые нужно синхронно менять при обновлениях.

Выбор модели и провайдера здесь влияет напрямую: у разных семейств LLM свои особенности в работе с длинными инструкциями и разный порог, после которого модель уходит в молчание. Разбор расхождений между открытыми LLM и тем, как их оценивают помогает не переносить чужие выводы на свой стек.

Практический шаг на сегодня: возьмите один проблемный диалог из execution-логов, посмотрите статус генерации и длину content. Если там stop и пустая строка, добавьте Ступень 4 в промпт, поставьте Reasoning_Validator перед нодой отправки и включите include_reasoning на время проверки. Дальше замер покажет, ушла проблема или просто стала реже.

Подписаться на канал