Qwen 3.5 0.8B играет в DOOM: туториал по VLM-агенту на Python | AiManual
AiManual Logo Ai / Manual.
10 Мар 2026 Гайд

Как заставить Qwen 3.5 0.8B играть в DOOM: пошаговый туториал по созданию VLM-агента

Полный гайд по созданию VLM-агента на Qwen 3.5 0.8B для игры в DOOM через VizDoom. Установка, код, настройка LM Studio и решение ошибок.

Когда 800 миллионов параметров слишком много для скромности

Вы скачали Qwen 3.5 0.8B. Подумали: "Ну, игрушка. Маловато будет". А я скажу: вы недооцениваете эту модель. В марте 2026 года мы имеем ситуацию, когда даже крошечные модели показывают чудеса в нишевых задачах. Например, в DOOM.

Проблема классического подхода к игровому ИИ - он требует или тонны правил, или тонны данных для обучения с подкреплением. А мы пойдём другим путём: Vision-Language Model агент. Грубо говоря, модель будет смотреть на экран (через скриншоты) и говорить, что делать. Без тонкого финтеста. Без месяцев тренировок.

Решение выглядит безумно: взять модель на 0.8B параметров, которая даже текст толком генерирует с оглядкой по сторонам, и заставить её играть в хардкорный шутер 90-х. Но именно в этой безумности и есть кайф.

Зачем это вообще нужно? Во-первых, это проверка гипотезы: могут ли маленькие VLM (Vision-Language Models) понимать динамичные визуальные сцены. Во-вторых, это смешной демо-проект для портфолио. В-третьих, если получится - вы станете локальным гуру, который заставил карликовую модель играть в DOOM.

Что нам понадобится (актуально на март 2026)

Не буду тянуть. Вот стек, который работает прямо сейчас:

  • Qwen 3.5 0.8B - самая новая версия на 10.03.2026. Не 1.5B, не 4B, именно 0.8B. Зачем? Потому что можем.
  • VizDoom - порт оригинального DOOM для исследований ИИ. Поддерживает Python API.
  • LM Studio 0.3.9+ - актуальная версия на март 2026. Именно через него будем поднимать локальный сервер для модели.
  • Python 3.11+ с пакетами: vizdoom, Pillow, requests, numpy.
  • Собственно, сам DOOM. WAD-файл. Но об этом позже.
💡
Если вы раньше работали с большими моделями типа Qwen3.5-397B, приготовьтесь к культурному шоку. Здесь всё работает на обычном CPU. И не греет комнату.

1Ставим VizDoom и разбираемся с DOOM

VizDoom - это не просто эмулятор. Это научный инструмент, который выверяет каждый кадр до миллисекунды. Установка проще, чем кажется:

pip install vizdoom

А вот с WAD-файлом (это данные игры) придётся повозиться. Лицензионные тонкости. Кратко: качаете свободный WAD (например, Freedoom) или покупаете оригинальный DOOM. Кладёте файл в папку с проектом.

Проверяем, что VizDoom работает:

import vizdoom as vzd

game = vzd.DoomGame()
game.set_doom_scenario_path("basic.wad")  # ваш WAD-файл
game.init()
print("VizDoom инициализирован. Можно начинать ад.")

Ошибка номер один: пытаться запустить VizDoom без WAD. Не делайте так. Игра просто не стартанёт, а вы будете час искать проблему в коде.

2Настраиваем LM Studio и Qwen 3.5 0.8B

Открываем LM Studio. В поиске моделей вбиваем "Qwen 3.5 0.8B". Качаем самую свежую версию в формате GGUF (на март 2026 это обычно Qwen3.5-0.8B-Q4_K_M.gguf).

Запускаем модель в серверном режиме. Вкладка "Local Server". Важные настройки:

  • Context Size: 4096 (больше не нужно, мы же не роман пишем)
  • Temperature: 0.1 (нам нужны детерминированные ответы)
  • Port: 1234 (или любой другой, но запомните)

Жмём "Start Server". В консоли должно появиться что-то вроде "Server running at http://localhost:1234".

Проверяем работу:

curl http://localhost:1234/v1/chat/completions -H "Content-Type: application/json" -d '{
  "model": "gpt-3.5-turbo",
  "messages": [{"role": "user", "content": "Hello"}],
  "max_tokens": 10
}'

Должен прийти JSON с ответом. Если нет - проверьте, что модель загружена и сервер запущен.

💡
LM Studio использует llama.cpp под капотом. Если возникнут проблемы с производительностью, посмотрите статью про оптимизацию llama.cpp для Qwen. Там есть тонкости по квантованию и кешированию.

3Пишем код агента: от скриншота к действию

А теперь магия. Наш агент будет:

  1. Делать скриншот текущего кадра из VizDoom
  2. Конвертировать его в base64 (потому что Qwen 3.5 через LM Studio поддерживает изображения в base64)
  3. Отправлять промпт с изображением в модель
  4. Парсить ответ и преобразовывать в действие для игры

