Alltokens

AI агенты с tool calls: архитектура и production-паттерны предсказуемого качества

Как строить AI-агентов с tool calls в production: оркестратор, валидация аргументов, retry/fallback, observability и circuit breaker.

Опубликовано: 2026-02-1820 мин чтения

AI агенты с tool callsархитектура AI агентаagent workflow

AI агенты с tool calls: архитектура и production-паттерны предсказуемого качества

Агентные системы на LLM переходят из категории «интересный эксперимент» в категорию «критический производственный компонент». Вместе с этим переходом приходит неприятное открытие: агент, который работает на демо, часто не работает в production. Не из-за плохой модели — из-за плохой архитектуры вокруг неё.

Эта статья о том, как строить AI агентов с tool calls так, чтобы они были предсказуемы, наблюдаемы и устойчивы к сбоям. Без академической теории — только production-паттерны, которые можно внедрить на следующей неделе.


Что ломается в агентных сценариях чаще всего

Прежде чем строить правильно, стоит понять, где именно падают системы.

Галлюцинация аргументов. Модель вызывает search_database(query="...") с аргументом, которого нет в схеме. Или передаёт число вместо строки. Или придумывает поле filter, которого вы не объявляли. Ваш код падает с необработанным исключением — и всё это без какого-либо сигнала в мониторинге.

Бесконечные циклы. Агент не может завершить задачу, потому что tool возвращает ошибку, а модель снова и снова пробует тот же вызов с незначительными вариациями. Без ограничения числа итераций это дорогостоящая петля.

Потеря контекста при длинных сессиях. К 10-му шагу агентного цикла контекстное окно заполнено историей tool calls, и модель начинает «забывать» исходную задачу или принимать решения, противоречащие ранним шагам.

Нет изоляции ошибок. Один упавший tool убивает весь агентный цикл. Нет partial result, нет fallback, нет graceful degradation — только 500-ка в ответе пользователю.

Непрозрачность. В логах нет ни одной строки о том, какие tools были вызваны, с какими аргументами, сколько времени занял каждый шаг. Дебаггинг production-инцидента превращается в многочасовое расследование.

Все эти проблемы решаются архитектурно — один раз, до выхода в production.


Базовая архитектура агента: оркестратор, tools, state

Компоненты и их границы ответственности

Хорошо спроектированный agent workflow состоит из четырёх чётко разделённых слоёв:

Оркестратор — это не бизнес-логика. Его единственная задача: запустить агентный цикл, получить от LLM следующее действие (текстовый ответ или tool call), выполнить его и вернуть результат в контекст. Оркестратор не знает, что делают tools — только как их вызывать.

Tool Registry — реестр доступных инструментов с их JSON Schema-определениями. Каждый tool регистрируется один раз; оркестратор передаёт список определений в каждый LLM-вызов.

State Store хранит историю сессии. Для простых агентов — in-memory массив messages. Для production — внешнее хранилище с session_id, чтобы сессию можно было возобновить после сбоя оркестратора.

Агентный цикл: псевдокод

python
## Оркестратор — агентный цикл

def run_agent(task: str, session_id: str) -> AgentResult:
    state = load_state(session_id)          # [] или восстановленная история
    state.append({"role": "user", "content": task})

    for step in range(MAX_ITERATIONS):      # жёсткий лимит итераций
        if state.total_tokens > TOKEN_BUDGET:
            return AgentResult(status="budget_exceeded",
                               partial=state.last_result)

        response = llm.call(
            messages=state.messages,
            tools=TOOL_REGISTRY.definitions(),
            tool_choice="auto"
        )

        if response.stop_reason == "end_turn":
            return AgentResult(status="done", result=response.text)

        if response.stop_reason == "tool_use":
            for tool_call in response.tool_calls:
                result = execute_tool(tool_call)   # см. следующий раздел
                state.append(tool_result_message(tool_call.id, result))

        save_state(session_id, state)

    return AgentResult(status="max_iterations_reached",
                       partial=state.last_result)

MAX_ITERATIONS = 15 — рабочее значение для большинства задач. Для простых агентов с одним tool — 5. Для сложных research-агентов — до 25, но с более строгим token budget.


Валидация аргументов и защита от ошибок

JSON Schema как контракт

Каждый tool должен иметь строгую JSON Schema с additionalProperties: false. Это самое важное поле, которое большинство команд игнорирует. Без него модель может передать любые произвольные поля — и ваш код либо упадёт, либо молча проигнорирует неожиданные данные.

