Проблема: привязка к платформе

Два прошлых урока дали важную вещь — трейсы. Но оба способа вендор-специфичны: @traceable знает только про LangSmith, @observe — только про Langfuse. Пока инструмент один, это ок. В реальном проде — нет:

  • Смена платформы = переписать код. Решил уйти с LangSmith на Langfuse — меняешь все декораторы и SDK по всему проекту.
  • Агент — часть большой системы. Вокруг LLM есть API, очереди, БД, другие микросервисы, у которых уже есть телеметрия — в Jaeger, Grafana, Datadog. LLM-трейсы в отдельной песочнице разрывают общую картину запроса.
  • Несколько бэкендов сразу. Хочется LLM-специфику в Langfuse, а инфраструктурные метрики — в Grafana. С вендорным SDK так не сделать.

Нужен единый стандарт инструментирования, отвязанный от того, кто в итоге хранит и показывает данные. Ровно эту проблему в мире бэкенда решил OpenTelemetry — и теперь он же стал стандартом и для LLM.

ℹ️ OpenTelemetry — стандарт, а не продукт

OTel (проект CNCF, как Kubernetes) — это спецификация + SDK + протокол для сбора телеметрии: трейсов, метрик, логов. Он не хранит и не показывает данные — он их собирает и отправляет в любой совместимый бэкенд. Аналогия: OTel — это USB-C для телеметрии. Один разъём (стандарт) — любые устройства (бэкенды) на другом конце. Инструментируешь раз — подключаешь что угодно.

Модель OTel: traces, spans, context

Хорошая новость: концептуально OTel — это тот же трейс из прошлых уроков, только стандартизованный и с точными терминами. LangSmith/Langfuse под капотом по сути повторяют эту модель.

  • Span — единица работы с началом, концом, статусом и атрибутами. Это «run» из LangSmith на языке стандарта: один LLM-вызов, один tool-вызов, один шаг.
  • Trace — дерево связанных spans одного запроса. У каждого span есть trace_id (общий на весь запрос) и span_id, а вложенность задаётся ссылкой на родителя.
  • Attributes — пары ключ-значение на span (модель, число токенов, имя инструмента). Именно здесь живёт LLM-специфика.
  • Context propagation — механизм, передающий trace_id между функциями и через границы сервисов, чтобы дерево не разрывалось.
  • Exporter / OTLP — как spans уходят наружу. OTLP (OpenTelemetry Protocol) — стандартный провод до бэкенда или Collector.
100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
Твой агент — инструментирован ОДИН раз по OTel span: agent.run (trace_id=abc) span: llm.chat · attrs: model, tokens span: tool.web_search · attrs: name context propagation: trace_id общий OTLP OTel Collector приём · обработка маршрутизация Langfuse (LLM-трейсы) Jaeger / Grafana Datadog / свой бэкенд Инструментируй раз → отправляй куда угодно (сменить бэкенд = сменить exporter, не код) GenAI semantic conventions — общий словарь атрибутов для LLM gen_ai.system = "openai" gen_ai.request.model = "gpt-4o" gen_ai.usage.input_tokens = 1120 gen_ai.usage.output_tokens = 340 стандартные имена → любой бэкенд поймёт токены, стоимость, модель

GenAI semantic conventions

Span сам по себе — просто «отрезок времени с атрибутами». Чтобы бэкенд понял, что это LLM-вызов, а не запрос к БД, атрибуты должны называться единообразно. Для этого в OTel есть semantic conventions — согласованный словарь имён. У GenAI-части свои ключи под LLM:

  • gen_ai.system — провайдер (openai, anthropic…).
  • gen_ai.request.model / gen_ai.response.model — модель.
  • gen_ai.usage.input_tokens / output_tokens — токены (отсюда бэкенд считает стоимость).
  • gen_ai.operation.name — тип операции (chat, embeddings…).

