Практическое руководство

Как сравнить локальные LLM по вызову инструментов на русском языке

Продвинутый уровень До 12 минут Результат: воспроизводимое сравнение моделей на собственных схемах

Почему общего рейтинга недостаточно

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

Поэтому полезнее проверять модели на тех схемах инструментов, которые действительно используются в вашем приложении. Стенд из этой статьи не исполняет функции и не обращается к реальным сервисам. Он сравнивает структурированный ответ модели с ожидаемым результатом, поэтому тестирование не создаёт письма, платежи, события календаря или другие побочные эффекты.

Что именно измерять

Разделите качество вызова инструментов на несколько независимых признаков:

  • Решение о вызове: модель вызывает функцию только тогда, когда она нужна.
  • Выбор функции: имя совпадает с ожидаемым.
  • Полнота аргументов: присутствуют все обязательные поля.
  • Корректность значений: значения совпадают после заранее определённой нормализации.
  • Отсутствие выдумок: модель не заполняет неизвестные обязательные поля догадками.
  • Соблюдение схемы: типы, перечисления и запрет дополнительных полей не нарушены.
  • Стабильность: повторные запуски дают одинаковый результат.

Не объединяйте всё сразу в «среднюю точность». Две модели с одинаковым итоговым баллом могут ошибаться по-разному: одна выбирает неверную функцию, другая — правильную функцию с опасно выдуманными аргументами.

Структура воспроизводимого стенда

Минимальный каталог может выглядеть так:

tool-bench/
├── schemas/
│   └── tools.json
├── cases/
│   └── ru.jsonl
├── outputs/
├── run.py
├── score.py
└── manifest.json

schemas/tools.json содержит одинаковые определения функций для всех моделей. cases/ru.jsonl хранит входы и ожидаемые решения. В outputs записываются необработанные ответы, а manifest.json фиксирует параметры эксперимента.

Шаг 1. Зафиксируйте схемы инструментов

Используйте строгую схему и явно запретите лишние поля. Пример:

{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "find_order",
        "description": "Найти заказ по его идентификатору",
        "parameters": {
          "type": "object",
          "properties": {
            "order_id": {
              "type": "string",
              "description": "Идентификатор заказа без преобразования"
            }
          },
          "required": ["order_id"],
          "additionalProperties": false
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "search_orders",
        "description": "Найти заказы по статусу и необязательному городу",
        "parameters": {
          "type": "object",
          "properties": {
            "status": {
              "type": "string",
              "enum": ["new", "processing", "shipped", "cancelled"]
            },
            "city": {
              "type": "string"
            }
          },
          "required": ["status"],
          "additionalProperties": false
        }
      }
    }
  ]
}

Не улучшайте описания между запусками разных моделей. Даже небольшое уточнение описания функции меняет условия эксперимента. Если исследуете влияние формулировок, оформите это как отдельную версию набора.

Шаг 2. Подготовьте русскоязычные случаи

Храните по одному JSON-объекту на строку. Ожидаемый результат должен быть проверяемым без интерпретации во время подсчёта:

{"id":"ru-001","input":"Проверь заказ A-1047","expected":{"action":"call","name":"find_order","arguments":{"order_id":"A-1047"}},"tags":["direct"]}
{"id":"ru-002","input":"Покажи отправленные заказы в Казани","expected":{"action":"call","name":"search_orders","arguments":{"status":"shipped","city":"Казань"}},"tags":["normalization"]}
{"id":"ru-003","input":"Какие статусы заказа бывают?","expected":{"action":"none"},"tags":["no_call"]}
{"id":"ru-004","input":"Найди мой заказ","expected":{"action":"clarify","missing":["order_id"]},"tags":["missing_required"]}
{"id":"ru-005","input":"Проверь заказ А-1047","expected":{"action":"call","name":"find_order","arguments":{"order_id":"А-1047"}},"tags":["cyrillic_lookalike"]}

В последнем примере первая буква идентификатора кириллическая. Это намеренный тест на сохранение исходного значения, а не утверждение о формате идентификаторов в реальной системе.

