Перейти к содержимому
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

ИИ-агенты и tool use: как модель вызывает функции

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

Начальный10 мин чтенияАвтор: NotHoly

Чат-модель выдаёт текст. Агент — это система, которая даёт модели больше: она читает запрос, решает, что ей нужны сведения или действие извне разговора, просит программу-хост выполнить работу и продолжает с полученным результатом. Почти каждый агент построен на одном механизме: tool use, он же function calling.

В статье этот механизм разобран по шагам на формах запросов и ответов из публичной документации API. Вы увидите, почему решение о том, что действительно запустится, принимает хост, а не модель, почему действиям с побочными эффектами нужно решение человека и что список OWASP называет excessive agency.

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

Из чего состоит агент

У агента три части.

Модель принимает текст и выдаёт текст. При tool use она может выдать ещё и структурированный запрос вида «вызови эту функцию с такими аргументами».

Хост — это программа, которую вы пишете или запускаете. Он отправляет запросы модели, хранит разговор, читает вывод и решает, что с ним делать. Настоящий код каждого инструмента находится у хоста.

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

Модель предлагает, хост распоряжается. Большинство вопросов безопасности дальше в статье вытекает из этого разделения.

Зачем нужен цикл

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

Цикл допускает и несколько шагов. Модель может выполнить поиск, прочитать результат, решить, что этого мало, перейти по найденной ссылке и затем подвести итог. Каждый шаг идёт отдельным обменом с моделью, и хост управляет каждым из них: сколько их разрешено, какие инструменты предложены и что происходит при сбое вызова.

Как работает вызов инструмента

Любой обмен с tool use идёт в одной и той же последовательности, какой бы ни был API.

  1. Хост отправляет модели разговор и список определений инструментов.
  2. Модель либо отвечает текстом, либо возвращает структурированный вызов инструмента и помечает ответ как остановленный ради инструмента.
  3. Хост читает вызов, проверяет его и запускает соответствующую функцию, если она существует и политика это разрешает.
  4. Хост добавляет результат в разговор, ссылаясь на вызов по его ID.
  5. Хост отправляет обновлённый разговор обратно. Модель либо вызывает следующий инструмент, либо пишет итоговый ответ.

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

Определения инструментов пишутся в JSON Schema

В определении инструмента есть имя, описание для модели и схема входных данных. Схема на JSON Schema объявляет тип каждого аргумента и то, какие аргументы обязательны. Модель выбирает инструменты, читая их описания, поэтому расплывчатое описание приводит к неверным вызовам.

Модель возвращает структурированный вызов

Сама модель ничего не запускает. Она возвращает данные с именем функции и аргументами в виде объекта JSON. Вывод структурирован, поэтому хосту не приходится угадывать имя функции из свободного текста.

Структурированный вызов может быть неверным. Модель может назвать несуществующий инструмент или передать значение, которое подходит под схему, но не подходит к ситуации.

Инструмент запускает хост

Этот шаг принадлежит только хосту. Хост ищет имя в своём реестре. Если имени нет, он возвращает ошибку. Если имя есть, хосту всё равно нужно решить, можно ли запустить этот вызов с этими аргументами для этого пользователя. Только после этого он выполняет код.

Результат отправляется обратно

Результат уходит сообщением, в котором указан ID исходного вызова. Модель читает его как данные и решает, что делать дальше. Относитесь к этим данным как к данным и не исполняйте их как инструкции. На скачанной странице могут быть предложения, похожие на команды. В этом суть prompt injection, и хост не должен позволять возвращённому содержимому менять свои правила.

Формы запроса и ответа с пояснениями

Формы ниже следуют двум публичным справочникам по tool use, проверено 2026-11-08. Идентификаторы моделей здесь условные. Имена полей точные.

Запрос с одним инструментом

json
{
  "model": "<model-id>",
  "max_tokens": 1024,
  "tools": [
    {
      "name": "get_weather",
      "description": "Get the current weather for a given location.",
      "input_schema": {
        "type": "object",
        "properties": {
          "location": {
            "type": "string",
            "description": "City and state, e.g. San Francisco, CA"
          }
        },
        "required": ["location"]
      }
    }
  ],
  "messages": [
    { "role": "user", "content": "What's the weather in San Francisco?" }
  ]
}
  • tools — полный список функций, которые может вызвать модель. Всё, чего в нём нет, для модели не существует.
  • name — то, что вернёт модель и по чему хост выберет функцию.
  • description читает модель, поэтому там нужно написать, что делает инструмент и когда его использовать.
  • input_schema — это JSON Schema. required сообщает модели, какие аргументы она обязана передать.

Ответ с запросом инструмента

