Пару месяцев назад я пытался научить модель отвечать в определённом стиле через промпт. Набивал системное сообщение примерами, объяснял тон, прикладывал образцы. Работало примерно на 60% — модель периодически съезжала обратно к своему дефолтному "Конечно! Рад помочь!" голосу. Потом попробовал файн-тюнинг. Разница оказалась заметной.
Ниже — что я делал, как это устроено и где успел облажаться.
Зачем тюнить, если есть промпты
Честный ответ: промпт справляется с большинством задач. Файн-тюнинг — не волшебная таблетка, OpenAI сами об этом пишут. Но есть случаи, когда он даёт принципиально другой результат.
Стиль и голос. Если нужно, чтобы модель стабильно писала как конкретный бренд или человек, промпт держит стиль непредсказуемо. Файн-тюнинг "вшивает" его глубже — это чувствуется.
Сокращение промпта. После тюнинга я убрал 400 токенов инструкций из системного сообщения. На масштабе это уже деньги.
Специфический формат вывода. Когда нужен JSON строго определённой структуры или ответы в очень узком домене — натренированная модель держится стабильнее.
Для общих задач типа "суммаризируй" или "переведи" тюнить не нужно. Усложнять без причины не стоит.
Что нужно подготовить
Файн-тюнинг GPT-4o-mini работает на данных в формате JSONL — JSON Lines, где каждая строка отдельный объект. Каждый объект — один пример разговора.
Минимальная структура выглядит так:
{"messages": [{"role": "system", "content": "Ты помощник, который отвечает кратко и по делу."}, {"role": "user", "content": "Что такое файн-тюнинг?"}, {"role": "assistant", "content": "Дообучение готовой модели на своих данных. Адаптирует поведение под конкретную задачу без обучения с нуля."}]}
Каждая строка — полный диалог со своим системным сообщением, вопросом и ответом.
Сколько примеров нужно? OpenAI говорит от 10, но это теоретический минимум. На практике я начинал с 50-100 и видел осмысленный результат. Для тонкой настройки стиля 200-300 примеров дают заметно более стабильную модель. Больше тысячи — уже территория серьёзных изменений поведения.
Про качество: 50 хороших примеров лучше 500 средних. Я однажды сгенерировал датасет через GPT-4 без особой проверки, потом удивлялся почему результат странный. Оказалось, модель натаскалась на несколько кривых примеров, которые я не заметил при беглом просмотре.
Процесс шаг за шагом
Начну с кода, потом объясню что происходит.
Установка и импорты:
pip install openai
import openai
import json
import time
client = openai.OpenAI(api_key="ваш_ключ")
Загрузка файла с данными:
with open("train.jsonl", "rb") as f:
response = client.files.create(
file=f,
purpose="fine-tune"
)
file_id = response.id
print(f"Файл загружен: {file_id}")
Запуск тюнинга:
job = client.fine_tuning.jobs.create(
training_file=file_id,
model="gpt-4o-mini-2024-07-18",
hyperparameters={
"n_epochs": 3
}
)
job_id = job.id
print(f"Джоб запущен: {job_id}")
Проверка статуса — джоб может идти от 15 минут до нескольких часов:
def wait_for_job(job_id):
while True:
job = client.fine_tuning.jobs.retrieve(job_id)
status = job.status
print(f"Статус: {status}")
if status in ["succeeded", "failed", "cancelled"]:
return job
time.sleep(60)
finished_job = wait_for_job(job_id)
Когда джоб завершился — достаём имя новой модели:
model_name = finished_job.fine_tuned_model
print(f"Модель готова: {model_name}")
# Выглядит примерно как: ft:gpt-4o-mini-2024-07-18:org::abc123
Использование натренированной модели — ровно как обычный API:
response = client.chat.completions.create(
model=model_name,
messages=[
{"role": "system", "content": "Ты помощник, который отвечает кратко и по делу."},
{"role": "user", "content": "Объясни градиентный спуск"}
]
)
print(response.choices[0].message.content)
Никакого специального SDK, никаких хитростей — просто другой идентификатор модели.
Где я потратил лишнее время
Несколько граблей, на которые я наступал.
Формат файла. JSONL капризный. Одна лишняя запятая, один неправильно экранированный символ — и файл не принимается. Я теперь проверяю его перед загрузкой:
def validate_jsonl(filepath):
errors = []
with open(filepath, "r", encoding="utf-8") as f:
for i, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
obj = json.loads(line)
if "messages" not in obj:
errors.append(f"Строка {i}: нет поля messages")
except json.JSONDecodeError as e:
errors.append(f"Строка {i}: {e}")
if errors:
for err in errors:
print(err)
else:
print("Файл валидный")
validate_jsonl("train.jsonl")
Количество эпох. По умолчанию оставлял 3 и иногда получал переобученную модель — она отвечала очень по шаблону, почти не варьируя ответы. Для небольших датасетов в 50-100 примеров попробуйте 1-2 эпохи.
Отсутствие validation_file. OpenAI не требует его жёстко, но если передать файл валидации, получаешь метрики потерь прямо в логах джоба и сразу видишь переобучение. Дело трёх минут, а информации даёт много.
Цена. Самая обидная грабля — я забыл что файн-тюнинг платный. GPT-4o-mini стоит $3 за миллион токенов при тюнинге, плюс $12 за миллион токенов при инференсе натренированной модели — против $0.15 у базовой. Разрыв большой. Перед запуском считайте токены в датасете через tiktoken, чтобы счёт не стал сюрпризом.
Как понять что получилось
После тюнинга я делаю простую вещь: беру 20-30 тестовых запросов, которых не было в обучающей выборке, и прогоняю через обе модели — базовую и натренированную. Смотрю на разницу глазами.
Метрики из логов тюнинга (training_loss, validation_loss) дают сигнал про переобучение, но не про то, насколько результат реально полезен. Только живой тест показывает настоящую картину.
Ещё один маркер — если натренированная модель начинает повторять одни и те же фразы из примеров почти дословно, это переобучение. Нужно либо больше данных, либо меньше эпох.
Стоит ли игра свеч
Для меня — в конкретном проекте да, стоило. Стиль стал стабильнее, промпт сократился, поведение модели в крайних случаях стало предсказуемее.
Но я бы не советовал файн-тюнинг как первый шаг. Вы исчерпали промпт инжиниринг? Попробовали few-shot примеры прямо в системном сообщении? Если да и всё равно не устраивает — тогда тюнить. Иначе потратите деньги и время на то, что решается тремя строчками в промпте.
