Идея выглядела разумно: отдавать простые задачи локальной модели, а дорогой облачный Codex беречь для сложного. Эксперимент с MCP-сервером и gemma3:4b показал обратное — маршрут через локальную модель потребовал вдвое больше облачного input на той же задаче. Вот что произошло и как это повторить без сюрпризов.

Зачем вообще это пробовать

Если у вас уже стоит Ollama с небольшой моделью, соблазн очевиден: зачем тратить лимит Codex на разбор короткого traceback, сводку лога или генерацию commit message? Пусть Codex делегирует рутину локально, а сам занимается архитектурой и ревью.

Для делегирования есть готовый механизм — MCP (Model Context Protocol). Codex поддерживает локальные STDIO MCP-серверы: процесс запускается на машине, читает JSON-RPC из stdin и возвращает результат через stdout. Конфигурация хранится в ~/.codex/config.toml.

Как собрать MCP-сервер для Ollama

Сервер получился компактным, без внешних Python-зависимостей. У него один инструмент — local_llm. Входящий запрос выглядит так:

json
{
  "prompt": "Что означает эта ошибка?",
  "context": "SMTPAuthenticationError: 535 authentication failed",
  "mode": "classify",
  "max_tokens": 160,
  "temperature": 0
}

В ответ приходят не только слова модели, но и метрики:

json
{
  "answer": "Incorrect username or password for the email server",
  "model": "gemma3:4b",
  "wall_time_ms": 6098,
  "prompt_tokens": 71,
  "output_tokens": 44,
  "tokens_per_second": 9.52
}

Конфигурация в ~/.codex/config.toml:

plain
[mcp_servers.ollama-worker]
command = "codex-ollama-worker"
default_tools_approval_mode = "approve"

[mcp_servers.ollama-worker.tools.local_llm]
approval_mode = "approve"
output_token_limit = 1200

Без явного approval_mode = "approve" первый же вызов падает с ошибкой:

plain
MCP tool call requires approval, but approval policy is never

Это не баг — неинтерактивный процесс не может показать диалог подтверждения, поэтому Codex блокирует новый инструмент по умолчанию.

Что умеют gemma3:4b и gemma2:2b на реальных задачах

Восемь задач из обычного проекта: TLS hostname mismatch, SMTP 535, извлечение портов из Docker Compose, path traversal, логика записей в БД, защита от повторной рассылки, добавление UNIQUE с дублями, commit message. Все запуски с temperature=0.

МодельВремя 8 задачМедианаСкоростьСгенерировано
gemma3:4b147,1 с9,4 с9,40 ток/с1202 токена
gemma2:2b109,1 с13,8 с14,19 ток/с1328 токенов

Обе модели работали на CPU — MacBook Pro 2019 с Radeon Pro 5300M (4 ГБ VRAM), но Ollama показывала 100% CPU. GPU не задействовался.

На механических задачах всё нормально: порты из Compose извлечены верно, Redis назван зависимостью, SMTP 535 объяснён, commit message написан.

На security review начались проблемы. Фрагмент с path traversal:

python
target = Path('/srv/uploads') / upload.filename
with target.open('wb') as output:
    output.write(await upload.read())

gemma3:4b упомянула directory traversal, но главным исправлением предложила заменить wb на w. Опасное имя файла осталось опасным, а загрузка бинарных файлов дополнительно сломалась. gemma2:2b правильно указала на upload.filename, но посоветовала Path.joinpath() — функция соединяет пути, но не обезвреживает ../../etc/passwd.

На задаче с логикой записей 4B-модель предложила перевести русское сообщение об ошибке на английский, проигнорировав настоящую проблему: запрос учитывает любую старую запись, и после первого занятия пользователь теряет возможность записываться навсегда.

Вывод по качеству: маленькие модели справляются с механической обработкой текста, но не с ревью безопасности и предметной логикой.

Куда исчезла экономия: разбор токенов

