Почему тонкая настройка Gemma 3 для вызова процедур — это не просто модно, а необходимо
Представьте: у вас есть идеальный текстовый ассистент. Он понимает контекст, отвечает на вопросы, пишет код. Но когда вы просите его "забронировать столик в ресторане" или "отправить email клиенту", он просто описывает, как это сделать. Не делает. Просто болтает.
Это основная проблема современных LLM — они отлично генерируют текст, но не умеют взаимодействовать с внешним миром. И вот здесь появляется fine-tuning для вызова процедур (tool calling или function calling). Мы берем Gemma 3 — одну из самых эффективных open-source моделей — и учим ее не просто отвечать, а действовать.
Важно: Эта статья не про теорию. Здесь будет рабочий код, реальный датасет и конкретные команды. Если у вас есть RTX 4090 (или любой GPU с 24 ГБ VRAM), через 2 часа у вас будет собственная модель, которая умеет вызывать процедуры.
Что такое вызов процедур и зачем это нужно
Вызов процедур (tool calling) — это способность модели понимать, когда пользователь хочет выполнить какое-то действие, и генерировать структурированный запрос для этого действия. Вместо "Я могу рассказать, как отправить email" модель должна выдать:
{
"function": "send_email",
"arguments": {
"to": "client@example.com",
"subject": "Договор",
"body": "Прикрепляю договор..."
}
}Зачем это нужно? Три основные причины:
- Конфиденциальность — ваши данные никогда не покидают ваш сервер. Если вы юрист или врач, это критически важно. Помните статью про юристов, которые не хотят сливать клиентские тайны? Здесь тот же принцип.
- Интеграция — модель может работать с вашей внутренней CRM, базой данных, API.
- Контроль — вы точно знаете, какие процедуры доступны, и можете их ограничивать.
Что вам понадобится: железо и софт
Давайте без иллюзий. Для fine-tuning Gemma 3 4B вам нужно:
- GPU с 24 ГБ VRAM — RTX 4090 идеально подходит. Можно использовать несколько карт с меньшей памятью, но это сложнее. Если у вас только 12 ГБ — посмотрите статью про работу на 12 ГБ VRAM.
- Python 3.10+ — более старые версии могут вызывать проблемы с библиотеками.
- CUDA 12.1+ — если у вас NVIDIA карта.
- Около 30 ГБ свободного места — для модели, датасета и чекпоинтов.
Совет: Не пытайтесь запускать это на CPU. Обучение займет дни, если не недели. Даже на Mac M-серии это будет мучительно медленно (хотя для инференса они подходят — об этом в гайде по Mac).
Подготовка: устанавливаем всё необходимое
1Создаем виртуальное окружение
Первое правило fine-tuning'а — изолировать зависимости. Иначе вы сломаете системный Python.
python -m venv gemma_finetune
source gemma_finetune/bin/activate # На Windows: gemma_finetune\Scripts\activate2Устанавливаем PyTorch с CUDA
Здесь многое зависит от вашей версии CUDA. Для RTX 4090 с CUDA 12.1:
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu1213Устанавливаем библиотеки для fine-tuning
pip install transformers datasets accelerate peft bitsandbytes trl
pip install sentencepiece protobufЧто здесь важно:
- PEFT — библиотека для parameter-efficient fine-tuning (QLoRA)
- bitsandbytes — для 4-битной квантизации
- trl — для reinforcement learning (хотя в этом гайде не используем, но полезно иметь)
Создаем датасет для обучения
Вот где большинство гайдов спотыкаются. Они говорят "используйте датасет", но не говорят, какой именно. Я покажу вам реальный датасет, который работает.
Нам нужно научить модель двум вещам:
- Распознавать, когда пользователь хочет вызвать процедуру
- Генерировать корректный JSON для вызова
Создадим файл dataset.jsonl:
{
"messages": [
{"role": "user", "content": "Отправь email Ивану с темой 'Встреча завтра' и текстом 'Жду тебя в 10:00'"},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"function": {
"name": "send_email",
"arguments": "{\"to\": \"ivan@example.com\", \"subject\": \"Встреча завтра\", \"body\": \"Жду тебя в 10:00\"}"
}
}
]
}
],
"tools": [
{
"type": "function",
"function": {
"name": "send_email",
"description": "Отправляет электронное письмо",
"parameters": {
"type": "object",
"properties": {
"to": {"type": "string", "description": "Email получателя"},
"subject": {"type": "string", "description": "Тема письма"},
"body": {"type": "string", "description": "Текст письма"}
},
"required": ["to", "subject", "body"]
}
}
}
]
}И еще несколько примеров для разнообразия:
{
"messages": [
{"role": "user", "content": "Найди рестораны итальянской кухни рядом со мной"},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"function": {
"name": "search_businesses",
"arguments": "{\"query\": \"итальянские рестораны\", \"location\": \"current_location\", \"radius\": 5000}"
}
}
]
}
],
"tools": [
{
"type": "function",
"function": {
"name": "search_businesses",
"description": "Ищет бизнесы по запросу",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Поисковый запрос"},
"location": {"type": "string", "description": "Локация для поиска"},
"radius": {"type": "number", "description": "Радиус поиска в метрах"}
},
"required": ["query", "location"]
}
}
}
]
}Основной код обучения
Теперь самое интересное — код для fine-tuning с использованием QLoRA. QLoRA (Quantized Low-Rank Adaptation) позволяет обучать огромные модели на скромном железе, замораживая основные веса и обучая только небольшие адаптеры.
Создаем файл train.py:
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
BitsAndBytesConfig
)
from peft import LoraConfig, prepare_model_for_kbit_training, get_peft_model
from trl import SFTTrainer
from datasets import load_dataset
import os
# Конфигурация 4-битной квантизации
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True
)
# Загружаем модель и токенизатор
model_id = "google/gemma-3-4b-it"
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token = tokenizer.eos_token
# Подготовка модели для k-bit обучения
model = prepare_model_for_kbit_training(model)
# Конфигурация LoRA
lora_config = LoraConfig(
r=16, # Rank
lora_alpha=32,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
# Загружаем датасет
dataset = load_dataset("json", data_files="dataset.jsonl", split="train")
# Функция форматирования данных
def format_dataset(example):
# Здесь мы форматируем данные для обучения
# В реальном коде нужно добавить обработку tool_calls
messages = example["messages"]
text = tokenizer.apply_chat_template(messages, tokenize=False)
return {"text": text}
dataset = dataset.map(format_dataset)
# Аргументы обучения
training_args = TrainingArguments(
output_dir="./gemma-tool-calling",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
warmup_steps=100,
logging_steps=10,
save_steps=100,
eval_steps=100,
learning_rate=2e-4,
fp16=True,
optim="paged_adamw_8bit",
save_total_limit=3,
report_to="none"
)
# Тренер
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
dataset_text_field="text",
max_seq_length=2048,
tokenizer=tokenizer
)
# Запускаем обучение
trainer.train()
# Сохраняем модель
trainer.save_model("./gemma-tool-calling-final")
tokenizer.save_pretrained("./gemma-tool-calling-final")Запускаем обучение
Теперь просто запускаем:
python train.pyИ ждем. На RTX 4090 с датасетом в 1000 примеров это займет около 2-3 часов.
Внимание: Если у вас заканчивается память, уменьшайте per_device_train_batch_size до 2 или даже 1. Также можно уменьшить max_seq_length до 1024, но это ухудшит качество.
Тестируем обученную модель
После обучения создаем файл test.py:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
# Загружаем базовую модель
base_model = "google/gemma-3-4b-it"
model = AutoModelForCausalLM.from_pretrained(
base_model,
torch_dtype=torch.float16,
device_map="auto"
)
# Загружаем адаптеры LoRA
model = PeftModel.from_pretrained(model, "./gemma-tool-calling-final")
model = model.merge_and_unload()
tokenizer = AutoTokenizer.from_pretrained(base_model)
# Тестовый промпт
prompt = "Отправь email Марии с темой 'Документы' и текстом 'Отправляю договор на подписание'"
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=200,
temperature=0.7,
do_sample=True
)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(response)Идеальный вывод должен быть похож на:
{
"function": "send_email",
"arguments": {
"to": "maria@example.com",
"subject": "Документы",
"body": "Отправляю договор на подписание"
}
}Проблемы, с которыми вы столкнетесь (и как их решить)
1. Out of memory ошибки
Самая частая проблема. Решения:
- Уменьшите
per_device_train_batch_size - Увеличьте
gradient_accumulation_steps(это компенсирует уменьшение batch size) - Используйте gradient checkpointing:
model.gradient_checkpointing_enable()
2. Модель не учится вызывать процедуры
Если после обучения модель просто продолжает генерировать текст вместо JSON:
- Проверьте датасет — возможно, там ошибки форматирования
- Увеличьте количество эпох (но не более 5, иначе переобучится)
- Попробуйте другой learning rate (1e-4 или 3e-4)
3. Плохое качество JSON
Если модель генерирует невалидный JSON:
- Добавьте в промпт явное указание на формат: "Ответь в формате JSON: {...}"
- Используйте constrained decoding (сложнее, но эффективнее)
- Добавьте в loss функцию штраф за невалидный JSON
Как интегрировать это в реальное приложение
Допустим, вы создаете ассистента для бронирования столиков. После того как модель сгенерировала JSON, вам нужно:
- Парсить JSON
- Валидировать параметры
- Вызывать реальную функцию
- Возвращать результат пользователю
Пример простой интеграции:
import json
import re
def parse_tool_call(response):
# Ищем JSON в ответе модели
json_match = re.search(r'\{.*\}', response, re.DOTALL)
if not json_match:
return None
try:
tool_call = json.loads(json_match.group())
return tool_call
except json.JSONDecodeError:
return None
def execute_tool_call(tool_call):
if tool_call["function"] == "send_email":
# Здесь реальная логика отправки email
send_email(**tool_call["arguments"])
return "Email отправлен"
elif tool_call["function"] == "search_businesses":
# Поиск в базе данных
results = search_database(**tool_call["arguments"])
return f"Найдено {len(results)} мест"
else:
return "Неизвестная процедура"Что дальше? Улучшаем модель
После того как базовый вариант работает, можно улучшать:
- Добавить больше процедур — чем больше разнообразных примеров в датасете, тем лучше модель обобщает
- Использовать Reinforcement Learning — награждать модель за корректные вызовы и наказывать за ошибки
- Добавить цепочку мыслей — заставить модель сначала рассуждать, потом действовать. Об этом есть отличная статья про темную цепочку мыслей
- Кэшировать результаты — если вы часто вызываете одни и те же процедуры
FAQ: частые вопросы
Можно ли использовать этот подход для других моделей?
Да, абсолютно. Тот же код с минимальными изменениями работает для Llama, Mistral, Qwen. Главное — поменять model_id и возможно target_modules в конфигурации LoRA.
Сколько нужно данных для качественного обучения?
Для простых процедур (отправка email, поиск) достаточно 500-1000 примеров. Для сложных (анализ данных, генерация кода) нужно 2000-5000. Качество данных важнее количества.
Почему именно Gemma 3, а не другие модели?
Gemma 3 4B — идеальный баланс между качеством и требованиями к памяти. Она достаточно умна для понимания контекста, но достаточно мала, чтобы обучаться на одной карте. Для сравнения, Qwen3-30B требует больше ресурсов.
Можно ли запускать обучение на нескольких GPU?
Да, добавьте в TrainingArguments:
ddp_find_unused_parameters=False
deepspeed="configs/deepspeed_config.json"И используйте DeepSpeed. Но это тема для отдельной статьи.
Ошибки, которые совершают все (и как их избежать)
| Ошибка | Почему происходит | Как исправить |
|---|---|---|
| Модель генерирует бесконечный текст | Нет стоп-токенов или max_new_tokens слишком большой | Добавить stop sequences или уменьшить max_new_tokens |
| Обучение слишком медленное | Слишком маленький batch size или нет fp16 | Использовать gradient accumulation и включить fp16 |
| Модель забывает базовые знания | Слишком высокий learning rate или много эпох | Уменьшить LR до 1e-5 и использовать меньше эпох |
| Невалидный JSON в ответах | Модель не обучена формату JSON | Добавить больше примеров с правильным JSON в датасет |
Что делать, если нет RTX 4090
Есть несколько вариантов:
- Использовать облако — Google Colab Pro или AWS с GPU инстансами
- Квантовать модель сильнее — использовать 3-битную или даже 2-битную квантизацию
- Взять меньшую модель — например, Gemma 2B вместо 4B
- Использовать CPU обучение — очень медленно, но работает
Или посмотрите как Tencent ускоряет генерацию на слабом железе.
Заключительные мысли
Fine-tuning Gemma 3 для вызова процедур — это не магия, а вполне доступная технология. За один вечер можно создать прототип, который превращает пассивного чат-бота в активного помощника.
Самое сложное — не код (он довольно стандартный), а создание качественного датасета и тестирование. Не экономьте на этом. Лучше потратить лишний день на подготовку данных, чем неделю на переобучение модели.
И помните: ваша натренированная модель — это только половина системы. Вторая половина — это надежная инфраструктура для выполнения этих самых процедур. Модель может идеально генерировать JSON для отправки email, но если ваша функция отправки падает каждые 10 минут — пользователь этого не оценит.
Начните с малого. Обучите модель на 2-3 простых процедурах. Протестируйте. Убедитесь, что это работает. И только потом масштабируйтесь.
И последнее: не зацикливайтесь на точности в 99.9%. Для большинства практических задач 85-90% вполне достаточно. Оставшиеся 10-15% случаев можно обработать через fallback-механизм или ручную проверку.