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

Вызов инструментов LLM: как проверить tool calling до продакшена

Вызов инструментов LLM ломается не только на JSON. Практический чек-лист для eval-набора: выбор нужного tool, валидность аргументов, отказ от лишних вызовов, ретраи и безопасные ограничения.

Схема проверки tool calling для OpenAI, Anthropic, Gemini и LangChain через JSON Schema и eval-набор
Схема проверки tool calling для OpenAI, Anthropic, Gemini и LangChain через JSON Schema и eval-набор
College of DuPage Hosts Career Fair 2016 6 | by COD Newsroom | openverse | by

Tool calling часто выглядит готовым к продакшену после первого удачного демо: модель выбрала функцию, собрала JSON, приложение вызвало API и показало результат. Проблема начинается на сотом запросе: пользователь не дал обязательный параметр, модель вызвала не тот инструмент, придумала поле вне схемы, повторила платежную операцию после тайм-аута или попыталась передать в tool то, что должно было остаться в текстовом ответе.

Вызов инструментов LLM стоит проверять как отдельный слой качества, а не как побочный эффект «хорошего промпта». Официальные документации OpenAI, Anthropic и Gemini описывают похожую идею — модель получает описание доступных функций и возвращает структурированный запрос на их выполнение, — но детали отличаются: формат схемы, управление выбором tool, поддержка параллельных вызовов, поведение при стриминге и гарантии структурированного вывода.

Ниже — практический способ собрать небольшой, но полезный eval-набор для приложения с tool calling: без доступа к реальным платежам, CRM или внутренним базам, но с проверкой тех ошибок, которые обычно всплывают уже после релиза.

Вызов инструментов LLM: что именно проверять

В документации OpenAI tool/function calling описан как способ дать модели список функций с параметрами, чтобы она могла вернуть аргументы для вызова внешнего кода. Отдельно OpenAI описывает Structured Outputs, где схема используется для ограничения формата ответа. У Anthropic похожий механизм называется tool use: модель может запросить выполнение инструмента, а приложение возвращает результат в следующий ход диалога. В Gemini API есть function calling с декларациями функций и аргументов. LangChain, в свою очередь, дает унифицированный слой для привязки tools к разным моделям.

Для eval важен не брендовый термин, а четыре наблюдаемых события:

Модель должна решить, нужен ли tool вообще.

Запрос «какая сегодня погода в Казани» требует внешнего источника, а «что такое циклон» — нет.

Модель должна выбрать правильный инструмент.

В приложении могут быть `search_docs`, `get_invoice`, `create_ticket`, `send_email`. Ошибка выбора иногда опаснее неверного текста.

Аргументы должны соответствовать схеме и бизнес-ограничениям.

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

Приложение должно безопасно исполнить или отклонить вызов.

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

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

Минимальный контракт: схема, разрешения и идемпотентность

Начинать стоит не с промпта, а с контракта инструмента. В простом случае он включает:

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

JSON Schema Draft 2020-12 описывает словари вроде `type`, `properties`, `required`, `enum`, `minimum`, `maximum`, `format`. Но схема не заменяет бизнес-логику. Например, она может проверить, что поле `amount` — число, но не знает, можно ли конкретному пользователю отправлять платеж на 300 000 рублей.

Пример инструмента для безопасного тестирования:

json
{
«name»: «create_support_ticket»,
«description»: «Создает тикет в службе поддержки, когда пользователь явно просит зарегистрировать проблему.»,
«parameters»: {
«type»: «object»,
«properties»: {
«priority»: {
«type»: «string»,
«enum»: [«low», «normal», «high»]
},
«summary»: {
«type»: «string»,
«minLength»: 8,
«maxLength»: 120
},
«user_contact»: {
«type»: «string»,
«maxLength»: 120
}
},
«required»: [«priority», «summary»],
«additionalProperties»: false
}
}

Здесь уже видны полезные ограничения: модель не может добавить произвольное поле `admin_note`, не может передать пустой `summary`, не может поставить приоритет `urgent`, если приложение его не поддерживает.

Но для реальной системы этого мало. Если tool создает тикет, нужно решить, что делать при повторном запросе после сетевой ошибки. Если tool отправляет письмо, нужен черновик или подтверждение пользователя. Если tool меняет данные, нужен audit log и запрет на исполнение только по «намерению» модели.

Eval-набор: четыре класса сценариев, которые ловят большинство сбоев

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

Класс сценария Что проверяет Пример ожидания
Прямой запрос Модель понимает очевидное намерение Вызывает `create_support_ticket` с `priority: normal`
Неполные данные Модель не выдумывает обязательные поля Задает уточняющий вопрос вместо вызова tool
Отрицательный кейс Модель не вызывает инструмент без необходимости Отвечает текстом, если пользователь просит объяснение
Конфликт или риск Модель выбирает безопасную траекторию Просит подтверждение или отказывает, если действие необратимо
Пограничные значения Аргументы проходят схему и бизнес-правила Не передает слишком длинный `summary`, не использует неизвестный enum

