Как отключить reasoning у локального DeepSeek-R1 и не сойти с ума
Третий пост из серии про грабли локальных LLM. Первый — про микрочанки, отравляющие RAG. Второй — про embedding модель, которая не знает русский. Сейчас — про reasoning, который жрёт ресурсы и не выключается.
Проблема
DeepSeek-R1-Distill-Qwen-32B — reasoning модель. На каждый запрос она сначала «думает» в блоке <think>...</think>, потом отвечает. Выглядит так:
<think>
Хорошо, мне нужно помочь пользователю распределить задачи
для проекта создания цифрового двойника для молочной фермы.
Я новичок в этом, поэтому постараюсь разобраться шаг за шагом.
Сначала, мне нужно понять, что такое цифровой двойник...
</think>
Разработка цифрового двойника для молочной фермы — это сложный проект...
Блок <think> может быть длиннее самого ответа. Это токены, это время, это VRAM. Для задач где рассуждения не нужны — чистый оверхед.
Наивное решение — не работает
Первая идея: убрать <think> из ответа регуляркой постфактум.
response_text = re.sub(r'<think>.*?</think>', '', response_text, flags=re.DOTALL).strip()
Проблема: модель всё равно генерирует рассуждения. Вы просто прячете их от пользователя, но GPU уже потратил время и токены.
Решение от сообщества
Пустой блок <think>\n\n</think> в конце промпта. Модель видит, что фаза рассуждений уже «завершена», и сразу переходит к ответу.
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
text = text + "<think>\n\n</think>\n\n"
Попробовал — не работает. Reasoning остаётся.
Ловушка с токенизатором
Смотрю в лог что реально уходит модели:
...ть задачи?<|Assistant|><think><think>
</think>
Два <think>. Токенизатор DeepSeek при add_generation_prompt=True уже добавляет <think> в конец промпта автоматически. Мой код добавляет второй. Модель видит незакрытый первый тег и начинает думать.
Причём <|Assistant|> — это не обычные символы |, а полноширинные юникодные |. Специальные токены DeepSeek. Если искать обычный | в строке — не найдёте.
Правильное решение
Проверять, что уже есть в промпте, и действовать по ситуации:
def prepare_prompt_no_thinking(messages, tokenizer):
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
if "<think>\n\n</think>" in text:
pass # Уже закрыт
elif "<think>" in text and "</think>" not in text:
text = text + "\n\n</think>\n\n" # Закрываем открытый
else:
text = text + "<think>\n\n</think>\n\n" # Добавляем пустой
return text
Три ветки — потому что разные версии токенизатора ведут себя по-разному. Кто-то добавляет <think>, кто-то нет.
Результат
Без тегов:
Хорошо, мне нужно помочь пользователю распределить задачи
для проекта создания цифрового двойника для молочной фермы.
Я новичок в этом, поэтому постараюсь разобраться шаг за шагом...
С правильными тегами:
Разработка цифрового двойника для молочной фермы — это сложный проект,
который требует участия специалистов из разных областей.
Вот примерное распределение задач:
Модель сразу отвечает по делу, без вступительных рассуждений. Экономия токенов и времени — в зависимости от запроса от 30% до 60%.
Вывод
Если используете DeepSeek-R1-Distill локально и reasoning вам не нужен — не режьте его регуляркой постфактум. Закройте <think> тег до генерации. Но обязательно проверяйте, что токенизатор уже добавил — иначе получите дубль и потратите час на дебаг того, что должно было занять минуту.



