Настройка локального кодинг-агента: oh-my-pi и Qwen3.8-27B на двух RTX 3090

· 10 мин чтения
local-llm vllm coding-agent oh-my-pi agent-tuning
Настройка локального кодинг-агента: oh-my-pi и Qwen3.8-27B на двух RTX 3090

Настройки кодинг-агента по умолчанию рассчитаны на большую облачную модель с большим запасом мощности. На домашней машине они решают, ответит агент за пять секунд или за пять минут. Автор потратил день, записывая трафик собственных сессий, и нашёл пять параметров, которые дали почти весь прирост скорости. Схема: oh-my-pi (в тексте — omp) как агент, Qwen3.8-27B как модель, vLLM как сервер на двух подержанных RTX 3090.

Параметр Было Стало
Доля промпта из кэша 55 % 95 %
Среднее ожидание первого слова 26–28 с 7,3 с
Худшее ожидание 514 с 54 с
Самые медленные 10 % ходов от 390 с от 77 с

Локальная модель или облачная

Облачная модель работает в чужом дата-центре, локальная — на вашей машине.

Локальная модель Облачная модель
Куда уходит код остаётся на машине уходит провайдеру с каждым запросом
За что платите железо и электричество сразу подписка или оплата по токенам
Ограничения сколько выдерживают видеокарты лимиты запросов и квоты
Качество хорошие открытые модели, на сложных задачах уступают лучшим облачным самые сильные доступные модели
Скорость в длинных диалогах медленнее и очень зависит от настроек быстро, провайдер всё настроил за вас
Кто следит за работой вы провайдер
Смена модели когда вы решите когда решит провайдер

Локальный запуск даёт приватность, предсказуемую стоимость и контроль: код не покидает сеть, а лимит запросов не обрывает задачу на середине. Плата за это — качество: фронтир-модели Anthropic и OpenAI намного крупнее того, что помещается под стол. С моделью на 27B параметров результат лучше, когда вы даёте агенту узкую задачу и чаще проверяете его работу. Домашняя конфигурация автора выдаёт 54–60 токенов в секунду, когда видеокарта отдана одному запросу, и 21–37 токенов в секунду, когда запросов несколько.

Префикс-кэш решает почти всё

У языковой модели нет памяти между ходами. Каждый раз сервер отдаёт ей весь диалог целиком: инструкции, описания инструментов, открытые файлы и вывод команд. Сначала модель проходит по этому тексту — это префилл, — а потом печатает ответ по одному токену. Этот этап называют декод.

Представьте человека, который перечитывает длинную ветку письма с самого начала перед каждым ответом. Префилл — это перечитывание, декод — это печать. На домашней видеокарте медленно именно перечитывание: сервер обрабатывает примерно 700–900 токенов в секунду на один запрос, а диалог агента легко доходит до 100 000 токенов. Полное перечитывание занимает от полутора до трёх минут.

Префикс-кэш снимает почти всю эту работу. Сервер сохраняет то, что уже посчитал для начала диалога. Если следующий ход начинается с точно такого же текста плюс новые сообщения, пересчитывать нужно только новую часть. Попавший в кэш ход стартует за секунды, не попавший — за минуты. Поэтому автор выбрал vLLM: он обслуживает несколько агентов из одной копии модели, держит большой префикс-кэш между ними и принимает бюджет размышлений на каждый запрос.

Пять настроек, которые дали основной эффект

  1. Явный уровень усилия на каждой роли, чтобы запрос не проваливался в самый дорогой режим модели.
  2. Бюджет размышлений 6000–7500 токенов, который уходит как thinking_token_budget.
  3. maxTokens поднят с 8192 до 32768, чтобы запись файлов успевала закончиться.
  4. Вывод инструментов больше 10 КБ уходит в файл, а не вставляется в диалог целиком.
  5. Не больше 4 субагентов сразу и режим контекста «только дописывать», который не ломает кэш.

Куда уходило время

Агент думал пять минут, ничего не делая

Qwen3.8 — рассуждающая модель. Перед ответом она пишет приватную цепочку размышлений, а объём размышлений задаёт уровень усилия: low, medium или xhigh. Утром самые медленные 10 % ходов шли по 390 секунд и дольше, а худший — 1110 секунд. Скорость декода была обычной, просто модель производила слишком много текста: 89 % её вывода приходилось на размышления.

Часть маленьких помощников внутри omp вообще не отправляла уровень усилия. Официальный шаблон диалога модели трактует запрос без уровня как xhigh, самый дорогой режим: по тесту сообщества xhigh тратит примерно в 6 раз больше токенов и в 7 раз больше времени, чем medium. При этом уровень усилия — только подсказка в промпте, и ничто не ограничивало длину размышлений.