Вот скелет агента:

import vizdoom as vzd
import numpy as np
import base64
import requests
from PIL import Image
import io
import json

class DoomVLMAgent:
    def __init__(self, lm_studio_url="http://localhost:1234"):
        self.game = vzd.DoomGame()
        self.game.set_doom_scenario_path("basic.wad")
        self.game.set_screen_format(vzd.ScreenFormat.RGB24)
        self.game.set_screen_resolution(vzd.ScreenResolution.RES_640X480)
        self.game.set_render_hud(False)  # HUD только мешает
        self.game.init()
        
        self.lm_studio_url = lm_studio_url
        self.actions = [
            [1, 0, 0],   # Вперёд
            [0, 1, 0],   # Поворот влево
            [0, 0, 1],   # Поворот вправо
            [1, 1, 0],   # Вперёд + влево
            [1, 0, 1],   # Вперёд + вправо
        ]
        
    def get_screenshot_base64(self):
        """Делаем скриншот и конвертируем в base64"""
        state = self.game.get_state()
        if state is None:
            return None
        
        screen_buffer = state.screen_buffer  # numpy array (height, width, 3)
        image = Image.fromarray(screen_buffer)
        
        # Ресайз до 224x224 для экономии токенов
        image = image.resize((224, 224))
        
        buffered = io.BytesIO()
        image.save(buffered, format="JPEG", quality=85)
        img_str = base64.b64encode(buffered.getvalue()).decode()
        return img_str
    
    def ask_model(self, image_base64):
        """Отправляем изображение и промпт в модель"""
        prompt = """Ты - солдат в игре DOOM. Посмотри на изображение экрана и выбери действие:
        1. Вперёд
        2. Поворот влево
        3. Поворот вправо
        4. Вперёд и влево
        5. Вперёд и вправо
        
        Ответь ТОЛЬКО номером от 1 до 5. Никаких пояснений."""
        
        payload = {
            "model": "gpt-3.5-turbo",
            "messages": [
                {
                    "role": "user",
                    "content": [
                        {"type": "text", "text": prompt},
                        {
                            "type": "image_url",
                            "image_url": {
                                "url": f"data:image/jpeg;base64,{image_base64}"
                            }
                        }
                    ]
                }
            ],
            "max_tokens": 5,
            "temperature": 0.1
        }
        
        try:
            response = requests.post(
                f"{self.lm_studio_url}/v1/chat/completions",
                json=payload,
                timeout=5
            )
            result = response.json()
            answer = result['choices'][0]['message']['content'].strip()
            return answer
        except Exception as e:
            print(f"Ошибка запроса к модели: {e}")
            return "1"  # fallback - вперёд
    
    def parse_action(self, answer):
        """Парсим ответ модели в действие для VizDoom"""
        try:
            action_idx = int(answer) - 1
            if 0 <= action_idx < len(self.actions):
                return self.actions[action_idx]
        except:
            pass
        return self.actions[0]  # по умолчанию - вперёд
    
    def run_episode(self, episodes=10):
        """Запускаем эпизод игры"""
        for episode in range(episodes):
            print(f"Эпизод {episode + 1}/{episodes}")
            self.game.new_episode()
            
            while not self.game.is_episode_finished():
                # Получаем скриншот
                img_b64 = self.get_screenshot_base64()
                if img_b64 is None:
                    break
                
                # Спрашиваем модель
                answer = self.ask_model(img_b64)
                print(f"Модель сказала: {answer}")
                
                # Преобразуем в действие
                action = self.parse_action(answer)
                
                # Совершаем действие в игре
                self.game.make_action(action, 4)  # 4 тика на действие
                
                # Небольшая пауза, чтобы не перегружать
                # time.sleep(0.05)
            
            score = self.game.get_total_reward()
            print(f"Эпизод окончен. Счёт: {score}")
        
        self.game.close()

if __name__ == "__main__":
    agent = DoomVLMAgent()
    agent.run_episode(3)

Это базовая версия. Она работает. Не идеально, но работает.

Критически важный момент: промпт. Мы заставляем модель отвечать ТОЛЬКО цифрой. Почему? Потому что Qwen 3.5 0.8B склонна к болтливости. Если дать ей волю, она начнёт рассуждать о философии насилия в видеоиграх вместо того, чтобы выбрать действие. А нам нужна цифра.

4Интеграция и первый запуск: когда всё ломается

Запускаете скрипт. Сначала VizDoom откроется в окне. Модель начнёт "думать". Первое, что вы увидите:

  • Модель отвечает не цифрой, а текстом типа "Я выбираю действие 3". Решение: ужесточаем промпт. Добавляем "Ответь ТОЛЬКО цифрой, без точек, без слов".
  • Задержки между кадрами огромные. Модель на CPU думает 2-3 секунды. Решение: уменьшаем размер изображения до 112x112. Или ставим более агрессивное квантование модели (Q2_K).
  • VizDoom крешится при закрытии. Решение: добавляем обработку сигналов и корректное закрытие игры.

