Мониторинг работы агента в реальном времени и логирование трасс (Tracing)
Асинхронный воркер запускает агентную задачу, выполняет цепочку вызовов в течение трёх минут, расходует 140 000 токенов и возвращает пользователю лаконичный ответ: {"status": "error", "message": "Failed to synthesize molecule data"}. Классический серверный лог сообщает лишь стандартную строку: HTTP 500 Internal Server Error: Task failed at worker-node-4. Что произошло внутри? На каком из двадцати промежуточных шагов рассуждения агент свернул не туда: выдал ли галлюцинацию SQL-парсер, вернуло ли пустоту внешнее API, или модель вошла в непродуктивный цикл саморефлексии?
Традиционный мониторинг микросервисов (APM), ориентированный на HTTP-коды и задержку эндпоинтов, оказывается бессилен перед стохастической природой ИИ-агентов. Если в детерминированном ПО одинаковый вход всегда порождает одинаковый путь выполнения, то агент при каждом запуске динамически строит собственный граф вычислений. Для контроля над недетерминированным поведением агентов в реальном времени классического логирования недостаточно — необходима распределённая трассировка (Tracing) с поддержкой специфических семантических конвенций.
Анатомия агентной трассы: от логов к дереву спанов
В традиционных системах лог — это плоская временная запись о свершившемся событии. В агентных архитектурах ключевое значение имеет причинно-следственная связь: какой системный промпт породил генерацию аргументов инструмента, какой ответ вернул инструмент и как это наблюдение изменило следующий шаг планировщика.
Для решения этой задачи применяется модель ориентированного дерева выполнения, стандартизированная в концепции распределённой трассировки (Distributed Tracing):
Трасса (Trace) — полное направленное дерево событий, отражающее сквозной жизненный цикл выполнения задачи от момента получения пользовательского запроса до финального ответа или фатального сбоя.
Спан (Span) — атомарная структурная единица трассы, представляющая непрерывный интервал выполнения конкретной операции (вызов LLM, исполнение инструмента, поиск в векторном индексе) с фиксированным временем начала, конца, контекстными атрибутами и статусом.
Каждый спан содержит:
- Уникальный идентификатор спана (
span_id) и идентификатор родительского спана (parent_id).
- Идентификатор глобальной трассы (
trace_id), связывающий все операции в единый контекст.
- Временные метки начала (
start_time) и завершения (end_time).
- Метаданные (атрибуты) и структурированные события (events).
[Trace: research_pipeline_run_9842] (Duration: 8.4s, Cost: 0.042 USD)
│
├── [Span 1: Agent Step 1 - Planning] (Type: CHAIN, Duration: 1.2s)
│ └── [Span 1.1: LLM Call - Generate Plan] (Type: LLM, Duration: 1.15s, Tokens: 1420)
│
├── [Span 2: Agent Step 2 - Data Retrieval] (Type: CHAIN, Duration: 4.8s)
│ ├── [Span 2.1: Tool Call - search_ncbi_database] (Type: TOOL, Duration: 0.8s)
│ └── [Span 2.2: Tool Call - run_python_analysis] (Type: TOOL, Duration: 3.9s)
│ └── [Span 2.2.1: Sandbox Execution] (Type: EXEC, Duration: 3.85s)
│
└── [Span 3: Agent Step 3 - Synthesis] (Type: CHAIN, Duration: 2.4s)
└── [Span 3.1: LLM Call - Final Formatting] (Type: LLM, Duration: 2.35s, Tokens: 3150)
В мультиагентных графах такая иерархия позволяет мгновенно локализовать сбой: вы видите не просто текст ошибки, а точное место в графе вызовов, входные параметры конкретного инструмента и полный контекст сообщений, переданных в модель непосредственно перед падением.
Семантические конвенции OpenTelemetry и OpenInference
Если каждый разработчик логирует шаги агента в произвольном формате, агрегация данных и автоматический анализ метрик по всему парку агентов становятся невозможными. Для унификации телеметрии генеративного ИИ консорциум OpenTelemetry совместно с проектом OpenInference разработал открытый стандарт семантических атрибутов.
Стандарт определяет типы спанов (Span Kinds) и строгие правила именования полей:
| Категория спана |
Типовое назначение |
Ключевые семантические атрибуты OpenTelemetry / OpenInference |
| LLM |
Прямой вызов языковой модели |
llm.model_name, llm.input_messages, llm.output_messages, llm.token_count.prompt, llm.token_count.completion, llm.temperature |
| TOOL |
Исполнение внешней функции или API |
tool.name, tool.description, tool.parameters, tool.output, tool.status_code |
| RETRIEVER |
Поиск релевантных фрагментов в БД/RAG |
retrieval.query, retrieval.documents, retrieval.top_k, retrieval.similarity_scores |
| AGENT / CHAIN |
Оркестрация логики, узел графа |
agent.name, agent.role, agent.framework, agent.step_number |
Использование унифицированных атрибутов позволяет подключать к агенту любые системы визуализации и анализа трасс (LangSmith, Phoenix от Arize AI, Langfuse, Grafana Tempo) без изменения бизнес-логики агента.
Ключевые метрики мониторинга в реальном времени
В отличие от стандартного веб-сервиса, где отслеживаются в основном задержка (Latency), трафик (Traffic), ошибки (Errors) и насыщение (Saturation) — так называемые Golden Signals, — мониторинг агентов требует специализированных метрик реального времени.
1. Скорость расхода бюджета и токенов (Token & Cost Velocity)
Агент, вошедший в цикл или обрабатывающий чрезмерно длинные наблюдения, способен за минуты исчерпать суточный баланс API-провайдера. Мониторинг отслеживает кумулятивную стоимость трассы и мгновенную скорость генерации токенов:
vcost=i=1∑ktelapsedCprompt⋅Nin(i)+Ccomp⋅Nout(i)
- vcost — мгновенная скорость расхода бюджета в единицу времени (USD/сек).
- k — число вызовов моделей внутри текущей трассы.
- Cprompt и Ccomp — тарифная стоимость 1 токена на входе и выходе соответственно.
- Nin(i) и Nout(i) — число входных и выходных токенов в i-м вызове.
- telapsed — общее время выполнения трассы в секундах от старта до текущего шага.
Пример: если за 30 секунд работы агент выполнил 3 шага, потребив суммарно 60 000 входных токенов (по 0.003 USD за 1000 токенов) и 2 000 выходных токенов (по 0.015 USD за 1000 токенов), суммарная стоимость составит:
Стоимость=100060000⋅0,003+10002000⋅0,015=0,18+0,03=0,21 USD
Мгновенная скорость расхода составит vcost=300,21=0,007 USD/сек (25,2 USD/час). Если установленный порог для одной задачи равен 0,10 USD, система мониторинга обязана немедленно сигнализировать о превышении лимита.
2. Частота отказов инструментов (Tool Error Rate)
Отношение количества неудачных исполнений инструментов к общему числу вызовов за скользящее временное окно:
Etool=MtotalMfailed
- Etool — коэффициент ошибок инструмента за интервал времени.
- Mfailed — число вызовов, завершившихся системным исключением, таймаутом или невалидным кодом ответа.
- Mtotal — общее число обращений к инструменту.
Пример: если инструмент доступа к базе query_chemical_db за последние 10 минут был вызван 50 раз и в 15 случаях вернул ошибку таймаута песочницы, Etool=5015=0,30 (30%). Резкий рост этой метрики свидетельствует о деградации внешней инфраструктуры, требуя включения предохранителя (Circuit Breaker).
3. Задержка компонентов траектории (Step Latency Decomposition)
Общее время шага агента Tstep раскладывается на три компонента:
- Задержка инференса LLM (Tllm=TTFT+TPOT⋅Ntokens).
- Задержка исполнения инструмента (Ttool) в песочнице или внешнем API.
- Накладные расходы оркестратора (Torch) на валидацию схем, сохранение чекпоинтов и маршрутизацию графа.
Раздельный сбор этих метрик позволяет сразу понять, где находится бутылочное горлышко: тормозит ли удалённый кластер песочниц Docker или время уходит на генерацию многословных цепочек рассуждений моделью.
Практическая реализация: ручная и автоматическая трассировка
Для глубокого понимания механики трассировки реализуем минимальный легковесный трассировщик на Python. Он формирует древовидную структуру спанов, собирает метрики токенов и фиксирует ошибки без использования тяжёлых сторонних библиотек.
import time
import uuid
from contextlib import contextmanager
from typing import Any, Dict, List, Optional
class Span:
def __init__(self, name: str, span_type: str, parent_id: Optional[str] = None):
self.span_id: str = str(uuid.uuid4())[:8]
self.parent_id: Optional[str] = parent_id
self.name: str = name
self.span_type: str = span_type # 'CHAIN', 'LLM', 'TOOL'
self.start_time: float = time.time()
self.end_time: Optional[float] = None
self.duration: float = 0.0
self.attributes: Dict[str, Any] = {}
self.status: str = "RUNNING"
self.error: Optional[str] = None
def finish(self, status: str = "SUCCESS", error: Optional[str] = None) -> None:
self.end_time = time.time()
self.duration = self.end_time - self.start_time
self.status = status
self.error = error
def to_dict(self) -> Dict[str, Any]:
return {
"span_id": self.span_id,
"parent_id": self.parent_id,
"name": self.name,
"type": self.span_type,
"duration_sec": round(self.duration, 4),
"status": self.status,
"attributes": self.attributes,
"error": self.error,
}
class AgentTracer:
def __init__(self, trace_name: str):
self.trace_id: str = str(uuid.uuid4())
self.trace_name: str = trace_name
self.spans: List[Span] = []
self._active_span_stack: List[Span] = []
@contextmanager
def start_span(self, name: str, span_type: str):
parent_id = self._active_span_stack[-1].span_id if self._active_span_stack else None
span = Span(name=name, span_type=span_type, parent_id=parent_id)
self.spans.append(span)
self._active_span_stack.append(span)
try:
yield span
if span.status == "RUNNING":
span.finish(status="SUCCESS")
except Exception as exc:
span.finish(status="ERROR", error=str(exc))
raise exc
finally:
self._active_span_stack.pop()
def export_trace(self) -> Dict[str, Any]:
total_tokens = sum(
s.attributes.get("llm.total_tokens", 0) for s in self.spans if s.span_type == "LLM"
)
return {
"trace_id": self.trace_id,
"trace_name": self.trace_name,
"total_spans": len(self.spans),
"total_tokens": total_tokens,
"spans": [s.to_dict() for s in self.spans],
}
Рассмотрим, как этот трассировщик интегрируется в реальный цикл работы агента:
tracer = AgentTracer(trace_name="scientific_literature_analysis")
def run_instrumented_agent(query: str):
with tracer.start_span("agent_execution", "CHAIN") as root_span:
root_span.attributes["query"] = query
# Шаг 1: Вызов LLM для планирования
with tracer.start_span("llm_plan_generation", "LLM") as llm_span:
# Имитация запроса к модели
time.sleep(0.15)
llm_span.attributes["llm.model_name"] = "gpt-4o"
llm_span.attributes["llm.prompt_tokens"] = 350
llm_span.attributes["llm.completion_tokens"] = 80
llm_span.attributes["llm.total_tokens"] = 430
action_selected = "query_database"
# Шаг 2: Исполнение инструмента
with tracer.start_span("execute_tool", "TOOL") as tool_span:
tool_span.attributes["tool.name"] = action_selected
tool_span.attributes["tool.parameters"] = {"search_term": "CRISPR-Cas9"}
# Выполнение инструмента с замером времени
time.sleep(0.20)
tool_result = {"records_found": 12, "top_match": "Cas9 endonuclease specificity"}
tool_span.attributes["tool.output"] = tool_result
return tracer.export_trace()
# Результат экспорта содержит связное дерево:
# root_span (CHAIN) -> llm_plan_generation (LLM)
# -> execute_tool (TOOL)
В промышленных фреймворках (например, LangGraph или CrewAI) ручная обвязка заменяется встроенными колбэками (Callbacks). Библиотеки автоматически инжектируют генераторы спанов в каждый узел графа и перехватчик вызова инструмента, транслируя телеметрию в фоновые коллекторы OpenTelemetry (OTel Collector) по протоколу OTLP (gRPC/HTTP).
Обнаружение аномалий и алертинг в реальном времени
Сбор трасс приобретает практическую ценность только тогда, когда система способна в автоматическом режиме выявлять сбои и аномалии выполнения без участия человека.
┌───────────────────────────────┐
│ Входящий поток трасс │
│ (Спаны, Метрики, Токены) │
└──────────────┬────────────────┘
│
Потоковый анализатор телеметрии
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Loop Monitor │ │ Cost Sentinel │ │ Schema Guard │
├───────────────┤ ├───────────────┤ ├───────────────┤
│ Детекция │ │ Превышение │ │ Рост ошибок │
│ зацикливания │ │ бюджета трассы│ │ валидации JSON│
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└───────────────────────┼───────────────────────┘
▼
Диспетчер защитных действий (Alert Engine)
├── Принудительный останов задачи (Kill Trace)
├── Переключение на fallback-модель
└── Отправка уведомления дежурному инженеру
Выделяют четыре критических паттерна аномалий, требующих мгновенной реакции:
-
Семантическое зацикливание (Deadlock / Ping-Pong Loop)
- Симптом: агент многократно подряд вызывает один и тот же инструмент с идентичными или незначительно модифицированными аргументами, получая одинаковые наблюдения.
- Реакция мониторинга: вычисление хеша от аргументов последних N вызовов инструмента в рамках одной трассы. При совпадении трёх последовательных состояний генерируется сигнал тревоги, а оркестратор принудительно внедряет в контекст агента синтетическое указание сменить стратегию.
-
Взрывной рост контекста (Context Explosion)
- Симптом: размер входного промпта (Nin) на каждом шаге растёт в геометрической прогрессии из-за накопления нефильтрованных выводов инструментов.
- Реакция мониторинга: триггер на превышение порога градиента роста контекста (ΔNin>10000 токенов за шаг). Автоматический запуск компрессии или суммаризации истории.
-
Аномальная деградация задержки (Tail Latency Spike)
- Симптом: значение TTFT для шага рассуждения превышает 99-й перцентиль (p99>15 секунд).
- Реакция мониторинга: перенаправление последующих шагов задачи на резервный регион API или переключение на легковесную резервную модель.
-
Дрейф формата вызова функций (Schema Hallucination Rate)
- Симптом: резкое увеличение доли ответов модели, не проходящих валидацию Pydantic на стороне оркестратора.
- Реакция мониторинга: автоматический откат версии системного промпта или временное понижение температуры генерации до T=0.
Архитектура сквозной платформы наблюдаемости (Observability)
Полноценная система наблюдаемости за автономными агентами объединяет три уровня:
[ Уровень 1: Исполнение ]
Агентные узлы / Воркеры ──(Инструментация OTel/OpenInference)──┐
│ (OTLP / Async Stream)
[ Уровень 2: Сбор и маршрутизация ] │
OTel Collector / Message Queue (Kafka/Redis) ◄─────────────────┘
│
├───► Хранилище временных рядов (Prometheus / ClickHouse) ──► Метрики (RPS, Cost, Latency)
│
├───► Хранилище распределённых трасс (Jaeger / Langfuse) ──► Детальный анализ деревьев спанов
│
[ Уровень 3: Аналитика и контроль ]
└───► Детектор аномалий (Real-time Stream Processor) ──► Инъекция команд прерывания в воркер
- Уровень сбора (Collector): агентные воркеры сбрасывают спаны асинхронно в неблокирующем потоке (фоновый буфер в памяти), чтобы накладные расходы на логирование не замедляли основной пайплайн рассуждений.
- Уровень хранения: разделение данных на агрегированные метрики (для дашбордов и алертов) и полные деревья трасс с текстами сообщений (для отладки и последующего формирования датасетов дообучения).
- Уровень обратной связи: мониторинг перестаёт быть пассивным экраном с графиками — он становится активным контуром безопасности, способным перехватывать управление и защищать агентную систему от деградации и финансовых потерь.