json
{
  "stop_reason": "tool_use",
  "content": [
    {
      "type": "tool_use",
      "id": "toolu_01A09q90qw90lq917835lq9",
      "name": "get_weather",
      "input": { "location": "San Francisco, CA" }
    }
  ]
}
  • stop_reason со значением tool_use означает, что модель ждёт результат. Хост, который читает только текст, пропустит вызов.
  • Блок с type, равным tool_use, и есть структурированный вызов. Его id нужно вернуть обратно.
  • input должен соответствовать схеме. Всё равно проверяйте его, потому что модель не умеет гарантировать соответствие схеме.

Продолжение с результатом

json
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
      "content": "15 degrees Celsius, partly cloudy"
    }
  ]
}
  • tool_use_id должен совпадать с id вызова. При несовпадении связь теряется.
  • Держите content коротким. Большие дампы расходуют контекст и могут нести внедрённый текст.

Тот же цикл в формате function calling

Во втором справочнике у той же идеи другие имена. У инструмента type равен function, а модель возвращает элемент function_call, поле arguments которого содержит строку JSON, и хост должен её разобрать.

json
{
  "type": "function",
  "name": "get_weather",
  "description": "Retrieves current weather for the given location.",
  "parameters": {
    "type": "object",
    "properties": {
      "location": { "type": "string" }
    },
    "required": ["location"],
    "additionalProperties": false
  },
  "strict": true
}
json
{
  "type": "function_call",
  "id": "fc_12345xyz",
  "call_id": "call_12345xyz",
  "name": "get_weather",
  "arguments": "{\"location\":\"Paris, France\"}"
}
json
{
  "type": "function_call_output",
  "call_id": "call_12345xyz",
  "output": "{\"temperature\":\"25\",\"unit\":\"C\"}"
}

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

Почему решает хост

Легко представить, что модель «пользуется» инструментом, как человек калькулятором. Эта картинка скрывает главное. Модель пишет текст с описанием вызова, а превратится ли этот текст в действие, решает хост.

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

Спецификация MCP описывает то же разделение на языке протокола. Хост — это LLM-приложение, клиенты — коннекторы внутри него, а серверы предоставляют инструменты. Согласно спецификации, хосты должны получить явное согласие пользователя перед вызовом любого инструмента, а описания инструментов, например аннотации, следует считать недоверенными, если они не получены от доверенного сервера. Страница спецификации, которую я проверял 2026-11-08, указывает на редакцию от 2026-07-28, так что прочитайте текущую редакцию, прежде чем строить на ней что-либо.

Зачем подтверждать побочные эффекты

Некоторые вызовы инструментов только читают. Поиск или справка ничего не меняют за пределами хоста. Другие что-то меняют: отправляют сообщение, правят запись, тратят деньги или удаляют данные.

Ошибочное чтение даёт неверный ответ, который кто-то может проверить. Ошибочная запись может оказаться необратимой, затронуть других людей и какое-то время оставаться незамеченной. Поэтому разумная схема такова: разделить инструменты по эффекту и требовать явного решения человека перед каждым вызовом с побочным эффектом. Хост показывает действие простыми словами с настоящими аргументами и ждёт согласия.

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

Excessive agency с точки зрения OWASP

OWASP Top 10 for LLM Applications называет excessive agency под номером LLM06:2025. Там описана уязвимость, из-за которой действия, наносящие ущерб, выполняются в ответ на неожиданный, неоднозначный или подстроенный вывод модели, независимо от причины. Частые триггеры — галлюцинации и prompt injection, прямая или косвенная.

Названы три первопричины: excessive functionality (инструменты, которые задаче не нужны), excessive permissions (инструменты, которые могут больше, чем требует задача) и excessive autonomy (действия с большими последствиями без проверки). Меры защиты следуют из этих причин. Предлагайте только те инструменты, которые нужны задаче. Предпочитайте узкие инструменты универсальным оболочкам и загрузчикам произвольных URL. Применяйте минимальные права в нижележащей системе, например через роли базы данных или узкие скоупы OAuth. Выполняйте действия с правами самого пользователя. Требуйте подтверждения человека для действий с большими последствиями. Проверяйте авторизацию в нижележащей системе и не полагайтесь на решение модели.

Логирование, мониторинг и лимиты запросов ограничивают ущерб. Проблему они не предотвращают.

Локальный набросок цикла

В этом наброске нет ни провайдера, ни сетевых обращений, ни секретов. «Модель» здесь — фиксированная заглушка. На первом ходу она возвращает вызов инструмента, а после получения результата выдаёт текст. Задача наброска — показать, где находится хост. Замените заглушку настоящим вызовом API в своей среде, а логику хоста оставьте.

Сохраните это как loop_demo.py:

python
# Local sketch: no provider, no network. The "model" is a fixed stub that
# returns the same shape a provider returns for a tool call.
import json

