Запись архива

Как протестировать tool calling в LLM до продакшена: сценарии, метрики и автоматическая проверка

Практическая методика проверки tool calling в AI-агентах: синтетические сценарии, валидация аргументов, тесты цепочек, обработка ошибок и критерии допуска в продакшен.

Панель тестирования tool calling для GPT-4o и Claude с аргументами функций, ошибками JSON Schema и результатами проверок
Панель тестирования tool calling для GPT-4o и Claude с аргументами функций, ошибками JSON Schema и результатами проверок
Kurdish people protest against the Turkiish government at Hay Hill, Norwich | by Roger Blackwell | openverse | by

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

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

Ниже — узкая практическая методика: как проверить именно маршрутизацию инструментов и корректность аргументов, не смешивая этот тест с общей оценкой качества ответов LLM.

Разделите ответственность модели и исполнителя

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

LLM решает, требуется ли инструмент, выбирает его и формирует аргументы.

Валидатор проверяет имя функции и структуру аргументов.
3. Исполнитель обращается к внешнему сервису и возвращает результат агенту.

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

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

Для каждого теста полезно сохранять четыре объекта:

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

Без такого журнала итоговый процент успешных ответов мало что говорит о причине ошибки.

Зафиксируйте контракт каждого инструмента

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

Пример функции создания заявки:

json
{
«name»: «create_support_ticket»,
«description»: «Создаёт обращение в службе поддержки»,
«parameters»: {
«type»: «object»,
«properties»: {
«customer_id»: {
«type»: «string»,
«minLength»: 1
},
«category»: {
«type»: «string»,
«enum»: [«billing», «access», «technical»]
},
«priority»: {
«type»: «integer»,
«minimum»: 1,
«maximum»: 3
},
«message»: {
«type»: «string»,
«minLength»: 10
}
},
«required»: [«customer_id», «category», «message»],
«additionalProperties»: false
}
}

Поле `additionalProperties: false` закрывает распространённую проблему: модель добавляет аргументы, которых исполнитель не ожидает. Ограничения `enum`, `minimum`, `maximum` и `minLength` позволяют отличить синтаксически правильный JSON от допустимого вызова.

Контракт должен соответствовать реальному API. Если приложение затем преобразует `customer_id` в другое поле или незаметно подставляет значения, тест начинает оценивать адаптер, а не поведение LLM.

Для критичных действий стоит добавить отдельные правила, которые не выражаются одной JSON Schema:

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

Соберите матрицу синтетических сценариев

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

Начните с небольшой матрицы:

Класс сценария Пример условия Ожидаемое поведение Критическая ошибка
Полный запрос Все обязательные значения указаны Один корректный вызов Пропуск или неверные аргументы
Неполные данные Нет `customer_id` Запросить уточнение Вызвать функцию с выдуманным ID
Неоднозначный выбор Доступны две похожие функции Выбрать функцию по назначению Вызвать более опасный инструмент
Ошибка сервиса Исполнитель вернул 500 Следовать политике повторов приложения Бесконечный цикл вызовов
Повтор операции В контексте уже есть успешный результат Использовать сохранённый результат Создать дубликат

Для первой итерации достаточно 50–100 сценариев, если они равномерно покрывают основные классы. Внутри каждого класса меняйте формулировку запроса, порядок фактов, наличие лишнего контекста и граничные значения.

Полезно включить как минимум следующие варианты:

Отсутствует одно обязательное поле.

Отсутствуют сразу несколько полей.
3. Значение имеет неверный тип.
4. Значение корректно по типу, но выходит за допустимый диапазон.
5. Пользователь упомянул поле, которого нет в контракте.
6. Доступны инструменты с похожими именами.
7. Вызов не требуется, поскольку ответ уже есть в контексте.
8. Для действия необходимо явное подтверждение.
9. Первый инструмент возвращает пустой результат.
10. Второй шаг цепочки получает данные от первого.

Отдельно добавьте отрицательные примеры. В них правильный результат — отсутствие вызова. Без этой группы тест легко оптимизировать под высокую полноту ценой большого числа ложных действий.

Генерируйте варианты, сохраняя ожидаемый результат

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

Удобный формат сценария:

json
{
«case_id»: «ticket_missing_customer_017»,
«user_input»: «Создай обращение: не могу войти в аккаунт»,
«available_tools»: [«create_support_ticket»],
«expected»: {
«action»: «ask_clarification»,
«missing_fields»: [«customer_id»],
«forbidden_tools»: [«create_support_ticket»]
}
}

Простой генератор мутаций на Python может удалять обязательные аргументы, заменять типы и добавлять неизвестные поля:

python
import copy
import random

def mutate_arguments(valid_args, schema):
variants = []

for field in schema.get(«required», []):
changed = copy.deepcopy(valid_args)
changed.pop(field, None)
variants.append({
«kind»: «missing_required»,
«field»: field,
«arguments»: changed,
«expected»: «reject_before_execution»
})

properties = schema.get(«properties», {})
integer_fields = [
name for name, spec in properties.items()
if spec.get(«type») == «integer»
]

if integer_fields:
field = random.choice(integer_fields)
changed = copy.deepcopy(valid_args)
changed[field] = «high»
variants.append({
«kind»: «wrong_type»,
«field»: field,
«arguments»: changed,
«expected»: «reject_before_execution»
})

changed = copy.deepcopy(valid_args)
changed[«unknown_field»] = «unexpected»
variants.append({
«kind»: «additional_property»,
«field»: «unknown_field»,
«arguments»: changed,
«expected»: «reject_before_execution»
})

return variants

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

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

Измеряйте решение, аргументы и исполнение отдельно

