Когда 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-файл. Но об этом позже.
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 с ответом. Если нет - проверьте, что модель загружена и сервер запущен.
3Пишем код агента: от скриншота к действию
А теперь магия. Наш агент будет:
- Делать скриншот текущего кадра из VizDoom
- Конвертировать его в base64 (потому что Qwen 3.5 через LM Studio поддерживает изображения в base64)
- Отправлять промпт с изображением в модель
- Парсить ответ и преобразовывать в действие для игры
Вот скелет агента:
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: """Коротко. Жёстко. По-военному.
Почему это работает (и где не работает)
Qwen 3.5 0.8B имеет встроенную поддержку vision, хоть и ограниченную. Она может распознавать простые паттерны: коридоры, стены, монстров вдалеке. На основе этого выбирает действие.
Что она НЕ может:
- Стратегическое планирование. Она живёт одним кадром.
- Сложные манёвры. Уворачиваться от снарядов не выйдет.
- Запоминать карту. Каждый кадр для неё - новая вселенная.
Но для демонстрации VLM-агента этого достаточно. Модель будет бродить по коридорам, иногда упираться в стену, иногда находить выход.
Если хотите улучшений:
- Добавьте контекст. Передавайте не один кадр, а последние 3-4 в истории. Но учтите, что Qwen 3.5 0.8B имеет ограниченный контекст.
- Внедрите простую память через векторную базу (но это уже для более крупных моделей).
- Добавьте рефлексию: пусть модель объясняет свой выбор, а затем выбирает действие на основе объяснения. Техника 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) и убедитесь, что окно в фокусе.
Что дальше? Когда 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. Но это уже совсем другая история.