Разберём, как настроить Ollama tool calling: вызов функций проходит через модель и код приложения. Модель возвращает список tool_calls с именами функций и аргументами. Приложение проверяет запросы, выполняет только известные функции, добавляет результаты сообщениями role=tool и продолжает диалог. Ниже — учебный пример для синтетического справочника мест.
Граница ответственности: модель просит, код исполняет
В документации Ollama (docs.ollama.com) tool calling описан как механизм, при котором модель вызывает инструменты и включает их результаты в свои ответы. Ключевая граница проходит между двумя участниками. Модель генерирует структуру вызова: имя функции и аргументы. Приложение принимает эту структуру, проверяет её и выполняет реальный код.
Модель не имеет доступа к вашей файловой системе, базе или сети. Она формирует текстовый запрос в формате, описанном схемой tools. Всё, что происходит после, — ответственность вашего кода.
Эта граница объясняет, почему tool calling называют ещё function calling: модель вызывает функцию опосредованно, через ваш диспетчер. В официальном примере Ollama показан инструмент get_temperature, который возвращает температуру города из словаря. Модель просит вызвать get_temperature с аргументом city, а функция исполняется в коде.
Выбор локальной модели с поддержкой tools
Инструменты работают с моделью, которая установлена локально и умеет формировать структурированные вызовы. В примерах документации Ollama используется модель qwen3. Это не означает, что любая установленная модель поддержит tools: способность формировать tool_calls зависит от конкретной модели и её шаблона.
Практический порядок выбора такой. Сначала проверьте, что модель уже загружена локально и отвечает на обычный запрос через /api/chat. Затем отправьте простой запрос с одним инструментом и посмотрите, появилось ли поле tool_calls в ответе. Пустой tool_calls означает, что в этом ответе вызов не запрошен. Проверьте поддержку tools у выбранной модели, схему и формулировку задачи; по одному пустому ответу определить причину нельзя.
Схема tools передаётся в теле запроса к /api/chat. В документации API поле tools описано как необязательный список функций, которые модель может вызвать во время чата. Рядом с ним в теле запроса живут model, messages, stream и think. Параметр think управляет выводом рассуждений: значение true запрашивает рассуждения, false отключает их, null использует значение модели по умолчанию.
Полный tools JSON: type, function, name, description, parameters
Схема инструмента — это JSON-объект с полями type и function. Внутри function лежат name, description и parameters. Поле parameters описывает объект аргументов: type, properties и required. Каждое свойство внутри properties имеет собственный type и description.
Вот полная схема для учебного инструмента available_seats. Справочник синтетический: python — 7 мест, n8n — 0 мест. Значение 0 здесь означает, что мест нет, и это корректный результат вызова.
{
"type": "function",
"function": {
"name": "available_seats",
"description": "Возвращает число свободных мест на учебный практикум",
"parameters": {
"type": "object",
"properties": {
"workshop": {
"type": "string",
"description": "Название практикума",
"enum": ["python", "n8n"]
}
},
"required": ["workshop"],
"additionalProperties": false
}
}
}
Разберём поля по назначению. type на верхнем уровне всегда function. name — идентификатор, по которому ваш диспетчер найдёт функцию. description помогает модели понять, когда инструмент уместен. parameters.type задаёт объект. properties перечисляет аргументы. required фиксирует обязательные. additionalProperties: false запрещает лишние ключи.
Поле enum внутри workshop — это allowlist значений. Модель увидит допустимые варианты и с большей вероятностью выберет один из них. Ваш код всё равно обязан проверить значение: схема подсказывает, а валидация защищает.
Аргументы в tool_calls: объект, ключи, тип, allowlist
Когда модель решает вызвать инструмент, в ответе появляется message.tool_calls. Каждый элемент содержит type и function, а внутри function — name, index и arguments. В примерах документации Ollama arguments показаны как объект: {"city": "New York"}. Это не строка, которую нужно разбирать, а готовый объект.
Проверка аргументов идёт по четырём пунктам. Первый: точные ключи. Ожидается workshop, и никакой другой ключ не принимается. Второй: тип значения. Для workshop это строка. Третий: allowlist. Значение обязано входить в python или n8n. Четвёртый: отсутствие лишних полей, если в схеме стоит additionalProperties: false.
Вот функция проверки, которая возвращает либо нормализованное значение, либо причину отказа:
ALLOWED_WORKSHOPS = {"python", "n8n"}
def validate_args(args):
if not isinstance(args, dict):
return None, "arguments_not_object"
extra = set(args) - {"workshop"}
if extra:
return None, "unexpected_keys"
if "workshop" not in args:
return None, "missing_workshop"
value = args["workshop"]
if not isinstance(value, str):
return None, "workshop_not_string"
if value not in ALLOWED_WORKSHOPS:
return None, "workshop_not_allowed"
return value, None
Функция не доверяет модели и не полагается на enum в схеме. Она проверяет тип контейнера, лишние ключи, наличие обязательного поля, тип значения и попадание в allowlist. Каждая ветка возвращает отдельную причину, чтобы в логе было видно, что именно пришло не так.
Диспетчер: словарь известных имён вместо eval
Вызов функции идёт через словарь известных имён. Модель присылает строку с именем, и вы сопоставляете её с реальной функцией. Прямое исполнение полученной строки через eval или exec открывает путь к произвольному коду, поэтому такой подход исключён.
В документации Ollama показан именно словарь: available_functions с ключами add и multiply, а затем обращение available_functions[tc.function.name]. Если имя не найдено, пример возвращает строку Unknown tool. Тот же приём работает для одного инструмента.
def available_seats(workshop: str) -> int:
directory = {"python": 7, "n8n": 0}
return directory[workshop]
DISPATCH = {
"available_seats": available_seats,
}
def run_tool(name, args):
fn = DISPATCH.get(name)
if fn is None:
return None, "unknown_tool"
value, error = validate_args(args)
if error:
return None, error
return fn(value), None
Словарь даёт две гарантии. Вызывается только та функция, которую вы явно зарегистрировали. Неизвестное имя превращается в понятную ошибку. Проверка аргументов идёт до вызова, поэтому функция получает уже нормализованное значение.
Учебная функция и синтетический справочник мест
Функция available_seats возвращает число мест из явного словаря. Словарь синтетический: python — 7, n8n — 0. Эти числа — условный вход учебного примера. Значение 0 означает, что мест нет, и это полноценный результат вызова.
def available_seats(workshop: str) -> int:
directory = {"python": 7, "n8n": 0}
return directory[workshop]
Функция детерминирована: одинаковый аргумент даёт одинаковый ответ. Это удобно для проверки цикла, потому что результат предсказуем. Если модель вызвала инструмент с workshop=»python», в историю уйдёт строка «7». Если с workshop=»n8n», уйдёт строка «0», и финальный ответ должен сообщить об отсутствии мест.
Держите справочник рядом с функцией и не смешивайте его с реальными данными. Когда пример заработает, замените словарь на настоящий источник, сохранив сигнатуру функции и проверку аргументов.
История: assistant message, затем role=tool
После получения tool_calls нужно сохранить исходное сообщение ассистента в историю. В примерах Ollama это делается строкой messages.append(response.message). Затем для каждого вызова добавляется сообщение с ролью tool, именем инструмента и строковым содержимым.
Порядок важен. Сначала в историю попадает assistant message с tool_calls. Потом — результат. Модель на следующем шаге видит связку: вот что она запросила, вот что вернул инструмент. Если пропустить assistant message, модель не свяжет результат с вызовом.
messages.append(response.message)
for call in response.message.tool_calls:
result, error = run_tool(call.function.name, call.function.arguments)
content = str(result) if error is None else f"error: {error}"
messages.append({
"role": "tool",
"tool_name": call.function.name,
"content": content,
})
Поле content приводится к строке через str. В документации Ollama результат добавляется как str(result) в Python и как String(result) в JavaScript. Ошибку тоже стоит вернуть строкой: модель увидит её и сможет объяснить пользователю, что пошло не так.
Повторный запрос, все вызовы и лимит раундов
После добавления результатов запрос повторяется с полной историей. В ответе может прийти несколько tool_calls сразу: в документации Ollama показан пример с четырьмя вызовами — get_temperature и get_conditions для New York и London. Обрабатывать нужно каждый вызов.
Цикл строится так: запрос, добавление assistant message, обработка всех вызовов, добавление результатов, снова запрос. Выход из цикла — отсутствие tool_calls в ответе. В примере на Python это условие else: break, в примере на JavaScript — if (!toolCalls.length) break.
Установите Python SDK командой pip install ollama -U. Сохраните следующий цельный пример как seats_tool.py; он предполагает работающую локальную Ollama и уже установленную модель qwen3 с поддержкой tools.
from ollama import chat
import json
ALLOWED_WORKSHOPS = {"python", "n8n"}
SCHEMA = {
"type": "function",
"function": {
"name": "available_seats",
"description": "Возвращает число свободных мест на учебный практикум",
"parameters": {
"type": "object",
"properties": {"workshop": {"type": "string", "enum": ["python", "n8n"]}},
"required": ["workshop"],
"additionalProperties": False,
},
},
}
def validate_args(args):
if not isinstance(args, dict):
return None, "arguments_not_object"
if set(args) - {"workshop"}:
return None, "unexpected_keys"
if "workshop" not in args:
return None, "missing_workshop"
value = args["workshop"]
if not isinstance(value, str):
return None, "workshop_not_string"
if value not in ALLOWED_WORKSHOPS:
return None, "workshop_not_allowed"
return value, None
def available_seats(workshop):
return {"python": 7, "n8n": 0}[workshop]
DISPATCH = {"available_seats": available_seats}
MAX_ROUNDS = 5
messages = [{"role": "user", "content": "Проверь по инструменту, сколько мест осталось на учебный практикум python."}]
cache = {}
executed = False
for _ in range(MAX_ROUNDS):
response = chat(model="qwen3", messages=messages, tools=[SCHEMA], stream=False, think=True)
messages.append(response.message)
calls = response.message.tool_calls or []
if not calls:
print("final:", response.message.content)
if not executed:
print("no_tool_execution: тест вызова не пройден")
break
print("assistant.tool_calls:", calls)
for call in calls:
name, args = call.function.name, call.function.arguments
key = json.dumps([name, args], ensure_ascii=False, sort_keys=True)
if key in cache:
content = cache[key]
print("cached:", name, args)
else:
fn = DISPATCH.get(name)
value, error = validate_args(args)
if fn is None:
content = "error: unknown_tool"
elif error:
content = "error: " + error
else:
print("local_call:", name, args)
result = fn(value)
executed = True
content = str(result)
print("local_result:", content)
cache[key] = content
tool_message = {"role": "tool", "tool_name": name, "content": content}
messages.append(tool_message)
print("history:", tool_message)
else:
print("round_limit_reached: финальный ответ не получен")
Записи assistant.tool_calls, local_call/local_result, history и final показывают четыре этапа проверки. Для python ручной эталон равен7, для n8n —0; проверьте, что финальный текст использует полученный результат. Это ожидаемый учебный исход, запуск редакцией не заявлен.
Счётчик раундов защищает от зацикливания. Модель может повторять один и тот же вызов, и без лимита цикл не завершится. Значение MAX_ROUNDS задаёт верхнюю границу числа запросов в учебном скрипте. При исчерпании лимита пример явно сообщает об остановке без финального ответа; достаточное число раундов зависит от поведения модели.
Ошибки: нет вызова, неизвестное имя, тип, повторы
Четыре ситуации требуют явной обработки. Разберём каждую с реакцией кода.
| Ситуация | Признак | Реакция кода |
|---|---|---|
| Нет вызова | tool_calls пуст или отсутствует | Выйти из цикла, взять текст ответа |
| Неизвестное имя | DISPATCH.get вернул None | Вернуть role=tool с текстом ошибки |
| Неверный тип аргумента | validate_args вернул ошибку | Вернуть role=tool с причиной |
| Повторяющийся вызов | То же имя и аргументы в истории | Ограничить раунды, вернуть прежний результат |
Отсутствие вызова — нормальный исход. Модель ответила текстом, и цикл завершается. Неизвестное имя означает, что модель попросила функцию вне вашего словаря: верните строку с ошибкой, чтобы модель скорректировала ответ. Неверный тип аргумента ловится до вызова функции. Повторяющиеся вызовы видны по истории: если та же пара имя-аргументы уже обработана, верните сохранённый результат и не выполняйте функцию заново.
Четыре доказательства успешного вызова
Успешный цикл подтверждается четырьмя артефактами.
- tool_calls в ответе модели: есть объект с именем available_seats и аргументом workshop.
- Запись локального вызова: ваш код зафиксировал имя и аргументы перед выполнением.
- tool-result в истории: сообщение role=tool с tool_name=available_seats и content=»7″.
- Финальный текст: ответ модели использует число 7 из результата.
Карточка trace для учебного примера выглядит так:
assistant.tool_calls → available_seats(workshop="python")
Локальная проверка аргументов и выполнение → 7
history role=tool, tool_name=available_seats, content="7"
Финальный ответ использует 7; если tool_calls не было, тест вызова не пройден.
Если первого пункта нет, вызова не было. Если есть первый, но нет третьего, вы не вернули результат в историю. Если есть третий, но финальный текст не связан с числом, модель проигнорировала результат.
Потоковый режим за границами примера
При stream=true ответ приходит частями. В документации Ollama сказано: при потоковой передаче нужно собрать каждый chunk с thinking, content и tool_calls, а затем вернуть эти поля вместе с результатами инструментов в следующем запросе. Пример накапливает значения в переменные thinking, content и tool_calls, а потом добавляет их одним сообщением ассистента.
messages.append({
"role": "assistant",
"thinking": thinking,
"content": content,
"tool_calls": tool_calls,
})
Малый пример из этой статьи работает без потока: stream=false. Так проще увидеть целый объект tool_calls и проверить аргументы. Потоковый режим добавляйте, когда базовый цикл уже проходит: он требует накопления chunks и аккуратной сборки сообщения ассистента.
Частые вопросы
Что такое Ollama tool calling простыми словами?
Это механизм, при котором модель возвращает запрос на вызов функции, а исполняет функцию код вашего приложения. Модель формирует имя и аргументы, приложение проверяет их и выполняет реальный код. Результат возвращается модели сообщением role=tool.
Как передать схему инструмента в Ollama?
Схема передаётся в поле tools тела запроса к /api/chat. Она содержит type, function, name, description и parameters с properties и required. Python SDK умеет принимать функцию напрямую и сам строит схему из её сигнатуры и docstring.
Почему модель не вызывает инструмент?
Возможные причины: выбранная модель не формирует tool_calls, описание инструмента не соответствует запросу, схема содержит ошибку. Проверьте ответ на наличие поля tool_calls и попробуйте модель из примеров документации, например qwen3.
Как обработать несколько tool_calls в одном ответе?
Пройдите циклом по всем элементам response.message.tool_calls. Для каждого вызовите функцию через словарь известных имён и добавьте отдельное сообщение role=tool с именем и строковым результатом. Затем повторите запрос с полной историей.
Нужен ли eval для вызова функции из tool_calls?
Нет. Имя функции сопоставляется со словарём зарегистрированных обработчиков, например DISPATCH.get(name). Неизвестное имя возвращает ошибку. Прямое исполнение строки из ответа модели открывает путь к произвольному коду.
