- BrainTools - https://www.braintools.ru -

Хотел упростить мониторинг проектов и в отпуск — пришлось обучать свой LLM.Часть 3.Дистилляция

С чего всё началось

После того как 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 из того же семейства. Дистилляция через “большую” модель работает только тогда, когда она даёт другой взгляд на задачу — другие паттерны рассуждений, другую структуру ответа, другую логику [1] подачи. Если “учитель” и “ученик” из одной семьи, ты получишь те же самые паттерны, те же ошибки [2], такое же поведение [3] — просто завёрнутые в более продвинутый формат. Дистилляция превращается в дублирование: модель учится у самой себя и не приобретает ничего нового. Поэтому Qwen-семейство для роли учителя отпадает по архитектурным причинам, а не по цене.

Opus и GPT-5.5 — довольно дорого, а если ещё учесть, что всё это потихоньку блокируется и скоро мы все переедем на проксирующие сервисы вроде RouterAI [4], цены станут просто космическими. Сейчас за тот же прогон 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

Семейство

qwen2.5-coder:32b-instruct-q5_K_M

Q5_K_M

~22 GB

25–30 tok/s

Qwen

qwen3.6:27b

Q4 default

~17 GB

40–50 tok/s

Qwen (newer)

gemma4:31b

Q4 default

~19 GB

25–30 tok/s

Google

deepseek-coder-v2:16b-lite-q6_K

Q6_K

~14 GB

50–60 tok/s

DeepSeek

Qwen-модели я оставил в тестировании специально — для ребят, кто пойдёт повторять [5] путь со своей моделью на базе 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 параметрам с весами:

Метрика

Вес

Что меряет

json_parses

1.0

Output это валидный JSON {messages, meta}

has_messages

1.0

≥4 messages в array (мин. system+user+assistant+user)

system_present

0.5

system role есть

assistant_has_thought_and_code

1.0

каждый assistant turn = Thought + <code>

tool_call_present

0.8

хотя бы один из 5 tools вызван

final_answer_present

1.0

последний assistant = final_answer

verification_before_final

0.7

была проверка перед final_answer

step_count_in_range

0.3

3–15 assistant turns

Итоговый балл — от 0 до 100. Порог отсечения — 84.8: всё, что ниже, считаем «недостаточно чистым» и в обучение [6] не пускаем. Порог взят из практики — на меньших значениях обученные модели стабильно валились.

Результаты бенчмарка

Модель

PILOT(5)

FULL(20)

100%

0.0%

ИТОГ

gemma4:31b

95.1

92.0

55%

0%

🏆 WINNER

qwen3.6:27b

91.8

72.7

30%

15%

runner-up, но проблемы

qwen2.5-coder:32b-instruct-q5_K_M

84.8

пилот стабильный

deepseek-coder-v2:16b-lite-q6_K

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 на месте. С точки зрения [7] инфраструктуры — успех.

С точки зрения полезности для агента — 38% on-topic. То есть на каждый рабочий трейс приходится 1.65 мусорных, отвалидированных как годные. И это среднее по больнице. А разброс по темам — драматический:

  • С одной стороны — frontend_fullstack 100% и eslint 100%. Это не фильтр сработал хорошо. Просто в Magicoder реально много фронтенд-задач, поэтому keyword-фильтр в этих темах почти не промахивался — нужный контент там был сам по себе.

  • С другой — design_patterns 1% и ci_cd_specific 6%. Из 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, про которые писал в предыдущей статье [8]), обучил.

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 [9].

— makarsuperstar, 2026

Автор: makarsuperstar

Источник [10]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/30095

URLs in this post:

[1] логику: http://www.braintools.ru/article/7640

[2] ошибки: http://www.braintools.ru/article/4192

[3] поведение: http://www.braintools.ru/article/9372

[4] RouterAI: https://routerai.ru/

[5] повторять: http://www.braintools.ru/article/4012

[6] обучение: http://www.braintools.ru/article/5125

[7] зрения: http://www.braintools.ru/article/6238

[8] предыдущей статье: https://habr.com/ru/articles/1033426/

[9] @oni_devops_lab: https://t.me/oni_devops_lab

[10] Источник: https://habr.com/ru/articles/1033434/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1033434

www.BrainTools.ru

Rambler's Top100