Одна метрика «успешный ответ» скрывает разные дефекты. Для tool calling нужны как минимум четыре независимых показателя.

Tool precision показывает, какая доля совершённых вызовов действительно требовалась:

text
TP / (TP + FP)

Tool recall показывает, какая доля необходимых вызовов была выполнена:

text
TP / (TP + FN)

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

Argument accuracy — доля вызовов, где значения совпали с эталоном. Вызов может пройти JSON Schema и всё равно содержать неверный `customer_id` или категорию.

Дополнительно фиксируйте:

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

Не объединяйте эти показатели в единственный балл до анализа ошибок. Две системы с одинаковой итоговой оценкой могут иметь разный риск: одна пропускает безопасные вызовы, другая выполняет лишние операции.

Проверяйте аргументы детерминированным валидатором

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

Базовая последовательность выглядит так:

python
from jsonschema import Draft202012Validator

def validate_tool_call(call, tool_registry):
tool_name = call.get(«name»)

if tool_name not in tool_registry:
return False, [«unknown_tool»]

schema = tool_registry[tool_name][«parameters»]
arguments = call.get(«arguments», {})
validator = Draft202012Validator(schema)

errors = sorted(
validator.iter_errors(arguments),
key=lambda item: list(item.path)
)

if errors:
return False, [error.message for error in errors]

return True, []

После JSON Schema добавьте прикладные проверки: права доступа, допустимость перехода состояния, принадлежность объекта пользователю и наличие подтверждения. Эти условия должны выполняться до обращения к внешнему API.

Опасная практика — автоматически «исправлять» аргументы без регистрации изменения. Если строка преобразована в число или неизвестное поле удалено, журнал должен показывать исходные и нормализованные данные. Иначе команда увидит высокий процент успеха, скрывающий нестабильную генерацию.

Тестируйте цепочки как конечный автомат

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

Пример:

text
получить идентификатор клиента
→ запросить список обращений
→ выбрать нужное обращение
→ получить подтверждение пользователя
→ изменить статус

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

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

Для действий с побочными эффектами применяйте тестовый исполнитель или мок-сервис. Он должен записывать вызовы, возвращать заранее заданные ответы и поддерживать сценарии ошибок: тайм-аут, HTTP 429, HTTP 500, повреждённый JSON и пустой результат.

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

Не поручайте LLM политику повторных попыток

Решение о повторе зависит от типа ошибки. Тайм-аут чтения иногда допускает повтор, а ошибка авторизации обычно требует остановки. После неудачного запроса на создание объекта нельзя автоматически считать, что объект не создан: сервис мог выполнить операцию, но не успеть вернуть ответ.

Политику стоит закрепить в коде:

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

В синтетическом тесте заранее задайте последовательность ответов, например `500 → 500 → 200`, и проверьте исполнителя. Отдельным сценарием задайте `401` и убедитесь, что повторов нет. Так можно отличить ошибку оркестрации от ошибки выбора инструмента.

Сравнивайте OpenAI, Claude и локальные модели на одинаковом адаптере

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

Сравнение будет корректным, если совпадают:

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

Каждый сценарий желательно выполнить несколько раз. Даже при низкой температуре ответы могут различаться из-за серверной реализации и обновлений модели. В отчёте указывайте точное имя модели, дату запуска, настройки и число повторов.

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

Для OpenAI полезен пример вызова функций из OpenAI Cookbook:

https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb

Документация Anthropic описывает структуру инструментов и блоки `tool_use`:

https://docs.anthropic.com/en/docs/build-with-claude/tool-use

Пример потоковой обработки вызовов Claude размещён в Anthropic Cookbook:

https://github.com/anthropics/anthropic-cookbook/blob/main/tool_use/streaming_tool_use.ipynb

Для локальных систем можно изучить NeMo Guardrails, однако наличие guardrails не заменяет проверку аргументов и авторизацию:

https://github.com/NVIDIA/NeMo-Guardrails

Спецификация JSON Schema Draft 2020-12 доступна на официальном сайте проекта:

https://json-schema.org/draft/2020-12/json-schema-core

Установите критерии допуска до запуска теста

Порог нельзя выбирать после просмотра результатов. Иначе команда неизбежно подгонит решение под уже полученные цифры.

Пример критериев для внутреннего агента:

  • 100% критичных вызовов проходят авторизацию;
  • 0 выполненных операций без обязательного подтверждения;
  • не менее 98% вызовов проходят JSON Schema;
  • не менее 95% точности выбора инструмента;
  • 0 бесконечных циклов;
  • не более одного фактического исполнения для одного ключа идемпотентности;
  • все неуспешные случаи имеют код причины и воспроизводимый вход.

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

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

Практический план проверки за один день

Перед тем как подключать агента к реальному API, выполните ограниченный прогон:

Выберите один инструмент с побочным эффектом.

Опишите его строгой JSON Schema.
3. Подготовьте 20 корректных и 20 отрицательных сценариев.
4. Добавьте 10 случаев с пропущенными или неверными аргументами.
5. Подключите мок-исполнитель с ответами 200, 401, 429, 500 и тайм-аутом.
6. Проверьте выбор инструмента, схему и значения аргументов отдельно.
7. Повторите каждый сценарий минимум три раза.
8. Разберите все ложные вызовы вручную.
9. Зафиксируйте пороги допуска и добавьте провалы в регрессионный набор.
10. Только после этого разрешите ограниченный доступ к тестовой среде.

Главный результат такого прогона — не красивый средний балл, а перечень конкретных отказов: какой запрос вызвал неверное действие, почему защита его пропустила и на каком уровне нужно исправление. Именно этот список позволяет принять обоснованное решение о готовности tool calling к продакшену.