Само по себе название “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, чтобы прочитать его содержимое.

Наш пример цепочки:

  1. Пользователь пишет: “Сколько нужно сахара в рецепте шарлотки?”.
  2. Модель формирует ответ и просит OpenClaw (GitHub Copilot, Antigravity или ChatGPT) вызвать инструмент search_files с параметром query="шарлотка", чтобы тот вернул ей результаты поиска, так как точный путь к рецепту ей еще неизвестен.
  3. OpenClaw ищет на диске и возвращает модели несколько вариантов, например: [{"path":"/старые_архивы/шарлотка_бабушки.txt"}, {"path":"/документы/рецепт_шарлотки.txt"}]. Модель анализирует этот список и сама решает, какой из файлов лучше подходит.
  4. Модель, выбрав из списка нужный путь, шлет второй запрос в OpenClaw с новым tool call — теперь это read_file с аргументом path=/документы/рецепт_шарлотки.txt. То есть она говорит OpenClaw: “прочитай этот файл с помощью read_file“.
  5. 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. Пользователь спросил про количество сахара в рецепте шарлотки.
  2. Я попросила вызвать функцию поиска.
  3. Мне вернули список из двух файлов.
  4. Я попросила вызвать функцию чтения для второго файла.
  5. Функция прочитала файл и вернула текст рецепта.

Модель анализирует этот текст и генерирует финальный человеческий ответ: “Для приготовления шарлотки вам понадобится 1 стакан сахара”.

Как технически выглядит этот финальный JSON от модели:

{
	"role": "assistant",
	"content": "Для приготовления шарлотки вам понадобится 1 стакан сахара"
}

Этот JSON возвращается в OpenClaw, а уже OpenClaw берёт текст из поля content и показывает его пользователю в интерфейсе.

И OpenClaw, и GitHub Copilot, и другие работают абсолютно идентично: они дают LLM список инструментов и просят её принять решение, а затем сами локально их исполняют.