Как проверить tool calling в LLM на регрессию: пошаговый чеклист для CI/CD

Tool calling — ключевой механизм для AI-агентов, но он ломается при смене модели или обновлении API. Разбираем, как собрать набор тестов, настроить регрессионные проверки и встроить их в пайплайн, чтобы не пропустить деградацию.

Схема проверки tool calling в LLM: схема вызова, валидация схемы и регрессионный тест в CI
Схема проверки tool calling в LLM: схема вызова, валидация схемы и регрессионный тест в CI
Divers — Illustrated London News Feb 6 1873-2.PNG | by Unknown artistUnknown artist | wikimedia_commons | Public domain

Когда AI-агент перестаёт вызывать нужный инструмент или передаёт в него мусорные параметры, проблема редко заметна на одном примере. Она проявляется в прогоне пайплайна, когда модель, которая ещё вчера корректно парсила дату и передавала её в календарь, вдруг начинает возвращать `null` или строку с лишними символами.

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

В этой статье — практический чеклист для настройки регрессионных проверок tool calling, который можно встроить в CI/CD за вечер.

Что ломается в tool calling и почему это не видно в чате

Tool calling (или function calling) — это способ LLM вернуть структурированный JSON с именем инструмента и аргументами вместо обычного текста. Модель решает, какой инструмент вызвать, и заполняет параметры по описанию схемы.

Проблема в том, что даже одна и та же модель с той же температурой может давать разные результаты на граничных случаях. А при смене модели — например, с Claude 3 Haiku на Claude 3.5 Sonnet — поведение меняется кардинально.

Типичные поломки:

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

В чате эти ошибки выглядят как «странное поведение агента». Без тестов вы не поймёте, что проблема именно в tool calling, а не в логике промпта.

Какие метрики измерять

Прежде чем писать тесты, нужно определить, что считать «правильным вызовом». Для регрессионных проверок достаточно трёх метрик.

Точность выбора инструмента (tool selection accuracy) — доля случаев, когда модель вызвала правильный инструмент из доступных. Если у агента три инструмента, а модель вызывает четвёртый (которого нет) или не вызывает ничего — это ошибка.

Точность аргументов (argument fidelity) — доля вызовов, где все переданные параметры соответствуют схеме по типу, обязательности и допустимым значениям.

Полнота вызова (completeness) — процент обязательных полей, которые модель заполнила, от общего числа обязательных полей в схеме.

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

Чеклист: 6 шагов к регрессионному тесту

Соберите датасет граничных случаев

Не нужно тысячу примеров. Достаточно 20–30 сценариев, которые покрывают:

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

Каждый сценарий — это пара: входной промпт (контекст пользователя) и ожидаемый вызов (инструмент + аргументы).

Напишите валидатор вызова

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

Пример на Python:

python
import json

def validate_tool_call(expected: dict, actual: dict) -> dict:
errors = []
if expected.get(«name») != actual.get(«name»):
errors.append(f»Expected tool {expected.get(‘name’)}, got {actual.get(‘name’)}»)
for key, expected_val in expected.get(«arguments», {}).items():
actual_val = actual.get(«arguments», {}).get(key)
if key == «date» and isinstance(actual_val, str):
# допускаем разные форматы даты
continue
if type(actual_val) != type(expected_val):
errors.append(f»Argument {key}: expected {type(expected_val).name}, got {type(actual_val).name}»)
return {«passed»: len(errors) == 0, «errors»: errors}

Добавьте прогон с разной температурой

Одна и та же модель при temperature=0 и temperature=0.5 может вести себя по-разному. Для регрессии используйте temperature=0 — это даёт детерминированный результат на большинстве провайдеров. Отдельно прогоняйте с temperature=0.3, чтобы поймать нестабильность на граничных случаях.

Интегрируйте в CI/CD

Регрессионный тест должен запускаться при каждом изменении промпта, схемы инструмента или версии модели. В GitHub Actions это выглядит так:

yaml
name: Tool Calling Regression
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
— uses: actions/checkout@v4
— name: Run tool calling tests
run: python tests/test_tool_calling.py

В тесте вы подставляете API-ключ из секретов, прогоняете датасет и фейлите пайплайн, если accuracy упала ниже порога (например, 95%).

Зафиксируйте baseline

Перед тем как начать тестировать регрессии, прогоните датасет на текущей рабочей модели и сохраните результат как baseline. Дальнейшие прогоны сравниваются с ним — не поштучно, а по метрикам. Если accuracy на новом вызове упала на 5% относительно baseline — это повод остановить деплой.

Добавьте проверку на невызов инструмента

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

Включите в датасет 3–4 сценария, где вызов инструмента обязателен. Если модель хотя бы в одном случае не вызвала инструмент — тест падает.

Что ещё учесть

Модели ведут себя по-разному на одинаковых схемах. Claude 3.5 Sonnet, например, стабильнее вызывает инструменты с enum-полями, чем GPT-4o mini на тех же схемах. При смене модели перепрогоняйте весь датасет и обновляйте baseline.

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

Не доверяйте одному прогону. Tool calling — вероятностный процесс даже при temperature=0. Запускайте тест дважды. Если результат различается — модель нестабильна на вашем датасете, и это повод сменить модель или доработать схемы.

Open-source фреймворки. Для быстрого старта можно использовать DeepEval или LangSmith — у обоих есть встроенные метрики для function calling. Но для регрессии в CI проще написать свой валидатор на 50 строк, чем тянуть зависимость.

Когда этого недостаточно

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

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

Что делать с результатами

Если тест упал — не отключайте его. Зафиксируйте, на каком сценарии произошла ошибка, и сравните с baseline. Чаще всего проблема в схеме: слишком длинное описание параметра, неоднозначное имя инструмента или конфликт имён. Реже — в модели, которая плохо обработала конкретный паттерн.

Ошибка в tool calling — это не баг модели, а несоответствие между тем, как вы описали инструмент, и тем, как модель его поняла. Чем раньше вы это поймаете, тем меньше шансов, что агент в проде вызовет не тот API.

Источники