json
{
  "name": "search_knowledge_base",
  "description": "Ищет информацию в корпоративной базе знаний. Используй для ответов на вопросы о продуктах и политиках.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "Поисковый запрос на русском или английском. Максимум 200 символов.",
        "maxLength": 200
      },
      "category": {
        "type": "string",
        "enum": ["products", "policies", "technical", "billing"],
        "description": "Категория поиска. Если неизвестна — опусти поле."
      },
      "top_k": {
        "type": "integer",
        "minimum": 1,
        "maximum": 10,
        "default": 3,
        "description": "Число возвращаемых результатов."
      }
    },
    "required": ["query"],
    "additionalProperties": false
  }
}

Ключевые моменты схемы: additionalProperties: false отклоняет лишние поля; enum для ограниченных наборов значений; maxLength для строк; описания в description — для модели, не для разработчика.

Двухуровневая валидация при выполнении tool call

Одной JSON Schema недостаточно. После того как модель вернула tool call, перед выполнением прогоните аргументы через два уровня проверки:

python
def execute_tool(tool_call: ToolCall) -> ToolResult:
    tool = TOOL_REGISTRY.get(tool_call.name)

    if tool is None:
        # Модель придумала несуществующий инструмент
        return ToolResult(
            tool_use_id=tool_call.id,
            content="Error: unknown tool. Use only tools from the provided list.",
            is_error=True
        )

    # Уровень 1: структурная валидация по JSON Schema
    errors = tool.validate_schema(tool_call.arguments)
    if errors:
        return ToolResult(
            tool_use_id=tool_call.id,
            content=f"Schema validation error: {errors}. Fix arguments and retry.",
            is_error=True
        )

    # Уровень 2: бизнес-валидация (права доступа, rate limits, sanity checks)
    access_error = tool.check_access(current_session)
    if access_error:
        return ToolResult(
            tool_use_id=tool_call.id,
            content=f"Access denied: {access_error}",
            is_error=True
        )

    # Выполнение с timeout
    try:
        result = tool.execute(tool_call.arguments, timeout=TOOL_TIMEOUT_SEC)
        return ToolResult(tool_use_id=tool_call.id, content=result)
    except ToolExecutionError as e:
        return ToolResult(tool_use_id=tool_call.id, content=str(e), is_error=True)

Ключевой принцип: ошибка tool call — это не исключение агентного цикла. Это валидный результат, который возвращается в контекст модели. Модель видит сообщение об ошибке и может скорректировать аргументы или выбрать другой инструмент. Обрыв цикла при ошибке tool — одна из самых частых архитектурных ошибок.


Retry, fallback и circuit breaker

Retry policy для LLM-вызовов

LLM API — это внешняя зависимость с rate limits и периодическими сбоями. Без retry policy каждый 429 или 503 — это упавший агентный цикл.

yaml
## Retry policy: конфигурация для оркестратора

llm_retry_policy:
  max_attempts: 3
  initial_delay_ms: 1000
  backoff_multiplier: 2.0       # экспоненциальный бэкофф: 1s → 2s → 4s
  jitter: true                   # случайный ±20% к задержке
  retryable_status_codes:
    - 429    # rate limit
    - 503    # service unavailable
    - 529    # overloaded (Anthropic)
  non_retryable_status_codes:
    - 400    # bad request — не пробуем повторно, исправляем запрос
    - 401    # auth error — не пробуем повторно, алертим
    - 413    # context too long — не пробуем повторно, сжимаем контекст

tool_retry_policy:
  max_attempts: 2
  initial_delay_ms: 500
  backoff_multiplier: 1.5
  retryable_exceptions:
    - NetworkError
    - TimeoutError
  non_retryable_exceptions:
    - ValidationError    # повтор не поможет — данные неверны
    - AuthorizationError

Fallback-стратегии

Retry не всегда помогает. Нужна иерархия fallback-решений:

Модель-резерв. Если основная модель недоступна дольше 30 секунд, переключайтесь на резервную. Важно: резервная модель должна быть предварительно протестирована с вашими tool definitions — разные модели по-разному следуют схемам.

Упрощённый режим. Если агент не справился за MAX_ITERATIONS шагов, верните пользователю partial result с объяснением: «Выполнено 8 из 10 шагов, результат частичный». Это лучше, чем тихая ошибка или зависание.

