14 сентября 2026

DDQN и игры Atari: разработка рабочего роутера для MAS

Привет! Меня зовут Кирилл Мозолев, я руководитель портфеля ИИ-продуктов в red_mad_robot, один из которых — red_mad_router. Когда мы с командой строили мультиагентные системы в наших проектах, мы столкнулись с интересной задачей: как эффективнее направить пользовательский запрос к нужным агентам? Как правило, при создании мультиагентных систем разработчики по умолчанию пользуются роутером-агентом на базе LLM, не подвергая такой подход сомнению. Мы же решили узнать на практике, какие существуют альтернативы для этого метода. Расскажу, как мы проверяли гипотезу: может ли агент с обучением с подкреплением (RL) научиться роутингу в мультиагентной системе. А ещё делать это лучше, быстрее и дешевле, чем LLМ. Заодно поделюсь, при чём тут Пакман и игры от Atari.

Проблема: куда отправить запрос?

Когда строишь мультиагентную систему, рано или поздно упираешься в задачу роутинга: нужно решить, какой из агентов обработает входящий запрос пользователя. Однако иногда один запрос требует сразу нескольких агентов одновременно. Тогда приходится искать целый набор подходящих агентов. Формально это задача выбора подмножества: на входе — текстовый запрос, на выходе — подмножество S агентов, которое должно максимально совпасть с эталонным набором R. Качество измеряем коэффициентом Жаккара (Jaccard similarity): |S ∩ R| / |S ∪ R|. Для её решения есть множество подходов, но для сравнения со своим методом, я взял следующие из них:
  • Правила на ключевых словах. Простые и быстрые, но они плохо масштабируются. Правило if «SQL» in query → SQL Agent не учитывает контекст и не может выбрать несколько агентов по косвенным признакам.
  • Мультиметочный классификатор. Работает, но принимает решение один раз и независимо по каждому агенту. Не учитывает, каких агентов уже выбрали.
  • LLМ-роутер. При миллионе запросов в месяц он обходится примерно в $150 только на роутинг (gpt-4o-mini, ~1000 токенов на промпт). Сюда же прибавим 0.5–1 с latency на каждый вызов, которая ложится поверх времени работы самих агентов.
У каждого решения при этом есть свои недостатки, зачастую критические. Отсюда возникла идея: а что если реализовать такой механизм роутинга через RL-агента? Он принимает решения последовательно и с учётом уже сделанного выбора — это принципиально отличается от классификатора. И, если всё получится, инференс роутера будет условно бесплатным, поскольку не будет требоваться LLМ.

При чём тут MDP

Выбор агентов — это последовательный процесс. В моём исследовании пул состоял из 9 специализированных агентов: Python-код, SQL, анализ данных (Pandas), математика, извлечение JSON, суммаризация, ТЗ/требования, рерайт под стиль и финансовые расчёты. Роутер-агент смотрит на запрос, выбирает первого агента, смотрит на уже выбранных, выбирает следующего, и так до тех пор, пока не решает остановиться. Это классический MDP — Марковский процесс принятия решений (Markov Decision Process):
  • Состояние (state): TF-IDF-вектор запроса + бинарная маска уже выбранных агентов + доля пройденных шагов t/T.
  • Действие (action): один из ещё не выбранных агентов или STOP — итого N+1 = 10 действий для нашего пула из 9 агентов.
  • Награда (reward): в конце эпизода — коэффициент Жаккара между выбранным и эталонным набором; плюс небольшой штраф за каждый шаг, о форме которого — ниже.
Для обучения я собрал датасет из 1053 запросов на русском языке, сгенерированных локальными моделями, причём они повторяют реальные запросы пользователей по выбранным доменам. Важно оговорить, что все методы я сравнивал на одном и том же датасете, так что относительные результаты сопоставимы, но абсолютные цифры на реальном трафике нужно перепроверять. Каждому запросу соответствовал эталонный набор агентов от 2 до 9, размеры набора сбалансированы. Разбивка осуществлена в соотношении 70/15/15 (train/val/test), стратификация — по размеру набора |R|.

DQN и Double DQN: что выбрать