Пример eval-кейсов для инструмента тикетов:

json
[
{
«id»: «ticket_happy_path»,
«user»: «Создай тикет: не открывается экспорт CSV в личном кабинете»,
«expected_tool»: «create_support_ticket»,
«expected_args»: {
«priority»: «normal»
}
},
{
«id»: «ticket_missing_summary»,
«user»: «Создай тикет»,
«expected_tool»: null,
«expected_behavior»: «ask_clarifying_question»
},
{
«id»: «no_tool_explanation»,
«user»: «Объясни, что обычно пишут в хорошем тикете поддержки»,
«expected_tool»: null,
«expected_behavior»: «answer_in_text»
},
{
«id»: «invalid_priority_trap»,
«user»: «Создай максимально срочный тикет: сервер лежит»,
«expected_tool»: «create_support_ticket»,
«expected_args»: {
«priority»: «high»
}
}
]

Последний кейс специально проверяет, не придумает ли модель `critical` или `urgent`, если в схеме разрешены только `low`, `normal`, `high`.

Воспроизводимая проверка аргументов на Python без реального API

Первый слой eval можно запускать локально: модель возвращает предполагаемый tool call, а тест проверяет имя инструмента и аргументы. Реальный backend при этом не вызывается.

Ниже — минимальный пример с библиотекой `jsonschema`. Он не зависит от конкретного провайдера: вместо ответа модели можно подставить JSON, полученный из OpenAI, Claude, Gemini или абстракции LangChain.

python
from jsonschema import Draft202012Validator

ticket_schema = {
«type»: «object»,
«properties»: {
«priority»: {
«type»: «string»,
«enum»: [«low», «normal», «high»]
},
«summary»: {
«type»: «string»,
«minLength»: 8,
«maxLength»: 120
},
«user_contact»: {
«type»: «string»,
«maxLength»: 120
}
},
«required»: [«priority», «summary»],
«additionalProperties»: False
}

expected_tool = «create_support_ticket»

model_tool_call = {
«name»: «create_support_ticket»,
«arguments»: {
«priority»: «urgent»,
«summary»: «Не открывается экспорт CSV»
}
}

def check_tool_call(call):
errors = []

if call.get(«name») != expected_tool:
errors.append(f»wrong tool: {call.get(‘name’)}»)

validator = Draft202012Validator(ticket_schema)
schema_errors = sorted(
validator.iter_errors(call.get(«arguments», {})),
key=lambda e: e.path
)

for error in schema_errors:
path = «.».join(str(part) for part in error.path) or ««
errors.append(f»{path}: {error.message}»)

return errors

result = check_tool_call(model_tool_call)

if result:
print(«FAIL»)
for item in result:
print(«-«, item)
else:
print(«PASS»)

Для примера выше результат должен быть ошибкой: `urgent` не входит в enum. Это полезный тест, потому что многие ручные проверки пропустили бы такой ответ как «логически понятный».

Дальше можно добавить сравнение с ожидаемыми аргументами. Но здесь стоит быть аккуратным: иногда несколько вариантов допустимы. Например, модель может поставить `normal` или `high` в зависимости от формулировки. Поэтому eval лучше делить на жесткие проверки и мягкие проверки:

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

Как сравнивать OpenAI, Claude, Gemini и LangChain без ложной симметрии

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

OpenAI в документации по function calling и Structured Outputs делает акцент на схемах и управляемом структурированном выводе. Anthropic описывает цикл, где модель запрашивает tool use, клиент выполняет инструмент и возвращает результат. Gemini API документирует function declarations и режимы вызова функций. LangChain показывает общий интерфейс `.bind_tools()` и нормализованное представление tool calls для разных моделей.

Практический вывод: единый eval-набор возможен, но адаптеры должны быть разными. Не стоит сравнивать «сырой JSON» от разных API как будто это один протокол.

Слой Что можно унифицировать Что останется специфичным
Тестовые запросы Пользовательские формулировки, ожидаемый intent Системные инструкции и формат сообщений
Схемы Названия полей, enum, required, ограничения Поддерживаемое подмножество JSON Schema
Результат Нормализованный объект `tool_name + arguments` Нативный формат ответа провайдера
Метрики Валидность, выбор tool, лишний вызов, отказ Логика retry, streaming, parallel calls
Отладка Логи решений и аргументов Консоли, SDK и трассировка конкретного стека

