Продакшн-разработка ИИ-агентов · Модуль 6 · Урок 6.3

Глава 14. Адаптация моделей (опционально)

Цели главы

После этой главы вы сможете:

  • Понимать, когда дообучение (fine-tuning) оправдано ВМЕСТО промптинга, а когда нет.
  • Различать дистилляцию (distillation) и reinforcement fine-tuning / RLHF-подобное обучение.
  • Оценивать стоимость и риски дообучения: данные, дрейф (drift), поддержка.
  • Применять главное правило: сначала выжать промптинг, контекст и инструменты; fine-tuning — последний рычаг.
  • Готовить датасет примеров в правильном формате (обзорно, на уровне схемы).

Что нового (дельта к базовому курсу)

Базовый курс жил целиком в режиме промптинга: всё поведение задавалось системным промптом, few-shot-примерами и инструментами. Этого хватает для подавляющего большинства задач.

Дельта этой главы — обзор того, что лежит за промптингом: адаптация самих весов модели под домен. Это не «следующий шаг по умолчанию», а специализированный, дорогой и редко нужный рычаг.

Важно: на момент написания прямое дообучение моделей Claude через нативный API общедоступно ограниченно (часть возможностей — через облачные платформы вроде Amazon Bedrock). Поэтому глава — концептуальная: критерии выбора и риски, а не пошаговый рецепт. Сверяйтесь с актуальными доками по доступности.

Когда fine-tuning ВМЕСТО промптинга — и когда НЕ нужен

Сначала о том, когда НЕ нужен (это частый случай). Если задача решается комбинацией: чёткий системный промпт + несколько few-shot-примеров + извлечение знаний из контекста (RAG, retrieval-augmented generation) — дообучение не нужно. Промптинг гибок, итерируется за минуты, не требует данных и не создаёт долга на поддержку.

Fine-tuning стоит рассматривать, когда промптинг честно выжат и упирается в одно из:

  • Стабильный формат/стиль/домен. Нужен строго один формат вывода или узкий доменный стиль, который few-shot воспроизводит ненадёжно.
  • Экономия токенов на длинных инструкциях. Если в каждый запрос приходится класть огромную инструкцию или десятки примеров, дообучение «зашивает» это в веса — короче промпт, дешевле инференс.
  • Латентность. Меньше промпта -> меньше input-токенов -> ниже TTFT на массовой однотипной нагрузке.

Эвристика: если вы можете описать желаемое поведение словами в промпте — начните с промпта. Fine-tuning оправдан, когда поведение проще *показать на тысячах примеров*, чем описать, и нагрузка достаточно велика, чтобы окупить обучение.

Дистилляция и reinforcement fine-tuning (обзорно)

Дистилляция (distillation). Большая сильная модель-учитель генерирует эталонные ответы на наборе типовых запросов; на этих парах «запрос -> ответ учителя» дообучается маленькая дешёвая модель-ученик. Результат — ученик приближается к качеству учителя на узком домене при кратно меньшей стоимости инференса. Это форма supervised fine-tuning, где разметку делает не человек, а сильная модель. Применяют, когда есть стабильная массовая нагрузка и хочется снизить её цену.

Reinforcement fine-tuning / RLHF-подобное. Здесь модель обучается не на готовых эталонах, а под сигнал награды (reward) или предпочтения (preferences): из пар «ответ A лучше ответа B» или из функции-оценщика модель учится максимизировать желаемое. Это сложнее и дороже supervised-подхода, требует качественного сигнала награды и легко уводит модель в нежелательное поведение при плохо заданном reward. Оправдано, когда «правильность» нельзя задать одним эталоном, но можно сравнивать или оценивать ответы.

Оба подхода — тяжёлая артиллерия. Для агентов их рассматривают в последнюю очередь.

Стоимость, риски и главное правило

Fine-tuning создаёт обязательства, которых у промптинга нет:

  • Данные. Нужен качественный, репрезентативный, очищенный датасет. Мусор на входе -> мусор в весах. Сбор и разметка — основная статья затрат.
  • Дрейф (drift). Дообученная модель зафиксирована на распределении обучающих данных. Когда домен, продукт или базовая модель меняются, дообученная начинает отставать — её нужно переобучать. Промпт правится за минуту.
  • Поддержка. Версии датасета, версии дообученной модели, эвалы под каждую версию, откаты — отдельный жизненный цикл артефакта.

Главное правило главы: сначала выжать промптинг, контекст (RAG) и инструменты. Каскад моделей, кеш и хороший промпт закрывают большинство задач стоимости и качества. Fine-tuning — последний рычаг, к которому прибегают, когда промптинг уперся в потолок, нагрузка велика и однотипна, а выигрыш заведомо окупает данные, дрейф и поддержку.

Подготовка датасета примеров для дообучения (иллюстрация схемы и валидации)
package adaptation

import (
	"bufio"
	"encoding/json"
	"errors"
	"io"
	"strings"
)

// Точный формат датасета для fine-tuning зависит от платформы и версии —
// сверяйтесь с актуальными доками. Здесь — иллюстративная схема в духе JSONL:
// одна строка = один обучающий пример (диалог из чередующихся ролей).