Смысл в том, что любой OTel-совместимый бэкенд понимает эти имена одинаково. Ты пишешь gen_ai.usage.input_tokens — и Langfuse, и кастомный дашборд в Grafana видят токены без всякой подгонки.

⚠️ GenAI-конвенции ещё развиваются

Semantic conventions для GenAI в OTel относительно молодые и активно дорабатываются — часть ключей может иметь статус experimental и меняться между версиями. Держи в голове принцип (стандартные имена атрибутов под LLM: система, модель, токены, операция), а точные ключи и их стабильность сверяй с актуальной спецификацией OTel своей версии, а не заучивай.

Инструментирование агента

На практике почти всё делают авто-инструментирующие библиотеки (например, OpenLLMetry/Traceloop, OpenInference и др.): подключаешь пакет — и вызовы OpenAI/Anthropic/LangChain автоматически превращаются в spans с правильными GenAI-атрибутами. Ручные spans добавляешь только для своей логики.

OTel: настройка экспорта + ручной span (концептуально)
python
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

# 1. Куда отправлять — ТОЛЬКО здесь задаётся бэкенд (сменить = сменить endpoint)
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(
    OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")  # OTLP → Collector/бэкенд
))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("my-agent")

# 2. Ручной span для своей логики (LLM-вызовы обычно инструментируются авто-либой)
def research(topic: str) -> str:
    with tracer.start_as_current_span("agent.research") as span:
        span.set_attribute("agent.topic", topic)          # свои атрибуты
        docs = search_web(topic)      # если инструментирован — вложится в этот span
        result = summarize(docs)
        span.set_attribute("gen_ai.usage.output_tokens", count_tokens(result))
        return result

Обрати внимание: имя бэкенда встречается в коде ровно один раз — в endpoint экспортёра. Переехать с одного бэкенда на другой = поменять endpoint (или настройку Collector), а не трогать инструментирование. Это и есть обещанная развязка кода и платформы.

✅ OTel Collector — рубильник маршрутизации

Collector — отдельный процесс между приложением и бэкендами: принимает OTLP, обрабатывает (фильтрует, семплирует, маскирует PII, обогащает) и раздаёт в один или несколько бэкендов. С ним приложение вообще не знает, куда в итоге идут данные, — маршрутизация настраивается в Collector, без передеплоя кода. Хочешь LLM-spans в Langfuse, а всё вместе в Grafana — это конфиг Collector, а не правка агента.

Когда OTel, а когда нативный SDK

OTel мощнее, но и сложнее нативных @traceable/@observe. Выбор зависит от масштаба системы.

СитуацияВыбор
Один инструмент, только LLM-приложение, быстрый стартНативный SDK (LangSmith/Langfuse)
Агент — часть большой системы с существующей телеметриейOpenTelemetry
Возможна смена платформы observability в будущемOpenTelemetry (нет lock-in)
Нужно слать в несколько бэкендов сразуOpenTelemetry + Collector
Строгие требования по PII/маскированию в пайплайнеOpenTelemetry + Collector-обработка
🎯 Это не «или-или»

Langfuse и (частично) LangSmith сами принимают OTLP — значит, можно инструментировать агента по OTel и всё равно смотреть трейсы в привычном LLM-UI. Часто это лучший вариант: стандартный, переносимый способ сбора + удобный специализированный бэкенд для просмотра. OTel не отменяет прошлые уроки — он их обобщает и делает переносимыми.

Типичные ошибки

❌ Ошибка 1: OTel «на всякий случай» для маленького проекта

Если у тебя одно LLM-приложение и один инструмент — OTel это лишняя сложность (Collector, экспортёры, конвенции). Нативный SDK даст результат за минуты. OTel окупается на масштабе и в гетерогенных системах.

❌ Ошибка 2: рвать context propagation

Если между сервисами/потоками не пробросить контекст, дерево распадётся на несвязанные куски и trace_id не сойдётся. При переходе границы сервиса (HTTP, очередь) контекст надо передавать явно.