Одним из базовых алгоритмов в обучении с подкреплением является DQN (Mnih et al., 2015). Его первым большим бенчмарком стали игры Atari 2600 — в том же наборе была и знаменитая игра Ms. Pac-Man. Когда Пакман съедает точку, он получает очки, но, попадаясь призраку, теряет жизнь. Так же и DQN: агент учится по награде от среды, не зная правил заранее. Среда выдаёт награду за положительный результат и штраф за отрицательный, а нейросеть учится предсказывать, сколько награды принесёт каждое действие — Q-значение. Однако стандартный DQN систематически завышает Q-значения: он использует одну сеть и для выбора действия, и для его оценки. В задаче с разреженным вознаграждением, когда оно появляется только в конце эпизода, это особенно критично. Double DQN решает проблему разделением ролей. Online-сеть выбирает действие, target-сеть оценивает его Q-значение. Поэтому для нашей задачи с выбором набора агентов я остановился на этом подходе.

Четыре baseline перед стартом

Перед тем, как обучать RL-агента, я зафиксировал четыре бейзлайна на одном датасете. В качестве основного метода я взял Supervised, который мы должны превзойти по F1 Score. При этом как нижний бенчмарк мы берём Random — ниже него падать нельзя.
baselines.png
Взглянем на метрики. Rule-based роутер внезапно показал результаты даже ниже, чем случайный подбор в Random. Но не потому, что правила «плохие», а потому, что у него провал по recall — он выбирает мало агентов. Мультиметочный классификатор (TF-IDF + OneVsRest LogReg) оказался неожиданно сильным: F1 = 0.876, recall = 0.952. LLМ-роутер показывает сопоставимый F1 при значительно лучшем precision, но проигрывает по recall. Цель у DDQN — превзойти лучший результат по F1, сохранив разумный баланс precision/recall.

Архитектура

На схеме ниже изображён полный путь, который проходит запрос пользователя внутри DDQN-роутера, от текста до финального набора выбранных агентов.
ddqn_routing_architecture.png
Пайплайн работы роутера
На вход Q-сети поступает не только представление пользовательского запроса, но и информация о том, какие агенты уже были выбраны ранее. Благодаря этому роутер принимает решения последовательно. Каждый следующий шаг зависит от предыдущих, а состояние обновляется после каждого выбора. Для обучения используется replay buffer на 100 000 переходов, а размер batch — 128. Один эпизод работы роутера можно представить при помощи такой схемы:
ddqn_episode_flow.png
Схема работы одного эпизода роутера
На первом шаге состояние содержит только описание запроса и пустую маску выбранных агентов. После каждого действия маска обновляется, поэтому сеть уже «видит», какие агенты были добавлены ранее, и может учитывать их при следующем выборе. Сеть не выбирает агентов повторно, а последовательность действий формирует итоговый набор маршрутизации.

9 итераций: хронология исследования

Для того, чтобы DDQN начал показывать результаты лучше других методов, ему пришлось пройти 9 итераций исследования. Первые восемь итераций объединяла одна проблема: агент не учился нажимать STOP. Вместо этого он выбирал сразу весь пул доступных агентов. Возникает эта ситуация так. Если награда (reward) равнак оэффициенту Жаккара (Jaccard), то каждый дополнительный агент из нужных даёт прирост ~0.05–0.10 к Jaccard. Любой flat-штраф за шаг (step_cost), который меньше этого прироста, агент игнорирует. Агенту выгоднее перебрать всех, чем рискнуть пропустить нужного. Назовём это select-all коллапсом. Вот так я пытался решить эту проблему в течение восьми итераций:
iterations.png
Особых улучшений не происходило, каждое новое изменение не поднимало результат. Настоящий прорыв случился только на девятой итерации, когда стало понятно, что нужно менять не уровень штрафа, а его геометрию во времени. Идея пришла из статьи Puppeteer. Штраф за шаг не должен быть постоянным — он должен расти с каждым следующим шагом. Формула награды выглядит так:
reward = Jaccard(S, R) − λ · Σ ln(1 + t/9), где t — номер шага от 1 до 9, λ = 0.05.
Этот подход работает потому, что первый шаг стоит ~0.005, девятый - ~0.035; за полный перебор набегает ~0.19, что сравнимо с потерей Jaccard от 1–2 лишних агентов. Flat-штраф одинаков на каждом шаге и поэтому не сообщает роутеру-агенту, когда следует остановиться. Растущий же штраф даёт сигнал: «Следующий агент обойдётся дороже предыдущего». Именно этот сигнал и позволил Q-сети научиться отличать пятый выбор от девятого.
final_graph.png
Средняя длина выбранного набора (avg_steps) по итерациям. Пунктир — среднее |R| в датасете.
Дополнительно критичным оказался epsilon schedule. Когда epsilon_decay_steps = total_steps, replay buffer заполняется «мусорными» эпизодами с длинными случайными траекториями, и Q-сеть обучается не на тех данных. Я вывел правило: epsilon_decay_steps = 50 000, независимо от total_steps. Обязателен также action masking, убирающий повторы и ускоряющий сходимость. Без него агент тратит ёмкость Q-сети на паттерны, которые никогда не должны встречаться. Также стоимт отметить, что была еще одна итерация с проверкой adaptive-ветки. В состояние я добавлял контекст промежуточных выходов уже вызванных агентов. По моей гипотезе, обратная связь должна была усилить роутер. Но результат вышел противоположным: F1 упал до 0.67, а select-all вернулся. Причина тут в энкодере — TF-IDF, обученный на пользовательских запросах, не знает словарь outputs агентов и превращает контекст в почти нулевой сигнал. К этому ограничению вернусь ниже.

