
AI-агент может корректно сформулировать ответ и при этом ошибиться в действии: выбрать функцию удаления вместо просмотра, передать строку вместо массива или придумать отсутствующий идентификатор. Обычные метрики генерации текста такие ошибки почти не отражают. Для интеграции с API, базами данных и внутренними сервисами нужен отдельный релизный барьер — контрактные тесты вызовов инструментов.
Их задача состоит не в оценке стиля ответа. Тест должен установить, выбрала ли LLM допустимое действие, соблюла ли схему аргументов и использовала ли значения, доступные во входных данных. Такой подход подходит для OpenAI Function Calling, Anthropic Tool Use, Gemini Function Calling и собственных агентных оболочек: меняется формат ответа провайдера, но проверяемый контракт остаётся тем же.
Что считать корректным вызовом
Проверка только имени функции слишком слабая. Вызов `send_email` формально может быть выбран правильно, но оказаться опасным из-за ошибочного адресата или выдуманного текста. Поэтому ожидаемый результат теста лучше описывать как контракт из нескольких условий:
Нужно ли вообще вызывать инструмент.
Какая функция или группа допустимых функций разрешена.
3. Какие аргументы обязательны.
4. Какие значения должны совпадать точно.
5. Какие значения можно сравнивать по правилам, а не буквально.
6. Разрешены ли дополнительные поля и повторные вызовы.
7. Важен ли порядок нескольких действий.
Для запроса «Найди заказ 4187» эталон можно представить так:
json
{
«expected_tool»: «get_order»,
«expected_arguments»: {
«order_id»: 4187
},
«allow_extra_arguments»: false,
«expected_call_count»: 1
}
Для более свободного сценария контракт может разрешать несколько эквивалентных вариантов. Например, агент вправе найти пользователя через `get_user_by_email` либо выполнить поиск через `search_users`, если оба пути безопасны и приводят к одному результату. Жёсткое сравнение с единственным эталоном в таком случае создаст ложные падения тестов.
Метрики для релизного барьера
Одной общей оценки недостаточно: она скрывает тип ошибки. Команде полезнее видеть отдельные показатели выбора действия, структуры и содержания аргументов.
| Метрика | Что проверяет | Пример ошибки | Рекомендуемая реакция |
|---|---|---|---|
| Tool selection accuracy | Совпало ли имя функции с допустимым вариантом | `delete_order` вместо `get_order` | Блокировать релиз для опасных действий |
| Tool decision precision | Как часто вызов действительно был нужен | Агент вызывает поиск для обычного приветствия | Добавить негативные примеры |
| Tool decision recall | Как часто агент вызвал инструмент, когда это требовалось | Ответил по памяти вместо обращения к базе | Уточнить условия обязательного вызова |
| Schema adherence | Соответствуют ли аргументы JSON Schema | Строка `»10″` вместо числа `10` | Исправить схему или инструкцию |
| Argument correctness | Верны ли значения аргументов по смыслу | Передан ID другого клиента | Проверить извлечение данных и контекст |
Для набора из \(N\) примеров точность выбора инструмента рассчитывается как доля примеров с допустимым именем функции. Однако для решения о выпуске безопаснее применять составной показатель: тест считается пройденным, только если выполнены все критические условия контракта.
Такой показатель часто называют Pass@1 — долей сценариев, успешно выполненных с первой попытки. Если агенту разрешено исправляться после ошибки инструмента, отдельно можно измерять Pass@2 или Pass@3. Смешивать эти результаты не стоит: успешное исправление полезно для устойчивости, но оно увеличивает задержку и стоимость выполнения.
Порог следует задавать по классу риска. Для поиска по публичному каталогу допустим один уровень, для отправки платежа или удаления записи — другой. Критические операции разумно проверять по правилу «ни одной опасной ошибки в контрольном наборе», а не только по среднему проценту.
Как собрать набор сценариев
Начальный набор должен отражать реальные решения агента, а не состоять из нескольких очевидных команд. Практичный минимум включает шесть типов сценариев:
Прямой вызов с обязательными аргументами.
Вызов с уместными необязательными параметрами.
3. Выбор между функциями с похожими описаниями.
4. Отказ от вызова, когда инструмент не требуется.
5. Обработка неполных или неоднозначных данных.
6. Последовательность из двух и более зависимых действий.
Особенно важны негативные сценарии. Если проверять только запросы, где функция нужна, невозможно измерить ложные срабатывания. В набор стоит добавить приветствия, вопросы общего характера, просьбы объяснить возможности агента и команды без обязательных данных.
Для опасных функций нужны отдельные проверки подтверждения. Запрос «Удали черновики старше месяца» не должен автоматически превращаться в массовое удаление, если правила продукта требуют предварительно показать список и запросить согласие пользователя.
Каждый пример удобно хранить как отдельную запись:
yaml
id: order_lookup_by_number
input: «Покажи статус заказа 4187»
allowed_tools:
— get_order
expected_arguments:
order_id: 4187
forbidden_tools:
— update_order
— cancel_order
expected_call_count: 1
risk: low
В тестовых данных полезно фиксировать причину ожидания. Короткий комментарий «номер заказа явно указан пользователем» поможет разобраться, почему тест существует и можно ли менять эталон после обновления схемы.
Источником новых сценариев должны служить обезличенные сбои из эксплуатации: неверно выбранные функции, пропущенные обязательные вызовы, ошибки типов и неожиданные комбинации аргументов. Перед добавлением в репозиторий из примеров необходимо удалить персональные данные, токены доступа и коммерчески чувствительные значения.
Валидация JSON Schema и аргументов
Официальные механизмы OpenAI, Anthropic и Gemini позволяют описывать инструменты через структурированные схемы. Это снижает число синтаксических ошибок, но не отменяет проверку на стороне приложения. Корректный JSON ещё не означает корректное действие.
Пример схемы инструмента:
json
{
«name»: «get_order»,
«description»: «Возвращает заказ по его числовому идентификатору»,
«input_schema»: {
«type»: «object»,
«properties»: {
«order_id»: {
«type»: «integer»,
«minimum»: 1
}
},
«required»: [«order_id»],
«additionalProperties»: false
}
}
Параметр `additionalProperties: false` позволяет обнаруживать выдуманные поля. Ограничения `enum`, `minimum`, `maximum`, `format` и регулярные выражения делают контракт точнее. При этом чрезмерно широкая схема, например поле типа `string` без ограничений, переносит значительную часть риска в прикладной код.
Базовую проверку на Python можно выполнить через библиотеку `jsonschema`:
python
from jsonschema import validate
from jsonschema.exceptions import ValidationError
def check_tool_call(actual_call, expected_tool, schema):
if actual_call[«name»] != expected_tool:
return False, (
f»Ожидался инструмент {expected_tool}, «
f»получен {actual_call[‘name’]}»
)
try:
validate(
instance=actual_call[«arguments»],
schema=schema
)
except ValidationError as error:
return False, error.message
return True, «ok»
После структурной валидации нужна проверка значений. Для идентификаторов, денежных сумм, адресов электронной почты и кодов операций обычно требуется точное совпадение. Для дат можно заранее определить часовой пояс и разрешённый формат. Строки поиска иногда допустимо нормализовать: удалить лишние пробелы или привести регистр, если это не меняет смысл.
Семантическую оценку с помощью другой LLM лучше применять как дополнительный сигнал для свободных текстовых аргументов. Она не должна заменять детерминированную проверку там, где доступны точные правила.
Почему одного прогона недостаточно
Ответы LLM могут меняться между запросами даже при одинаковых входных данных. На результат влияют параметры генерации, версия сервиса, маршрутизация у провайдера и особенности декодирования. Нулевая температура уменьшает вариативность, но не является универсальной гарантией идентичного ответа.
Поэтому критические сценарии следует запускать несколько раз. Помимо среднего Pass@1, полезно сохранять:
- минимальный результат по серии прогонов;
- число нестабильных примеров;
- долю сценариев, где меняется выбранный инструмент;
- распределение числа вызовов;
- задержку и расход токенов.
Если пример проходит четыре раза из пяти, считать его надёжным нельзя. Такой тест лучше пометить как нестабильный и проверить отдельно: возможно, описания функций пересекаются, входные данные неоднозначны или контракт допускает слишком мало корректных вариантов.
Чтобы сравнение версий было честным, фиксируют конфигурацию инструментов, параметры генерации, набор примеров и правила проверки. Если провайдер предоставляет идентификатор версии или снимка модели, его также сохраняют в отчёте.
Как запускать проверки в CI
Контрактные тесты стоит выполнять при изменении описаний инструментов, системных инструкций, агентной логики и версии используемой LLM. Для быстрых проверок достаточно небольшого стабильного набора, а расширенную выборку можно запускать по расписанию.
Последовательность CI-проверки выглядит так:
Загрузить определения инструментов и тестовые примеры.
Отправить каждый входной запрос в тестовое окружение.
3. Нормализовать ответ провайдера до единого формата.
4. Проверить необходимость вызова, имя функции и число действий.
5. Провалидировать аргументы по JSON Schema.
6. Сравнить значения с контрактом сценария.
7. Сформировать отчёт с причинами расхождений.
8. Завершить задачу ошибкой при нарушении порога.
Пример теста на Pytest:
python
import pytest
@pytest.mark.parametrize(«case», load_cases(«tests/tool_cases»))
def test_tool_contract(case, llm_client):
response = llm_client.run(
user_input=case[«input»],
tools=load_tool_definitions()
)
result = evaluate_tool_calls(
actual=response.tool_calls,
expected=case
)
assert result.passed, result.diff
Отчёт должен показывать фактический вызов и ожидаемый контракт, а не только статус `failed`. Полезный diff отвечает на конкретный вопрос: неверно выбрана функция, отсутствует аргумент, нарушен тип, появилось лишнее поле или использовано неправильное значение.
Секреты нельзя помещать в тестовые записи и журналы. Интеграции с внешними системами следует заменять заглушками или изолированным тестовым контуром, особенно для платежей, рассылок, удаления данных и изменения прав доступа.
Порог качества и классификация ошибок
Единый порог вроде 85% удобен для сводного отчёта, но плохо учитывает последствия. Ошибка чтения и ошибочное необратимое действие имеют разный вес.
Практичнее разделить тесты по риску:
- критический — платежи, удаление, изменение доступа, отправка сообщений внешним адресатам;
- высокий — запись в CRM, изменение заказа, создание публичной публикации;
- средний — чтение внутренних данных и построение отчётов;
- низкий — поиск по открытому каталогу или получение погоды.
Для критических сценариев релиз блокирует любой вызов запрещённого инструмента, подмена идентификатора, отсутствие требуемого подтверждения или выход аргумента за установленный диапазон. Для низкого риска можно использовать статистический порог и разбирать отдельные отклонения после прогона.
Также стоит различать регрессию и известный дефект. Если новая версия снизила общую точность с 94% до 91%, но исправила критическую ошибку отмены заказов, одно агрегированное число не даст правильного решения. В отчёте нужны результаты по категориям риска и типам инструментов.
Ограничения контрактных тестов
Контрактные тесты проверяют заранее описанные ожидания. Они не доказывают, что агент безопасен во всех возможных диалогах.
Основные ограничения:
- тестовый набор может не содержать редких формулировок;
- корректный вызов способен привести к ошибке на стороне внешнего API;
- JSON Schema проверяет структуру, но не бизнес-правила;
- несколько допустимых планов действий трудно описать одним эталоном;
- обновление внешнего сервиса может изменить поведение без изменения агента;
- высокая оценка на статичном наборе создаёт риск подгонки под известные примеры.
Эти ограничения закрывают слоями. После контрактной проверки запускают интеграционные тесты с заглушками API, тесты бизнес-правил и проверки разрешений. В рабочей среде отслеживают долю ошибок инструментов, повторы, отменённые действия и неожиданные аргументы. Новые классы сбоев возвращают в регрессионный набор.
Практический план внедрения
Начать можно с небольшого контролируемого набора, не пытаясь сразу оценить все диалоги агента.
- Выберите 20–30 наиболее частых или рискованных операций.
- Добавьте для каждой операции позитивный и негативный сценарий.
- Зафиксируйте допустимые функции, обязательные аргументы и запрещённые действия.
- Подключите JSON Schema с запретом лишних полей там, где это возможно.
- Запустите каждый критический сценарий несколько раз.
- Разделите ошибки выбора функции, структуры и значений.
- Установите отдельные пороги для разных классов риска.
- Блокируйте выпуск при любой новой критической регрессии.
- Пополняйте набор обезличенными примерами из реальных сбоев.
Первый полезный результат — не высокий общий балл, а отчёт, по которому разработчик за несколько минут понимает причину отказа. Если тест показывает только процент успешности, его будет трудно использовать для исправления агента.
Источники
- OpenAI Function Calling Guide — описание объявления функций, структурированных аргументов и обработки вызовов.
- Anthropic Tool Use — официальная документация по определению инструментов и циклу их выполнения.
- Google Gemini Function Calling Cookbook — практический пример вызова функций через Gemini.
- Databricks: LLM Auto-Evaluation for Complex Tasks — подходы к автоматизированной оценке сложных задач с несколькими критериями.
- arXiv 2407.04769 — исследовательская работа 2024 года об оценке LLM в сценариях взаимодействия с инструментами.
