
Tool calling позволяет языковой модели обращаться к поиску, корпоративным базам, календарям, платёжным шлюзам и другим внешним системам. Для AI-агента это переход от генерации текста к выполнению действий. Ошибочный вызов может создать запись, изменить данные, отправить сообщение или запустить платную операцию.
Поэтому регрессионный набор должен отвечать на конкретные вопросы:
Выбрала ли модель нужную функцию?
Передала ли аргументы по заданной схеме?
3. Отказалась ли от вызова, когда инструмент не требовался?
4. Запросила ли недостающие данные вместо их выдумывания?
5. Соблюла ли допустимый порядок действий в многошаговом сценарии?
Такой набор стоит запускать при изменении системной инструкции, описаний инструментов, JSON-схем, параметров модели и версии API.
Почему нельзя сравнивать готовые ответы
Классический юнит-тест часто сопоставляет результат с заранее записанной строкой. Для LLM эта проверка слишком хрупкая: два корректных ответа могут отличаться формулировкой, порядком слов и пояснениями.
В сценариях с инструментами важнее структурированное действие. Тесту обычно достаточно разобрать ответ модели и проверить:
- количество вызовов;
- имя выбранного инструмента;
- наличие обязательных аргументов;
- типы и допустимые значения;
- соответствие аргументов запросу пользователя;
- отсутствие неизвестных полей;
- решение не вызывать инструмент;
- порядок вызовов при выполнении цепочки.
Строгая JSON-схема покрывает только структуру. Например, она подтвердит, что `event_name` является строкой, однако не определит, правильно ли модель извлекла название мероприятия. Структурную и смысловую корректность следует измерять отдельно.
Как хранить тестовые примеры
Каждый пример удобно хранить как запись с входным запросом, доступными инструментами и ожидаемым поведением. Одной пары «запрос — ответ» недостаточно: исполнитель тестов должен знать, допустим ли вызов и какие ограничения применяются к аргументам.
Пример положительного сценария:
json
{
«id»: «schedule_001»,
«query»: «Когда начинается конференция AI Summit?»,
«tools»: [«get_event_time»],
«expected»: {
«action»: «call»,
«function»: «get_event_time»,
«arguments»: {
«event_name»: «AI Summit»
}
}
}
Отрицательный сценарий задаётся явно:
json
{
«id»: «schedule_002»,
«query»: «Объясни разницу между AI-агентом и чат-ботом»,
«tools»: [«get_event_time»],
«expected»: {
«action»: «no_call»
}
}
Для запроса с недостающими данными полезно выделить отдельное действие:
json
{
«id»: «schedule_003»,
«query»: «Найди время начала мероприятия»,
«tools»: [«get_event_time»],
«expected»: {
«action»: «clarify»,
«missing»: [«event_name»]
}
}
Разделение `no_call` и `clarify` важно. В первом случае инструмент не нужен по смыслу запроса. Во втором он нужен, однако выполнить вызов безопасно пока нельзя.
Минимальный набор сценариев
Для каждой функции нужны положительные, отрицательные и пограничные примеры. Если тестировать только очевидные запросы, набор покажет высокую точность, но пропустит наиболее опасные ошибки.
| Запрос | Ожидаемое действие | Что проверить | Основной риск |
|---|---|---|---|
| Когда начинается AI Summit? | Вызвать `get_event_time` | `event_name = «AI Summit»` | Пропущенный вызов |
| Во сколько открытие Robotics Expo? | Вызвать `get_event_time` | Полное название события | Потеря части аргумента |
| Расскажи, что такое function calling | Не вызывать инструмент | Ноль вызовов | Ложное срабатывание |
| Найди время начала | Запросить уточнение | Отсутствие выдуманного названия | Галлюцинация аргумента |
| Когда начинается AI Summit и что взять с собой? | Вызвать инструмент и ответить на вторую часть | Один релевантный вызов | Игнорирование составного запроса |
К пограничным случаям также относятся опечатки, разговорные формулировки, несколько сущностей в одном запросе, конфликтующие условия и попытки пользователя передать значения за пределами разрешённого диапазона.
Проверка одного вызова
Базовый валидатор может проверять число вызовов, имя функции и аргументы:
python
def validate_single_call(response, expected):
calls = response.tool_calls or []
action = expected[«action»]
if action in {«no_call», «clarify»}:
assert len(calls) == 0
return
assert len(calls) == 1
call = calls[0]
assert call.function.name == expected[«function»]
assert call.arguments == expected[«arguments»]
Полное равенство аргументов подходит не всегда. Регистр, лишние пробелы и допустимые варианты записи лучше нормализовать:
python
def normalize_text(value):
return » «.join(value.strip().lower().split())
def assert_event_name(actual, expected):
assert normalize_text(actual) == normalize_text(expected)
При этом нормализация не должна скрывать смысловые ошибки. Значения `AI Summit` и `AI Summit Europe` нельзя считать одинаковыми только из-за общего фрагмента.
Схема и смысл аргументов
Рассмотрим функцию поиска товаров:
python
search_products(
query: str,
min_price: float | None,
max_price: float | None,
currency: str
)
Для запроса «Найди беспроводные наушники до 10 000 рублей» структурный валидатор проверяет типы и обязательные поля. Смысловой валидатор должен дополнительно подтвердить:
- `query` содержит категорию товара;
- `max_price` равен 10000;
- `min_price` не выдуман;
- `currency` соответствует рублям;
- верхняя и нижняя границы не перепутаны.
JSON Schema можно проверять библиотекой `jsonschema`:
python
from jsonschema import validate
validate(instance=call.arguments, schema=tool_schema)
После этого выполняются предметные проверки:
python
args = call.arguments
assert args[«max_price»] == 10000
assert args[«currency»] == «RUB»
assert args.get(«min_price») is None
assert «наушник» in args[«query»].lower()
Если аргументы влияют на деньги, доступ или удаление данных, следует применять более строгие правила: точное соответствие идентификаторов, белые списки значений и запрет на неописанные поля.
Метрики для регрессионного набора
Одной общей доли успешных тестов недостаточно. Она не показывает, что именно сломалось после изменения промпта или модели.
Полезно считать несколько метрик:
- Tool selection accuracy — доля примеров, где выбрана правильная функция.
- Argument schema validity — доля вызовов, прошедших проверку JSON-схемы.
- Argument semantic accuracy — доля вызовов с корректным смыслом аргументов.
- No-call accuracy — доля запросов, где модель правильно отказалась от инструмента.
- Clarification accuracy — доля неполных запросов, для которых модель запросила данные.
- Sequence accuracy — доля многошаговых сценариев с допустимым порядком вызовов.
- Execution safety rate — доля сценариев без запрещённых или лишних действий.
Метрики лучше выводить отдельно по инструментам и типам сценариев. Например, общий результат 94% может скрывать, что безопасная функция поиска проходит почти все проверки, а функция отмены заказа регулярно получает неверный идентификатор.
Для критичных операций полезно установить жёсткие пороги. Один ошибочный вызов удаления или платежа может блокировать выпуск, даже если среднее качество набора остаётся высоким.
Многошаговые сценарии
AI-агент может сначала найти объект, затем запросить его состояние и только после подтверждения выполнить действие. Проверять лишь финальный результат в таком случае недостаточно.
Допустимая последовательность для отмены заказа может выглядеть так:
`find_order`
`get_order_status`
3. запрос подтверждения пользователя
4. `cancel_order`
Тест должен отклонить сценарий, если `cancel_order` вызван до получения идентификатора, без проверки статуса или без подтверждения.
python
actual_names = [
call.function.name
for call in response.tool_calls
]
assert actual_names == [
«find_order»,
«get_order_status»,
«cancel_order»
]
В реальной системе подтверждение пользователя часто происходит между разными запросами к модели. Поэтому состояние диалога и ответы моков нужно сохранять в тестовой фикстуре. Для нескольких допустимых маршрутов лучше описывать граф разрешённых переходов, а не одну жёсткую цепочку.
Моки вместо реальных сервисов
Регрессионные тесты не должны создавать встречи, отправлять письма или проводить платежи. Вместо настоящего исполнения инструментов используются детерминированные заглушки.
python
FAKE_EVENTS = {
«AI Summit»: «10:00»,
«Robotics Expo»: «12:30»
}
def fake_get_event_time(event_name):
if event_name not in FAKE_EVENTS:
return {«status»: «not_found»}
return {
«status»: «ok»,
«start_time»: FAKE_EVENTS[event_name]
}
Мок должен сохранять журнал обращений. Тогда тест проверит не только ответ агента, но также фактическое количество вызовов и переданные параметры.
Для инструментов с побочными эффектами полезны три уровня защиты:
- подмена клиента внешнего API;
- тестовые учётные данные без доступа к продуктивной среде;
- сетевой запрет на обращения к неизвестным адресам во время CI.
Даже если мок настроен неверно, сетевое ограничение не позволит тесту случайно выполнить реальную операцию.
Как учитывать недетерминированность
Один успешный запуск не гарантирует стабильности. Результат может меняться из-за семплирования, обновления модели или небольших различий контекста.
Для базовой регрессии стоит использовать минимальную доступную температуру и фиксированные версии моделей, если провайдер поддерживает версионирование. Критичные сценарии можно запускать несколько раз и считать долю успеха.
Например, если тест выполняется пять раз, можно потребовать пять успешных результатов для платежей и не менее четырёх для безопасного поиска. Порог выбирается по цене ошибки, а не единообразно для всего набора.
Не следует автоматически повторять только упавший тест до первого успеха: такая схема скрывает нестабильность. В отчёте нужно сохранять результаты всех попыток и фактические ответы модели.
Запуск проверок в CI
Набор удобно разделить на два уровня:
- быстрые проверки схем, валидаторов и моков без обращения к LLM;
- модельные проверки через API для выбранного регрессионного набора.
Первый уровень запускается на каждый коммит. Второй можно запускать при изменении промптов, схем инструментов, клиентского кода или перед выпуском.
Пример команды:
bash
pytest tests/tool_calling \
—junitxml=reports/tool-calling.xml
В CI следует передавать ключ API через защищённое хранилище секретов, ограничивать бюджет и задавать тайм-ауты. В журнал нельзя выводить токены доступа, персональные данные и полное содержимое чувствительных запросов.
Полезный отчёт по неудачному примеру содержит:
- идентификатор теста;
- версию модели;
- ожидаемое действие;
- фактическое имя функции;
- аргументы после маскирования чувствительных полей;
- ошибку схемы или смысловой проверки;
- длительность и число попыток.
Что блокирует выпуск
Перед подключением тестов к CI команда должна определить правила остановки сборки. Практичный минимальный вариант:
Любой неизвестный инструмент блокирует выпуск.
Любой реальный побочный эффект в тестовой среде блокирует выпуск.
3. Ошибка схемы в критичной функции блокирует выпуск.
4. Снижение no-call accuracy ниже установленного порога блокирует выпуск.
5. Ухудшение некритичной метрики создаёт предупреждение и отчёт для разбора.
Порог следует хранить рядом с тестами, чтобы его изменение проходило код-ревью. Иначе качество можно незаметно «исправить» снижением требований.
Практический следующий шаг
Начните с 20–30 примеров для одной функции: добавьте успешные вызовы, нерелевантные запросы, неполные формулировки, опечатки и опасные значения. Затем подключите проверку JSON-схемы, журнал моков и отдельные метрики для выбора функции и аргументов.
После первого запуска сохраните ошибки как новые регрессионные примеры. Такой набор должен расти из реальных сбоев: если модель однажды перепутала валюту, вызвала инструмент без подтверждения или потеряла идентификатор, этот случай больше не должен оставаться только записью в журнале.
При проектировании формата можно сверяться с руководствами OpenAI по function calling и Anthropic по tool use, а подходы к оценке выбора инструментов сопоставить с материалом LangChain:
- https://platform.openai.com/docs/guides/function-calling
- https://docs.anthropic.com/en/docs/build-with-claude/tool-use
- https://github.com/anthropics/anthropic-cookbook/blob/main/tool_use/tool_use_overview.ipynb
- https://blog.langchain.dev/evaluating-tool-calling-llms/
Документация провайдеров меняется, поэтому перед внедрением следует проверить актуальные форматы ответов, правила выбора инструментов и доступность закреплённых версий моделей.