Какие группы нужны в наборе

  • прямой запрос с полными аргументами;
  • перефразирование и разговорная лексика;
  • склонения, числа, даты и неоднозначные временные выражения;
  • похожие функции с пересекающимися описаниями;
  • отсутствующий обязательный аргумент;
  • запрос, не требующий вызова;
  • провокация на неизвестную функцию или дополнительное поле;
  • смешение кириллицы и латиницы;
  • опечатки и пунктуационный шум;
  • несколько возможных вызовов, если агент поддерживает такую последовательность.

Формулировки должны происходить из вашей предметной области, но не переносите в набор реальные персональные данные, секреты, токены или содержимое закрытых обращений. Сначала обезличьте и проверьте примеры вручную.

Шаг 3. Задайте единый контракт ответа

Разные серверы локального инференса возвращают вызовы функций в разных оболочках. Адаптер каждой модели должен привести ответ к одному внутреннему формату:

{
  "case_id": "ru-001",
  "model": "LOCAL_MODEL_ID",
  "repeat": 1,
  "result": {
    "action": "call",
    "name": "find_order",
    "arguments": {
      "order_id": "A-1047"
    }
  },
  "raw_response": {}
}

Если сервер не поддерживает нативный вызов инструментов и вы просите модель вывести JSON текстом, отмечайте режим отдельно. Сравнение нативного протокола и свободной генерации JSON без такой отметки смешивает две разные задачи.

Ошибку разбора тоже сохраняйте как результат, например {"action":"parse_error"}. Не исправляйте JSON модели вручную: иначе вы измерите качество модели вместе с незадокументированной постобработкой.

Шаг 4. Зафиксируйте параметры запуска

Манифест должен позволять повторить эксперимент:

{
  "dataset": "ru-tools-example",
  "dataset_version": "1.0.0",
  "schema_version": "1.0.0",
  "models": [
    {
      "id": "LOCAL_MODEL_ID",
      "artifact_digest": "FILL_WITH_LOCAL_DIGEST",
      "tool_mode": "native"
    }
  ],
  "generation": {
    "temperature": 0,
    "top_p": 1,
    "max_output_tokens": 512,
    "seed": 42
  },
  "repeats": 5,
  "system_prompt_version": "1.0.0",
  "adapter_version": "1.0.0"
}

LOCAL_MODEL_ID, контрольная сумма и параметры — заполняемые поля примера. Укажите фактически использованные значения. Даже при фиксированном seed результат может зависеть от движка, аппаратной платформы, квантования и версии библиотек, поэтому одного имени модели недостаточно.

Сохраняйте точный системный промпт, порядок инструментов, шаблон чата и сырые ответы. Порядок функций тоже лучше не менять: некоторые модели чувствительны к позиции описания.

Шаг 5. Запустите модели безопасно

Сначала используйте только фиктивные функции. Если ваш локальный сервер предоставляет HTTP-интерфейс, передавайте его адрес через явно заданную переменную и не помещайте токены в команды или набор данных:

python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install --require-hashes -r requirements.txt

BENCH_ENDPOINT="http://127.0.0.1:8000" \
python3 run.py \
  --manifest manifest.json \
  --cases cases/ru.jsonl \
  --tools schemas/tools.json \
  --output outputs/run-001.jsonl

Это шаблон команды: он предполагает, что вы самостоятельно реализовали run.py и подготовили файл зависимостей с хешами. Адрес ограничен локальным интерфейсом в целях примера. Не подключайте стенд к производственным инструментам и не исполняйте полученные аргументы.

Для честного сравнения прогоняйте случаи в одинаковом порядке либо сохраняйте seed перемешивания. Если сервер обслуживает параллельные запросы, зафиксируйте уровень параллелизма: он может влиять на задержку и, в некоторых реализациях, на воспроизводимость.

Шаг 6. Проверяйте ответ слоями

Оценщик должен последовательно выполнить следующие проверки:

  1. Ответ разобран и приведён к внутреннему контракту.
  2. Действие call, none или clarify совпало с ожидаемым.
  3. При вызове совпало имя функции.
  4. Аргументы прошли исходную JSON Schema.
  5. После разрешённой нормализации совпали ожидаемые значения.
  6. Не появились лишние или выдуманные поля.