Одна и та же задача — объяснить SMTP 535 и назвать первую проверку — прогнана тремя маршрутами:

МаршрутВремяОблачный inputОблачный outputЛокальные токены
Ollama напрямую6,1 с0071 + 44
Codex напрямую8,8 с14 309490
Codex → MCP → Ollama → Codex21,4 с29 30138471 + 44

Маршрут через MCP использовал примерно вдвое больше облачного input, чем прямой Codex. Причина структурная: локальная модель не заменяет облачный проход, а добавляет к нему дополнительные. Codex сначала читает запрос и решает вызвать инструмент — это один полный inference со всей историей. Потом читает ответ Gemma и формирует итог — ещё один. Каждый такой «поворот» тянет за собой весь накопленный контекст.

Это подтверждается известной проблемой Codex: каждый вызов инструмента возвращает управление модели и запускает новую полную инференцию с полной историей. В одном из публичных тредов 28 из 34 вызовов инструментов оказались связаны с ожиданием и опросом статуса. Промпт-кэширование Codex CLI чувствительно к порядку инструментов: стоит поменять конфигурацию или добавить новый MCP-сервер — кэш сбрасывается и стоимость растёт.

Где это ломается

Кэш слетает при любом изменении конфигурации. Добавили инструмент, поменяли порядок в config.toml — Codex пересчитывает всё с нуля. Экономия на кэше исчезает.

Polling убивает бюджет. Если задача требует нескольких обращений к инструменту (проверка статуса, итерация), каждый вызов — отдельный полный inference. На длинных цепочках расход токенов растёт нелинейно.

Маленькие модели галлюцинируют на безопасности. Результат выглядит убедительно, но содержит ошибки, которые сложно заметить без глубокого знания темы. Использовать вывод gemma3:4b как окончательный ревью — риск.

GPU может не подключиться. Ollama не всегда подхватывает дискретную видеокарту автоматически. Проверьте ollama ps — если видите 100% CPU при наличии GPU, нужна ручная настройка.

Что попробовать дальше

Схема имеет смысл для строго механических задач без итераций: извлечение структурированных данных из текста, форматирование, классификация с заранее известными категориями, генерация commit message по diff. Там один вызов — один ответ, без polling.

Если хочется реально сократить расход — стоит смотреть в сторону промпт-кэширования внутри самого Codex и минимизации числа tool call в сессии, а не в сторону делегирования через MCP.


FAQ

Почему MCP-маршрут дороже прямого Codex?

Каждый вызов инструмента через MCP — это отдельный полный inference Codex со всей историей диалога. Вместо одного прохода получается минимум два: решение вызвать инструмент и обработка его ответа. На коротких задачах накладные расходы превышают любую экономию от локального inference.

Можно ли заставить Ollama использовать GPU на Mac?

Ollama поддерживает Metal на Apple Silicon, но на Intel Mac с дискретной Radeon автоматического подключения GPU может не быть. Проверьте командой ollama ps — в колонке Processor должно быть GPU или GPU+CPU, а не 100% CPU.

bash
ollama ps

Для каких задач локальная модель через MCP всё-таки окупается?

Для однократных механических преобразований без итераций: извлечение портов из конфига, классификация ошибки по коду, генерация commit message. Главное условие — один вызов инструмента на задачу, без polling и без цепочек.

Как разрешить вызов MCP-инструмента без диалога подтверждения?

В ~/.codex/config.toml нужно явно указать approval_mode = "approve" для конкретного инструмента. Без этого Codex заблокирует вызов с ошибкой approval policy is never.

Влияет ли порядок MCP-серверов в конфиге на расход токенов?

Да. Codex CLI использует промпт-кэширование, которое чувствительно к изменениям конфигурации. Если поменять порядок инструментов или добавить новый сервер, кэш сбрасывается и стоимость inference растёт до следующего «прогрева».

Источники