// Turn — одна реплика в примере. Теги json опущены намеренно (учебный код);
// в реальном коде поля размечаются json-тегами: Role -> "role", Text -> "content".
type Turn struct {
	Role string // "user" или "assistant"
	Text string
}

// Example — один обучающий пример: вход(ы) пользователя и эталонный ответ.
type Example struct {
	Messages []Turn
}

var (
	errEmpty       = errors.New("пример без сообщений")
	errRole        = errors.New("недопустимая роль в реплике")
	errNoAssistant = errors.New("в примере нет эталонного ответа assistant")
)

// validate проверяет один пример перед добавлением в датасет.
// Чистота данных — основная статья затрат и риска fine-tuning.
func validate(ex Example) error {
	if len(ex.Messages) == 0 {
		return errEmpty
	}
	hasAssistant := false
	for _, t := range ex.Messages {
		switch t.Role {
		case "user":
		case "assistant":
			hasAssistant = true
		default:
			return errRole
		}
		if strings.TrimSpace(t.Text) == "" {
			return errEmpty
		}
	}
	if !hasAssistant {
		return errNoAssistant
	}
	return nil
}

// WriteDataset сериализует валидные примеры в JSONL (по одному на строку)
// и возвращает число записанных и число отброшенных примеров.
func WriteDataset(w io.Writer, examples []Example) (written, skipped int, err error) {
	bw := bufio.NewWriter(w)
	defer bw.Flush()

	enc := json.NewEncoder(bw)
	for _, ex := range examples {
		if verr := validate(ex); verr != nil {
			// Отбраковываем некорректный пример, а не подмешиваем мусор в веса.
			skipped++
			continue
		}
		if werr := enc.Encode(ex); werr != nil {
			return written, skipped, werr
		}
		written++
	}
	return written, skipped, nil
}

Anti-patterns

Грабли адаптации моделей
ГрабляПочему плохоКак избегать
Идти в fine-tuning, не выжав промптингДорогой и негибкий рычаг там, где хватило бы системного промпта, few-shot и RAGПравило: сначала промптинг/контекст/инструменты; fine-tuning — последний шаг
Дообучение на грязном или нерепрезентативном датасетеМусор на входе закрепляется в весах; качество падает необратимо до переобученияВалидация и очистка примеров; отбраковка некорректных перед записью датасета
Игнорировать дрейф после деплоя дообученной моделиМодель зафиксирована на старом распределении и отстаёт при смене домена/продуктаЭвалы на свежих данных, мониторинг качества, план переобучения
Брать reinforcement fine-tuning с плохо заданным rewardМодель максимизирует кривой сигнал и уходит в нежелательное поведениеНачинать с supervised/дистилляции; RL — только при качественном сигнале награды
Дообучать ради экономии при малой нагрузкеЗатраты на данные, обучение и поддержку не окупаются на низком объёмеСчитать окупаемость: fine-tuning оправдан на массовой однотипной нагрузке

Практическое задание (pro-m6-model-adaptation)

  • Возьмите реальную задачу и честно выжмите промптинг: системный промпт, few-shot, RAG; зафиксируйте достигнутое качество как базовую планку.
  • Сформулируйте критерий, при котором промптинг считается упёршимся в потолок (метрика и порог), — без него решение о fine-tuning субъективно.
  • Подготовьте иллюстративный датасет примеров в JSONL: соберите пары запрос->эталонный ответ и прогоните через валидацию (роли, непустота, наличие assistant).
  • Оцените стоимость и риски для вашего кейса: объём и чистота данных, частота дрейфа, цена поддержки версий.
  • Сравните альтернативы: даст ли каскад моделей и кеш тот же выигрыш по стоимости/латентности дешевле, чем дообучение.
  • Опишите для дистилляции: какая модель-учитель, на каком наборе запросов генерирует эталоны, какая модель-ученик и как меряется приближение к учителю.

Проверка знаний

Команда хочет, чтобы агент всегда отвечал в строгом доменном формате. Этого почти удалось добиться чётким системным промптом и few-shot-примерами.

Какой следующий шаг правильный?

  • A Немедленно запустить fine-tuning — формат важен
  • B Сначала доработать промптинг и контекст; fine-tuning рассматривать, только если упрётся в потолок
  • C Перейти на reinforcement fine-tuning с RLHF
  • D Увеличить MaxTokens

Большая сильная модель генерирует эталонные ответы на типовых запросах, и на этих парах дообучают маленькую дешёвую модель.

Как называется этот подход и зачем он нужен?

  • A Reinforcement fine-tuning — обучение под сигнал награды
  • B Дистилляция — приблизить качество дешёвой модели к дорогой на узком домене и снизить стоимость инференса
  • C Prompt caching — кеширование стабильного префикса
  • D RAG — извлечение знаний из внешней базы

Дообученную модель выкатили в прод; через несколько месяцев продукт и домен заметно изменились.

Какой риск fine-tuning здесь проявляется?

  • A Дрейф: модель зафиксирована на старом распределении и отстаёт, требуя переобучения
  • B Утечка кеша промпта
  • C Превышение rate limit провайдера
  • D Рост числа round-trip'ов в цикле