Помогают обе вещи: явный уровень усилия на каждой роли и бюджет размышлений — жёсткий предел на число токенов размышлений в одном ответе, который omp передаёт серверу как thinking_token_budget. Сервер должен его поддерживать: vLLM умеет, если запущен с парсером рассуждений и с настройкой рассуждений. На значении 64 модель прекращала размышления на 63 токенах и нормально давала ответ.

При уровне усилия medium по умолчанию и бюджете в 6000 токенов:

До После
Средняя длина ответа 2481 токен 867 токенов
Самые медленные 10 % ходов от 390 с от 77 с

Позже бюджет подняли до 7500, а субагентам оставили low.

Файлы записывались наполовину

maxTokens — это ограничение длины одного ответа в omp. Оно покрывает и размышления, и видимый ответ, и содержимое любого файла, который агент пишет через инструмент. При бюджете в 6000 токенов на всё остальное оставалось около 2200, а файл примерно на 8000 символов требует 2500–3000 токенов.

Шесть ответов остановились ровно на 8192 токенах посреди записи файла. Сервер отметил их как завершённые вызовы инструментов, а не как обрыв, и omp выполнил сломанный вывод всё равно: два файла сохранились наполовину с сообщением «Successfully wrote», а ещё четыре вызова упали с запутанными ошибками.

Лечение простое: maxTokens = 32768. Размышления по-прежнему ограничены бюджетом, а ответы и запись файлов получают место, чтобы закончиться. В следующем прогоне обрезанных записей не было, включая ответы на 10 286 и 13 964 токена, которые раньше были бы обрезаны.

Вывод инструментов забивал диалог

Всё, что возвращает инструмент, становится частью промпта, который модели нужно перечитать. omp держит результат прямо в диалоге, пока он не больше 50 КБ. В худших ходах это добавляло по три-пять результатов примерно по 40 000 символов и 10–50 секунд префилла до начала ответа.

Снижение порога до 5 КБ не сломало работу: вывод инструментов на ход уполовинился, а полную версию агент забирал только для 14–17 % сокращённых результатов. Сейчас автор держит 10 КБ — и как порог, и как размер начала и конца, которые остаются в диалоге.

Агент «забывал» то, что только что прочитал

Несколько раз в час ход, который должен был почти целиком попасть в кэш, возвращался почти без кэша и минутами думал над первым словом. Кэш работает только когда новый промпт начинается ровно с того же текста: меняется что-то в начале — сервер пересчитывает всё, что после этого места. Таких мест автор нашёл три:

  • Субагенты omp получают список других агентов и их статусы (работает, простаивает, отложен) прямо в середине инструкций. Каждая смена статуса меняла промпт в начале. В худшем случае главный агент вернулся через 24 минуты к промпту примерно на 580 000 символов и получил из кэша только 14 %.
  • Сжатие контекста — это когда omp пересказывает старую историю, потому что диалог стал слишком длинным. Оно заменяет начало диалога пересказом и по замыслу приводит к полному пересчёту.
  • Описания инструментов приходили с полями в разном порядке в 8 % ходов, хотя сами инструменты не менялись. Это оказался баг в Bifrost — шлюзе, который стоит между omp и vLLM.

Живой список статусов отключить нельзя, он встроен в промпт субагента. Зато можно сделать сжатие редким: указать модели полное контекстное окно (262 144 токена для Qwen3.8), чтобы агент не считал диалог заполненным раньше времени, и поднять порог сжатия. И включить appendOnlyContext — его автоматический режим работает только для localhost и приватных IP, так что для сервера с обычным именем нужно ставить on.

Слишком много помощников сразу

По умолчанию omp разрешает до 32 субагентов одновременно. Для облачного сервиса это разумно, но у домашней видеокарты скорость префилла фиксированная: десять субагентов, которые читают большие файлы, закончат работу не быстрее трёх, потому что делят одну и ту же скорость на всех.

В лучшем прогоне сервер был занят всего тремя запросами, а память заполнена максимум на 37 % — запас был. Агенты, которые в основном читают файлы и ищут, проводят время в префилле, поэтому 4 — хорошее начало, а те, что пишут код, можно поднять чуть выше. Плюс длинный таймаут на первое слово ответа: отказ на старте выбрасывает всю работу. А если между omp и сервером есть прослойка, неисправные запросы должен повторять только один слой — два слоя удваивают нагрузку.

Железо, сервер и разделение моделей

Модель Qwen3.8-27B работает на двух подержанных RTX 3090 по 24 ГБ, без NVLink, в обычных слотах PCIe. Это 4-битная версия W4A16 AutoRound, которая помещается на одну карту; автор делит её между обеими. На каждую карту помещается два-три длинных агентских диалога одновременно. Карты ограничены 400 и 450 Вт, вместе это 850 Вт.

