Перейти к содержанию
modelium.
Начать с задачи
←
Абстрактная композиция: слои фактур и направленный свет в сиреневых тонах.

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

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

Документация сверена 25 сентября 2026 года. Инструкция описывает проверку на вашем компьютере. Modelium не запускал модель и не измерял долю успешных вызовов; версия Ollama и выбранная модель в этой статье не зафиксированы. Для Git-документации llama.cpp зафиксирован commit 84e76d8a23162eca70490da131945ebec1f09bf4 s3s4.

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

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

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

Как выбрать 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 — Сырой снимок s1-ollama-tools.html; извлечение s1-ollama-tools.txt.. Получено 25.09.2026.
  2. Ollama — Generate a chat message — POST /api/chat, tools, stream и message.tool_calls; s2-ollama-chat.html.. Получено 25.09.2026.
  3. llama.cpp — Function Calling — revision 84e76d8a23162eca70490da131945ebec1f09bf4; s3-llama-functions.md.. Получено 25.09.2026.
  4. llama.cpp — llama-server README — revision 84e76d8a23162eca70490da131945ebec1f09bf4; s4-llama-server.md, endpoint and --jinja.. Получено 25.09.2026.
  5. vLLM — Tool Calling — s5-vllm-tools.html; auto/required/named и парсер.. Получено 25.09.2026.
  6. vLLM — Online Serving — s6-vllm-serving.html; chat endpoint и chat template.. Получено 25.09.2026.
  7. Ollama — List models — s7-ollama-list.html; GET /api/tags.. Получено 25.09.2026.

Вариант

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

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