def get_weather(location: str) -> str:
    return json.dumps({"location": location, "temp_c": 15, "sky": "partly cloudy"})

def delete_file(path: str) -> str:
    return json.dumps({"deleted": path})  # never reached in this demo

# The host decides what may run. The model only names a tool.
REGISTRY = {"get_weather": get_weather}          # delete_file is NOT registered
REQUIRES_APPROVAL = {"delete_file"}

def fake_model(messages):
    # Stub: first turn asks for weather, second turn answers with text.
    last = messages[-1]
    if last["role"] == "user" and isinstance(last["content"], str):
        return {"stop_reason": "tool_use",
                "content": [{"type": "tool_use", "id": "toolu_demo_1",
                             "name": "get_weather", "input": {"location": "Lisboa, PT"}}]}
    return {"stop_reason": "end_turn", "content": [{"type": "text", "text": "done"}]}

messages = [{"role": "user", "content": "Weather in Lisboa?"}]
for step in range(5):  # hard cap on turns
    reply = fake_model(messages)
    messages.append({"role": "assistant", "content": reply["content"]})
    if reply["stop_reason"] != "tool_use":
        print("final:", reply["content"][0]["text"])
        break
    results = []
    for block in reply["content"]:
        if block["type"] != "tool_use":
            continue
        name, args = block["name"], block["input"]
        if name not in REGISTRY:
            out, err = f"unknown tool {name}", True
        elif name in REQUIRES_APPROVAL:
            out, err = "approval required", True
        else:
            out, err = REGISTRY[name](**args), False
        print(f"host ran {name}({args}) -> {out}")
        results.append({"type": "tool_result", "tool_use_id": block["id"],
                        "content": out, "is_error": err})
    messages.append({"role": "user", "content": results})

Запускайте из папки, где лежит только этот файл:

bash
python -I loop_demo.py

Флаг -I запускает Python в изолированном режиме. Переменные окружения игнорируются, а текущая папка не попадает в путь импорта. Это полезная привычка для кода, который писали не вы.

На машине с Windows 11, Git Bash и Python 3.12.10 запуск вывел:

text
host ran get_weather({'location': 'Lisboa, PT'}) -> {"location": "Lisboa, PT", "temp_c": 15, "sky": "partly cloudy"}
final: done

Заглушка вернула причину остановки tool_use и вызов get_weather. Хост нашёл имя в реестре, запустил функцию и записал tool_result обратно в разговор. Затем заглушка ответила текстом, и цикл закончился. delete_file не запускался, потому что хост его не регистрировал. Чтобы увидеть ветку подтверждения, заставьте заглушку запросить delete_file и посмотрите, как хост откажет.

Частые ошибки

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

Писать расплывчатые описания. «Что-то делает с файлами» провоцирует неверные вызовы. Напишите, что инструмент читает или записывает и когда его использовать не надо.

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

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

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

Подтверждать сессию целиком. Подтверждайте конкретные вызовы с видимыми аргументами.

Логировать не то. Записывайте имена инструментов, решения и результаты, следя за персональными данными. В полных промптах и ответах в логах могут оказаться данные, которые вы не должны были хранить.

Что дальше

Соберите цикл с настоящим API-ключом и одним инструментом только для чтения, например функцией, которая возвращает текущее время. Изучите тела запроса и ответа и найдите блок tool_use и возвращаемый вами tool_result.

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

Прочитайте разделы спецификации MCP об инструментах и безопасности и статью OWASP об excessive agency целиком. Сопоставьте каждую меру защиты со строкой в своём коде.

Источники

Проверено 2026-11-08. Страницы документации меняются, поэтому уточните текущую версию, прежде чем полагаться на имя поля.

Источники

  1. Tool use overview, public API documentation
  2. Function calling guide, public API documentation
  3. OWASP Top 10 for LLM Applications: LLM06:2025 Excessive Agency
  4. Model Context Protocol specification

Ещё по теме: ИИ-агенты для безопасности

ИИ-агенты для безопасности

Зачем автоматизировать recon и тесты на API NotHoly

Модель, которая не отказывает, плюс CVE, advisory и чтение публичных страниц без оплаты токенами. Активную работу делают ваши инструменты.

Средний3 мин чтения

ИИ-агенты для безопасности

Соберите hackbot, часть 1: архитектура

Устройство агента для bug bounty: модель, инструменты, память и ограничители в цикле на вашей машине под вашим подтверждением.

Средний3 мин чтения

ИИ-агенты для безопасности

Соберите hackbot, часть 2: код агента

Небольшой агент bug bounty на Python с вызовом инструментов на API NotHoly: предлагает команды recon, ждёт подтверждения, читает вывод и ведёт заметки.

Средний3 мин чтения