Если приложение использует Model Context Protocol, MCP Inspector может помочь проверить сервер инструментов отдельно от модели: доступны ли tools, какие схемы они объявляют, что возвращают на тестовый вызов. Это не заменяет eval поведения модели, но отделяет две разные проблемы: «модель неверно выбрала tool» и «сервер tool работает неправильно».

Метрики, которые полезнее общей «точности»

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

Лучше считать несколько метрик отдельно:

Tool selection accuracy — доля кейсов, где выбран правильный инструмент. Для отрицательных кейсов правильный выбор — отсутствие вызова.

Argument schema validity — доля вызовов, где аргументы проходят JSON Schema. Это машинная проверка, ее легко запускать в CI.

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

Unnecessary tool call rate — доля случаев, где модель вызвала инструмент без необходимости. Эта метрика влияет на стоимость, задержку и риск утечки данных во внешние системы.

Unsafe action rate — доля кейсов, где модель попыталась выполнить опасное действие без подтверждения, прав или достаточного контекста. Здесь нужна ручная разметка или отдельные policy-тесты.

Argument semantic match — совпадают ли аргументы с намерением пользователя. Схема не поймает ситуацию, где дата валидна, но выбрана не та; или где `summary` переписан так, что потерял важную деталь.

Для CI обычно достаточно первых трех машинных проверок и небольшого ручного сэмпла по спорным кейсам. Для критичных инструментов — платежи, удаление данных, отправка сообщений клиентам — нужен отдельный набор safety-кейсов.

Где чаще всего ломается tool calling

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

Второй — выдуманные параметры. Пользователь пишет «завтра», модель превращает это в дату без учета часового пояса. Или пользователь просит «отправь Ивану», а в системе несколько Иванов.

Третий — лишние поля. Если сервер молча принимает дополнительные параметры, модель может передать то, что разработчик не планировал обрабатывать. Поэтому `additionalProperties: false` полезен хотя бы в тестовом контуре.

Четвертый — повтор опасного действия. Тайм-аут между приложением и внешним API не должен приводить к повторной отправке письма или повторному списанию. Идемпотентный ключ часто важнее промпта.

Пятый — расхождение между tool schema и реальным backend. Модель видит поле `user_id`, а API уже ждет `account_id`. Такие ошибки хорошо ловятся контрактными тестами и генерацией тестовых вызовов из той же схемы, которая передается модели.

Шестой — стриминг и частичные состояния. В интерфейсе пользователь может увидеть промежуточный текст до tool call или нажать кнопку до завершения проверки. UX должен учитывать, что tool call — не просто текстовый ответ, а переход к действию.

Как встроить eval в разработку без тяжелой платформы

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

Зафиксировать список tools и схемы в репозитории.

Не хранить единственную копию схемы в промпте или UI. Схема должна версионироваться как код.

Собрать JSONL с тестовыми запросами.

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

Написать адаптер ответа модели к единому формату.

Например: `{«tool»: «…», «arguments»: {…}, «text»: «…»}`. Нативные ответы OpenAI, Anthropic, Gemini или LangChain преобразуются в этот формат перед проверкой.

Запускать schema validation и intent checks в CI.

При изменении системного промпта, описания tool или версии модели тест должен показывать, какие кейсы изменили поведение.

Разделить sandbox и реальные действия.

Eval не должен создавать настоящие тикеты клиентам, отправлять email или менять данные. Для этого нужны mock-сервисы, dry-run режим или отдельный тестовый tenant.

Если команда уже использует LangSmith, OpenAI Evals или внутреннюю систему наблюдаемости, этот подход можно перенести туда. Но начинать можно с обычного Python-скрипта, JSONL-файла и валидатора схем.

Чек-лист перед выпуском инструмента в продакшен

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

  • у каждого tool есть описание «когда использовать» и «когда не использовать»;
  • обязательные поля действительно обязательны в схеме;
  • enum не допускает значений, которых нет в backend;
  • лишние поля отклоняются или явно игнорируются с логированием;
  • неполные пользовательские запросы ведут к уточняющему вопросу;
  • опасные действия требуют подтверждения или отдельного server-side правила;
  • повторный вызов не приводит к двойному эффекту;
  • тесты включают отрицательные и пограничные сценарии, а не только happy path;
  • результаты eval сохраняются при смене модели, SDK или системного промпта;
  • логи позволяют отличить ошибку выбора tool от ошибки исполнения backend.

Главное ограничение такого eval — он не доказывает, что модель всегда поведет себя правильно. Он снижает вероятность известных классов ошибок и делает регрессии видимыми. Следующий практический шаг — взять один самый рискованный инструмент в вашем приложении, описать 20 кейсов вокруг него и прогнать их через тот же адаптер, который будет использоваться в продакшене.

Источники