Содержание
  1. Граница ответственности: модель просит, код исполняет
  2. Выбор локальной модели с поддержкой tools
  3. Полный tools JSON: type, function, name, description, parameters
  4. Аргументы в tool_calls: объект, ключи, тип, allowlist
  5. Диспетчер: словарь известных имён вместо eval
  6. Учебная функция и синтетический справочник мест
  7. История: assistant message, затем role=tool
  8. Повторный запрос, все вызовы и лимит раундов
  9. Ошибки: нет вызова, неизвестное имя, тип, повторы
  10. Четыре доказательства успешного вызова
  11. Потоковый режим за границами примера
  12. Частые вопросы
Гайды

Как настроить Ollama tool calling: вызов функций

5 ноября 2026 · 14 минут чтения

Разберём, как настроить 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 здесь означает, что мест нет, и это корректный результат вызова.

JSON19 строк
{
  "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.

Вот функция проверки, которая возвращает либо нормализованное значение, либо причину отказа:

PYTHON16 строк
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. Тот же приём работает для одного инструмента.

PYTHON16 строк
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 означает, что мест нет, и это полноценный результат вызова.

PYTHON3 строки
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, модель не свяжет результат с вызовом.

PYTHON10 строк
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.

PYTHON76 строк
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 с причиной
Повторяющийся вызов То же имя и аргументы в истории Ограничить раунды, вернуть прежний результат

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

Четыре доказательства успешного вызова

Успешный цикл подтверждается четырьмя артефактами.

  1. tool_calls в ответе модели: есть объект с именем available_seats и аргументом workshop.
  2. Запись локального вызова: ваш код зафиксировал имя и аргументы перед выполнением.
  3. tool-result в истории: сообщение role=tool с tool_name=available_seats и content=»7″.
  4. Финальный текст: ответ модели использует число 7 из результата.

Карточка trace для учебного примера выглядит так:

ТЕКСТ4 строки
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, а потом добавляет их одним сообщением ассистента.

PYTHON6 строк
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). Неизвестное имя возвращает ошибку. Прямое исполнение строки из ответа модели открывает путь к произвольному коду.

Читайте также

3 материала