LoRA для function calling

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: LoRA для function calling

1. Цель задачи

Разработать и обучить LoRA-адаптер на основе небольшой LLM (например, Phi-2, LLaMA-2-7B или Qwen2-1.5B) для задачи function calling – преобразования инструкции пользователя на естественном языке в форматированный вызов API (имя функции + параметры). Создать синтетический датасет из 1000 примеров «инструкция → API call». Цель – достичь точности (accuracy) не менее 90% на тестовой выборке (200 примеров) при условии, что модель правильно определяет имя функции и корректно извлекает все обязательные параметры.

Ключевой результат Файл обученной LoRA-адаптации (≈5–20 MB) и скрипт инференса, который по новой инструкции возвращает валидный JSON с вызовом функции.


2. Исходные данные

Перед началом необходимо иметь:

Что нужноОткуда взять
Базовая LLM (трансформер)Hugging Face Hub: microsoft/phi-2, NousResearch/Llama-2-7b-hf или Qwen/Qwen2-1.5B-Instruct
Среда для дообученияGoogle Colab (T4/A100) / локальный GPU (минимум 10 ГБ VRAM)
Датасет (1000 примеров)Сгенерировать синтетически (см. Этап 1)
Библиотекиtransformers>=4.37, peft, bitsandbytes, accelerate, datasets, scikit-learn, json, fire
Git-репозиторийСоздать новый в GitHub/GitLab (код + адаптер)

Если нет доступа к GPU — используем Google Colab (бесплатный T4) или Kaggle Notebooks (GPU P100). Для моделей 7B потребуется quantization (QLoRA).

Если нет реального датасета — симулируем:

  1. Определить 5-10 функций (например, get_weather(city, date), send_email(to, subject, body), search_web(query), calculate(expression), get_news(topic, limit))
  2. Написать Python-скрипт, который генерирует 1000 различных инструкций (варьируя параметры, формулировки, опечатки) и соответствующие JSON-вызовы.
  3. Разделить на train/val/test (700/100/200) и сохранить в формате JSONL.

3. Технологический стек

КомпонентИнструментыНазначение
Базовая модельPhi-2 / LLaMA-2-7B / Qwen2-1.5BГенерация function call
LoRA (PEFT)peft, bitsandbytes (4-bit QLoRA)Параметро-эффективное дообучение
Датасетdatasets, JSONLЗагрузка и препроцессинг
Токенизацияtransformers.AutoTokenizerПодготовка входных последовательностей
ОбучениеTrainer или SFTTrainer от TRLТренировка с early stopping
Метрикиscikit-learn, jsonAccuracy, F1 по параметрам
ЛогированиеWandB / TensorBoard (опционально)Мониторинг лосса и метрик
СредаGoogle Colab / локальный DockerВычисления

4. Этапы выполнения

Этап 1: Генерация и подготовка датасета (2-3 часа)

Действия

  1. Определить схему функций Например, 5 функций с обязательными и опциональными параметрами:

    functions = {
        "get_weather": ["city", "date"],
        "send_email": ["to", "subject", "body"],
        "search_web": ["query"],
        "calculate": ["expression"],
        "get_news": ["topic", "limit"]
    }
    
  2. Написать генератор примеров Использовать шаблоны инструкций (не менее 10 вариаций на функцию):

    • Прямой запрос: «Какая погода в Москве завтра?» → {"name": "get_weather", "args": {"city": "Москва", "date": "2025-02-01"}}
    • Косвенный запрос: «Узнай, не будет ли дождя в Лондоне сегодня» → аналогично
    • С синонимами: «отправь письмо …», «отправь e-mail …», «напиши письмо …»
    • С лишними деталями, опечатками.
  3. Создать скрипт generate_dataset.py, который:

    • Выбирает случайную функцию, генерирует случайные аргументы (из заранее заданных списков городов, дат, тем).
    • Составляет инструкцию (вставляя параметры в шаблон).
    • Формирует JSON-вызов.
    • Записывает в JSONL: {"instruction": "...", "output": "..."}.
  4. Разделить на train/val/test (700/100/200).

  5. Проверить качество – нет ли дубликатов, все ли JSON валидны, все ли параметры присутствуют.

Ожидаемый результат этапа Файлы train.jsonl, val.jsonl, test.jsonl с 1000 примеров, скрипт генерации.


Этап 2: Настройка токенизации и формат для function calling (1 час)

