MAATRIX / Блог / Tool calling в LLM: как работает вызов функций моделью

Tool calling в LLM: как работает вызов функций моделью

Tool calling в LLM: как работает вызов функций моделью

MAATRIX

Со стороны выглядит как чудо: модель «поняла», что нужны актуальные курсы валют, «решила» вызвать функцию get_exchange_rate, «дождалась» результата и ответила уже с цифрами. Разработчики, которые впервые подключают tool calling, часто представляют это как встроенную способность LLM — будто внутри модели есть исполнительный модуль, умеющий ходить в интернет или дёргать API. На деле модель не выполняет ничего: у неё нет доступа ни к сети, ни к файловой системе, ни к процессам, всё, что она умеет, — предсказывать следующий токен текста. Ниже — разбор механизма без метафор: что физически происходит между запросом пользователя и «вызовом функции», кто на самом деле дёргает API, и почему без понимания этого цикла невозможно спроектировать агента или интеграцию с MCP.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Модель не вызывает функцию — она генерирует текст, похожий на вызов

Ключевая ошибка в ментальной модели — думать, что «tool calling» это глагол, который делает LLM. На самом деле это существительное: формат вывода. Когда модели во время обучения показали тысячи примеров вида «если нужен внешний инструмент — выведи JSON с именем функции и аргументами вместо обычного текста», она научилась предсказывать именно такую структуру в подходящий момент. Это тот же механизм, что генерирует обычный ответ — предсказание следующего токена на основе контекста, — просто натренированный распознавать ситуации, когда более вероятным продолжением является не проза, а структурированный объект.

Технически современные API дают модели два режима вывода в одном запросе: обычный текстовый ответ и специальный формат «structured output», зарезервированный под описание вызова инструмента. Если в промпте пользователь спросил «сколько сейчас стоит биткоин», а среди доступных инструментов есть get_crypto_price, вероятностное распределение над токенами сильно смещается в сторону генерации вызова этой функции с параметром symbol: "BTC". Но смещается — не гарантированно происходит. Модель может ошибиться, выбрать не тот инструмент, придумать несуществующий параметр или просто ответить текстом «я не знаю точный курс» — она не обязана вызывать что-либо, даже если инструмент доступен и подходит по смыслу.

Реальный вызов функции — фактическое выполнение кода, HTTP-запрос к бирже, чтение файла, запрос к базе данных — делает не модель, а код вокруг неё: серверная часть приложения, которая получает от API модели JSON-объект, парсит его и сама вызывает соответствующую функцию в своей рантайм-среде. Модель никогда не покидает границу генерации текста. Это разделение — источник почти всех архитектурных решений в системах на базе LLM: раз выполнение всегда происходит на вашей стороне, вы полностью контролируете, что реально можно вызвать, с какими правами и с какой валидацией.

Как модель узнаёт, какие инструменты вообще существуют

Модель не «знает» о доступных функциях в смысле обучения — она не помнит, что в проекте есть get_crypto_price. Список инструментов передаётся заново в каждом запросе к API как часть контекста, наравне с системным промптом и историей сообщений. Формат описания почти везде сходится к одной идее — схема с тремя обязательными частями:

{
  "name": "get_crypto_price",
  "description": "Возвращает текущую цену криптовалюты в USD по её тикеру",
  "parameters": {
    "type": "object",
    "properties": {
      "symbol": {
        "type": "string",
        "description": "Тикер криптовалюты, например BTC или ETH"
      },
      "vs_currency": {
        "type": "string",
        "description": "Валюта котировки, по умолчанию usd",
        "default": "usd"
      }
    },
    "required": ["symbol"]
  }
}

Формат параметров почти у всех провайдеров основан на JSON Schema — том же стандарте, что используется для валидации конфигов и API-контрактов; переиспользовать готовый язык описания типов оказалось проще, чем изобретать новый. Поле description — не декоративное, а самое важное: именно по тексту описания функции и её параметров модель решает, подходит ли инструмент под задачу и как заполнить аргументы. Расплывчатое description: "получает данные" без уточнения, какие именно данные и в каком формате, — частая причина того, что модель вызывает не тот инструмент или подставляет мусор в параметры.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть LiteLLM

Параллельные вызовы: несколько инструментов за один шаг

Ранние реализации function calling позволяли модели запросить только один вызов инструмента за раз: узнать курс биткоина и эфириума требовало двух полных кругов запрос-ответ подряд. Современные модели умеют возвращать сразу несколько вызовов в одном ответе — если задача распадается на независимые подзапросы, модель формирует список объектов вызова, а не один.