Деградация до прямого ответа. Если все tools недоступны (сетевой сбой инфраструктуры), агент может ответить напрямую из своих знаний с явной пометкой об ограниченности ответа.

Circuit breaker для tools

Если конкретный tool падает в 80% случаев за последние 5 минут, не стоит продолжать его вызывать — это перегружает downstream-сервис и тратит токены впустую.

python
class ToolCircuitBreaker:
    def __init__(self, failure_threshold=0.8, window_seconds=300, 
                 min_calls=5, half_open_after=60):
        self.failure_threshold = failure_threshold
        self.window = window_seconds
        self.min_calls = min_calls
        self.half_open_after = half_open_after
        self.state = "closed"  # closed → open → half_open → closed

    def call(self, tool_fn, *args, **kwargs):
        if self.state == "open":
            if time.time() - self.opened_at > self.half_open_after:
                self.state = "half_open"
            else:
                raise CircuitOpenError(
                    f"Tool unavailable, circuit open. Retry after "
                    f"{int(self.half_open_after - (time.time()-self.opened_at))}s"
                )
        try:
            result = tool_fn(*args, **kwargs)
            self._record_success()
            return result
        except Exception as e:
            self._record_failure()
            raise

Ошибка CircuitOpenError возвращается в контекст LLM как tool result с is_error=True. Модель видит сообщение «инструмент недоступен» и может переключиться на альтернативный подход.


Observability и алерты

Что логировать в каждом агентном шаге

Без структурированных логов дебаггинг агентных сессий невозможен. Минимально необходимый набор событий:

jsonc
// Событие: начало агентного цикла
{
  "event": "agent_session_started",
  "session_id": "sess_abc123",
  "task_hash": "sha256:...",   // хеш задачи (не сам текст для PII)
  "model": "claude-sonnet-4-5",
  "max_iterations": 15,
  "token_budget": 50000,
  "timestamp": "2026-02-18T10:23:45Z"
}

// Событие: tool call
{
  "event": "tool_called",
  "session_id": "sess_abc123",
  "step": 3,
  "tool_name": "search_knowledge_base",
  "arguments_schema_valid": true,
  "latency_ms": 342,
  "is_error": false,
  "result_tokens": 187,
  "timestamp": "2026-02-18T10:23:47Z"
}

// Событие: завершение сессии
{
  "event": "agent_session_finished",
  "session_id": "sess_abc123",
  "status": "done",            // done / max_iterations / budget_exceeded / error
  "total_steps": 5,
  "total_input_tokens": 12400,
  "total_output_tokens": 3200,
  "total_latency_ms": 8750,
  "tools_called": ["search_knowledge_base", "send_email"],
  "timestamp": "2026-02-18T10:23:53Z"
}

Метрики для дашборда

Выстройте мониторинг вокруг четырёх сигналов:

Session success rate — доля сессий со статусом done от общего числа. Если падает ниже 90% — что-то сломалось. Разбивайте по типу задачи.

Steps per session (p50/p95/p99) — распределение числа шагов. Резкий рост p95 означает появление задач, которые агент не умеет решать эффективно, или деградацию качества модели.

Tool error rate по каждому инструменту — сигнал о деградации downstream-сервисов. Алертите при превышении 10% ошибок за 5-минутное окно.

Context utilization — среднее заполнение context window к моменту завершения сессии. Если p90 > 70% — скоро начнётся потеря контекста на сложных задачах.

Трассировка через LangSmith / Langfuse / самописный трейсер

Каждая агентная сессия должна быть одним трейсом с вложенными span-ами для каждого LLM-вызова и каждого tool call. Это позволяет: воспроизвести любую сессию пошагово, сравнить поведение до и после изменений в промпте, выявить конкретные задачи, которые системно вызывают проблемы.

Подробнее об интеграции с конкретными моделями — в документации и FAQ.


Чек-лист production readiness

Архитектура и контракты

  • Все tools описаны JSON Schema с additionalProperties: false
  • Оркестратор имеет жёсткий лимит MAX_ITERATIONS (рекомендуется 10–15)
  • Бюджет токенов (TOKEN_BUDGET) задан явно и проверяется перед каждым LLM-вызовом
  • State сохраняется во внешнем store с session_id (не только in-memory)
  • Tool errors возвращаются в контекст LLM как is_error: true, не вызывают исключений оркестратора