❌ Ошибка 3: произвольные имена атрибутов

Назовёшь токены my_tokens вместо gen_ai.usage.input_tokens — бэкенды не распознают их как LLM-метрику, дашборды по стоимости не построятся. Следуй semantic conventions.

❌ Ошибка 4: писать промпты/PII в атрибуты без обработки

Полные промпты и ответы в spans удобны, но это PII и объём. Настраивай маскирование/семплирование в Collector, иначе рискуешь приватностью и раздуванием телеметрии.

Шпаргалка

OpenTelemetry для агентов — всё в одном месте
text
OTEL = открытый вендор-НЕЗАВИСИМЫЙ стандарт телеметрии (CNCF): traces+metrics+logs
  «USB-C для observability»: инструментируй РАЗ → шли КУДА УГОДНО
  это стандарт, а не продукт — сам не хранит/не показывает данные

ЗАЧЕМ (боль нативных SDK): смена платформы = переписать код · агент в большой
  системе с готовой телеметрией · нужно слать в несколько бэкендов

МОДЕЛЬ (= трейс из прошлых уроков, но стандартизован):
  span   = единица работы (≈ run): начало/конец/статус/attributes
  trace  = дерево spans (общий trace_id) · вложенность через parent
  attributes = ключ-значение на span (тут LLM-специфика)
  context propagation = проброс trace_id между функциями И сервисами
  OTLP = стандартный протокол экспорта · Exporter = куда слать

GENAI SEMANTIC CONVENTIONS (общий словарь под LLM, статус — развивается):
  gen_ai.system · gen_ai.request.model · gen_ai.usage.input/output_tokens · operation.name
  → любой бэкенд понимает токены/модель/стоимость одинаково

ИНСТРУМЕНТИРОВАНИЕ: авто-либы (OpenLLMetry/OpenInference) + ручные spans для своей логики
  имя бэкенда в коде РОВНО раз (endpoint экспортёра) → переезд = сменить endpoint
  COLLECTOR = рубильник: приём OTLP → обработка (фильтр/семпл/маскировка) → раздача в N бэкендов

ВЫБОР: одно приложение/старт → нативный SDK · большая/гетерогенная система,
  риск смены платформы, много бэкендов, PII-обработка → OTel
  НЕ или-или: Langfuse/LangSmith принимают OTLP → OTel + удобный LLM-UI

ГРАБЛИ: OTel для мелкого проекта · рвать context · свои имена атрибутов · PII без маскировки

Практическое задание

Задание: инструментируй агента по OTel

  1. Подключи авто-инструментирующую OTel-библиотеку (OpenLLMetry/OpenInference) к агенту из модулей 03–05 и настрой OTLP-экспорт. Убедись, что LLM-вызовы стали spans.
  2. Найди в span'ах GenAI-атрибуты (модель, токены). По каким ключам они названы? Сверь с semantic conventions.
  3. Добавь ручной span для своей бизнес-логики (например, agent.research) с парой своих атрибутов. Проверь, что вложенные вызовы попали внутрь него.
  4. Настрой экспорт трейсов в Langfuse через OTLP (Langfuse принимает OTLP). Тот же код — но данные в LLM-UI. Что изменилось в коде при смене бэкенда?
  5. Подними OTel Collector и настрой раздачу трейсов в два бэкенда одновременно (например, Langfuse + Jaeger/консоль). Где настраивается маршрутизация — в коде или в Collector?
  6. Для своего проекта реши по таблице: нативный SDK или OTel? Аргументируй тремя факторами.
  7. Со звёздочкой: добавь в Collector обработку — маскирование содержимого промптов/PII перед отправкой в бэкенд.

Что дальше

Трейсы (LangSmith, Langfuse, OTel) показывают структуру запроса. Но рядом нужен второй сигнал — логи: что именно и в каком виде записывать, чтобы их можно было искать и агрегировать. Следующий урок — про structured logging для агентов.