Со стороны кода это меняет обработку: вместо одного tool_call нужно проходить по массиву, выполнять каждый вызов (в идеале параллельно, если функции независимы) и возвращать результаты все разом, каждый со своим идентификатором, чтобы модель на следующем шаге корректно сопоставила результат с конкретным запросом. Перепутанный порядок или потерянный идентификатор — и модель на следующем шаге спутает, какой результат к какому инструменту относится.

Параллельные вызовы полезны там, где подзадачи независимы: узнать погоду в трёх городах, проверить статус нескольких заказов, посчитать курс сразу для пяти валют. Там, где вызовы зависят друг от друга (сначала узнать ID пользователя, потом по нему запросить его заказы), модель обычно корректно разносит их на последовательные шаги — аргумент второго вызова физически неизвестен до получения результата первого.

Типичные ошибки при реализации

Слепое доверие аргументам от модели. JSON генерируется вероятностно, а не детерминированно — модель может выдать symbol: "биткоин" вместо тикера "BTC", забыть обязательное поле или выдумать значение, которого в реальности не существует. Передавать такие аргументы напрямую в SQL-запрос или платёжный API без валидации по той же JSON Schema, что описывала параметры функции, — прямой путь к падениям и уязвимостям уровня инъекций. Валидация — обязательная часть обработчика, отдельная от парсинга.

Игнорирование стоимости циклов. Каждый раунд «модель просит инструмент → код выполняет → результат уходит обратно» — отдельный запрос к API с оплатой всех входных токенов заново, включая накопленную историю и схемы инструментов. Цепочка из четырёх последовательных вызовов — это минимум пять полных обращений к модели, каждое дороже предыдущего из-за растущего контекста. Это быстро становится основной статьёй расхода токенов в агенте с длинными цепочками рассуждений.

Молчаливая обработка ошибок выполнения. Если вызов функции упал — API биржи недоступен, таймаут базы — распространённая ошибка сделать вид, что ничего не произошло, и не сообщить об этом модели. Тогда она либо зависает, либо в следующем ответе выдумывает правдоподобные данные вместо честного «не удалось получить курс». Ошибку нужно так же явно класть в историю сообщений, как и успешный результат.

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

Один интерфейс на всех провайдеров: зачем нужен LiteLLM

Формат tool calling у разных провайдеров совместим по идее, но не идентичен по деталям: где в структуре запроса лежит список инструментов, как называется поле с результатом вызова, как оформляется сообщение с ролью «ответ инструмента» — детали отличаются между API. Приложение, написанное напрямую под один SDK, при переходе на другую модель или добавлении резервного провайдера требует переписывать слой обработки tool calling заново.

Здесь и решается задача унифицированным шлюзом — LiteLLM самый распространённый пример. Он даёт единый интерфейс запроса и единый формат объекта «вызов инструмента» в ответе, а сам транслирует их в нативный формат конкретного провайдера. Цикл из раздела про полный запрос-ответ пишется один раз против этого интерфейса и продолжает работать при смене модели под капотом — меняется только параметр с именем модели в конфиге, а не логика парсинга и валидации аргументов.

Практический эффект — не только меньше кода: смена провайдера или фолбэк на случай перегрузки основного API становится конфигурационным изменением, а не переписыванием интеграции. Для агентных систем, опирающихся на стандартизированный доступ к внешним инструментам через MCP, унификация tool calling на уровне шлюза — логичный первый слой: MCP описывает, откуда инструменты берутся, а LiteLLM выравнивает то, как их вызовы выглядят на уровне API конкретной модели. Разница между простым чат-ботом и системой, способной довести многошаговую задачу до результата, разбирается в материале про ИИ-агентов и чат-ботов — в основе этой разницы лежит связка «структурированный вывод плюс цикл выполнения» из разделов выше.

Развернуть такой шлюз проще на своём сервере: LiteLLM — обычное Python-приложение, которому нужны стабильный аптайм и открытый исходящий доступ к API провайдеров. VPS с оплатой картой или криптой из России закрывает это без лишних вопросов к легальности расчётов.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть LiteLLM

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Частые вопросы

Чем tool calling отличается от обычной генерации текста?

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

Может ли модель вызвать функцию, которой нет в списке инструментов запроса?

Нет в штатном режиме — она видит только переданные в текущем запросе инструменты. Ошибочный вызов с несуществующим именем не найдёт обработчика и должен быть отклонён валидацией.

Нужно ли писать отдельный код под каждого провайдера при использовании LiteLLM?

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

Что будет, если не провалидировать аргументы от модели перед выполнением функции?

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

Сколько запросов к API уходит на задачу с двумя последовательными вызовами инструментов?

Минимум три: запрос с решением вызвать первый инструмент, запрос с его результатом и решением вызвать второй, запрос с результатом второго, где формируется финальный текст.

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.