Почему для агента нужна отдельная модель наблюдаемости
У обычного API-запроса обычно есть понятные начало, обработчик и ответ. AI-агент добавляет цикл. Он выбирает модель, решает, нужно ли вызвать инструмент, ждёт удалённую систему, читает результат и иногда снова обращается к модели. Один пользовательский запрос может включать несколько вызовов модели, запросы поиска, выполнение инструментов, повторы и проверки политик. Строка журнала «запрос завершился ошибкой» не показывает, какая ветка потратила время или бюджет.
Наблюдаемость делает этот путь видимым через связанные трассировки, метрики и журналы. OpenTelemetry описывает трассировку как путь запроса, а span как отдельную операцию внутри него. Используйте один корневой span для пользовательского запуска агента и дочерние spans для инференса модели, поиска, инструментов, защитных проверок и сериализации. Имена моделей и инструментов должны иметь низкую кардинальность. Идентификаторы конкретного запроса помещайте в контекст трассировки или структурированные журналы, а не в метки метрик. Тогда оператор видит, что случилось в одном запуске, как часто это происходит и какие сценарии затронуты.
Далее используется нейтральная схема. Точные поля использования и правила оплаты определяет документация провайдера. Соглашения OpenTelemetry для GenAI и метрические соглашения GenAI дают общий словарь для развивающихся интеграций.
Схема трассы, которая выдерживает цикл агента
Создайте корневой span в момент приёма запроса приложением, а не при первом обращении к модели. Назовите операцию, например, invoke_agent. Добавьте атрибуты сервиса, окружения, развёртывания, версии рабочего процесса и несекретного класса арендатора. Идентификатор диалога записывайте только при его наличии и разрешённом сроке хранения. Пользовательский текст, полный промпт и параметры инструмента не должны быть метками метрик.
Каждый дочерний span должен отвечать на один эксплуатационный вопрос. Минимальный набор выглядит так:
| Span | Что записывать |
|---|---|
| agent.run | имя и версия рабочего процесса, итог, число попыток, длительность |
| gen_ai.inference | провайдер, запрошенная и фактическая модель, операция, поток, причина завершения, токены |
| gen_ai.retrieval | класс индекса или источника, режим запроса, число результатов, попадание в кэш |
| gen_ai.tool | имя и тип инструмента, решение авторизации, тайм-аут, итог |
| guardrail.check | версия политики, решение, код причины, длительность |
Для неуспешной операции задайте статус span и значение error.type. Для повтора запишите событие с номером попытки, задержкой перед повтором и кодом причины. Повтор не является вторым корневым запуском. Это новая попытка той же логической операции с отдельным клиентским span при отправке запроса по сети. Так дашборд не завышает число пользовательских запросов, но показывает число обращений к провайдеру.
Ниже приведена форма экспортируемой записи, а не утверждение о формате конкретного поставщика. В ней намеренно есть счётчики и коды, но нет содержимого.
{
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"parent_span_id": "b7ad6b7169203331",
"name": "chat model",
"kind": "CLIENT",
"status": "OK",
"attributes": {
"gen_ai.operation.name": "chat",
"gen_ai.provider.name": "provider.example",
"gen_ai.request.model": "model.example",
"gen_ai.response.model": "model.example-2026-01",
"gen_ai.usage.input_tokens": 820,
"gen_ai.usage.output_tokens": 146,
"gen_ai.response.finish_reasons": ["stop"],
"app.agent.attempt": 1
}
}
Имя span должно описывать класс операции, а не ID или пользовательский текст. Версию семантического соглашения храните в метаданных инструментации. Если провайдер отдельно сообщает оплачиваемые и фактически обработанные токены, сохраняйте оба значения в закрытом учёте, а в представлении стоимости используйте оплачиваемые. Не складывайте клиентскую и серверную инструментацию одного запроса без указания уровня, иначе токены будут посчитаны дважды.
Correlation ID и распространение контекста
Trace ID связывает сервисы. Span ID обозначает одну операцию. Собственный request ID удобен для поддержки, но не заменяет контекст трассировки. Передавайте заголовок W3C traceparent через API-шлюз, сервис агента, сервис поиска и адаптеры инструментов. Руководство OpenTelemetry по распространению контекста описывает извлечение удалённого контекста и создание дочернего span. Тот же контекст можно добавлять в структурированные журналы, чтобы поиск журнала приводил к трассе.
Если поддержке нужен короткий идентификатор, создавайте отдельный случайный request ID. Связь с trace ID храните в журналах, а не в метрике с высокой кардинальностью. Для очереди помещайте контекст в метаданные сообщения и создавайте span потребителя при обработке. Для независимых параллельных заданий используйте span links, если единственного родителя нет. Это сохраняет причинную связь, не выдавая конкурентную работу за обычный стек вызовов.
Считайте входящие заголовки трассировки недоверенными данными на публичной границе. Проверяйте формат, применяйте политику сэмплирования и не передавайте внутренний baggage провайдерам и сторонним инструментам. OpenTelemetry отдельно предупреждает, что baggage может содержать учётные данные или персональную информацию. Корреляция должна помогать расследованию, а не создавать канал утечки.
Почему spans инструментов показывают реальное поведение
Когда это важно, разделяйте решение модели и выполнение. Span модели показывает, что модель попросила инструмент. Span инструмента показывает, что приложение действительно выполнило. В нём нужны стабильное имя tool.name, тип function, extension или datastore, решение политики и внешняя операция. Метод запроса или класс поиска добавляйте только без раскрытия секрета. Не сохраняйте токен доступа, параметры SQL, текст документов или полный URL с query-параметрами.
Для вызова инструмента записывайте время начала и конца, тайм-аут, класс результата, число повторов и ограниченный размер результата. Тайм-аут, бизнес-отказ, запрет политики и ошибка upstream 5xx должны иметь разные коды причин. Если инструмент вызывает другой сервис, передавайте текущий контекст. Если он запускает локальную команду, сохраняйте только семейство команды и класс завершения, а не пользовательский ввод.
Показывайте также ожидание, которое не является вызовом инструмента. Добавляйте spans для задержки в очереди, сна из-за лимита, открытого circuit breaker и потоковой выдачи. Иначе медленный model span может скрывать ожидание слота конкуренции. В многoагентном рабочем процессе задавайте имя каждого делегированного рабочего процесса и связывайте его с родительской трассой. Не создавайте новую трассу для каждой внутренней мысли или смены состояния.
Учёт токенов и стоимости
Берите usage из ответа провайдера, когда оно доступно. Входные, выходные, кэшированные, reasoning-токены, пакетная обработка, изображения и единицы инструментов могут иметь разные ставки. Руководство OpenAI по токенам отмечает, что токенизация зависит от модели и языка, а usage в ответе служит основой для завершённого запроса. Локальный токенизатор полезен для предварительного лимита, но не заменяет usage провайдера при сверке счёта.
Для каждой попытки модели храните провайдера, запрошенную и фактическую модель, категорию токена, количество, валюту, версию таблицы цен и внутренний центр затрат. Считайте стоимость после ответа:
cost = input_billable_tokens * input_price
+ cached_input_tokens * cached_input_price
+ output_billable_tokens * output_price
+ provider_units * unit_price
Цены должны быть конфигурацией с датой действия, а не константой экспортера трассировок. Сохраняйте исходное количество и вычисленную сумму, чтобы исправление прайса можно было проверить. Складывайте все попытки: неуспешный вызов тоже может потребить токены. Отложенную или оценочную сумму помечайте как предварительную и сверяйте с отчётом провайдера.
Используйте измерения, на которые владелец может повлиять: рабочий процесс, семейство модели, окружение, класс арендатора и итог. Не добавляйте ID пользователя, промпт или аргументы инструмента в метки метрик. Дашборд затрат должен показывать сумму, стоимость завершённого запуска, токены на запуск, долю повторов и долю дорогих моделей. Рост выходных токенов при обычной задержке может означать неограниченный ответ, цикл или изменение промпта.
Распределение задержки: p50, p95 и p99
Среднее скрывает хвост, который видит пользователь при ожидании очереди или медленного инструмента. Записывайте длительность корневого запуска и важных spans как гистограмму в секундах. Измеряйте time to first token, интервал между потоковыми частями, если он влияет на восприятие, и полное время ответа. Разделяйте обработку сервера, очередь и сеть, когда провайдер сообщает эти значения.
p50 показывает типичный случай, p95 описывает медленного пользователя, а p99 выявляет редкий тяжёлый хвост. Это квантили распределения, а не три средних. Подберите границы гистограммы вокруг реального SLO, от долей секунды до минут, и не смешивайте единицы. Функция Prometheus histogram_quantile оценивает квантиль по корзинам. Для классической гистограммы сначала агрегируйте по le.
histogram_quantile(
0.95,
sum by (le, workflow) (
rate(agent_run_duration_seconds_bucket[10m])
)
)
Не помещайте trace ID в метки. Разбивайте задержку по небольшому набору контролируемых измерений: рабочий процесс, семейство модели, регион и итог. Выбранная трасса объяснит, почему сдвинулся p95. Не складывайте длительности параллельных дочерних spans: используйте критический путь.
Ошибки, повторы и лимиты
Определите классификацию ошибок до создания оповещений. Разделяйте валидацию клиента, запрет политики, аутентификацию, лимит, тайм-аут, ошибку upstream, неверный ответ модели, сбой инструмента и отмену. Коды провайдера переводите в эти стабильные классы, сохраняя ограниченный исходный код. Ожидаемую отмену пользователем отмечайте отдельно от сбоя сервера.
У каждого повтора должны быть причина, номер попытки, backoff и итог. Используйте ограниченную экспоненциальную задержку с jitter только там, где это разрешает контракт провайдера. Не повторяйте проверки, авторизацию, решения политики и детерминированные ошибки схемы. Задайте дедлайн всего запуска и каждой попытки. Корневой span сообщает retry_count, attempt_count и итог, а span каждой попытки сохраняет собственный статус. Высокая доля повторов увеличивает стоимость даже при нормальном success rate.
Выбор запасной модели записывайте отдельным событием или span. Дашборд должен отличать запасной вариант из-за лимита, задержки, политики безопасности и проверки возможностей. Считайте отдельно неверные аргументы инструмента и циклы исправления схемы. Если такой цикл разрешён, ограничьте число итераций и выдайте причину loop_limit при достижении границы.
Конфиденциальность, редактирование и sampling
Промпт и данные инструментов могут содержать персональную, конфиденциальную или security-sensitive информацию. Безопасное значение по умолчанию: метаданные, токены, хэши и коды причин без содержания. Если отладке нужны примеры, храните их в отдельном контролируемом хранилище с согласием, коротким сроком хранения, шифрованием, журналом доступа и редактированием полей. Редактируйте данные до отправки, а не после попадания в backend.
Используйте allowlist атрибутов. Удаляйте заголовки авторизации, cookie, ключи API, контактные данные, номера счетов, URL с query-параметрами и текст документов. Хэширование само по себе не делает данные анонимными. Отделяйте отпечаток промпта от промпта и документируйте, кто может выполнить соединение. Проверяйте редактирование фикстурами с реалистичными секретами и персональными данными на разных языках.
Sampling уменьшает объём, но не должен скрывать инциденты. Используйте parent-based sampling для целостности трассы. В Collector можно применить tail sampling, чтобы сохранять ошибки, тайм-ауты, дорогие и медленные трассы, а обычные успешные запускать реже. Спецификация sampling OpenTelemetry различает запись и экспорт, поэтому локальный sampler может не создавать дорогие атрибуты. Метрики не сэмплируйте, а traces используйте для exemplars и расследования.
OpenTelemetry, Grafana и Sentry вместе
Инструментируйте агента API OpenTelemetry и семантическими соглашениями, отправляйте OTLP в OpenTelemetry Collector, а в Collector применяйте batching, лимит памяти, редактирование, sampling и маршрутизацию. Трассы, метрики и журналы отправляйте в соответствующие backends с одинаковыми атрибутами сервиса, версии, окружения, региона и развёртывания. Перед production sampling проверьте в staging одну полную трассу.
Grafana даёт общие эксплуатационные представления. Сделайте панели объёма, success ratio, p50/p95/p99 корневой задержки, time to first token, токенов, стоимости, повторов, длительности инструментов и состояния провайдера. Свяжите панель с поиском трасс и runbook. Документация Grafana по alert rules описывает запросы, условия, период оценки и уведомления. Sentry добавляет группировку проблем, контекст ошибки и просмотр трассы. Его Trace API возвращает spans и ошибки одной трассы. В любой error backend отправляйте только отредактированные данные и явно задавайте sampling.
SLO и оповещения, пригодные для операторов
SLO должен выражать обещание пользователю, а не здоровье Collector. Доступность можно определить как долю запусков без классифицированной ошибки сервера, провайдера или инструмента. Задержку можно определить как долю корневых запусков ниже выбранного порога. Ограничение стоимости отслеживайте отдельным budget SLI.
Выбирайте цели по измеренной базовой линии и требованиям продукта. Разделяйте рабочие процессы с разными ожиданиями. Показывайте error budget на длинном окне и отдельный короткий вид для релизов. Документация Grafana SLO описывает SLI, расход бюджета, fast-burn и slow-burn alerts. Страница нужна только при быстром расходе, на который оператор может повлиять. Медленный тренд лучше отправить задачей.
Практические alerts: устойчивый рост ошибок корневого запуска, быстрый расход бюджета, p95 выше контракта, всплеск rate limit, рост повторов, пропавшие usage records, неожиданный темп стоимости и застрявшая очередь. В alert указывайте рабочий процесс, регион, развёртывание, значение, порог, ссылку на трассу, владельца и runbook. Pending period не даёт одной точке вызвать страницу. Группируйте уведомления по сервису и severity. Если немедленного действия нет, нужна панель, а не alert.
Путь от симптома к причине
Начните с SLO или сообщения пользователя и выберите представительскую трассу. Проверьте дочерние spans корня и передачу контекста через шлюз, очередь и границу инструмента. При пропавших spans сначала исследуйте exporter, sampling и injection контекста.
Для медленных запусков сравните задержку очереди, time to first token, длительность вывода, поиск и критические пути инструментов. Высокий p99 при обычном p50 обычно указывает на хвостовую зависимость, лимит конкуренции или повтор. Сдвиг p50 часто связан с релизом, моделью, промптом или регионом. При всплеске стоимости сгруппируйте данные по фактической модели, категории токена, версии рабочего процесса и попыткам, затем сверяйте usage с отчётом провайдера.
Для ошибки начинайте с первого неуспешного span, а не с исключения-обёртки. Смотрите status, error.type, код провайдера, бюджет тайм-аута и события повторов. Разделяйте отказ инструмента, сбой провайдера, неверный ответ модели и ошибку парсера. Убедитесь, что редактирование сохранило безопасный диагностический код. Ожидаемый блок политики учитывайте как продуктовый исход и оповещайте только при неожиданном изменении доли.
Поддерживайте синтетические запросы с детерминированными несекретными фикстурами. После изменения инструментации, модели, промпта или маршрутизации проверяйте, что traces, metrics, записи токенов и ошибки имеют один correlation ID. Контекст архитектуры есть в материалах production AI agent architecture, AI agent evaluations, prompt injection and MCP security и hybrid RAG with pgvector. Эти темы определяют набор spans и приемлемый SLO, а наблюдаемость остаётся нейтральным слоем доказательств.