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

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

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

Коротко

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

  1. 01

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

  2. 02

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

  3. 03

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

  4. 04

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

Когда 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. Но это уже совсем другая история.

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