Технологии

Function calling: как модель вызывает ваш код

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

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

#Короткий ответ

Модель не вызывает функции. Она печатает текст, который выглядит как вызов функции. Ваша программа читает этот текст, решает, выполнять ли его, выполняет и дописывает результат в контекст. Затем модель получает управление снова.

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

#Как это устроено

Полный обмен на один вызов инструмента выглядит так.

ВАШ КОД                         МОДЕЛЬ
   │
   │  системный промпт:
   │  цель + СПИСОК ИНСТРУМЕНТОВ
   │  + синтаксис вызова
   ├──────────────────────────────▶
   │
   │                      генерирует текст:
   │                      "Сейчас посмотрю.
   │                       <tool>read_file(
   │                         {"path":"notes.md"})
   │                       </tool>"
   │◀──────────────────────────────
   │
   │  1. распарсить вызов
   │  2. ПРОВЕРИТЬ ПРАВА      ← точка ответственности
   │  3. выполнить функцию
   │  4. обрезать результат
   │
   │  контекст + результат
   ├──────────────────────────────▶
   │
   │                      продолжает ответ
   │◀──────────────────────────────
   ▼

Существуют два способа оформить шаг «модель напечатала вызов».

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

Текстовый маркер. Инструменты описываются прямо в системном промпте, модель печатает условленную конструкцию внутри обычного текста, программа выуживает её регулярным выражением. Работает с любой моделью, включая локальную, но синтаксис ломается чаще.

В Persona используется второй вариант: описание инструментов собирается в системный промпт функцией build_tools_prompt, а вызов выглядит буквально как <tool>имя({"параметр": "значение"})</tool> и разбирается регулярным выражением в app/mcp/builtin_tools.py. Никакого нативного поля tools в запросах к провайдеру нет — это осознанный выбор ради совместимости с локальными моделями, у которого есть своя цена: разбор вынужден быть снисходительным. Если модель напечатала аргумент не как JSON, парсер пытается угадать, что она имела в виду.

#Как выглядит описание инструмента

Модель видит инструмент только через его описание. Всё остальное — реализация, о которой она не знает. Описание из реальной системы устроено примерно так:

read_file — прочитать текстовый файл из рабочей папки.
  path: путь относительно корня рабочей папки, например "notes.md"
  Возвращает содержимое файла или строку с префиксом [error].
  Файлы больше 500 КБ не открываются.

Четыре элемента, каждый несёт нагрузку:

  1. Имя — короткое, глагольное, без синонимов у соседних инструментов. Если рядом есть read_file и open_file, модель будет выбирать между ними случайно.
  2. Одна строка назначения — что делает и когда применять.
  3. Параметры с примером значения. Пример важнее типа: маленькая модель копирует формат из примера гораздо надёжнее, чем выводит его из слова «string».
  4. Что вернётся, включая ошибку. Модель должна уметь отличить успех от отказа, чтобы не повторять тот же вызов.

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

#Что из этого следует на практике

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

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

Результат инструмента надо обрезать. Инструмент, вернувший мегабайт, вытеснит из контекста саму задачу. В коде чата стоят три предела: до 4000 символов на один результат, до 24000 суммарно за ход и не больше 16 вызовов инструментов.

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

Ошибку надо возвращать текстом, а не исключением. Строка [error] не существует: notes.md — это полезное наблюдение, из которого модель может исправить путь. Упавший процесс — это конец хода.

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

#Где интуиция подводит

«Провайдер выполняет функцию у себя». Нет. Провайдер возвращает текст. Даже в нативном формате, где ответ структурирован, содержимое — это описание намерения, а не результат. Функция физически исполняется на вашей машине вашим кодом.

«Если модель предложила вызов, значит он корректен». Модель уверенно печатает вызов несуществующего инструмента, вызов с недостающим параметром и вызов с путём, придуманным по аналогии. Валидация имени и аргументов обязательна: в Persona ход, где встретился неизвестный или неправильно оформленный вызов, обрывается заранее заготовленным безопасным ответом, а не выполняется наполовину.

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

«Ошибка в цепочке — это ошибка последнего шага». Почти никогда. Обычно первый инструмент вернул не то, что ожидал автор, и дальше модель рассуждала логично на неверных данных. Поэтому читать надо трассировку с начала: как это делать — в материале про чтение трассировки агента.

#Когда function calling не нужен

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

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

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

#Кто отвечает за то, что инструмент удалил файл?

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

#Чем нативный tool use отличается от текстового маркера?

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

#Почему модель придумывает несуществующие инструменты?

Потому что она продолжает текст, а не сверяется со списком. Если в промпте есть read_file, модель легко напечатает read_files или open_file по аналогии. Лечится проверкой имени по списку до выполнения и различимыми, непохожими друг на друга именами инструментов.

#Сколько инструментов можно дать модели одновременно?

Чем меньше, тем надёжнее. Каждое описание занимает место в контексте и добавляет вариант для ошибочного выбора. Практический ориентир для компактных моделей — единицы инструментов на задачу; если их получается больше десятка, обычно это признак того, что задачу стоит разбить.

#Что вернуть модели, если инструмент упал?

Короткую текстовую строку с признаком ошибки и причиной — так, чтобы из неё можно было исправиться: «файл не существует», «путь за пределами рабочей папки». Не стектрейс: он займёт контекст и не поможет. И не пустую строку: модель воспримет её как успех.

#Вывод

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

Если вы строите систему с инструментами: сначала напишите проверку прав в этой точке, потом описания инструментов с примерами значений, потом всё остальное. Если у вас единственный инструмент, который вызывается всегда, — не давайте модели выбирать, вызывайте сами.

Хотите ИИ, который помнит вас?

Persona держит контекст между чатами и работает на вашем железе — если развернуть её у себя и подключить локальную модель.

Создать аккаунт