Надёжность

  • Retry policy настроена для LLM API с экспоненциальным бэкоффом и jitter
  • Circuit breaker реализован для каждого внешнего tool
  • Fallback-модель протестирована с вашими tool definitions
  • Partial result возвращается при max_iterations и budget_exceeded
  • Таймаут на выполнение каждого tool (рекомендуется 10–30 секунд)

Observability

  • Структурированные JSON-логи для всех событий агентного цикла
  • Трейсинг: каждая сессия — один трейс с nested spans для LLM и tools
  • Дашборд с session success rate, steps p95, tool error rate по инструменту
  • Алерт при tool error rate > 10% за 5 минут
  • Алерт при session success rate < 90% за 15 минут

Тестирование

  • Unit-тесты для каждого tool с моком LLM-вызовов
  • Integration-тесты для agent workflow с детерминированными сценариями (минимум 20)
  • Evaluation на реальных данных с LLM-judge (минимум 50 эталонных сессий)
  • Load test: поведение системы при 10× нормальной нагрузки

Безопасность

  • Авторизация на уровне каждого tool (не только на уровне API)
  • Sanitization аргументов перед передачей в downstream-сервисы (SQL injection и т.д.)
  • Логи не содержат PII (хешируйте или маскируйте идентификаторы пользователей)
  • Rate limit на число агентных сессий per user per hour

Актуальный список рекомендуемых моделей для агентных сценариев — в рейтинге coding-моделей.


FAQ

Чем tool calls отличаются от обычных function calls?
Tool calls — это механизм, при котором LLM возвращает структурированный JSON с именем инструмента и аргументами вместо текстового ответа. Оркестратор перехватывает этот JSON, вызывает реальную функцию и возвращает результат обратно в контекст модели. Ключевое отличие: именно LLM решает, какой инструмент вызвать и с какими аргументами, — это и делает систему агентной.

Как ограничить число итераций агента, чтобы избежать бесконечного цикла?
Используйте max_iterations на уровне оркестратора — типичное значение 10–15 шагов для большинства задач. Дополнительно добавляйте бюджет токенов: если суммарный context_tokens превысил порог (например, 80% context window), завершайте агентный цикл с partial result и логируйте причину остановки. Два независимых ограничения надёжнее, чем одно.

Как тестировать агентов до выхода в production?
Три уровня тестирования: unit-тесты для каждого tool (с моком LLM-вызовов), integration-тесты для agent workflow с детерминированными сценариями и evaluation-тесты на реальных сессиях с LLM-judge для оценки качества финального ответа. Минимум 50 эталонных сценариев покрывают большинство edge cases. Подробнее — в документации по тестированию агентов.

Нужен ли внешний state store для простых агентов?
Для агентов с числом шагов до 5 и context window до 32K достаточно in-memory state, передаваемого в messages. Внешний store нужен при необходимости возобновить сессию после сбоя, поддерживать несколько параллельных агентных треков или сохранять историю для аудита и дебаггинга.

Как выбрать модель для агентного workflow?
Ключевые критерии: качество следования tool-calling схеме (часть моделей систематически нарушает JSON Schema), поддержка параллельных tool calls и длина context window. Для сложных многошаговых агентов приоритет — точность tool calling, а не максимальная скорость или минимальная цена. Сравнительный обзор — в рейтинге моделей.


Заключение и следующий шаг

Архитектура AI агента — это не LLM плюс несколько функций. Это система с явными контрактами (JSON Schema), изоляцией ошибок (circuit breaker, tool errors в контекст), наблюдаемостью (структурированные логи, трейсинг) и жёсткими лимитами (итерации, токены, таймауты).

Агент, у которого нет ни одного из этих компонентов, работает на демо и ломается в production. Агент, у которого всё это есть, становится предсказуемым инженерным артефактом, который можно поддерживать, улучшать и масштабировать.

Начните с малого: возьмите ваш текущий агентный код и добавьте MAX_ITERATIONS, JSON Schema с additionalProperties: false и структурированное логирование для каждого tool call. Эти три изменения сразу дадут вам контроль и видимость, которых сейчас нет.

Следующий шаг: изучите список моделей с лучшей поддержкой tool calls, прочитайте подробную документацию по agent patterns или загляните в FAQ по агентным системам — там собраны ответы на наиболее частые архитектурные вопросы.


Есть вопрос о конкретном production-кейсе? Опишите его в комментариях — разберём архитектуру вместе.

Похожие статьи

МИРVisaMastercardСБП
AllTokens

© 2026 Alltokens. Все права защищены.