DDQN и другие подходы

Результаты, которые показал DDQN после девяти итераций исследования, я также сравнил с другими методами.
DDQN-and-other-methods.png
Сравнение подходов по различным метрикам
DDQN с логарифмическим штрафом (λ = 0.05) показал лучшие F1 (0.888) и Jaccard (0.824) среди всех методов, опережая Supervised по F1 совсем немного, но с заметно более высокой precision (0.927 против 0.836) и меньшим набором агентов (4.94 против 6.16). Его главное преимущество – сопоставимое или лучшее качество с учётом фактически бесплатного инференса, при latency ~1 мс и без внешних API.

Использование и ограничения

DDQN показывает хорошие результаты в сравнении с остальными методами, но в его использовании есть нюансы. Метод подойдёт, если:
  • есть 3+ специализированных агента и нужен subset routing: один запрос → несколько агентов;
  • есть ≥ 500 примеров для разметки; оптимально — 2000;
  • распределение запросов стабильно.
Использовать DDQN не стоит, если:
  • меньше ~300 примеров: в таком случае проще использовать keyword-matcher или прямой LLМ-вызов;
  • нужен single-agent routing: здесь справится и обычный классификатор;
  • состав агентов меняется часто, минимум раз в неделю, и каждое изменение требует переобучения. Впрочем, в DDQN-роутере есть инструмент разметки на базе LLМ. С его помощью переобучение можно сделать максимально автоматизированным, хотя избежать его совсем не получится.
Пока что DDQN нельзя назвать универсальным решением, которое справится с любой задачей по распределению агентов в MAS. Но работа по преодолению ограничений уже идёт. Главное из них — TF-IDF как state encoder. Он хорошо работает на статичном датасете, но не знает язык промежуточных outputs агентов. Переход на dense embeddings (например, BGE-M3) в теории снимает это ограничение и открывает adaptive-ветку. Ещё один недостаток — размер датасета. 1053 примера достаточно для проверки гипотезы, но мало для продуктовой системы с длинным хвостом запросов. В будущем для дальнейших экспериментов с DDQN я планирую увеличить его размеры, а также использовать датасеты и на английском языке.

Библиотека ddqn-router

Разработку можно опробовать уже сейчас. Результаты исследования я упаковал в open-source библиотеку ddqn-router. А здесь можно посмотреть основные команды для установки и использования. Описание агентов:
# config.yaml
agents:
  - id: 0
    name: "Billing Agent"
    description: "Handles billing, invoices, payments"
  - id: 1
    name: "Technical Agent"
    description: "Handles bugs, errors, <Aitip>API</Aitip> issues"
Разметка и обучение датасета:
# 1. Разметить датасет через <Aitip>LLM</Aitip> (один раз)
ddqn-router label --config config.yaml --input queries.txt --output data/tasks.jsonl

# 2. Обучить роутер
ddqn-router train --config config.yaml

# 3. Использовать

Использование роутера:
from ddqn_router import DDQNRouter

router = DDQNRouter.load("./artifacts/")
result = router.route("my invoice was charged twice")
print(result.agents)       # [0, 2]
print(result.confidence)   # 0.87
print(result.steps)        # 3
Также есть REST API через FastAPI:
ddqn-router serve --artifacts ./artifacts/ --port 8000

Что в итоге

RL-роутер для мультиагентных систем — это реализуемая история. DDQN с логарифмическим штрафом превзошёл Supervised по F1 и precision при той же latency и условно нулевой стоимости инференса. Путь занял 9 итераций и несколько тупиковых веток. Хотя финальные результаты у нас получились лучше, чем при других методах, самыми ценными стали даже не они. Важнее всего оказалось выявить select-all коллапс и понять, насколько форма награды важнее его абсолютного значения.