Нормализация должна быть узкой и описанной заранее. Например, можно разрешить различия Unicode-нормализации в обычном названии города, но нельзя автоматически заменять кириллические буквы латинскими внутри идентификатора. Не приводите все строки к нижнему регистру, если регистр значим для вашего API.

Минимальные метрики

decision_accuracy = верные решения о действии / все случаи
function_accuracy = верные имена / случаи, где ожидался вызов
argument_exact_match = точное совпадение аргументов / ожидаемые вызовы
schema_valid_rate = валидные аргументы / фактические вызовы
hallucinated_argument_rate = вызовы с выдуманными значениями / фактические вызовы
parse_error_rate = ошибки разбора / все ответы
all_correct_rate = полностью верные случаи / все случаи

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

Как проверять результат

После подсчёта сформируйте отчёт по моделям и тегам:

Модель Решение Функция Аргументы Схема Полностью верно
LOCAL_MODEL_A заполнить заполнить заполнить заполнить заполнить
LOCAL_MODEL_B заполнить заполнить заполнить заполнить заполнить

Таблица — шаблон, а не опубликованный результат теста. Значения нужно получить на собственном стенде.

Затем вручную просмотрите все расхождения и присвойте им категории: неверный вызов, неверная функция, пропуск поля, лишнее поле, ошибочное значение, ошибка формата или необходимость уточнения. Особенно внимательно изучите случаи, где ответ формально валиден, но семантически опасен.

Перед выводами выполните три контрольные проверки:

  • повторный запуск того же артефакта даёт сопоставимые результаты;
  • оценщик проходит небольшие искусственные примеры с заведомо правильными и неправильными ответами;
  • не менее двух проверяющих одинаково трактуют спорные эталоны либо спорные случаи исключены до сравнения моделей.

Типовые ошибки

Оценивать только валидность JSON

Валидный объект может вызывать неверную функцию или содержать выдуманный идентификатор. Схема проверяет форму, но не подтверждает смысл.

Засчитывать отсутствие вызова как ошибку всегда

Если обязательного аргумента нет, безопасное уточнение часто правильнее догадки. Такое поведение должно быть представлено в эталоне отдельным действием.

Тестировать только очевидные команды

Прямые формулировки плохо различают сильные модели. Добавляйте похожие функции, отрицания, неполные запросы и случаи, где инструмент не нужен.

Менять сразу несколько условий

Замена модели вместе с шаблоном чата, квантованием и описаниями функций не позволяет понять причину изменения результата. Фиксируйте всё, кроме исследуемого фактора.

Скрыто ремонтировать ответы

Удаление префиксов, исправление кавычек и подбор ближайшего имени функции повышают итоговый балл. Если такая постобработка является частью агента, версионируйте её и показывайте результаты до и после неё.

Использовать данные из разработки как финальный тест

Если примеры многократно применялись для настройки промпта, они перестают быть независимой проверкой. Разделите набор на часть для разработки и закрытую часть для итогового сравнения.

Ограничения метода

Стенд измеряет поведение на выбранных схемах и формулировках, а не универсальную способность модели пользоваться любыми инструментами. Результат нельзя автоматически переносить на другой язык, системный промпт, движок инференса или набор функций.

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

Изолированная проверка одного вызова не измеряет весь агентный цикл: обработку результата функции, повторное планирование, восстановление после ошибки и защиту от инструкций в данных инструмента. Эти свойства требуют отдельных сценариев.

Наконец, высокий балл не делает модель безопасной для автоматического исполнения. Перед реальным вызовом проверяйте схему, права пользователя, допустимые значения и необходимость подтверждения для необратимых действий.

Критерий готовности стенда

Сравнение можно считать воспроизводимым, если другой инженер способен взять зафиксированный артефакт модели, схемы, набор случаев, системный промпт, адаптер и манифест — и получить сопоставимый отчёт без ручного исправления ответов.

Практический выбор модели делайте не по одной строке рейтинга, а по профилю ошибок. Для агента поиска допустим один компромисс, для финансового или административного действия — другой. Частота выдуманных аргументов и ложных вызовов нередко важнее средней доли правильных ответов.