Pull to refresh

Как отключить 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> тег до генерации. Но обязательно проверяйте, что токенизатор уже добавил — иначе получите дубль и потратите час на дебаг того, что должно было занять минуту.

Tags:
Total votes 3: ↑2 and ↓1+2
Comments3

Articles