Гайд · TNWS AI
Как собрать многошаговый agent loop с tools в Ollama
Реализуем ограниченный цикл tool calling, добавляем assistant/tool messages, проверяем имена функций и завершаем по финальному content.
Задача и применимость
Цель этого руководства — позволить модели выполнить несколько зависимых вычислений, сохранив предел итераций и строгий dispatcher. Используется точный интерфейс: multi-turn tool calling. Материал подходит для локального сервиса, тестового контура или внутреннего API, где Ollama доступна по доверенному адресу. Не публикуйте порт 11434 напрямую в интернет и не подставляйте в примеры реальные клиентские данные.
В отличие от общего обзора здесь есть проверяемый контракт: запрос, структура ответа, позитивные assertions и негативный сценарий. Название модели в коде — пример; выберите установленную модель, которая поддерживает нужную capability, и зафиксируйте её имя в конфигурации проекта.
Проверенные факты и первоисточники
Сведения проверены 13 сентября 2026 года по официальной документации Ollama. Официальный пример agent loop повторяет chat, добавляет assistant message и результаты tools, пока модель не вернёт ход без tool_calls. Дополнительно структура базовых ответов сверена с Generate API, Chat API и Embed API.
Цены и тарифы не приводятся. Доступность конкретной модели, качество ответа и аппаратная производительность зависят от локальной установки, поэтому они не выдаются за универсальный факт. Метрики берутся только из фактического JSON-ответа вашего запуска.
Подготовка безопасного тестового стенда
- Убедитесь, что Ollama запущена локально и endpoint доступен приложению.
- Получите список установленных моделей через официальный endpoint tags и выберите модель, подходящую capability.
- Не зашивайте имя модели в десятки файлов: храните его в переменной окружения или одном конфигурационном модуле.
- Используйте искусственные значения DEMO, тестовые SKU и вымышленные тексты.
- Задайте timeout и проверяйте HTTP status до разбора JSON.
- Сохраняйте исходный request без секретов, версию приложения и имя модели для воспроизводимости.
- Разделяйте сетевую ошибку, ошибку JSON, нарушение схемы и неверный бизнес-результат.
Ollama генерирует вероятностный результат. Даже при стабильном запросе текст может отличаться, поэтому тестируйте инварианты: наличие поля, роль сообщения, допустимое имя tool, формат аргумента, число векторов или ожидаемое вычисление. Не сравнивайте длинный ответ целиком со snapshot без необходимости.
Пошаговая реализация
- Сформируйте payload только из документированных полей.
- Укажите model и обязательный prompt, messages либо input.
- Для примеров, которым нужен один JSON, явно задайте stream:false.
- Отправьте POST с Content-Type: application/json.
- При status вне диапазона 200–299 прочитайте текст ошибки и остановите обработку.
- Разберите JSON и проверьте типы нужных полей до использования.
- Проверьте связь ответа с входом: роли, индексы, tool_name или сохранённые фрагменты.
- Не исполняйте tool по имени напрямую через eval или динамический import; используйте allowlist dispatcher.
- Ограничьте число шагов agent loop и общий timeout.
- Запишите технические счётчики отдельно от пользовательского результата.
- Выполните негативный тест и убедитесь, что он действительно отклоняется.
- Только после этого переносите код в основной сервис.
Готовый пример
const fns={add:({a,b})=>a+b,multiply:({a,b})=>a*b};
const messages=[{role:"user",content:"Вычисли (12+8)*3, используя tools"}];
const tools=Object.keys(fns).map(name=>({type:"function",function:{name,description:name,parameters:{type:"object",required:["a","b"],properties:{a:{type:"integer"},b:{type:"integer"}}}}}));
for(let turn=0;turn<5;turn++){const r=await fetch("http://localhost:11434/api/chat",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({model:"qwen3",messages,tools,stream:false})});const d=await r.json();messages.push(d.message);const calls=d.message.tool_calls||[];if(!calls.length){if(!d.message.content.includes("60"))throw Error("неверный итог");console.log(d.message.content);break;}for(const c of calls){const fn=fns[c.function.name];if(!fn)throw Error("unknown tool");messages.push({role:"tool",tool_name:c.function.name,content:String(fn(c.function.arguments))});}if(turn===4)throw Error("max turns");}
Код рассчитан на Node.js с глобальным fetch. Его можно адаптировать к Python, сохранив ту же JSON-схему и последовательность проверок. Перед production добавьте AbortController, структурированный журнал без содержимого пользовательского prompt и retry только для безопасных транспортных сбоев.
Не повторяйте автоматически запрос, который уже мог вызвать внешний tool с побочным эффектом. Для инструментов записи используйте idempotency key на стороне приложения и сохраняйте состояние выполненного вызова. Модель предлагает аргументы, но доверять им без валидации нельзя.
Пример входа и ожидаемого результата
Вход: Выражение (12+8)*3, функции add и multiply, предел пять model turns.
Ожидаемый результат: Цикл сначала получает промежуточное 20, затем умножает на 3 и завершает content с числом 60 до достижения лимита.
Фактическая формулировка модели может отличаться. Критерием служат явно перечисленные свойства. Если критерий зависит от знания модели, используйте небольшой набор тест-кейсов и ручную выборочную проверку, но не выдумывайте процент точности без измерения.
Негативный тест
Смоделируйте модель, которая бесконечно вызывает add. На пятом ходе код обязан завершиться max turns, а не продолжать цикл.
Негативный тест запускайте автоматически рядом с позитивным. Он должен завершаться конкретной ошибкой и не оставлять частично записанное состояние. Для tools проверьте неизвестное имя, лишние аргументы, пропущенное обязательное поле и неверный тип. Для embeddings проверяйте количество, размерность и конечность каждого числа.
Production-обёртка
Создайте одну функцию client.request, которая добавляет endpoint, timeout, заголовок и обработку статусов. Поверх неё сделайте отдельные функции generate, chat и embed с собственными схемами валидации. Так изменение одного endpoint не разнесётся по всему приложению.
В журнал пишите request_id, model, endpoint, status, done_reason и длительности, но не полный prompt, системную инструкцию, tool output или embedding. Вектор сам по себе не следует считать анонимным: применяйте к хранилищу те же правила доступа и срока хранения, что и к исходному документу.
Для agent loop храните messages в рамках одного задания, ограничивайте их размер и число ходов. Assistant message с tool_calls нужно добавить в историю до сообщений role=tool. Все результаты независимых параллельных вызовов добавьте до следующего обращения к модели.
Копируемый шаблон проверки
Endpoint: multi-turn tool calling
Задача: позволить модели выполнить несколько зависимых вычислений, сохранив предел итераций и строгий dispatcher
Модель и capability проверены: <да/нет>
Искусственный вход: <значение>
Обязательные поля request: <список>
Обязательные поля response: <список>
Позитивный invariant: <условие>
Негативный сценарий: <условие>
Timeout: <значение проекта>
Max turns для цикла: <значение>
Allowlist tools: <имена>
PII и секреты отсутствуют: <да>
Источник: https://docs.ollama.com/capabilities/tool-calling
Дата проверки: 13 сентября 2026 года
Финальная проверка результата
Выполните тест дважды: после холодного старта и при загруженной модели. Не смешивайте эти измерения. Для времени переводите наносекунды явно и сохраняйте исходные значения. Для text generation проверяйте done и done_reason, если они участвуют в логике. Для chat проверяйте role и непустой content либо валидные tool_calls.
После обновления Ollama или модели снова прогоните контрактные тесты. Имя поля из документации важнее визуального сходства ответа. Если capability не подтверждена для выбранной модели, не маскируйте это повторными запросами: выберите совместимую модель или измените UX.
Чек-лист
- Slug и title уникальны.
- Endpoint и поля сверены 13 сентября 2026 года.
- Модель поддерживает нужную capability.
- Вход содержит только искусственные данные.
- Установлен timeout.
- HTTP status проверяется до JSON.
- Типы ключевых полей проверяются.
- Никакие tool calls не исполняются через eval.
- Аргументы tools проходят схему и allowlist.
- Agent loop имеет max turns.
- Индексы embeddings не смещаются.
- Метрики считаются из реальных полей ответа.
- Негативный тест завершается ожидаемо.
- Логи не содержат prompt, PII и секреты.
FAQ
Почему в примерах указан stream:false?
Так проще получить один JSON и проверить итоговые служебные поля. Для потокового UX нужен отдельный NDJSON-парсер, аккумуляция чанков и обработка финального done.
Можно ли считать ответ модели детерминированным?
Нет. Проверяйте структуру и бизнес-инварианты. Если нужна воспроизводимость, фиксируйте поддерживаемые параметры и модель, но всё равно оставляйте валидацию результата.
Нужно ли доверять аргументам tool_calls?
Нет. Это сгенерированный ввод. Проверяйте имя по allowlist, аргументы по JSON Schema и полномочия пользователя до выполнения функции.
Можно ли логировать весь payload для отладки?
Только на искусственном стенде. В production журнал ограничивают техническими идентификаторами, статусом и измерениями; чувствительное содержимое редактируют или исключают.
Что делать при несовместимой модели?
Остановить сценарий с понятной диагностикой и выбрать модель, для которой нужная capability подтверждена. Нельзя выдавать случайный текст за корректный structured/tool/FIM результат.
Читайте также
Как дополнить код через suffix в Ollama Generate API
Используем suffix для fill-in-the-middle: передаём код до и после пропуска, извлекаем вставку и проверяем итоговый файл.
Как измерить скорость генерации по полям Ollama API
Разбираем total_duration, load_duration, prompt_eval_count и eval_count и считаем наблюдаемую скорость без выдуманных метрик.
Как получить batch embeddings для массива текстов в Ollama API
Передаём массив строк в /api/embed, проверяем число и размерность векторов и сохраняем соответствие индексов исходным документам.
Комментарии
Пока тихо. Скажите первое слово