Само по себе название “Tool Calling” или “Function Calling” часто сбивает с толку, потому что кажется, будто нейросеть (LLM) сама запускает код.
Как именно работает Tool Calling в OpenClaw или GitHub Copilot?
Вся магия “Агента” (будь то OpenClaw, NanoClaw, ZeroClaw, Hermes, GitHub Copilot или ChatGPT) рождается из диалога (пинг-понга) между Моделью и Исполнителем (для примера возьмём OpenClaw).
Модель никогда не выполняет код сама. LLM — это просто генератор текста. Она понятия не имеет, как запустить bash, nodejs, python, как прочитать файл, сделать запрос в базу или залогиниться на сайте.
Представим, пользователь пишет в Telegram: “Сколько нужно сахара в рецепте шарлотки?”.
Под капотом Telegram формирует это запрос в виде json-объекта и передаёт его в OpenClaw:
{
"message": {
"text": "Сколько нужно сахара в рецепте шарлотки?",
"chat": { "id": 123456789 }
}
} Шаг 1: OpenClaw готовит почву (Системный промпт)
OpenClaw тупой - он не знает, что с этим запросом от пользователя делать - он просто берет сообщение пользователя и “прикрепляет” к нему список своих инструментов (tools) в формате JSON Schema и отправляет запрос в модель - он как бы спрашивает модель: вот, что я могу, а вот запрос от пользователя - что мне с этим сделать?
Он отправляет в модель такой запрос:
{
"messages": [
{
"role": "user",
"content": "Сколько нужно сахара в рецепте шарлотки?"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "search_files",
"description": "Ищет файлы по имени или шаблону",
"parameters": {
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "Читает содержимое файла",
"parameters": {
"type": "object",
"properties": {
"path": { "type": "string" }
},
"required": ["path"]
}
}
},
{
"type": "function",
"function": {
"name": "list_files",
"description": "Показывает список файлов в директории",
"parameters": {
"type": "object",
"properties": {
"path": { "type": "string" }
}
}
}
},
{
"type": "function",
"function": {
"name": "run_terminal_command",
"description": "Запускает команду в терминале",
"parameters": {
"type": "object",
"properties": {
"command": { "type": "string" }
},
"required": ["command"]
}
}
},
{
"type": "function",
"function": {
"name": "http_request",
"description": "Делает HTTP-запрос к внешнему API",
"parameters": {
"type": "object",
"properties": {
"url": { "type": "string" },
"method": { "type": "string" }
},
"required": ["url", "method"]
}
}
}
]
} Шаг 2: Модель понимает, что делать
Модель (например, GPT-4o, Claude 3.7 Sonnet или Gemini 1.5 Pro) получает этот запрос. Она видит вопрос пользователя и видит перед собой сразу целый набор инструментов. Дальше она сопоставляет задачу пользователя с доступными tools и пытается выбрать подходящий.
Так как пользователь спрашивает про рецепт, модель должна догадаться, где именно его искать. И вот тут кроется важный нюанс: модель может ошибиться и выбрать неправильный инструмент (tool).
Например, она может подумать: “Ага, рецепт! Поищу-ка я его в интернете!” и попытается сделать http_request к Google. Или модель может попробовать “угадать” имя файла и сразу вызвать read_file с путем path="шарлотка.txt", что выдаст ошибку, если пути не существует. Но при наличии хорошего инструмента для поиска (и, возможно, правильных базовых инструкций) модель поймёт, что ей лучше сначала поискать файл по локальному диску, и выберет search_files.
На практике выбор делается не “по магии”, а по нескольким сигналам сразу: что просит пользователь, как названы инструменты, что написано в их description, какие аргументы они принимают и что уже происходило раньше в этом диалоге.
Поэтому решение задачи часто представляет собой целую цепочку вызовов. Например, модель сначала просит агента вызвать search_files, чтобы найти точный путь к файлу, и только потом просит вызвать read_file, чтобы прочитать его содержимое.
Наш пример цепочки:
- Пользователь пишет: “Сколько нужно сахара в рецепте шарлотки?”.
- Модель формирует ответ и просит OpenClaw (GitHub Copilot, Antigravity или ChatGPT) вызвать инструмент
search_filesс параметромquery="шарлотка", чтобы тот вернул ей результаты поиска, так как точный путь к рецепту ей еще неизвестен. - OpenClaw ищет на диске и возвращает модели несколько вариантов, например:
[{"path":"/старые_архивы/шарлотка_бабушки.txt"}, {"path":"/документы/рецепт_шарлотки.txt"}]. Модель анализирует этот список и сама решает, какой из файлов лучше подходит. - Модель, выбрав из списка нужный путь, шлет второй запрос в OpenClaw с новым tool call — теперь это
read_fileс аргументомpath=/документы/рецепт_шарлотки.txt. То есть она говорит OpenClaw: “прочитай этот файл с помощьюread_file“. - OpenClaw читает файл и отправляет его содержимое обратно в модель. Только после этого, изучив текст рецепта, модель пишет финальный ответ пользователю.
То есть tool calling может быть не одиночным действием, а маленькой цепочкой: нашли -> прочитали -> ответили.
Модель НЕ читает файл и НЕ ищет сама на диске или в интернете — она просто выбирает tool из списка, который ей даёт агент. Выбрав, она говорит агенту: “используй этот tool”. Агент физически его использует и возвращает результат вызова обратно в модель.
Технически самый первый шаг формирует специальный ответ (JSON), в котором модель говорит: “Эй, агент (OpenClaw, GitHub Copilot или ChatGPT), вызови эту свою функцию search_files с аргументом query="шарлотка" и верни мне результат - я посмотрю”.
Как выглядит такой первый ответ от LLM в OpenClaw:
{
"role": "assistant",
"content": null, // Нет текста ответа пользователю!
"tool_calls": [
{
"id": "call_search_6c3bc42b",
"type": "function",
"function": {
"name": "search_files",
"arguments": "{"query":"шарлотка"}"
}
}
]
} Шаг 3: OpenClaw (Исполнитель) применяет первый инструмент
OpenClaw на сервере получает этот JSON. Он смотрит: “Модель просит запустить search_files с параметром query="шарлотка"”.
Именно в этот момент код OpenClaw на сервере берет и физически выполняет поиск файлов по слову “шарлотка” на диске. OpenClaw не думает - он просто запускает функцию и возвращает результат поиска (массив найденных путей) обратно в модель, прикрепляя его к истории сообщений.
Шаг 4: Модель анализирует результат и просит прочитать файл
Модель получает список найденных файлов: [{"path":"/старые_архивы/шарлотка_бабушки.txt"}, {"path":"/документы/рецепт_шарлотки.txt"}].
Она смотрит на них и принимает решение: “Ага, нужный мне рецепт, скорее всего, во втором файле”. И теперь она отдаёт новый JSON-ответ с просьбой вызвать второй инструмент — read_file:
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_read_d88b07ff",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{"path":"/документы/рецепт_шарлотки.txt"}"
}
}
]
} Шаг 5: OpenClaw читает файл и возвращает результат
OpenClaw получает это новое указание. Скрипт отрабатывает, физически читает файл рецепт_шарлотки.txt с диска и получает текст: "Яблоки - 1 кг, мука - 200г, сахар - 1 стакан".
Теперь OpenClaw формирует новый запрос в LLM, передавая в нём всю накопившуюся историю этой цепочки:
{
"messages": [
{
"role": "user",
"content": "Сколько нужно сахара в рецепте шарлотки?"
},
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_search_6c3bc42b",
"type": "function",
"function": {
"name": "search_files",
"arguments": "{"query":"шарлотка"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_search_6c3bc42b",
"content": "[{"path":"/старые_архивы/шарлотка_бабушки.txt"}, {"path":"/документы/рецепт_шарлотки.txt"}]"
},
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_read_d88b07ff",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{"path":"/документы/рецепт_шарлотки.txt"}"
}
}
]
},
{
"role": "tool", // Специальная роль для результатов функций
"tool_call_id": "call_read_d88b07ff",
"content": "Яблоки - 1 кг, мука - 200г, сахар - 1 стакан" // Результат чтения файла
}
],
"tools": [
{
"type": "function",
"function": {
"name": "search_files",
"description": "Ищет файлы по имени или шаблону",
"parameters": { "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] }
}
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "Читает содержимое файла",
"parameters": { "type": "object", "properties": { "path": { "type": "string" } }, "required": ["path"] }
}
},
"..." // Остальные инструменты также передаются при каждом запросе
]
} Этот запрос снова летит к LLM модели (например, в OpenAI).
Важный нюанс: память и контекстное окно
Заметили, как разрастается список
messages? Классические LLM (по крайней мере, подавляющее большинство моделей до 2025-2026 годов) сами по себе работают “без памяти” (они stateless). Поэтому OpenClaw (и любой другой агент) прикрепляет все прошлые сообщения (запросы пользователя, вызовы инструментов и их результаты) при каждом новом обращении.Эти сообщения будут накапливаться и передаваться снова и снова до тех пор, пока они влезают в контекстное окно (лимит памяти конкретной модели, например, 128 тысяч токенов). Как только история диалога разрастается слишком сильно и перестает влезать в этот лимит, агент начинает обрезать самые старые сообщения из начала истории (поэтому модель начинает “забывать”), чтобы освободить место для новых шагов и влезть в лимиты контекстного окна модели.
P.S. Индустрия ИИ развивается стремительно. Сейчас на рынке уже появляются модели со встроенной памятью (stateful API) и модели-агенты, которые сами умеют хранить контекст диалога на своей стороне или даже имеют “под капотом” собственные инструменты. Тем не менее, описанный выше механизм “stateless-общения” (когда клиент сам передаёт всю историю и схемы инструментов в JSON) — это база, на которой построено 99% текущих AI-приложений.
Шаг 6: Финальный ответ
LLM (например, OpenAI) смотрит на обновленную историю. Она видит:
- Пользователь спросил про количество сахара в рецепте шарлотки.
- Я попросила вызвать функцию поиска.
- Мне вернули список из двух файлов.
- Я попросила вызвать функцию чтения для второго файла.
- Функция прочитала файл и вернула текст рецепта.
Модель анализирует этот текст и генерирует финальный человеческий ответ: “Для приготовления шарлотки вам понадобится 1 стакан сахара”.
Как технически выглядит этот финальный JSON от модели:
{
"role": "assistant",
"content": "Для приготовления шарлотки вам понадобится 1 стакан сахара"
} Этот JSON возвращается в OpenClaw, а уже OpenClaw берёт текст из поля content и показывает его пользователю в интерфейсе.
И OpenClaw, и GitHub Copilot, и другие работают абсолютно идентично: они дают LLM список инструментов и просят её принять решение, а затем сами локально их исполняют.