Действия

  1. Выбрать формат промпта Для Phi-2 / Qwen2 подойдёт простой chat template:

    User: {instruction}
    Assistant: {function_call}
    

    Для LLaMA – использовать стандартный шаблон [INST] ... [/INST].

  2. Написать функцию токенизации, которая:

    • Добавляет токены <|user|> и <|assistant|> (или EOS).
    • Форматирует output как валидный JSON (можно минифицировать).
    • Обрезает последовательность до max_length=256 (инструкция короткая).
    • Важно Убедиться, что в labels маскируются токены инструкции (loss только на output).
  3. Проверить на 5 примерах – запустить токенизацию и проверить, что input_ids и labels корректны.

def tokenize_fn(examples, tokenizer, max_length=256):
    texts = [f"User: {inst}\nAssistant: {out}" for inst, out in zip(examples["instruction"], examples["output"])]
    tokenized = tokenizer(texts, truncation=True, max_length=max_length, padding="max_length", return_tensors="pt")
    labels = tokenized["input_ids"].clone()
    # маскируем User-часть
    user_len = tokenizer(f"User: {examples['instruction']}\nAssistant:", return_tensors="pt")["input_ids"].shape[1]
    labels[:, :user_len] = -100
    tokenized["labels"] = labels
    return tokenized

Ожидаемый результат этапа Готовый DataLoader или Dataset с корректными input_ids, attention_mask, labels.


Этап 3: Настройка LoRA и обучение (2-3 часа)

Действия

  1. Загрузить базовую модель с 4-bit QLoRA (если память ограничена):

    from transformers import AutoModelForCausalLM, BitsAndBytesConfig
    bnb_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16)
    model = AutoModelForCausalLM.from_pretrained("microsoft/phi-2", quantization_config=bnb_config, device_map="auto")
    
  2. Настроить LoRA через peft.LoraConfig:

  3. Использовать SFTTrainer из библиотеки TRL:

    • per_device_train_batch_size=4, gradient_accumulation_steps=4 (эффективный batch 16).
    • learning rate = 2e-4, LR scheduler cosine, warmup steps=50.
    • evaluation_strategy="steps", eval_steps=50, save_steps=100, metric_for_best_model="eval_loss".
    • max_seq_length=256.
  4. Запустить обучение на 5 эпох Логировать loss и accuracy (кастомная функция compute_metrics – см. Этап 4).

  5. После обучения сохранить LoRA-адаптер model.save_pretrained("./lora-function-calling").

Ожидаемый результат этапа Папка lora-function-calling/ с adapter_model.bin, adapter_config.json, а также best_model/ с лучшим чекпоинтом.


Этап 4: Оценка модели (1-2 часа)

Действия

  1. Загрузить адаптер на базовую модель (без QLoRA, в полной точности или снова 4-bit):

    from peft import PeftModel
    base = AutoModelForCausalLM.from_pretrained("microsoft/phi-2", device_map="auto")
    model = PeftModel.from_pretrained(base, "./lora-function-calling")
    
  2. Написать функцию инференса, которая:

    • Принимает инструкцию.
    • Форматирует как User: ...\nAssistant:.
    • Генерирует ответ (temperature=0.1, max_new_tokens=128).
    • Парсит JSON из вывода (игнорируя текст до первого { и после последнего }).
  3. Прогнать на тестовой выборке (200 примеров). Для каждого:

    • Сравнить предсказанное name и args с эталоном (перебор параметров с учётом порядка).
    • Точность (accuracy) считается так
      • Если name совпадает И все обязательные параметры совпадают (тип и значение) – пример верный.
      • Показатель: доля верных примеров.
  4. Если accuracy < 90%

    • Проанализировать ошибки (неправильный парсинг, неверное имя, лишние параметры).
    • Увеличить количество эпох, изменить LoRA rank, добавить больше шаблонов в генератор.
    • Повторить обучение.

Ожидаемый результат этапа Отчёт с accuracy (цель ≥90%), разбивка по каждой функции, примеры ошибок.


Этап 5: Упаковка и документирование (1 час)

Действия

  1. Создать финальный скрипт inference.py:

    import fire, json
    from transformers import AutoModelForCausalLM, AutoTokenizer
    from peft import PeftModel
    
    class FunctionCaller:
        def __init__(self, base_model, lora_adapter):
            self.tokenizer = AutoTokenizer.from_pretrained(base_model)
            self.tokenizer.pad_token = self.tokenizer.eos_token
            base = AutoModelForCausalLM.from_pretrained(base_model, device_map="auto")
            self.model = PeftModel.from_pretrained(base, lora_adapter)
        
        def call(self, instruction, temperature=0.1, max_tokens=128):
            prompt = f"User: {instruction}\nAssistant:"
            inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
            outputs = self.model.generate(**inputs, temperature=temperature, max_new_tokens=max_tokens, do_sample=True)
            response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
            # извлечь JSON из ответа
            start = response.find('{')
            end = response.rfind('}')+1
            return json.loads(response[start:end])
    
    if __name__ == "__main__":
        fire.Fire(FunctionCaller)
    
  2. Добавить requirements.txt и README.md с инструкцией по запуску.

  3. Зафиксировать вес адаптера в Git LFS или загрузить на Hugging Face Hub.

