Содержание
- Что такое Tool Calling и зачем он нужен языковой модели
- Из чего состоит объявление инструмента
- Архитектура вызова инструментов и роли участников
- Принцип работы, цикл вызова по шагам
- Агентный цикл и параллельные вызовы
- Чем различаются форматы OpenAI, Anthropic и Ollama
- Ограничения и подводные камни Tool Calling
- Когда применять Tool Calling, а когда обойтись без него
- Практика, вызов инструментов локальной моделью через Ollama
- Заключение
- Референсные ссылки
Tool Calling (вызов инструментов, function calling) это механизм, которым большая языковая модель выбирает заранее объявленную внешнюю функцию, формирует для неё аргументы и отдаёт этот вызов приложению на исполнение. Инструментом может быть REST API, SQL-запрос, поиск по базе знаний, функция на Python, калькулятор или корпоративный сервис. Модель сама решает, нужен ли инструмент, какой именно и с какими параметрами, а затем встраивает полученный результат в ответ. Именно на Tool Calling держатся современные ИИ-агенты: без него языковая модель умеет только генерировать текст по своим весам.
Что такое Tool Calling и зачем он нужен языковой модели
Языковая модель работает с тем, что попало в контекст. Она не знает сегодняшний курс валюты, не видит вашу базу данных и регулярно ошибается в арифметике. Tool Calling закрывает этот разрыв. Разработчик описывает набор функций, модель получает эти описания вместе с запросом пользователя и вместо обычного текста возвращает структурированный вызов: имя функции и объект с аргументами.
Ключевой момент, который часто понимают неправильно. Модель ничего не выполняет. Она возвращает намерение вызвать инструмент, а запускает код ваше приложение. Отсюда следуют и главные свойства механизма, и его риски: решение принимает вероятностная модель, а последствия ложатся на ваш сервис. Модель предлагает. Код проверяет и выполняет. Эту границу стоит держать в голове весь дальнейший разбор.
Из чего состоит объявление инструмента
Инструмент объявляется тремя вещами, и все три одинаково важны для качества выбора:
- Имя функции. Короткий идентификатор вида get_course_info, по нему приложение находит обработчик.
- Описание на естественном языке. Единственное, по чему модель понимает, когда инструмент уместен. Пустое или размытое описание ломает выбор надёжнее, чем кривая схема.
- Схема параметров в формате JSON Schema. Типы, обязательные поля, пояснения к каждому аргументу.
Таким образом, объявление инструмента это одновременно техническая спецификация и кусок промпта, поэтому описания пишутся так же тщательно, как системная инструкция. Схема задаёт форму. Описание задаёт смысл.
ИИ-агенты для оптимизации бизнес-процессов
Код курса
AGENT
Ближайшая дата курса
26 октября, 2026
Продолжительность
24 ак.часов
Стоимость обучения
66 000
Архитектура вызова инструментов и роли участников
В схеме Tool Calling три роли. Приложение объявляет каталог инструментов, принимает вызовы и выполняет их. Модель принимает решение и заполняет аргументы. Сам инструмент это обычный код или внешний сервис, который про языковую модель ничего не знает.
Провайдеры делят инструменты по месту исполнения. Клиентские (client tools) работают на вашей стороне: модель возвращает блок вызова, ваш код запускает функцию и присылает результат обратно. Серверные (server tools), например веб-поиск или песочница для кода у Anthropic, исполняются на инфраструктуре провайдера, и результат приходит уже готовым в том же ответе. Для локального стенда на Ollama доступны только клиентские инструменты, что даже удобнее: весь цикл виден целиком.
Важная деталь архитектуры состоит в том, что состояние диалога целиком лежит в списке сообщений. Модель не помнит предыдущий вызов, она видит его в истории. Поэтому результат инструмента обязательно добавляется в переписку отдельным сообщением, иначе следующий шаг пойдёт вслепую. История растёт с каждым вызовом. Растёт и счёт за токены.
Принцип работы, цикл вызова по шагам
Механизм под капотом сводится к четырём шагам, которые повторяются, пока модель не соберёт ответ.
- Шаг 1, запрос с каталогом. Приложение отправляет вопрос пользователя вместе с описаниями инструментов.
- Шаг 2, решение модели. Модель либо отвечает текстом, либо возвращает структурированный вызов с именем и аргументами.
- Шаг 3, исполнение. Приложение находит обработчик по имени, проверяет аргументы и запускает код.
- Шаг 4, возврат результата. Результат уходит обратно в историю сообщений отдельной ролью, и модель получает шанс ответить или позвать следующий инструмент.
Как следствие, один пользовательский вопрос легко превращается в три и более обращений к модели, и это нормальная цена механизма. Задержка складывается из всех кругов. Планировать её надо заранее.
Агентный цикл и параллельные вызовы
Когда шаги 2-4 закольцованы в while, получается агентный цикл. Он и превращает модель с инструментами в агента, способного к многошаговой работе. Обязательный элемент такого цикла это предохранитель по числу итераций, потому что модель может зациклиться на одном инструменте.
Отдельная возможность это параллельные вызовы, когда в одном ответе приходит сразу несколько независимых обращений к инструментам. Anthropic разрешает их по умолчанию и позволяет выключить флагом disable_parallel_tool_use, OpenAI управляет этим параметром parallel_tool_calls. Практический смысл прост: четыре запроса погоды по четырём городам уходят одним пакетом, а не четырьмя кругами диалога.
Чем различаются форматы OpenAI, Anthropic и Ollama
Идея одна, а детали протокола у всех свои, и при переносе кода между провайдерами ломается именно это.
| Что сравниваем | OpenAI | Anthropic | Ollama |
|---|---|---|---|
| Поле со схемой | parameters | input_schema | parameters |
| Как приходит вызов | tool_calls в сообщении | блок tool_use, stop_reason tool_use | message.tool_calls |
| Как вернуть результат | сообщение с ролью tool и tool_call_id | блок tool_result внутри сообщения user | сообщение с ролью tool и tool_name |
| Гарантия схемы | strict true, режим structured outputs | strict true в объявлении инструмента | нет, схема соблюдается на усмотрение модели |
| Параллельные вызовы | параметр parallel_tool_calls | флаг disable_parallel_tool_use | зависит от модели и её шаблона |
Вывод из таблицы практический. Переносить между провайдерами стоит бизнес-логику инструментов, а слой упаковки вызовов писать отдельно под каждый API. Функции остаются теми же. Меняется только обёртка вокруг них.
Ограничения и подводные камни Tool Calling
Механизм выглядит детерминированным, но решение принимает вероятностная модель, и отсюда растут почти все проблемы эксплуатации.
- Выбор инструмента нестабилен. На нашем стенде один и тот же вопрос про цену со скидкой в первом прогоне дал два вызова подряд, а во втором модель взяла только справочник курсов, а проценты посчитала сама. Ответ случайно совпал, но арифметика ушла обратно в модель, то есть туда, откуда её и убирали.
- Каталог инструментов стоит токенов на каждом запросе. Замер на qwen2.5:7b: пустой запрос это 32 токена промпта, с двумя инструментами 261, с десятью уже 869. У Anthropic служебная часть промпта под инструменты для Claude Opus 5 занимает от 286 до 406 токенов сверх самих описаний.
- Большой каталог ухудшает выбор. Поэтому провайдеры добавляют отсев: список allowed_tools у OpenAI, инструмент поиска по инструментам у Anthropic, который подгружает нужные схемы по требованию.
- Аргументам нельзя доверять. Модель способна выдумать имя функции или прислать строку вместо числа, поэтому валидация до вызова обязательна.
- Результат инструмента это недоверенный ввод. Текст, пришедший из внешнего API, попадает прямо в контекст и может содержать инструкции для модели, то есть промпт-инъекцию.
Отсюда простое правило эксплуатации: каталог держим коротким, аргументы валидируем схемой, а всё, что делает необратимые вещи, закрываем подтверждением человека. Логировать стоит каждый вызов. Без журнала разбирать инциденты нечем.
Когда применять Tool Calling, а когда обойтись без него
Инструменты нужны там, где ответ зависит от данных или действий вне модели. Это свежие и приватные данные, точные вычисления, запись в системы, поиск по документам. Хорошая эвристика такая: если задачу решает детерминированный код, отдайте её коду, а модели оставьте выбор и формулировку. Арифметика тоже сюда. Считать должен калькулятор, а не веса модели.
Не стоит тянуть инструменты туда, где маршрут известен заранее. Если порядок шагов фиксирован, обычный конвейер на LangGraph или простой цепочке промптов дешевле и предсказуемее. Не нужен вызов инструментов и для чистой генерации текста, где никаких внешних фактов не требуется. Стандартизация подключения инструментов это отдельная тема, ей посвящён разбор протокола MCP в нашем блоге, а построению агентов на этом фундаменте посвящён курс по ИИ-агентам для оптимизации бизнес-процессов.
Высокопроизводительная обработка данных на Python
Код курса
HPPY
Ближайшая дата курса
16 ноября, 2026
Продолжительность
24 ак.часов
Стоимость обучения
76 800
Практика, вызов инструментов локальной моделью через Ollama
По традиции весь код используемый в статье выкладываем на наш GitHub репозиторий
Разберём механизм на локальной модели qwen2.5:7b под управлением Ollama. В файле tool_calling_demo.py объявлены два инструмента, справочник курсов и калькулятор скидки, а также агентный цикл с предохранителем на пять шагов.
# Прогон: ollama-python 0.6.2, Ollama 0.32.9, модель qwen2.5:7b, Python 3.12.13
"""Tool Calling на локальной модели: объявление инструментов, диспетчер и агентный цикл."""
import json
from ollama import Client
MODEL = "qwen2.5:7b"
client = Client(host="http://localhost:11434")
# Мини-справочник курсов, роль внешнего источника данных
COURSES = {
"AGENT": {"name": "ИИ-агенты для оптимизации бизнес-процессов", "hours": 24, "price": 90000},
"MLOPS": {"name": "Разработка и внедрение ML-решений", "hours": 24, "price": 90000},
"KAFKA": {"name": "Apache Kafka: администрирование кластера", "hours": 24, "price": 96000},
}
def get_course_info(code: str) -> str:
"""Инструмент 1: отдаёт карточку курса по коду."""
course = COURSES.get(code.upper())
if course is None:
return json.dumps({"error": f"курс {code} не найден"}, ensure_ascii=False)
return json.dumps(course, ensure_ascii=False)
def calc_discount(price: int, percent: int) -> str:
"""Инструмент 2: считает цену со скидкой, арифметика вынесена из модели."""
return json.dumps({"final_price": round(price * (100 - percent) / 100)}, ensure_ascii=False)
# Объявление инструментов: имя, описание и JSON Schema параметров.
# Именно этот блок модель видит вместе с запросом и по нему принимает решение.
TOOLS = [
{
"type": "function",
"function": {
"name": "get_course_info",
"description": "Вернуть название, длительность и цену курса по его коду",
"parameters": {
"type": "object",
"required": ["code"],
"properties": {
"code": {"type": "string", "description": "Код курса, например AGENT"}
},
},
},
},
{
"type": "function",
"function": {
"name": "calc_discount",
"description": "Посчитать итоговую цену после скидки в процентах",
"parameters": {
"type": "object",
"required": ["price", "percent"],
"properties": {
"price": {"type": "integer", "description": "Цена без скидки в рублях"},
"percent": {"type": "integer", "description": "Размер скидки в процентах"},
},
},
},
},
]
REGISTRY = {"get_course_info": get_course_info, "calc_discount": calc_discount}
def run(question: str, max_steps: int = 5) -> None:
"""Агентный цикл: модель зовёт инструменты, пока не соберёт ответ."""
messages = [{"role": "user", "content": question}]
for step in range(1, max_steps + 1):
reply = client.chat(model=MODEL, messages=messages, tools=TOOLS)
messages.append(reply.message)
calls = reply.message.tool_calls or []
if not calls:
# Инструменты не нужны, модель отвечает текстом и цикл закрывается
print(f"[шаг {step}] ответ модели: {reply.message.content.strip()}")
return
for call in calls:
name = call.function.name
args = call.function.arguments
print(f"[шаг {step}] модель вызвала {name} с аргументами {dict(args)}")
fn = REGISTRY.get(name)
# Имя инструмента приходит строкой, поэтому реестр проверяется до вызова
result = fn(**args) if fn else json.dumps({"error": "неизвестный инструмент"})
print(f"[шаг {step}] результат инструмента: {result}")
messages.append({"role": "tool", "tool_name": name, "content": result})
print("[стоп] превышен лимит шагов, цикл остановлен предохранителем")
if __name__ == "__main__":
print("=== Сценарий 1: нужны оба инструмента ===")
run("Сколько стоит курс AGENT со скидкой 15 процентов? Ответь одной фразой.")
print("\n=== Сценарий 2: инструменты не нужны ===")
run("Что означает аббревиатура API? Ответь одним предложением.")
Вывод первого прогона показывает обе ветки поведения: цепочку из двух инструментов и прямой ответ там, где инструменты не нужны.
=== Сценарий 1: нужны оба инструмента ===
[шаг 1] модель вызвала get_course_info с аргументами {'code': 'AGENT'}
[шаг 1] результат инструмента: {"name": "ИИ-агенты для оптимизации бизнес-процессов", "hours": 24, "price": 90000}
[шаг 2] модель вызвала calc_discount с аргументами {'price': 90000, 'percent': 15}
[шаг 2] результат инструмента: {"final_price": 76500}
[шаг 3] ответ модели: Курс AGENT со скидкой 15 процентов стоит 76 500 рублей.
=== Сценарий 2: инструменты не нужны ===
[шаг 1] ответ модели: Аббревиатура API означает Application Programming Interface.
Второй файл, tool_calling_guard.py, закрывает два практических вопроса: как не пустить в обработчик мусорный вызов и во сколько токенов обходится сам каталог инструментов.
# Прогон: ollama-python 0.6.2, Ollama 0.32.9, модель qwen2.5:7b, pydantic 2.13.4, Python 3.12.13
"""Две проверки вокруг Tool Calling: валидация вызова и цена каталога инструментов в токенах."""
import copy
from pydantic import BaseModel, ValidationError
from ollama import Client
from tool_calling_demo import MODEL, TOOLS, REGISTRY
client = Client(host="http://localhost:11434")
class CourseInfoArgs(BaseModel):
"""Контракт аргументов инструмента, модель его не гарантирует."""
code: str
class DiscountArgs(BaseModel):
price: int
percent: int
SCHEMAS = {"get_course_info": CourseInfoArgs, "calc_discount": DiscountArgs}
def safe_dispatch(name: str, args: dict) -> str:
"""Диспетчер, который сначала проверяет имя и аргументы, и только потом вызывает функцию."""
if name not in REGISTRY:
return f"отказ: инструмент {name} не объявлен"
try:
checked = SCHEMAS[name](**args)
except ValidationError as err:
return f"отказ: аргументы не прошли валидацию, {err.error_count()} ошибка(и)"
return REGISTRY[name](**checked.model_dump())
def measure(n_tools: int) -> int:
"""Замер служебных токенов: сколько стоит сам факт объявления инструментов."""
tools = None
if n_tools:
tools = []
for i in range(n_tools):
tool = copy.deepcopy(TOOLS[i % len(TOOLS)])
tool["function"]["name"] = f"{tool['function']['name']}_{i}"
tools.append(tool)
reply = client.chat(
model=MODEL,
messages=[{"role": "user", "content": "Привет"}],
tools=tools,
options={"num_predict": 1},
)
return reply.prompt_eval_count
if __name__ == "__main__":
print("=== Валидация вызовов, аргументы подставлены вручную ===")
cases = [
("get_course_info", {"code": "AGENT"}),
("get_weather", {"city": "Москва"}),
("calc_discount", {"price": "девяносто тысяч", "percent": 15}),
]
for name, args in cases:
print(f"{name}({args}) -> {safe_dispatch(name, args)}")
print("\n=== Цена каталога инструментов в токенах промпта ===")
for n in (0, 2, 10):
print(f"инструментов {n}: prompt_eval_count = {measure(n)}")
Прогон подтверждает обе вещи сразу: диспетчер отбивает выдуманное имя и неверный тип аргумента, а каталог из десяти инструментов удорожает каждый запрос почти в двадцать семь раз против пустого промпта.
=== Валидация вызовов, аргументы подставлены вручную ===
get_course_info({'code': 'AGENT'}) -> {"name": "ИИ-агенты для оптимизации бизнес-процессов", "hours": 24, "price": 90000}
get_weather({'city': 'Москва'}) -> отказ: инструмент get_weather не объявлен
calc_discount({'price': 'девяносто тысяч', 'percent': 15}) -> отказ: аргументы не прошли валидацию, 1 ошибка(и)
=== Цена каталога инструментов в токенах промпта ===
инструментов 0: prompt_eval_count = 32
инструментов 2: prompt_eval_count = 261
инструментов 10: prompt_eval_count = 869
Заключение
Tool Calling это тонкий, но принципиальный слой между языковой моделью и остальной системой. Модель получает описания функций и возвращает структурированное намерение, а исполнение, проверки и ответственность остаются на приложении. Работоспособность механизма держится на трёх вещах: внятных описаниях инструментов, коротком каталоге и жёсткой валидации аргументов перед вызовом. Всё остальное, от агентных циклов до мультиагентных систем, надстраивается уже над этим фундаментом. Начинать стоит с одного инструмента. Второй добавляется, когда первый работает стабильно.
Референсные ссылки
- Ollama Docs, Tool calling, официальное описание формата вызовов, агентного цикла и работы со стримингом
- Claude Platform Docs, Tool use with Claude, разделение клиентских и серверных инструментов, строгие схемы и расход токенов
- OpenAI, Function calling, параметр tool_choice, список allowed_tools и ограничения строгого режима
- Ollama Blog, Tool support, разбор поддержки инструментов в моделях и примеры объявления функций


