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 |
|---|---|---|
| Статус генерации | stop | error, таймаут, обрыв соединения |
| Расход completion-токенов | израсходованы | нулевые при таймауте, частичные при обрыве |
| content в ответе | пустая строка | отсутствует или содержит текст ошибки |
| HTTP-код | 200 | 429, 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
- Снимите текущий системный промпт и посчитайте его объём в токенах. Значение около 6k и выше вместе с плотными запретами повышает риск.
- Проверьте, есть ли в промпте ветка для запросов, не попавших ни под один сценарий. Если её нет, тупик возможен.
- Прогоните 10-20 живых диалогов, которые раньше давали пустой ответ, и сохраните execution-логи.
- Отфильтруйте генерации со статусом stop и пустой строкой в content. Это и есть случаи Reasoning Lock.
- Включите include_reasoning и повторите проблемные диалоги: в рассуждениях будет видно, где модель перебрала варианты и остановилась.
- Сверьте расход токенов в карточке генерации OpenRouter с числом пустых ответов.
- Зафиксируйте метрики до правок, добавьте три слоя защиты и повторите замер через неделю.
Ограничения трёх слоёв защиты и когда они могут не сработать
Слои снижают частоту и дают диагностику. Причину они не убирают: промпт на ~6.4k токенов остаётся сложной конструкцией, и любой новый запрет может снова закрыть последний легальный ход.
- Ступень 4 снижает строгость поведения. В задачах с жёстким форматом нейтральный ответ-заглушка может уйти пользователю вместо ожидаемой структуры.
- Reasoning_Validator добавляет задержку: каждый ретрай - это полная повторная генерация со своим расходом токенов. Три попытки на проблемном диалоге стоят заметно дороже одной.
- include_reasoning раздувает логи и может повысить стоимость, если reasoning-токены тарифицируются провайдером.
- Смена модели или провайдера меняет поведение. Настройки, подобранные под DeepSeek V4 Pro через OpenRouter, переносятся на другую модель не автоматически.
- Поддержка усложняется: теперь есть промпт, Code-нода и режим логирования, которые нужно синхронно менять при обновлениях.
Выбор модели и провайдера здесь влияет напрямую: у разных семейств LLM свои особенности в работе с длинными инструкциями и разный порог, после которого модель уходит в молчание. Разбор расхождений между открытыми LLM и тем, как их оценивают помогает не переносить чужие выводы на свой стек.
Практический шаг на сегодня: возьмите один проблемный диалог из execution-логов, посмотрите статус генерации и длину content. Если там stop и пустая строка, добавьте Ступень 4 в промпт, поставьте Reasoning_Validator перед нодой отправки и включите include_reasoning на время проверки. Дальше замер покажет, ушла проблема или просто стала реже.