# Tool calling локальных моделей: выбор и проверка вызова

Источник: https://modelium.tech/zhurnal/tool-calling-lokalnyh-modeley/

Tool calling позволяет локальной LLM предложить вызов функции с аргументами, но сама модель не исполняет функцию: приложение проверяет запрос, выполняет разрешённое действие и возвращает результат в диалог. Для первого опыта достаточно уже установленной модели в Ollama и тестовой функции без доступа к файлам или сети. Отдельно проверьте структуру вызова, факт исполнения функции и итоговый ответ; совместимость и качество конкретной модели без прогона не гарантированы.

**Документация сверена 25 сентября 2026 года.** Инструкция описывает проверку на вашем компьютере. Modelium не запускал модель и не измерял долю успешных вызовов; версия Ollama и выбранная модель в этой статье не зафиксированы. Для Git-документации llama.cpp зафиксирован commit `84e76d8a23162eca70490da131945ebec1f09bf4` [s3](#src-s3)[s4](#src-s4).

## Что именно делает локальная модель при tool calling?

Пользователь даёт задачу, приложение сообщает модели имена и схемы разрешённых инструментов, а модель может вернуть **предложение** вызвать функцию с аргументами. В Ollama оно находится в `message.tool_calls` ответа `/api/chat`; приложение затем вызывает собственную функцию и отправляет отдельное сообщение `role: tool` с результатом [s1](#src-s1)[s2](#src-s2). Следовательно, текст «вызвал инструмент» или даже корректный `tool_calls` не доказывает, что действие действительно произошло. Для проверки нужны отдельный журнал принятого вызова, результат исполняющего кода и ответ после передачи результата модели.

Это не то же, что structured output: JSON Schema может ограничить форму ответа, но не выполняет функцию. [Гайд по структурированному JSON](/zhurnal/structured-output-lokalno-json-schema/) помогает проверять поля; здесь важна ещё и граница полномочий приложения. Начните с функции чтения фиктивного значения, а не с оболочки, файловой системы, браузера, корпоративной базы или сетевого запроса.

## Как выбрать runtime для первого опыта?

        

          
        
        
Вариант

      
    
        
Что сообщает документация

      
    
        
На что смотреть

      
    

`  </tr><tr>
    <td>
    <p>Ollama</p>
  </td>
<td>
    <p>В нативном <code>POST /api/chat</code> есть массив <code>tools</code>, а пример показывает получение <code>message.tool_calls</code>, локальный запуск функции и следующий запрос с сообщением <code>role: tool</code> <a href="#src-s1" rel="noopener noreferrer">s1</a><a href="#src-s2" rel="noopener noreferrer">s2</a>.</p>
  </td>
<td>
    <p>Удобен для короткого теста с уже доступной локальной моделью; совместимость и точность вызова проверяются на выбранной модели, а не по одному имени runtime.</p>
  </td>

  </tr><tr>
    <td>
    <p>llama.cpp</p>
  </td>
<td>
    <p><code>llama-server</code> принимает OpenAI-совместимый <code>/v1/chat/completions</code>; для function calling важны Jinja и tool-aware chat template <a href="#src-s3" rel="noopener noreferrer">s3</a><a href="#src-s4" rel="noopener noreferrer">s4</a>.</p>
  </td>
<td>
    <p>Посмотрите <code>chat_template</code>/<code>chat_template_tool_use</code> в <code>/props</code>; при несовместимом шаблоне может понадобиться <code>--chat-template-file</code> <a href="#src-s3" rel="noopener noreferrer">s3</a>. Не путайте описание вызова с экспериментальными встроенными <code>--tools</code> сервера, которые могут дать доступ к файлам и shell <a href="#src-s4" rel="noopener noreferrer">s4</a>.</p>
  </td>

  </tr><tr>
    <td>
    <p>vLLM</p>
  </td>
<td>
    <p>Chat Completions работает с моделью, имеющей chat template <a href="#src-s6" rel="noopener noreferrer">s6</a>. Для <code>tool_choice="auto"</code> документация требует <code>--enable-auto-tool-choice</code> и подходящий <code>--tool-call-parser</code>; named/required — отдельные режимы <a href="#src-s5" rel="noopener noreferrer">s5</a>.</p>
  </td>
<td>
    <p>Парсер и шаблон должны соответствовать формату модели; named/required ограничивают структуру аргументов, но не гарантируют осмысленный выбор инструмента <a href="#src-s5" rel="noopener noreferrer">s5</a>.</p>
  </td>

  </tr></tbody>
    </table>
  <p>Это сравнение <strong>контрактов API</strong>, а не скорости или качества моделей. В vLLM режим <code>auto</code> без подходящего структурного ограничения может извлекать аргументы из свободного текста, поэтому даже корректно полученный <code>tool_calls</code> следует проверять по собственным правилам <a href="#src-s5" rel="noopener noreferrer">s5</a>. В llama.cpp универсальный fallback не означает равную эффективность всех шаблонов и моделей <a href="#src-s3" rel="noopener noreferrer">s3</a>.</p><h2>Что нужно до безопасной проверки в Ollama?</h2><p>Предусловия: установленная и запущенная Ollama, Python 3 и локально доступная модель, которую вы хотите проверить. <a href="/zapusk/ollama/" rel="noopener noreferrer">Гайд по Ollama</a> описывает подготовку runtime. Документированный <code>GET /api/tags</code> возвращает список <code>models</code> с именами; выбирайте из своего ответа, а не из примера документации <a href="#src-s7" rel="noopener noreferrer">s7</a>. Скрипт ниже не скачивает веса, не устанавливает пакеты и не обращается к платному API.</p><p>curl --fail-with-body -sS http://127.0.0.1:11434/api/tags<br />export MODEL='впишите-точное-имя-локальной-модели'<br /></p><p>Если сервер недоступен или список пуст, остановитесь и подготовьте модель; не подменяйте адрес облачным endpoint. Имя и поддержку инструментов уточняйте по карточке/документации выбранной модели, затем проверяйте её фактический ответ: само наличие модели в списке не доказывает tool calling.</p><h2>Как отделить предложенный вызов от реального исполнения?</h2><p>Создайте файл <code>tool_demo.py</code> из кода ниже. Он объявляет только <code>lookup_stock(sku)</code> и заранее фиксирует <code>A-42 → 7</code> в памяти приложения. Первый запрос может вернуть предложение вызвать функцию, но изменение журнала <code>executed</code> происходит <strong>только после</strong> проверки имени, количества вызовов и аргументов. Фиктивный остаток не относится к реальной системе. Схема инструмента и формат сообщений следуют примеру Ollama <a href="#src-s1" rel="noopener noreferrer">s1</a><a href="#src-s2" rel="noopener noreferrer">s2</a>.</p><p>import json<br />import os<br />from urllib.request import Request, urlopen<br /><br />API = 'http://127.0.0.1:11434/api/chat'<br />model = os.environ.get('MODEL', '')<br />if not model or model.startswith('впишите-'):<br />    raise SystemExit('Задайте MODEL точным именем из GET /api/tags')<br /><br />messages = [{'role': 'user', 'content': 'Сколько единиц тестового товара A-42? Используй lookup_stock.'}]<br />tools = [{'type': 'function', 'function': {<br />    'name': 'lookup_stock',<br />    'description': 'Получить фиктивный остаток тестового товара',<br />    'parameters': {'type': 'object',<br />                   'properties': {'sku': {'type': 'string'}},<br />                   'required': ['sku'], 'additionalProperties': False}<br />}}]<br /><br />def chat(body):<br />    payload = json.dumps({'model': model, 'messages': messages,<br />                          'stream': False, **body}, ensure_ascii=False).encode('utf-8')<br />    req = Request(API, data=payload, headers={'Content-Type': 'application/json'})<br />    with urlopen(req, timeout=120) as response:<br />        return json.load(response)<br /><br />first = chat({'tools': tools})<br />message = first['message']<br />calls = message.get('tool_calls') or []<br />if len(calls) != 1:<br />    raise SystemExit(f'Ожидался один вызов, получено: {len(calls)}; действие не выполнено')<br />function = calls[0]['function']<br />arguments = function.get('arguments')<br />if function.get('name') != 'lookup_stock' or not isinstance(arguments, dict) or arguments != {'sku': 'A-42'}:<br />    raise SystemExit('Неизвестный инструмент или неверные аргументы; действие не выполнено')<br />print('proposed: lookup_stock(A-42)')<br /><br /># Только код приложения исполняет разрешённую операцию.<br />stock = {'A-42': 7}<br />result = stock[arguments['sku']]<br />print(f'executed: lookup_stock(A-42) -&gt; {result} (тестовые данные)')<br />messages.append(message)<br />messages.append({'role': 'tool', 'tool_name': 'lookup_stock', 'content': str(result)})<br />final = chat({})  # tools больше не передаём: этот шаг просит только ответ.<br />print('final:', final['message'].get('content', ''))<br /></p><p>Запустите <strong>на своём стенде</strong>, сохранив исходный ответ API и терминальный журнал, если хотите повторить эксперимент. Код показывает строгую границу: при отсутствии вызова, нескольких вызовах или любом SKU кроме <code>A-42</code> функция не исполняется. Исключения сети, таймаут и ошибка JSON означают неуспешный тест, а не доказательство отсутствия поддержки. Строки <code>proposed</code>, <code>executed</code> и <code>final</code> — три разных наблюдения; сам факт вывода <code>final</code> не означает, что модель верно объяснила результат.</p><h2>Как интерпретировать результат и ошибки?</h2><p>Успех <strong>этого сценария</strong>: в первом ответе ровно один <code>message.tool_calls</code> с разрешёнными именем и аргументами, журнал фиксирует исполнение <code>lookup_stock(A-42) -&gt; 7</code>, а последняя реплика корректно сообщает тестовый остаток 7. Проверяйте последний пункт глазами: в коде он только печатается, не сертифицируется автоматически. Один такой прогон не измеряет надёжность; для своей задачи повторите сценарии без вызова, с неверными аргументами, несколькими предложенными вызовами и отказом инструмента. В рабочей интеграции дополнительно ограничьте права, валидируйте схему аргументов и требуйте отдельного подтверждения перед изменяющим действием.</p><p>Если <code>tool_calls</code> пуст, начните с совместимости модели и её шаблона, затем проверьте, что <code>tools</code> действительно отправлены в запросе. Если получен вызов, но приложение не вывело <code>executed</code>, это отказ правила безопасности, не ошибка «модель не вызвала инструмент». Если есть <code>executed</code>, но ответ неправильный, проверьте историю <code>assistant → tool → assistant</code> и текст последней реплики. Ollama отдельно документирует передачу сообщения об инструменте в следующий запрос <a href="#src-s1" rel="noopener noreferrer">s1</a>. Для llama.cpp проверяйте шаблон в <code>/props</code> <a href="#src-s3" rel="noopener noreferrer">s3</a>, для vLLM — парсер и режим <code>tool_choice</code> <a href="#src-s5" rel="noopener noreferrer">s5</a>.</p><p>После эксперимента удалите <code>tool_demo.py</code> и снимите переменную <code>MODEL</code> командой <code>unset MODEL</code>; демонстрация не создаёт внешних записей и не требует rollback базы. <strong>Следующий шаг:</strong> <a href="/zapusk/ollama/" rel="noopener noreferrer">подготовьте Ollama</a> и выполните безопасную проверку одного вызова на выбранной локальной модели; не подключайте реальные действия, пока не увидите все три результата.</p>
`

## Источники

1. [Ollama — Tool calling](https://docs.ollama.com/capabilities/tool-calling) — получено 2026-09-25
2. [Ollama — Generate a chat message](https://docs.ollama.com/api/chat) — получено 2026-09-25
3. [llama.cpp — Function Calling](https://raw.githubusercontent.com/ggml-org/llama.cpp/84e76d8a23162eca70490da131945ebec1f09bf4/docs/function-calling.md) — получено 2026-09-25
4. [llama.cpp — llama-server README](https://raw.githubusercontent.com/ggml-org/llama.cpp/84e76d8a23162eca70490da131945ebec1f09bf4/tools/server/README.md) — получено 2026-09-25
5. [vLLM — Tool Calling](https://docs.vllm.ai/en/stable/features/tool_calling/) — получено 2026-09-25
6. [vLLM — Online Serving](https://docs.vllm.ai/en/stable/serving/online_serving/) — получено 2026-09-25
7. [Ollama — List models](https://docs.ollama.com/api/tags) — получено 2026-09-25