Вот улучшенная версия промпта, которая работает лучше:

prompt = """IMAGE: First-person view from DOOM game.

INSTRUCTION: Choose action number:
1=FORWARD, 2=LEFT, 3=RIGHT, 4=FORWARD+LEFT, 5=FORWARD+RIGHT

OUTPUT FORMAT: Single digit 1-5 only.

ANSWER: """

Коротко. Жёстко. По-военному.

💡
Если модель упорно игнорирует промпт и генерирует ерунду, возможно, проблема в загрузке модели. Проверьте, что в LM Studio загружена именно Qwen 3.5 0.8B, а не какая-то другая. И что включена поддержка изображений (в LM Studio 0.3.9+ она есть по умолчанию для Qwen).

Почему это работает (и где не работает)

Qwen 3.5 0.8B имеет встроенную поддержку vision, хоть и ограниченную. Она может распознавать простые паттерны: коридоры, стены, монстров вдалеке. На основе этого выбирает действие.

Что она НЕ может:

  • Стратегическое планирование. Она живёт одним кадром.
  • Сложные манёвры. Уворачиваться от снарядов не выйдет.
  • Запоминать карту. Каждый кадр для неё - новая вселенная.

Но для демонстрации VLM-агента этого достаточно. Модель будет бродить по коридорам, иногда упираться в стену, иногда находить выход.

Если хотите улучшений:

  1. Добавьте контекст. Передавайте не один кадр, а последние 3-4 в истории. Но учтите, что Qwen 3.5 0.8B имеет ограниченный контекст.
  2. Внедрите простую память через векторную базу (но это уже для более крупных моделей).
  3. Добавьте рефлексию: пусть модель объясняет свой выбор, а затем выбирает действие на основе объяснения. Техника chain-of-thought для бедных.

FAQ: вопросы, которые зададут вам после демо

ВопросОтвет
Почему именно 0.8B, а не 4B или 7B?Потому что это вызов. И потому что работает на Raspberry Pi 5. Серьёзно.
LM Studio обязателен? Можно ли использовать Ollama?Можно. Но в марте 2026 LM Studio проще для vision-моделей. Ollama требует отдельной настройки для Qwen с vision.
Сколько FPS получается?На CPU: 0.3-0.5 FPS. На GPU (если модель туда поместится): 2-3 FPS. Это не для киберспорта.
Можно ли научить модель стрелять?Да. Добавьте действие "ОГОНЬ" в список. Но модель редко будет его выбирать - у неё нет инстинкта самосохранения.
Почему модель иногда выбирает одно и то же действие подряд?Потому что картинка меняется незначительно. И потому что temperature=0.1 делает ответы детерминированными.

Ошибки, которые сломают ваш день (и как их избежать)

Ошибка: "VizDoom fatal error: W_GetNumForName: DOOM.WAD not found"

Решение: положите WAD-файл в правильную директорию. Не в папку со скриптом, а в папку, которую ищет VizDoom. Часто это рабочая директория Python.

Ошибка: "model does not support images" в LM Studio

Решение: скачайте именно vision-версию Qwen 3.5. В названии обычно есть "vision" или "V". Или убедитесь, что в LM Studio включена опция поддержки изображений.

Ошибка: модель всегда возвращает "1"

Решение: проверьте, что изображение действительно передаётся. Распечатайте размер base64 строки. Должно быть несколько десятков тысяч символов. Если мало - проблема с захватом экрана.

Ошибка: VizDoom окно открывается, но ничего не происходит

Решение: скорее всего, игра на паузе. Добавьте self.game.set_window_visible(True) и убедитесь, что окно в фокусе.

💡
Если хотите серьёзно улучшить агента, посмотрите руководство по долгой памяти для LLM. Там техники, которые можно адаптировать даже для маленьких моделей.

Что дальше? Когда 0.8B маловато

Этот проект - proof of concept. Если он зажёг в вас интерес к игровым агентам, вот куда двигаться:

1. Увеличить модель. Возьмите Qwen 3.5 4B или 7B. Разница в качестве решений будет заметной. Но потребует GPU.

2. Добавить обучение с подкреплением

3. Сменить игру. VizDoom поддерживает сценарии разной сложности. Попробуйте "defend the center" или "deadly corridor".

4. Сделать мультимодального агента. Добавьте звук из игры. Или данные о здоровье и патронах. Qwen 3.5 умеет работать с мультимодальными входами.

Главное - не ожидайте, что ваш агент пройдёт DOOM на Ultra-Violence. Он даже на "I'm too young to die" будет спотыкаться о каждую стену. Но он будет играть. Сам. И в этом вся магия.

P.S. Если вдруг захотите повторить с гигантской моделью, посмотрите как уместить 235 миллиардов параметров в 72 ГБ VRAM. Но это уже совсем другая история.

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