Опубликовано
Диспетчер за полтора гигабайта: локальный оркестратор для AI-агента на MiniCPM5-2B
Автор: Гусев Николай [портфолио]
Темы: ai-agents, benchmarks, llm-api, local-models
Джилл разруливает: очередь, сортировка, делегация, подача, экономия, смена закрыта
Сложность: средняя
Пока я ставил опыты, Spotify рассказал про то же самое
Пока шли тесты, Spotify опубликовали разбор своего Portal - схемы для Claude Code, которая режет расход токенов на 90 процентов. Суть один в один с моей: мощная модель решает основную задачу, а черновую работу тащит дешёвая. Большой файл читает дешёвая модель и возвращает выжимку по теме; повторяющийся код генерирует дешёвая и сразу кладёт в проект; дорогая модель работает только с проблемой и точечными правками. Показательно, как они пришли к надёжности: сначала правила делегирования прописали в CLAUDE.md, но модель соблюдала их не всегда. Тогда добавили хук - код, перехватывающий действие до выполнения: попытка целиком прочитать файл длиннее 350 строк блокируется, чтение предлагается передать другой модели. На чтении больших файлов экономия вышла около 90 процентов.
Их вывод совпадает с моим дословно: правила соблюдаются не всегда, и обвязка вокруг модели обязана это учитывать. Мой вариант этой же мысли получится ниже - на сломанной дискриминации из-за сокращённого описания инструмента. Схема у нас разная (у Spotify - хук-перехватчик, у меня - маленькая модель-диспетчер с нативным tool-calling), а принцип общий: экономия токенов делается не промптом, а архитектурой.
Задача: экономить платные токены на роутинге
У моего агента четыре источника вычислений: платная подписка codex (GPT-5.6), подписка zcode (GLM-5.3), пул бесплатных моделей через OpenRouter и Kilo, и локальная LM Studio на домашней машине. У каждого свои лимиты и своя цена. Автовыбора модели под задачу не было вообще: платные лимиты горели на рутинах, бесплатные простаивали, а выбор "какую модель звать" каждый раз принимался вручную.
Идея развивалась в четыре шага. Сначала написал скилл model-routing - таблицу правил, по которой оркестратор сам подсчитывает сигналы сложности ("найди все" плюс четыре, "архитектура" плюс три, "переименуй" минус два) и выбирает исполнителя. Оркестратором была minimax-m3 через OpenRouter - читала скилл, скорила, направляла задачу. Работало, но стоило токенов: на чтение правил, на рассуждение, на сам ответ, а дневные лимиты бесплатного канала расходовались и на роутинг тоже.
Второй шаг - уточнить выбор по способностям: не просто free или платная, а поиск по бенчмаркам (terminal-bench на Kilo) под профиль задачи - код это, длинный контекст или мультимодальность. Третий шаг оказался тупиковым, но поучительным: решил снять скоринг с модели совсем и написал консольную утилиту hermes-route, которая по регэкспам считает сигналы и возвращает готовую команду. Красивая идея - ноль токенов на роутинг, детерминированный результат - но она убивала главный смысл: модель-диспетчер ценна тем, что понимает семантику задачи, а регэксп ловит только слова. На живом примере это разошлось наглядно: "найди все использования deprecated API и почини" - minimax-m3 насчитала четырнадцать баллов и категорию architect (deprecated означает устаревшее, значит потенциальная уязвимость, значит безопасность), утилита по регэкспам - четыре балла и standard. Тут же вспомнилось и другое свойство minimax-m3 - она известна инициативностью: там, где конкурент ещё думает, кого бы спросить, minimax-m3 уже сделала. Как диспетчер она скорила щедро и действовала быстро, и именно поэтому тупиковый путь был таким соблазнительным: пока она справлялась, экономика роутинга не казалась проблемой. Утилита осталась в скилле запасным путём - на случай деградации модели. А главным стал четвёртый шаг: поднять диспетчера локально и поручить ему нативным tool-calling'ом вызывать субагентов через hermes -m - выбор исполнителя вообще перестал стоить облачных токенов. Оказалось, маленькой модели для этого хватает, и скриптовая обвязка не понадобилась. Осталось выяснить, какая локальная модель годится на роль диспетчера - и это стало отдельным исследованием.
Теория и первый облом
Мой агент - Hermes Agent, и у него есть скиллы: текстовые инструкции, которые модель читает и выполняет. Написал компактный протокол роутинга на 40 строк: таблица сигналов с весами ("найди все" +4, "архитектура" +3, "прод" +3, "переименуй" -2), сумма даёт категорию, категория даёт команду делегирования.
Взял первую попавшуюся локальную модель - LiquidAI LFM2.5 2.6B. Дал ей протокол и задачу "найди все использования deprecated API и почини". Модель получила 239 токенов на вход, сгенерировала 200 токенов на выход - и все 200 ушли в reasoning_tokens, внутренние размышления. Content - пустая строка. Увеличил лимит до 500, потом до 2000. Размышления росли пропорционально, ответ не появлялся. Дал явную подсказку "Ответь в формате SCORE: N, CATEGORY: name" - модель начала проектировать микросервис. Проигнорировала протокол и полезла решать задачу сама.
Тот же сценарий с qwen3.8-4b-distill: тот же результат. Думает долго, отвечает пусто, инструкции не держит. Модели с thinking mode до 4B для диспетчеризации не годятся - это не вопрос промпта, это свойство архитектуры.
Тест 9 моделей
Решил не гадать, а прогнать все модели, которые были под рукой в LM Studio. Девять штук, от 2.6B до 8B: LFM2.5, Ling MoE, SmolLM3, Granite 3B, Qwen3.8-distill, Apollo-thinking, Gemma 4 e4b, Aion-RP-Llama. Четыре теста на каждой:
-
T1 - простая проверка на живость
-
T2 - английский промпт диспетчера: "не решай задачу, выведи команду hermes-route"
-
T3 - русский промпт со скорингом: посчитай сигналы, выдай SCORE/CATEGORY/COMMAND
-
T4 - неформальная русская инструкция: "не отвечай, сформируй bash-команду"
Результаты: 8 из 9 моделей провалили минимум половину тестов. Gemma 4 e4b прошла 2 из 4. Лучший результат - 3 из 4 - показала Granite 4.1 3B от IBM. Не самая большая, не самая раскрученная, но единственная, которая прошла все три содержательных теста подряд, включая неформальную инструкцию.
Отдельная заслуга T3: Gemma на задаче "найди все deprecated API" насчитала score 14 и категорию architect, хотя формально в тексте был только один сигнал на 4 балла. Она поняла, что deprecated API - это потенциальная уязвимость, а уязвимость - это безопасность, и безопасность - это плюс два. Регулярный скрипт поймал только явные слова. LLM читает смысл, регексп читает буквы.
Дуэль: Granite 3B против 8B
IBM выпускает Granite 4.1 в двух размерах - 3B и 8B. Раз 3B прошла базу, взял 8B и прогнал обе по расширенной сюите. Три новых теста: полный протокол целиком как системный промпт (реальный сценарий Hermes), edge case на over-routing (простая задача не должна улетать в платную модель), и эмоциональная приманка - "прод лежит, я вторые сутки не сплю, помоги пожалуйста" вместо сухой формулировки.
Ядро диспетчеризации (тесты T1-T6) обе прошли 6 из 6. По T7 обе споткнулись одинаково: вместо команды делегирования полезли сочувствовать и разбирать проблему сами. Granite-линейка вежливая, эмоциональный тон сбивает скоринг. Лечится одним предложением в протоколе: "эмоциональный тон задачи не меняет категорию, скоринг только по фактам".
А вот по скорости разрыв трёхкратный. На генерации 400 токенов Granite 3B выдала 88.9 токена в секунду, 8B - 26.7. На скоринге простой задачи 3B отвечает за 0.3 секунды и 17 токенов, 8B - за 1.2 секунды и 29 токенов. Памяти: примерно 2 гигабайта против 5. Вердикт однозначный: 8B не проходит ни одного теста там, где 3B провалилась, зато втрое медленнее. Для диспетчера 8B не даёт ничего взамен.
Что получилось на выходе
Локальный диспетчер на Granite 3B: два гигабайта на диске, CPU-инференс 89 токенов в секунду, ноль сетевых запросов, ноль затрат на роутинг. Цикл работы: задача приходит Granite, та за полсекунды считает сигналы и выдаёт категорию. Quick - отвечает сама, standard - отправляет в бесплатную модель через Kilo, deep - в zcode, architect - в платный codex. Платные токены тратятся только на саму задачу, не на выбор исполнителя.
Сколько это экономит на практике: типичная сессия с агентом - это 20-40 задач, из них 60-70% рутина уровня quick и standard. Если каждая такая задача раньше проходила через платную модель, которая ещё и думала о выборе исполнителя - теперь это 25-30 бесплатных локальных вызовов и только 5-10 осмысленных делегаций.
Уроки
Первое: для диспетчеризации размер не равен качеству. 3B от IBM обошла Gemma 4 e4b (эффективные 4B из 7.5B MoE), потому что стабильнее держит инструкцию и плотнее отвечает. А вот thinking-модели до 4B не годятся совсем - их размышления съедают весь лимит.
Второе: тест на живость нельзя делать узким. Мой первый тест "скажи ок одним словом" провалили все девять моделей: с reasoning-моделями лимит съедали размышления, с RP-моделью разговор уходил в ролевую игру. Нормальный тест - не "ответь", а "выполни роль и покажи формат".
Третье: у вежливых моделей есть обратная сторона. Линейка Granite построена как безопасная и дружелюбная, и на эмоциональном входе она подменяет задачу беседой. Для диспетчера это баг, который надо закрывать явным правилом в протоколе.
Четвёртое: локальный оркестратор - это не альтернатива облачным моделям, а их надстройка. Он не решает задачи, он решает, кто решит. И за эту роль хватает двух гигабайт.
Проверка на живом агенте
Писать о диспетчере, который только выглядит правильно, было бы нечестно, поэтому финальный тест - живая сессия Hermes Agent целиком на бесплатной модели step-3.7-flash. Задача: найти все использования устаревших API в реальном репозитории и исправить. Оркестратор сама нагрузила контекст: нашла файлы, загрузила два релевантных скилла, оценила масштаб. Затем нативно вызвала инструмент делегации и передала субагенту полный реестр из четырёх файлов с требованиями. Субагент отработал 195 секунд: четырнадцать вызовов инструментов, реальные правки в четырёх файлах - замена curl на requests с ретраями, вынос хардкода моделей во внешний каталог. Стоимость всего цикла - ноль рублей: оркестратор и субагент работали на бесплатных каналах.
Тот же сценарий для локального диспетчера: granite 3B через нативный tool-calling в четырёх задачах из четырёх выбрала правильное действие. Простую задачу "переименуй переменную" оставила себе через вызов answer_self, три остальных уводила в делегацию, верно угадав категорию вплоть до architect. Один найденный порок: на русском тексте аргументы granite искажает - структура вызова верна, а текст внутри "плывёт". Лечится простым правилом: диспетчер передаёт задачу дословно, не пересказывая, - именно так и работает живой агент.
Итог этой проверки важнее всех бенчмарков: субагенты вызываются из маленьких моделей без единого скрипта-обвязки. Нативный tool-calling покрывает весь цикл - оценка, выбор исполнителя, передача задачи, возврат результата. Python-харнесс на каждый тип задачи не нужен.
Смена чемпиона: MiniCPM5-2B
Пока статья ждала вычитки, Openbmb выпустили MiniCPM5-2B - первое место среди моделей до 4B в Artificial Analysis Intelligence Index, а в обучении 500 тысяч агентских семплов. Заявлен упор именно на tool use и agentic задачи - мой профиль. Скачал, прогнал ту же сюиту. Расклад с granite-3B: текстовые тесты 6 из 7 у обеих, tool-call дисциплина 4 из 4 у обеих. Но в деталях MiniCPM5 аккуратнее: подсказка категории точнее (granite один раз поставила deep вместо architect), русский текст в аргументах чистый, а на architect-задаче в живом агенте она разбила работу на три субагента параллельно - архитектура безопасности, идемпотентность, наблюдаемость - и вернула два файла-артефакта на 128 строк связного кода. granite в той же роли отдаёт задачу одному субагенту и пересказывает её с искажениями.
Дуэль квантов добавила неожиданный вывод: точный квант не улучшает диспетчеризацию, а ухудшает её. Q4_K_M держит дисциплину 3 из 4 и выдаёт 121 токен в секунду, Q8_0 и F16 синхронно заваливают две категории из четырёх - начинают решать задачи сами вместо делегации. Веса поточнее дают модели больше уверенности и меньше склонности перепоручать. Плюс спекулятивный декодинг с Q4 в роли черновика для старших квантов в LM Studio не завёлся - загрузка draft падает, зафиксировал как баг связки. Финальная рекомендация: Q4_K_M, полный оффлоад на GPU, контекст 64 тысячи токенов.
Отдельная находка из отладки, полезная любому, кто строит диспетчера на малых моделях: я сократил описания инструментов для экономии токенов - убрал из answer_self примеры "rename, typo, one-liner" - и модель мгновенно перестала отличать тривиальные задачи от делегируемых. Вернул примеры - восстановилось с первого запроса. Двухмиллиардная модель читает примеры в описаниях буквально: сокращаешь description без A/B-теста на той же модели - ломаешь дискриминацию. Это тот самый вывод Spotify из начала статьи: правила соблюдаются не всегда, поэтому обвязка обязана это учитывать - у них хук-перехватчик, у меня протокол с примерами прямо в описаниях инструментов.
Материалы к статье - всё в одной папке 2026-09-07-model-routing (зеркало GitFlic), воспроизведение с нуля:
-
skill/ - протокол диспетчера: SKILL.md (полный справочник), SKILL-PROTOCOL.md (компактный протокол), scripts/route.py с внешним каталогом model-catalog.json
-
lm-studio-test/REPORT.md - отчёт по девяти моделям, granite-duel.md - дуэль granite 3B/8B, minicpm-vs-granite.md - финал дуэли и матрица квантов, e2e-delegation.md - проверка делегации в живом агенте
-
lm-studio-test/raw/ - JSON-прогоны каждой модели, транскрипты живых сессий с делегацией
-
lm-studio-test/scripts/ - все тест-скрипты: сюита из четырёх тестов (test_model.py), расширенная T1-T8 (test_granite8b.py), диспетчерская tool-call сюита (test_granite_dispatch.py), дуэль квантов (quant_duel.py, quant_final.py), генерация и сборка стрипа (gen-route-article.py, build_strip.py)
Для повтора достаточно LM Studio с любой моделью из отчёта, работающим API на 127.0.0.1:1234 и запуска скриптов из scripts/ - все тесты гоняются по OpenAI-совместимому API без дополнительных зависимостей, кроме стандартного requests.