Фраза «модель вызвала функцию» создаёт неверную картину: будто языковая модель исполняет код где-то у себя и присылает результат. Из этой картины растут все дальнейшие заблуждения — про безопасность, про то, кто виноват в удалённом файле, и про то, почему одна и та же схема на маленькой модели ломается, а на большой нет.
#Короткий ответ
Модель не вызывает функции. Она печатает текст, который выглядит как вызов функции. Ваша программа читает этот текст, решает, выполнять ли его, выполняет и дописывает результат в контекст. Затем модель получает управление снова.
Из этого следует одно практическое утверждение, вокруг которого построена вся статья: вся безопасность живёт в вашем коде между «модель напечатала» и «функция выполнилась». Ни модель, ни провайдер не участвуют в этом решении.
#Как это устроено
Полный обмен на один вызов инструмента выглядит так.
ВАШ КОД МОДЕЛЬ
│
│ системный промпт:
│ цель + СПИСОК ИНСТРУМЕНТОВ
│ + синтаксис вызова
├──────────────────────────────▶
│
│ генерирует текст:
│ "Сейчас посмотрю.
│ <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 КБ не открываются.
Четыре элемента, каждый несёт нагрузку:
- Имя — короткое, глагольное, без синонимов у соседних инструментов. Если рядом есть
read_fileиopen_file, модель будет выбирать между ними случайно. - Одна строка назначения — что делает и когда применять.
- Параметры с примером значения. Пример важнее типа: маленькая модель копирует формат из примера гораздо надёжнее, чем выводит его из слова «string».
- Что вернётся, включая ошибку. Модель должна уметь отличить успех от отказа, чтобы не повторять тот же вызов.
Плохое описание — самая частая причина того, что «модель не умеет вызывать инструменты». В большинстве разобранных случаев модель умеет; она просто не поняла, какой из двух похожих инструментов вы имели в виду, или скопировала формат пути из соседнего примера.
#Что из этого следует на практике
Проверять права надо в коде, а не в промпте. Инструкция «не удаляй ничего без спроса» в системном промпте — это пожелание. Оно нарушается ошибкой модели и обходится текстом с чужой веб-страницы. Единственная работающая проверка — та, что стоит между разбором вызова и его выполнением.
Список инструментов надо перечитывать перед каждым вызовом, а не один раз за ход. Если владелец выключил инструмент в середине длинного ответа, это должно сработать немедленно. В Persona список разрешённых имён перечитывается прямо перед каждым побочным эффектом, а не берётся из снимка на начало хода.
Результат инструмента надо обрезать. Инструмент, вернувший мегабайт, вытеснит из контекста саму задачу. В коде чата стоят три предела: до 4000 символов на один результат, до 24000 суммарно за ход и не больше 16 вызовов инструментов.
Повторный одинаковый вызов надо ловить. Модель, увидев в контексте собственный предыдущий вызов, часто печатает его снова. Без дедупликации по точному тексту вызова один и тот же инструмент отработает три раза подряд.
Ошибку надо возвращать текстом, а не исключением. Строка [error] не существует: notes.md — это полезное наблюдение, из которого модель может исправить путь. Упавший процесс — это конец хода.
Про то, как класс инструмента определяет допустимую степень автономии, — в руководстве по белым спискам и правам агента по умолчанию. Про то, как подключить чужие инструменты, не переписывая клиент, — в разборе протокола MCP.
#Где интуиция подводит
«Провайдер выполняет функцию у себя». Нет. Провайдер возвращает текст. Даже в нативном формате, где ответ структурирован, содержимое — это описание намерения, а не результат. Функция физически исполняется на вашей машине вашим кодом.
«Если модель предложила вызов, значит он корректен». Модель уверенно печатает вызов несуществующего инструмента, вызов с недостающим параметром и вызов с путём, придуманным по аналогии. Валидация имени и аргументов обязательна: в Persona ход, где встретился неизвестный или неправильно оформленный вызов, обрывается заранее заготовленным безопасным ответом, а не выполняется наполовину.
«Function calling — это функция модели». Скорее свойство её обучения. Модель, которую специально дообучали на диалогах с вызовами, соблюдает формат чаще. Модель, которую не дообучали, тоже может — просто с большим процентом брака. Разница особенно заметна на компактных моделях; количественная сторона — в разборе пределов агентов на маленькой локальной модели.
«Ошибка в цепочке — это ошибка последнего шага». Почти никогда. Обычно первый инструмент вернул не то, что ожидал автор, и дальше модель рассуждала логично на неверных данных. Поэтому читать надо трассировку с начала: как это делать — в материале про чтение трассировки агента.
#Когда function calling не нужен
Если задача решается одним детерминированным действием, которое вы можете определить по тексту запроса без модели, — определяйте его кодом. Классификатор из десяти регулярных выражений отработает мгновенно, бесплатно и одинаково каждый раз. Модель нужна там, где намерение неоднозначно и параметры приходится извлекать из свободного текста.
И отдельно: если у вас один инструмент и он вызывается всегда — вызывайте его всегда. Спрашивать модель, стоит ли делать то, что вы и так собираетесь сделать, — лишний вызов и лишний способ ошибиться. Общая рамка того, где вообще нужна агентность, — в базовой статье о том, что такое ИИ-агент.
#Частые вопросы
#Кто отвечает за то, что инструмент удалил файл?
Код, который выполнил вызов. Модель напечатала текст; решение исполнить его приняла ваша программа. Отсюда практический вывод: проверка прав, границы каталогов и подтверждения на необратимое — не «дополнительная защита», а единственная существующая.
#Чем нативный tool use отличается от текстового маркера?
В нативном варианте инструменты передаются отдельным полем запроса, а модель отвечает структурированным объектом; формат ломается реже, потому что модель обучалась именно ему. Текстовый маркер описывается в системном промпте и работает с любой моделью, включая локальную, но требует снисходительного парсера и чаще даёт брак.
#Почему модель придумывает несуществующие инструменты?
Потому что она продолжает текст, а не сверяется со списком. Если в промпте есть read_file, модель легко напечатает read_files или open_file по аналогии. Лечится проверкой имени по списку до выполнения и различимыми, непохожими друг на друга именами инструментов.
#Сколько инструментов можно дать модели одновременно?
Чем меньше, тем надёжнее. Каждое описание занимает место в контексте и добавляет вариант для ошибочного выбора. Практический ориентир для компактных моделей — единицы инструментов на задачу; если их получается больше десятка, обычно это признак того, что задачу стоит разбить.
#Что вернуть модели, если инструмент упал?
Короткую текстовую строку с признаком ошибки и причиной — так, чтобы из неё можно было исправиться: «файл не существует», «путь за пределами рабочей папки». Не стектрейс: он займёт контекст и не поможет. И не пустую строку: модель воспримет её как успех.
#Вывод
Function calling — это протокол обмена, а не способность модели исполнять код. Модель предлагает, ваш код располагает, и вся ответственность сосредоточена в одной точке — между разбором вызова и его выполнением.
Если вы строите систему с инструментами: сначала напишите проверку прав в этой точке, потом описания инструментов с примерами значений, потом всё остальное. Если у вас единственный инструмент, который вызывается всегда, — не давайте модели выбирать, вызывайте сами.