С чего всё началось
После того как 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
Автор: makarsuperstar