Сервером может быть любой с OpenAI-совместимым API. Автор использует vLLM в образе syv-ai для RTX 3090 и дальше считает сервер чёрным ящиком.

omp умеет отправлять разные задачи разным моделям: облачная фронтир-модель планирует и делает сложные ревью, а локальная водит субагентов, которые читают файлы и делают рутинные правки. У себя автор разделил так же, только большую роль играет Qwen3.8 Flash Next — модель постарше в том же семействе, которая ведёт главного агента, планирование и ревью. Она работает на мини-ПК Minisforum MS-S1 Max с 128 ГБ единой памяти под управлением Halogen — закрытого движка, написанного под это семейство моделей.

Маршрутизация идёт через шлюз Bifrost: главный агент, планирование и ревью уходят на Flash Next, субагенты и мелкие задачи — на vLLM с Qwen3.8-27B на двух RTX 3090.

Установка и настройка

Установить omp можно любым из трёх способов:

curl -fsSL https://omp.sh/install | sh
brew install can1357/tap/omp
bun install -g @oh-my-pi/pi-coding-agent

Собственных провайдеров omp читает из ~/.omp/agent/models.yml. Адрес в примере нужно заменить на свой, а id должен совпадать с именем модели, которое отдаёт сервер.

providers:
  local:
    baseUrl: http://localhost:8000/v1  # OpenAI-совместимый URL
    auth: none                         # локально ключ не нужен
    api: openai-completions
    models:
      - id: qwen3.8-27b          # имя модели на сервере
        name: qwen3.8-27b
        contextWindow: 262144    # полное окно, сжатие позже
        maxTokens: 32768         # размышления плюс большие записи файлов
        reasoning: true
        # только за Bifrost, см. ниже
        # headers:
        #   x-bf-passthrough-extra-params: S0
        thinking:
          mode: effort
          efforts: [low, medium, xhigh]
          defaultLevel: medium
        compat:
          extraBody:
            thinking_token_budget: 7500  # требует поддержки сервера

Проверить, что omp видит модель, можно командой omp models local. У автора Bifrost молча выбрасывал thinking_token_budget, пока omp не начал слать заголовок x-bf-passthrough-extra-params, который в конфиге выше закомментирован; когда omp говорит с vLLM напрямую, заголовок убирают. Замечание к редакции от 23 сентября 2026 года: в более ранней версии этой записи был ключ timeout: 900, но такого ключа на уровне модели в omp нет — он просто игнорировался. Работают два таймаута потока в config.yml.

Остальное живёт в ~/.omp/agent/config.yml. Суффикс :level в селекторе роли важнее defaultLevel из записи модели и defaultThinkingLevel, которые применяются только к запросам без уровня. Чтобы отправить роль на облачную модель, вместо её селектора ставят модель того провайдера.

modelRoles:
  default: local/qwen3.8-27b:medium  # главный агент
  plan: local/qwen3.8-27b:xhigh      # режим планирования, думает один раз долго
  slow: local/qwen3.8-27b:xhigh      # ревьюер
  task: local/qwen3.8-27b:low        # общие субагенты
  smol: local/qwen3.8-27b:low        # разведка, простые правки
  tiny: local/qwen3.8-27b:low        # заголовки, память, фон
  commit: local/qwen3.8-27b:low      # сообщения коммитов

defaultThinkingLevel: low  # запросы, которые не покрыл ни одна роль

task:
  maxConcurrency: 4  # субагентов одновременно, на одной GPU меньше

tools:
  artifactSpillThreshold: 10  # КБ, большие результаты идут в файл
  artifactHeadBytes: 10       # КБ начала, которые остаются в диалоге
  artifactTailBytes: 10       # КБ конца, которые остаются в диалоге

compaction:
  thresholdTokens: 200000  # пересказывать только почти полный диалог

provider:
  appendOnlyContext: "on"  # только дописывать в диалог

providers:
  streamFirstEventTimeoutSeconds: 900  # 15 минут на первое слово
  streamIdleTimeoutSeconds: 900        # и на паузы в ответе

Как проверить, что всё работает

Первый ход нового диалога обычно самый медленный: в кэше пока почти ничего нет. Дальше ходы должны стартовать за секунды даже когда диалог вырос больше 100 000 токенов. Если ход в глубине длинного диалога думает минутами, значит изменилось что-то в начале промпта — скорее всего сжатие контекста или новый субагент.

Если сервер показывает долю токенов промпта из кэша, за ней стоит следить: у автора она выросла с 55 % до 95 %, а любое резкое падение стоит разбирать. Ответ, длина которого равна maxTokens в момент, когда агент пишет файл, почти наверняка означает, что файл обрезали.

Источник: https://doug.sh/posts/tuning-a-local-coding-agent-oh-my-pi/