С чего всё началось
После того как delta-merge оказался неподходящим и я перешёл на fresh-from-base, обнаружилась нехватка трейсов. У меня было примерно 1700 hand-crafted трейсов — это полный цикл: система → запрос пользователя → размышление модели → вызов инструмента → наблюдение → следующий шаг → final_answer. И за каждым из них стоит работа: каждый трейс — это итерация с Claude Code, ревью, правки, повторная генерация. На 1700 рабочих трейсов я потратил неделю времени и финансовые ресурсы. Чтобы удвоить — ещё столько же. А мне нужно было покрыть как минимум ещё 5–7 областей, до которых руки тогда не дошли: SSH, продвинутый docker, kubernetes, postgres, мониторинг и логи.
Стало ясно: hand-crafting в чистом виде постоянно клянчить с Claude не получится. Нужны генераторы.
Варианты, которые я рассматривал
1. Внешний API. Очевидное решение: даём модели эталонные примеры, просим сгенерировать ещё. Качество предсказуемо хорошее. Считаем стоимость для ~4000 трейсов (~6K input + 3.5K output на трейс) — берём флагманы:
Провайдер | Модель | In $/1M | Out $/1M | ИТОГО |
|---|---|---|---|---|
Anthropic | Opus 4.7 | $5 | $25 | ~$482 |
OpenAI | GPT-5.5 | $5 | $30 | ~$554 |
Qwen | Qwen3 Max | $0.78 | $3.90 | ~$75 |
Qwen3 Max за $75 выглядит почти бесплатным — но он мне не подходит. Моя базовая модель — qwen3:14b, и Qwen3 Max из того же семейства. Дистилляция через "большую" модель работает только тогда, когда она даёт другой взгляд на задачу — другие паттерны рассуждений, другую структуру ответа, другую логику подачи. Если "учитель" и "ученик" из одной семьи, ты получишь те же самые паттерны, те же ошибки, такое же поведение — просто завёрнутые в более продвинутый формат. Дистилляция превращается в дублирование: модель учится у самой себя и не приобретает ничего нового. Поэтому Qwen-семейство для роли учителя отпадает по архитектурным причинам, а не по цене.
Opus и GPT-5.5 — довольно дорого, а если ещё учесть, что всё это потихоньку блокируется и скоро мы все переедем на проксирующие сервисы вроде RouterAI, цены станут просто космическими. Сейчас за тот же прогон 4000 трейсов через RouterAI получается: GPT-5.5 ~53 000 ₽, остальные модели можете сами посчитать на сайте.
2. HuggingFace бесплатно. Готовые датасеты, об этом ниже — спойлер: ничего не вышло.
3. Локальная дистилляция через большую модель. Берём сырой датасет → подаём в большую локальную модель вместе с эталонными примерами → просим переоформить в наш формат → пропускаем через валидатор. На локалке и бесплатно — никому не платим и на сторону ничего не отдаем.
Идея дистилляции
Пример:
SYSTEM: You are a converter. Given a raw <instruction, response> pair, output a JSON agent-trace in the EXACT format shown in the examples. Format requirements (HARD): - Output ONE JSON object: {"messages": [...], "meta": {...}} - system identical to the one in examples - assistant turns: "Thought: ...\n<code>tool(...)</code>" - user turns after assistant are "Observation: ..." - Last assistant turn calls final_answer(...) - One file per write_file call (if scaffolding) EXAMPLES (5 anchor traces from base-7 champion): [5 эталонных трейсов в JSON] CONVERT THIS: instruction: "<сырая инструкция>" response_hint: "<сырой ответ как подсказка>" Output JSON only.
Берём «мусор» (это не к тому, что плохо — а к тому, что много ненужного для нашей конкретной задачи) — сырые трейсы, в которых смешано всё подряд: код, алгоритмы, паттерны, фронтенд. Берём наши 5 эталонных hand-crafting трейсов как образец. Скармливаем всё это «большой» модели и просим переоформить сырой пример в наш формат.
Дальше — фильтр. Каждый трейс, который выдала модель, прогоняем через набор автоматических проверок: распарсился ли JSON, есть ли нужные поля, правильный ли порядок шагов, вызывался ли tool calling, есть ли final_answer в конце. Что прошло — в обучающий датасет. Что не прошло — в файл записываем с отказами и причиной.
Какую модель брать для дистилляции? Бенчмарк 4 кандидатов
Я перебрал много моделей, оставил тех, кто «смог». Финалисты:
Модель | Quant | VRAM | Speed | Семейство |
|---|---|---|---|---|
| Q5_K_M | ~22 GB | 25–30 tok/s | Qwen |
| Q4 default | ~17 GB | 40–50 tok/s | Qwen (newer) |
| Q4 default | ~19 GB | 25–30 tok/s | |
| Q6_K | ~14 GB | 50–60 tok/s | DeepSeek |
Qwen-модели я оставил в тестировании специально — для ребят, кто пойдёт повторять путь со своей моделью на базе gemma или какой-нибудь другой. Почему мне Qwen не подходит — писал выше.
Методология бенчмарка
Что тестируем. Взял 20 примеров из публичного датасета: 5 коротких, 5 средних, 5 длинных и 5 случайных — чтобы покрыть разные размеры задач, а не получить случайный перекос.
Эталоны. 5 «образцовых» трейсов из моей лучшей версии
oni:base-7.v2— по одному на каждый шаблон, который должна уметь модель: цепочка bash-команд (bash_chain), создание нескольких файлов (multi_file_scaffold), «написал → проверил» (write_then_validate), «нашёл ошибку → исправил» (validate_fix) и честное «не смог» (honest_failure).Объём прогона. 4 модели × 20 примеров = 80 тестов. Хватает, чтобы увидеть стабильность, а не разовое попадание.
Параметры генерации.
temperature=0.7,num_predict=4000,num_ctx=16384— окно контекста: системный промпт + 5 эталонов + сырой пример 5–7 тысяч токенов, плюс место под ответ.
Оценка качества: 8 параметров
Каждый сгенерированный trace оценивается по 8 параметрам с весами:
Метрика | Вес | Что меряет |
|---|---|---|
| 1.0 | Output это валидный JSON |
| 1.0 | ≥4 messages в array (мин. system+user+assistant+user) |
| 0.5 | system role есть |
| 1.0 | каждый assistant turn = Thought + |
| 0.8 | хотя бы один из 5 tools вызван |
| 1.0 | последний assistant = |
| 0.7 | была проверка перед final_answer |
| 0.3 | 3–15 assistant turns |
Итоговый балл — от 0 до 100. Порог отсечения — 84.8: всё, что ниже, считаем «недостаточно чистым» и в обучение не пускаем. Порог взят из практики — на меньших значениях обученные модели стабильно валились.
Результаты бенчмарка
Модель | PILOT(5) | FULL(20) | 100% | 0.0% | ИТОГ |
|---|---|---|---|---|---|
| 95.1 | 92.0 | 55% | 0% | 🏆 WINNER |
| 91.8 | 72.7 | 30% | 15% | runner-up, но проблемы |
| 84.8 | — | — | — | пилот стабильный |
| 44.8 | FAIL | — | — | контекст слишком мал |
Победитель — gemma4:31b. Не просто выиграл, а выполнил почти идеально: 55% трейсов , а средний балл по всем 20 примерам — 92 из 100.
Сложности
num_ctx=8192 ломает gemma4
Gemma 4 на 24 GB GPU при num_ctx=16384 не помещается целиком в VRAM и уходит в CPU offload — распределение GPU/CPU выходит 13/87. Это медленно: ~235 секунд на тестовом прогоне вместо ожидаемых 80–100. Я попробовал уменьшить контекст до 8192, чтобы модель влезла на GPU целиком.
Получил 9× ускорение на первых ~15 примерах. И тут же acceptance rate обвалился до 0%. Все следующие примеры уходили в reject — модель попадала в infinite loops или обрезала output на полпути.
Урок: 8K контекста маловато. Промпт вместе с 5 few-shot-примерами уже весит 5–7 тысяч токенов, плюс модель должна сгенерить ещё 3–4 тысячи в ответе — итого под 10 тысяч. В 8K это просто не помещается: модель обрезает ответ, либо уходит в циклы. Лекарство простое: возвращаем num_ctx=16384 и для подстраховки добавляем repeat_penalty=1.15. Платим скоростью — получаем стабильность.
Архитектура pipeline
┌─ Source data (raw HF/публичный источник) ──────────────┐ │ raw/data.jsonl │ │ N items × {instruction, response} │ └──────────────┬─────────────────────────────────────────┘ │ ▼ ┌─ Few-shot reference ───────────────────────────────────┐ │ meta/few_shot_reference.jsonl │ │ 5 эталонных трейсов из моей лучшей версии модели │ └──────────────┬─────────────────────────────────────────┘ │ ▼ ┌─ Teacher (gemma4:31b in Ollama) ───────────────────────┐ │ prompt = system + 5 few-shot + raw item │ │ → multi-turn JSON response │ └──────────────┬─────────────────────────────────────────┘ │ ▼ ┌─ Validator ────────────────────────────────────────────┐ │ json.loads → 8 metric scoring → composite │ │ if score >= 84.8: ACCEPT else: REJECT │ └──────────────┬─────────────────────────────────────────┘ │ ▼ ┌─ Output ───────────────────────────────────────────────┐ │ data/distilled_<topic>/data.jsonl (accepted only) │ │ data/distilled_<topic>/rejected/ (с причинами) │ │ data/distilled_<topic>/state.json (для resume) │ └────────────────────────────────────────────────────────┘
На вход берём сырой датасет, рядом кладём наши 5 эталонных трейсов как образец, отдаём всё это teacher-модели и просим её переоформить сырой пример в наш формат. То, что вернулось, гоняем через валидатор. Заодно сохраняем state.json, чтобы при падении можно было продолжить с того же места, а не с нуля.
В таком режиме я неделю гонял дистилляцию по ночам. Результат — 3042 чистых трейса по формату, готовые к обучению.
Magicoder: первый прогон, первый провал
Взял Magicoder — там много примеров с bash, docker, ssh (это как раз наши дыры в агенте, то, что я искал). Через keyword-фильтр выдрал подмножества по интересующим меня темам, прогнал через pipeline.
Результаты дистилляции по subset'ам — 15 категорий:
Subset Raw Accepted % ───────────────────────────────────────────────────────── distilled_js_only 400 351 88% ⭐ best distilled_ci_cd_specific 250 215 86% distilled_docker_advanced 300 248 83% distilled_bash_pipes 300 243 81% distilled_eslint 10 8 80% distilled_express 250 199 80% distilled_frontend_fullstack 400 310 78% distilled_solid 250 192 77% distilled_ts_only 400 308 77% distilled_ssh 300 225 75% distilled_design_patterns 250 182 73% distilled_django 300 199 66% distilled_postgres_advanced 250 150 60% distilled_microservices 250 133 53% distilled_kubernetes 200 79 40% worst ───────────────────────────────────────────────────────── ИТОГО: 4110 3042 74% avg
74% acceptance rate — то что нужно. Я добавил 225 distilled SSH-трейсов в oni:base-norm.v2, обучил.
Тестирование
На простых SSH-задачах галлюцинировала output — фабриковала результат вместо реального exec'а. Получили oni:base-console.v2 (2229 трейсов = base-norm + 225 Magicoder SSH) с hallucinated success: модель формально проходила тесты, но на realworld-тестах выдавала несуществующие результаты команд. Регрессия по сравнению с предыдущим чемпионом — заметная. Следующий заход (oni:base-ssh-clean.v2) пришлось убить на 14% обучения — стало ясно, что нет смысла продолжать на Magicoder. Что вообще пошло не так?
Что произошло
Я проанализировал результат дистилляции и понял.
Magicoder — это в основном алгоритмические задачи в обёртке. Задачи там примерно такие: «напишите функцию, которая фильтрует список по условию», «реализуйте бинарный поиск», «сделайте утилиту для подсчёта частоты слов». Формально под keyword-фильтр bash_pipes попадают примеры, где где-то в ответах встречается | или grep. Но смысл примера — это leetcode-стайл алгоритмы, а не работа с bash.
Моя дистилляция упаковала это в идеальный JSON-формат с Thought/Observation/final_answer. По формату — 100%. По смыслу — модель училась решать задачи-алгоритмы, а не управлять серверами.
Когда я подал такие трейсы на обучение моему агенту, он научился очень красиво рассуждать про алгоритмы, который не умеет выполнять, и галлюцинировать вывод команд, которых не запускал.
Дальше прошёлся по каждому принятому трейсу: реально ли там то, что покрывает необходимую область нашего агента, или это просто алгоритмическая задачка с упоминанием bash.
Subset Всего On-topic % ───────────────────────────────────────────────────────── distilled_design_patterns 182 2 1% ☠️ distilled_ci_cd_specific 215 13 6% ☠️ distilled_ssh 225 16 7% ☠️ distilled_bash_pipes 243 22 9% ☠️ distilled_js_only 351 65 19% ⚠️ distilled_microservices 133 42 32% ⚠️ distilled_solid 192 68 35% ⚠️ distilled_ts_only 308 110 36% ⚠️ distilled_docker_advanced 248 105 42% distilled_express 199 90 45% distilled_postgres_advanced 150 85 57% distilled_kubernetes 79 58 73% ✅ distilled_django 199 153 77% ✅ distilled_eslint 8 8 100% ✅ distilled_frontend_fullstack 310 310 100% ✅ ───────────────────────────────────────────────────────── ИТОГО 3042 1147 38%
3042 принятых трейса, 1147 реально по теме. 38% в среднем. Для критичных DevOps-тем (SSH, CI/CD, design patterns, bash pipes) — менее 10%.
Давайте разберём это в цифрах. «Дистиллятор» отработал корректно: 74% acceptance — это не баг, а хорошая работа по нашим критериям. Все 8 параметров валидатора срабатывали как задумано: JSON парсится, структура правильная, tool calls есть, final_answer на месте. С точки зрения инфраструктуры — успех.
С точки зрения полезности для агента — 38% on-topic. То есть на каждый рабочий трейс приходится 1.65 мусорных, отвалидированных как годные. И это среднее по больнице. А разброс по темам — драматический:
С одной стороны —
frontend_fullstack100% иeslint100%. Это не фильтр сработал хорошо. Просто в Magicoder реально много фронтенд-задач, поэтому keyword-фильтр в этих темах почти не промахивался — нужный контент там был сам по себе.С другой —
design_patterns1% иci_cd_specific6%. Из 182 принятых трейсов про паттерны — реально полезных два. Два, Карл. На остальные 180 я потратил время и получил упакованный по нашему формату Java-учебник.
Если развернуть в эффективную стоимость трейса: на DevOps-критичных темах (где у меня и были основные дыры в покрытии) реальный полезных оказалось — 53 трейса из 925 (SSH + CI/CD + bash_pipes + design_patterns суммарно). 5.7%. То есть 94% затраченного времени ушло в никуда.
Именно поэтому модель oni:base-console.v2 галлюцинировала. Она училась не работать с SSH — она училась решать алгоритмические задачи. Дистилляция сработала — просто данные оказался неподходящими. Урок на будущее.
Я думал, что keyword-фильтр сам отделит нужное — не сработало.
Regex bash|ssh|docker по 110K примеров даёт тысячи совпадений, но в большинстве — упоминания, а не использование. «Напишите функцию, которая разбирает аргументы как в bash» формально проходит фильтр. Валидатор смотрит структуру JSON, не смысл. Acceptance высокий, только толку ноль.
Решение: ищите подходящие датасеты
Когда стало понятно, что Magicoder не подходит, я стал искать другой источник.
Критерии:
Реальные задачи — ssh, nginx, docker, postgres и т.д., которые нужны вашему агенту
Объём — несколько тысяч элементов минимум.
Структурированность — желательно теги или категории, чтобы можно было быстро отфильтровать.
Конкретные источники раскрывать пока не буду — просто не уверен, что они сработают на других задачах. Обжечься, как это было с Magicoder, я больше не хочу — поэтому заранее не рекомендую.
Главное правило, которое я для себя вынес: прежде чем тащить любой сторонний датасет в дистилляцию, удостоверьтесь, что он действительно закрывает нужную область для вашего агента и что внутри лежат реальные трейсы по теме, а не мусор — материал, который агенту никак не пригодится. Сделать это просто: вытащите 20–30 примеров из конкретной области, откройте и прочитайте глазами. Если 7 из 10 не имеют отношения к задаче агента — keyword-фильтр обманул, дальше можно не идти.
Погнали дальше
Прогнал дистилляцию на новом источнике с GitHub. Та же модель "Дистиллятор", тот же валидатор. Подобрал 16 тем по областям — ssh, nginx, docker, postgres и т.д.
Acceptance rate: ~76% в среднем по 16 субсетам, разброс от 64% до 88%. On-topic rate: ~95%.
Чтобы было понятно, как это выглядит на практике: процесс шёл 5 ночей. В дистилляцию попали: ssh, nginx, docker, systemd, postgres, диагностические команды, мониторинг, dns, ssl, storage, прокси, бэкапы, vpn, файрвол и так далее. Для каждой темы сначала pilot на 30 примерах (с порогом ≥50% acceptance — иначе тему отсекаем), потом prod на 200–500. Из 17 prod-прогонов pilot прошли все 16 (один тег слили в общий, отсюда -1).
Показатели:
Лучший pilot — _backup, 30/30 (100%). Тема настолько четко описана в источнике, что валидатор не отверг ни одного примера.
Лучший prod acceptance — firewall (88%) и monitoring/bash_general (87%). Чёткие, технические, без полёта мысли.
Худший prod acceptance — nginx (64%). Много мусора — формально nginx, по сути фронтенд.
Самый ценный для меня — _diag (247 трейсов про
whoami,lsof,dpkg,/etc/os-release). Это конкретная дыра в покрытии Oni, T7-T12 диагностические тесты раньше валились — теперь должны закрыться.
Итого: 3899 принятых трейсов из ~5000 сырых. Сравним с Magicoder: 3042 из 4110 сырых → формально схожий acceptance, но on-topic у нового источника в 2.5 раза выше. То есть из ~3000 принятых трейсов реально полезных стало ~2850 вместо 1147.
Финальный результат
Добавил 351 distilled-трейс из этого источника в oni:base-norm.v2, провёл нормализацию (5 операций над train.jsonl, про которые писал в предыдущей статье), обучил.
oni:base-clean.v2, 2107 трейсов, обучение с чистого Qwen3:14B, 2 epochs.
Realworld тесты: 10/10. Без галлюцинаций. Без выдуманного вывода команд. С честным honest_failure, когда модель не уверена.
Это первый раз за полтора месяца, когда модель давала стабильный результат на реальных задачах, а не точечный пик на узком тесте.
Главный вывод
Я собрал рабочую дистилляцию с нуля — на своём железе, с воспроизводимым pipeline и acceptance rate под 76%. Пользуйтесь. Все шишки, которые набил по пути, я вам рассказал: и про num_ctx, и про teacher из своего семейства, и про keyword-фильтр по большому датасету. Главное — будьте внимательны и не суйте в дистилляцию мусор. Если на входе материал не по теме агента — на выходе ничего полезного не получится.
Что дальше
Как говорит мой друг и учитель в разработке: «Совет начинающим — начните». А дальше посмотрим. Продолжаю обучать своего ИИ-агента. Если всё получится — следующие статьи будут уже про целую мультиагентность, где мой oni-agent будет руками, а вокруг него вырастим ещё агентов: для оркестрирования, тестирования и написания кода.
Если интересно следить — @oni_devops_lab.
— makarsuperstar, 2026