Ожидаемый результат этапа Рабочий репозиторий с кодом, обученным адаптером, инструкцией и результатами оценки.


5. Критерии приемки (Definition of Done)

  • Сгенерирован датасет из 1000 примеров (train/val/test) с валидными JSON.
  • LoRA-адаптер обучен до достижения accuracy ≥90% на тестовой выборке (минимум 90/100 верных ответов).
  • Средняя длина генерации ≤128 токенов.
  • Адаптер и инференс-скрипт работают на CPU (с той же моделью) без ошибок.
  • Код загружен в git-репозиторий, все зависимости фиксированы в requirements.txt.
  • README.md содержит: цель, структуру репозитория, инструкцию по установке и запуску, описание датасета и метрики.
  • Проведён анализ ошибок – в отчёте указаны основные типы ошибок (неверное имя функции, неправильные аргументы, невалидный JSON).

6. Ожидаемый результат

Основной артефакт

  • Папка lora-function-calling/ с файлами:
    • adapter_model.bin
    • adapter_config.json
    • (опционально) tokenizer_config.json, если использован кастомный токенизатор.

Сопутствующие файлы

  • data/train.jsonl, val.jsonl, test.jsonl
  • scripts/generate_dataset.py, train.py, inference.py, evaluate.py
  • results/ – файл evaluation_report.md с точностью, матрицей ошибок, примерами.
  • requirements.txt, README.md

Дополнительно (по желанию):

  • Загрузка адаптера на Hugging Face Hub (репозиторий yourusername/phi2-lora-function-calling).
  • Простая демонстрация в виде Streamlit-приложения.

7. Возможные сложности и их решение

СложностьРешение
Не хватает VRAM для 7B моделиИспользовать QLoRA (4-bit) или выбрать меньшую модель (Phi-2 2.7B, Qwen2-1.5B).
Генерация датасета даёт однообразные инструкцииДобавить больше шаблонов, использовать LLM (ChatGPT) для парафраза 50-100 примеров вручную.
Модель не может распарсить JSON из выводаУвеличить вес парсинга: обучить модель генерировать строго после ключевого слова Assistant:, убрать лишние токены.
Accuracy низкая (70-80%)Увеличить rank LoRA до 16, добавить эпохи, проверить маскировку loss (не учить User-часть).
Генерация слишком длинная или содержит лишний текстУменьшить max_new_tokens, добавить stop_token для завершения JSON, использовать temperature=0.1.
Не удаётся воспроизвести обучение из-за случайных seedЗафиксировать seeds в train.py (random.seed, np.random.seed, torch.manual_seed).

8. Бюджет времени (оценка)

ЭтапВремя
1. Генерация датасета2-3 часа
2. Настройка токенизации1 час
3. Настройка LoRA и обучение2-3 часа
4. Оценка и дообучение1-2 часа
5. Упаковка и документирование1 час
Итого7-10 часов

Примечание Для первого раза может потребоваться 10-12 часов, если возникнут сложности с выбором модели, отладкой парсинга или настройкой QLoRA.


9. Связанные вопросы из базы знаний

ВопросТема
42Как работает LoRA?
67Сравнение PEFT методов (LoRA, AdaLoRA, IA3)
128Форматы данных для instruction fine-tuning
219Использование SFTTrainer из TRL
304Оценка качества function calling (метрики)
411QLoRA: квантизация и обучение в 4-bit
538Как сгенерировать синтетический датасет с помощью LLM?
672Masking loss для обучения чат-моделей
745Инференс адаптеров на CPU
819Деплой LoRA-модели через Hugging Face Inference API

10. Чек-лист самопроверки

  • Я проверил, что датасет содержит ровно 1000 уникальных примеров, никакой не повторяется.
  • Я проверил, что валидационный и тестовый наборы не пересекаются с тренировочным.
  • Я убедился, что скрипт обучения воспроизводится при фиксированном seed.
  • Я запустил inference.py --instruction="Как погода в Нью-Йорке на послезавтра?" и получил корректный JSON.
  • Я сохранил логи обучения и выгрузил их вместе с репозиторием (опционально).
  • Я проверил, что accuracy на тесте ≥90% минимум в двух запусках с разными seed.