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, чтобы сессию можно было возобновить после сбоя оркестратора.
Агентный цикл: псевдокод
## Оркестратор — агентный цикл
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. Это самое важное поле, которое большинство команд игнорирует. Без него модель может передать любые произвольные поля — и ваш код либо упадёт, либо молча проигнорирует неожиданные данные.
{
"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, перед выполнением прогоните аргументы через два уровня проверки:
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 — это упавший агентный цикл.
## 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 # повтор не поможет — данные неверны
- AuthorizationErrorFallback-стратегии
Retry не всегда помогает. Нужна иерархия fallback-решений:
Модель-резерв. Если основная модель недоступна дольше 30 секунд, переключайтесь на резервную. Важно: резервная модель должна быть предварительно протестирована с вашими tool definitions — разные модели по-разному следуют схемам.
Упрощённый режим. Если агент не справился за MAX_ITERATIONS шагов, верните пользователю partial result с объяснением: «Выполнено 8 из 10 шагов, результат частичный». Это лучше, чем тихая ошибка или зависание.
Деградация до прямого ответа. Если все tools недоступны (сетевой сбой инфраструктуры), агент может ответить напрямую из своих знаний с явной пометкой об ограниченности ответа.
Circuit breaker для tools
Если конкретный tool падает в 80% случаев за последние 5 минут, не стоит продолжать его вызывать — это перегружает downstream-сервис и тратит токены впустую.
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 и алерты
Что логировать в каждом агентном шаге
Без структурированных логов дебаггинг агентных сессий невозможен. Минимально необходимый набор событий:
// Событие: начало агентного цикла
{
"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-кейсе? Опишите его в комментариях — разберём архитектуру вместе.