Зачем вообще это нужно?
Представьте: вы хотите запустить AI-агента, который играет в Pokemon Red. Обычный путь - арендовать GPU, поднять сервер, настроить API. Но что если все это можно сделать прямо в браузере? Без долларов на облака, без сложной инфраструктуры.
Именно это мы и сделаем. На чистом клиенте. С использованием Qwen 2.5 через WebLLM, TensorFlow.js для обучения нейросети-политики и WASM-эмулятора Game Boy. Звучит как магия, но к 2026 году это уже стандартная практика.
Важный момент: все работает локально. Никаких API-ключей, никаких счетов за облако. Открыли страницу - агент начал играть. Закрыли - все остановилось. Идеально для демонстраций и экспериментов.
Архитектура, которая не сломается
Прежде чем лезть в код, нужно понять, как все устроено. Наша система состоит из трех основных компонентов:
- WASM-эмулятор Game Boy - запускает Pokemon Red прямо в браузере, отдает скриншоты и принимает нажатия кнопок
- WebLLM с Qwen 2.5 1.5B - локальная LLM, которая анализирует состояние игры и принимает стратегические решения
- TensorFlow.js с нейросетью-политикой - учится на действиях LLM, со временем начинает играть лучше и быстрее
| Компонент | Задача | Почему именно он |
|---|---|---|
| WASM GB Emulator | Эмуляция игры, захват состояния | Работает в браузере, не требует плагинов |
| Qwen 2.5 1.5B | Стратегическое планирование | Оптимальное соотношение размер/качество для клиента |
| TensorFlow.js Policy Net | Оптимизация действий | Может обучаться в реальном времени |
Собираем по частям
1WASM-эмулятор: ставим игру на паузу
Сначала нужен эмулятор. Берем gbemu-wasm - он уже умеет все, что нам нужно. Устанавливаем:
npm install gbemu-wasm@latestИнициализируем эмулятор и загружаем ROM:
import { GBEmulator } from 'gbemu-wasm';
const emulator = await GBEmulator.create();
await emulator.loadROM('/roms/pokemon-red.gb');
// Получаем текущий кадр
const frame = emulator.getFrame(); // Uint8Array с пикселями
// Отправляем действие
emulator.pressButton('A');
emulator.releaseButton('A');2WebLLM: запускаем Qwen 2.5 локально
Здесь начинается магия. WebLLM от MLCommons позволяет запускать LLM прямо в браузере через WebGPU. На 2026 год актуальная версия - 0.3.8 с поддержкой Qwen 2.5.
npm install @mlc-ai/web-llm@0.3.8Инициализируем модель. Важно: модель качается при первом запуске, так что первый раз будет долго.
import { CreateWebWorkerEngine } from '@mlc-ai/web-llm';
const engine = await CreateWebWorkerEngine(
new Worker(new URL('./worker.ts', import.meta.url)),
{
model: 'Qwen2.5-1.5B-Instruct-q4f16_1',
temperature: 0.7,
maxTokens: 512
}
);
// Промпт для анализа состояния игры
const analyzePrompt = `Ты играешь в Pokemon Red. Текущий экран: [описание экрана].
Твои покемоны: [список]. Противник: [противник].
Какое действие предпринять? Выбери одно:
1. Атаковать [атака]
2. Сменить покемона
3. Использовать предмет
4. Сбежать
Объясни выбор.`;
const response = await engine.chat.completions.create({
messages: [{ role: 'user', content: analyzePrompt }]
});Qwen 2.5 1.5B весит около 900 МБ в квантизации q4. Убедитесь, что у пользователей есть быстрый интернет и достаточно места в кэше. Для мобильных устройств лучше использовать еще более легкие версии.
3TensorFlow.js: учимся на своих ошибках
LLM умная, но медленная. Каждый запрос занимает секунды. Нейросеть-политика учится повторять действия LLM, но делает это за миллисекунды.
Создаем простую сверточную сеть, которая по скриншоту предсказывает действие:
import * as tf from '@tensorflow/tfjs';
// Сеть для анализа скриншотов 160x144 с 4 каналами (RGBA)
const createPolicyNetwork = () => {
const model = tf.sequential();
// Сверточные слои для анализа изображения
model.add(tf.layers.conv2d({
inputShape: [144, 160, 4],
filters: 32,
kernelSize: 8,
strides: 4,
activation: 'relu'
}));
model.add(tf.layers.conv2d({
filters: 64,
kernelSize: 4,
strides: 2,
activation: 'relu'
}));
model.add(tf.layers.conv2d({
filters: 64,
kernelSize: 3,
strides: 1,
activation: 'relu'
}));
model.add(tf.layers.flatten());
// Полносвязные слои для принятия решений
model.add(tf.layers.dense({ units: 512, activation: 'relu' }));
model.add(tf.layers.dense({ units: 256, activation: 'relu' }));
// Выход: вероятности для каждого действия
model.add(tf.layers.dense({
units: 14, // A, B, Start, Select, Up, Down, Left, Right + комбинации
activation: 'softmax'
}));
return model;
};
const policyNet = createPolicyNetwork();
policyNet.compile({
optimizer: tf.train.adam(0.001),
loss: 'categoricalCrossentropy'
});Связываем все вместе
Теперь нужна логика, которая координирует все компоненты. Создаем основной цикл агента:
class PokemonAgent {
constructor(emulator, llmEngine, policyNet) {
this.emulator = emulator;
this.llm = llmEngine;
this.policy = policyNet;
this.memory = []; // Для обучения с подкреплением
this.useLLM = true; // Начинаем с LLM, потом переключаемся на политику
}
async step() {
// 1. Получаем текущее состояние
const frame = this.emulator.getFrame();
const gameState = this.extractState(frame);
// 2. Выбираем действие
let action;
if (this.useLLM && Math.random() < 0.3) {
// Иногда спрашиваем LLM для улучшения стратегии
action = await this.getLLMAction(gameState);
this.recordForTraining(frame, action); // Запоминаем для обучения
} else {
// Обычно используем быструю политику
action = this.getPolicyAction(frame);
}
// 3. Применяем действие
this.executeAction(action);
// 4. Обучаем политику на новых данных
if (this.memory.length > 100) {
await this.trainPolicy();
}
// 5. Через час переключаемся на политику
if (this.steps > 3600 && this.useLLM) {
this.useLLM = false;
console.log('Переключились на чистую политику');
}
}
async getLLMAction(state) {
const prompt = this.buildPrompt(state);
const response = await this.llm.chat.completions.create({
messages: [{ role: 'user', content: prompt }]
});
return this.parseLLMResponse(response.choices[0].message.content);
}
getPolicyAction(frame) {
const tensor = tf.tensor(frame).reshape([1, 144, 160, 4]);
const prediction = this.policy.predict(tensor);
const actionIdx = tf.argMax(prediction, 1).dataSync()[0];
tensor.dispose();
prediction.dispose();
return this.actions[actionIdx];
}
}Ошибки, которые всех ломают
Видел десятки попыток повторить эту архитектуру. Все спотыкаются об одно и то же:
- Утечки памяти в TensorFlow.js. Каждый тензор нужно явно освобождать с помощью
.dispose(). Иначе через час браузер упадет. - Слишком частые вызовы LLM. WebLLM не предназначен для запросов каждые 100 мс. Делайте паузы между стратегическими решениями.
- Игнорирование контекста игры. Pokemon Red - игра с состоянием. Нужно отслеживать не только текущий экран, но и историю.
Самая частая ошибка: пытаться обрабатывать каждый кадр через LLM. Это бессмысленно. Игрок-человек не принимает 60 решений в секунду. Делайте стратегические паузы: оценили ситуацию, приняли решение, выполнили серию действий.
Оптимизации, которые реально работают
Базовая версия работает. Но медленно. Вот что ускорит агента в 5-10 раз:
- Кэширование решений LLM. Похожие ситуации = одинаковые действия. Хэшируйте состояние игры, сохраняйте решения.
- Предобработка скриншотов. Не нужно скармливать сети полный 160x144 кадр. Ресайз до 80x72, конвертация в grayscale - и производительность взлетает.
- Пакетное обучение политики. Не обучайте сеть после каждого действия. Копите 100-200 примеров, потом одним батчем.
Вот как выглядит оптимизированный предобработчик:
class FrameProcessor {
static preprocess(frame) {
// Быстрее делать все в одном тензоре
return tf.tidy(() => {
let tensor = tf.tensor(frame).reshape([144, 160, 4]);
// Только красный канал (достаточно для Pokemon)
tensor = tensor.slice([0, 0, 0], [144, 160, 1]);
// Ресайз для сети
tensor = tf.image.resizeBilinear(tensor, [72, 80]);
// Нормализация
tensor = tensor.div(255.0);
return tensor;
});
}
}Что дальше? Куда развивать архитектуру
Рабочий агент - только начало. Дальше можно:
- Добавить долговременную память. LLM забывает, что было 10 минут назад. Нужно хранить ключевые события (поймал покемона, проиграл битву).
- Иерархическое планирование. Одна LLM для стратегии ("идти в пещеру Диджи"), другая для тактики ("бить электричеством против воды").
- Мультиагентность. Запустить 10 копий агента параллельно, чтобы исследовать разные стратегии быстрее.
Если хотите углубиться в проектирование сложных агентов, посмотрите гайд по современным AI-агентам. Там разобраны именно такие продвинутые архитектуры.
Чеклист перед запуском
Перед тем как запускать агента на долгую сессию:
- Проверьте, что WebGPU поддерживается браузером (
navigator.gpuне undefined) - Убедитесь, что модель Qwen 2.5 полностью загрузилась (индикатор в WebLLM)
- Протестируйте эмулятор вручную - игра должна запускаться без артефактов
- Настройте автосохранение состояния каждые 1000 шагов (в LocalStorage)
- Добавьте логгирование действий для отладки
И главное - не ждите, что агент с первого раза пройдет игру. Он будет тупить, зацикливаться, проигрывать. Но с каждым часом будет становиться умнее. Как настоящий покемон.
Эта архитектура - не только про Pokemon. Те же принципы работают для любых игр, симуляторов, даже для автоматизации браузера. Клиентский AI-агент на WebLLM и TensorFlow.js открывает дверь в мир автономных приложений, которые работают полностью локально.
Если хотите копнуть еще глубже в локальные AI-системы, рекомендую полное руководство по локальным RAG-системам. Там те же принципы, но примененные к работе с документами.