ADG Оставить заявку
Блог Автоматизация 5 мин чтения

Мониторинг LLM-агента через OTel GenAI: как мы наконец увидели, что происходит внутри

OpenTelemetry GenAI Semantic Conventions вышли в GA. Рассказываем, как подключили их к LLM-агенту хелпдеска и что теперь видим в Grafana без переписывания агента.

Контекст момента

OpenTelemetry GenAI Semantic Conventions достигли GA - стандарт для трассировки LLM-агентов на базе OTel

Год назад мы запустили LLM-агента на IT-хелпдеске у одного клиента. С тех пор агент работает, тикеты закрывает, клиент доволен. Но был один стойкий дискомфорт: мы видели, что агент что-то делает, но не понимали, что именно и сколько это стоит. Логи были, метрики - нет. Точнее, были самописные счётчики, которые мы воткнули в нескольких местах, и они врали немного.

В конце прошлого года OpenTelemetry GenAI Semantic Conventions вышли в GA. Это не революция - работа над ними шла давно, и часть инструментария уже была доступна в preview. Но GA означает стабильный контракт: атрибуты span-ов не будут переименовываться каждые два месяца, можно строить dashboard-ы без страха, что завтра всё сломается.

Мы потратили несколько дней, подключили это к агенту, и теперь у нас есть Grafana с нормальными цифрами. Рассказываем, как это выглядит.

Что такое GenAI Semantic Conventions и зачем они нужны

OTel GenAI Semantic Conventions - это набор стандартных атрибутов для спанов, связанных с вызовами LLM. Идея простая: вместо того чтобы каждая команда придумывала свои метрики (llm_tokens_used, ai_response_time, model_call_latency - у всех по-разному), есть единый словарь.

Атрибуты вроде gen_ai.system, gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens теперь фиксированы. Это значит, что любой инструмент, который понимает OTel, сможет читать ваши трейсы без дополнительного маппинга. Grafana, Jaeger, любой backend - им без разницы, какую именно LLM вы используете.

Для нас важно было ещё одно: GenAI Conventions охватывают не только вызовы модели, но и tool-call-ы. Когда агент вызывает инструмент (поиск по базе знаний, запрос к API), это тоже попадает в трейс как отдельный спан с понятными атрибутами. Раньше мы видели итоговое время ответа и не понимали, где агент тратит его: в LLM или в поиске.

Как подключили без переписывания агента

Наш агент написан на Python, использует самописный orchestrator поверх GigaChat API. Переписывать его не хотелось - он стабильно работает, трогать лишний раз не надо.

Хорошая новость: для Python есть opentelemetry-instrumentation-langchain и похожие пакеты, но у нас не LangChain. Поэтому пошли через opentelemetry-sdk напрямую и добавили инструментацию вручную - это заняло один вечер.

Минимальный стек:

  • opentelemetry-sdk - базовый SDK для трейсов.
  • opentelemetry-exporter-otlp-proto-http - экспортер в OTLP/HTTP, отдаёт данные в OpenTelemetry Collector.
  • OTel Collector в режиме sidecar - принимает трейсы, добавляет общие атрибуты окружения, пробрасывает дальше.
  • Tempo как бекенд для трейсов.
  • Grafana - дашборды поверх Tempo и Prometheus.

Сам код инструментации небольшой. В каждом месте, где агент вызывает LLM, оборачиваем вызов в спан:

from opentelemetry import trace
from opentelemetry.semconv.attributes import gen_ai_attributes

tracer = trace.get_tracer("helpdesk-agent")

with tracer.start_as_current_span("gen_ai.chat") as span:
    span.set_attribute(gen_ai_attributes.GEN_AI_SYSTEM, "gigachat")
    span.set_attribute(gen_ai_attributes.GEN_AI_REQUEST_MODEL, model_name)

    response = llm_client.chat(messages=messages)

    span.set_attribute(gen_ai_attributes.GEN_AI_USAGE_INPUT_TOKENS,
                       response.usage.prompt_tokens)
    span.set_attribute(gen_ai_attributes.GEN_AI_USAGE_OUTPUT_TOKENS,
                       response.usage.completion_tokens)

Tool-call-ы аналогично: каждый вызов инструмента - отдельный дочерний спан с именем gen_ai.execute_tool и атрибутом gen_ai.tool.name. В итоге весь цикл обработки тикета - один корневой трейс с деревом спанов.

Что теперь видим

Дашборд в Grafana выглядит неожиданно информативно для той работы, которую мы вложили.

Latency по компонентам. Сразу стало видно, что при длинных тикетах, где агент дважды ходит в LLM (сначала на поиск запроса, потом на генерацию ответа), второй вызов заметно длиннее первого. Мы знали, что так должно быть, но не видели разницу. Теперь видим конкретные перцентили.

Стоимость токенов. Метрика gen_ai.usage.input_tokens + gen_ai.usage.output_tokens в разрезе по типу запроса дала неожиданный результат: категория тикетов «объясни процедуру» потребляет вдвое больше токенов на генерацию, чем «не работает X». Это логично - там длиннее контекст из базы знаний - но видеть это как факт, а не предположение, полезно.

Tool-call latency. Поиск по векторной базе занимает значимую долю в общем времени ответа. Мы думали, что LLM - основной bottleneck, а оказалось, что на части запросов индекс отвечает медленнее, чем хотелось бы. Теперь это видно, можно разбираться.

Ошибки и retries. Спаны с ошибкой стали видны в трейсах без ковыряния в логах. GigaChat API иногда возвращает 429, агент делает retry - теперь это отдельный спан в трейсе, а не строчка в логе, которую нужно искать.

Что не сработало сразу

Пара грабель, которые могут пригодиться.

Атрибут gen_ai.usage.input_tokens в Python SDK живёт в opentelemetry.semconv.attributes - не в _incubating. Если импортировать из _incubating, код упадёт с невнятной ошибкой при обновлённом SDK.

OTel Collector по умолчанию не передаёт gen_ai.* атрибуты дальше без явной настройки allowlist в attributes процессоре. Потратили полчаса, пока разобрались - атрибуты доходили до Collector, но дальше не шли.

Tempo нужно настроить на хранение трейсов с достаточным TTL. По умолчанию там 24 часа, а для анализа паттернов нужно хотя бы несколько дней. Мелочь, но неприятно обнаруживать это, когда хочется посмотреть динамику за неделю.

Где это пригодится дальше

Мониторинг - не самоцель. Конкретная ценность, которую мы уже видим: понимаем, где агент тормозит, и можем обосновать, куда смотреть в первую очередь. Это разница между «что-то медленно, не знаем почему» и «вот спан, вот latency, вот запрос».

Токенная аналитика полезна для другого: можно осознанно работать с контекстом - какие части запроса в базу знаний добавляют токены без реальной пользы для ответа.

Стек получился небольшой и без вендорного lock-in. Если клиент завтра скажет «хотим другой LLM-провайдер» - трейсы переедут вместе с агентом, Grafana останется та же. Это, наверное, и есть главная причина, зачем OTel GenAI нужен именно как стандарт, а не как ещё один vendor SDK.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.