← LLMOps and agent observability: debugging the black box
02 / 07 · 55 min

Instrumentation: OpenTelemetry, LangSmith, Langfuse

Three instrumentation paths — auto-instrumentation, SDK decorators, and the manual OTel API: the SDK → OTLP → backend pipeline, @observe in Langfuse, @traceable in LangSmith, and picking a stack for your project.

Lesson content is currently available in Russian.

What you will learn

  • →Выбирать между авто-инструментацией, декораторами SDK и ручным OTel API по трейд-оффам.
  • →Инструментировать агента с Langfuse или LangSmith: дерево спанов, usage, user_id и session_id.
  • →Понимать OTel-пайплайн SDK → processor → OTLP exporter → бэкенд и роль batch-процессора.

Три пути инструментирования

Инструментирование добавляется тремя способами, и выбор — это трейд-офф между усилием и контролем.

Авто-инструментация — обёртки над клиентами и фреймворками: langfuse.openai как drop-in замена клиента OpenAI, wrap_openai у LangSmith, библиотеки инструментаторов OpenInference (Arize) и OpenLLMetry (Traceloop), которые патчат популярные SDK. Ноль изменений в бизнес-коде: каждый вызов модели автоматически становится generation-спаном с моделью, usage и латентностью. Слабость — она видит только вызовы библиотек: ваши собственные шаги (парсинг, роутинг, пост-обработка) остаются невидимыми, и трейс превращается в плоский список генераций без бизнес-структуры.

Декораторы SDK — @observe() у Langfuse, @traceable у LangSmith: вешаете декоратор на функции, и дерево спанов строится по стеку вызовов — какая функция кого вызвала, с какими аргументами и результатом. Минимум кода, максимум структуры; основной рабочий вариант для большинства команд.

Ручной OTel API — сами создаёте спаны и проставляете атрибуты. Полный контроль и вендор-независимость: трейсы уезжают в любой OTLP-совместимый бэкенд, от Jaeger до Langfuse. Цена — заметно больше кода и дисциплины именования.

Практичная стратегия: начать с декораторов плюс обёртка клиента — это час работы; переходить на явный OTel, когда LLM-вызовы вплетены в большую микросервисную систему с уже существующей наблюдаемостью.

OTel-пайплайн своими руками

Путь спана от кода до дашборда: SDK создаёт спаны → SpanProcessor буферизует → OTLP exporter отправляет по протоколу OTLP → опциональный Collector маршрутизирует → бэкенд хранит и рисует. В проде используется BatchSpanProcessor: спаны копятся в буфере и уходят пачками в фоне, не добавляя латентность запросу (в отличие от SimpleSpanProcessor, который блокирует на каждом спане).

Минимальный generation-спан руками:

from opentelemetry import trace

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

def ask_llm(client, messages):
    with tracer.start_as_current_span("chat gpt-4o-mini") as span:
        span.set_attribute("gen_ai.operation.name", "chat")
        span.set_attribute("gen_ai.request.model", "gpt-4o-mini")
        resp = client.chat.completions.create(
            model="gpt-4o-mini", messages=messages
        )
        span.set_attribute("gen_ai.response.model", resp.model)
        span.set_attribute("gen_ai.usage.input_tokens", resp.usage.prompt_tokens)
        span.set_attribute("gen_ai.usage.output_tokens", resp.usage.completion_tokens)
        span.set_attribute("gen_ai.response.finish_reasons", [resp.choices[0].finish_reason])
        return resp.choices[0].message.content

Контекст-менеджер start_as_current_span кладёт спан в контекст, поэтому всё, что вызовется внутри, автоматически станет его потомком. Куда уедут спаны, решает конфигурация экспортёра: Langfuse, Phoenix и Grafana-стек принимают OTLP напрямую, так что один и тот же код работает с разными бэкендами — меняется только endpoint и заголовки авторизации.

Langfuse: декораторы и drop-in клиент

Langfuse — open-source платформа (self-host через docker compose или облако), и её питоновский SDK закрывает оба нижних слоя сразу. Декоратор строит дерево, drop-in клиент наполняет generation-спаны:

from langfuse import observe, get_client
from langfuse.openai import OpenAI  # drop-in вместо openai.OpenAI

client = OpenAI()

@observe()
def search_kb(query: str) -> list[str]:
    return vector_store.search(query, top_k=5)

@observe()
def answer(question: str, user_id: str, session_id: str) -> str:
    get_client().update_current_trace(
        user_id=user_id, session_id=session_id, tags=["support"]
    )
    docs = search_kb(question)
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=build_messages(question, docs),
    )
    return resp.choices[0].message.content

Что получается без дополнительных усилий: answer — корневой спан трейса, search_kb — дочерний спан с аргументами и результатом, вызов через langfuse.openai — generation с моделью, промптом, ответом, usage и посчитанной стоимостью. update_current_trace пришивает user_id, session_id и теги — по ним потом фильтруются трейсы и считаются затраты на пользователя.

Важная деталь продакшена: SDK шлёт данные асинхронно пачками, но при завершении короткоживущего процесса (serverless, CLI-скрипт) буфер надо сбросить явно — get_client().flush(), иначе последние трейсы молча потеряются.

LangSmith и выбор стека

LangSmith включается двумя переменными окружения — LANGSMITH_TRACING=true и LANGSMITH_API_KEY — после чего весь код на LangChain/LangGraph трейсится автоматически, без единой строчки. Для кода вне фреймворка — @traceable на функции и wrap_openai(client) для клиента OpenAI: получается то же дерево run-ов с типами llm/tool/chain, поиском по проектам и фильтрами.

Как выбирать стек:

  • LangSmith — если вы уже на LangChain/LangGraph: нулевая стоимость подключения, зрелые датасеты и эвалы. Managed-сервис; self-host — только в enterprise-плане.
  • Langfuse — open source, self-host под контролем PII, принимает и свои SDK, и OTLP; сильная связка трейсы + scores + датасеты + prompt management.
  • Arize Phoenix — OTel-native и удобен локально: поднимается одной командой, отлично подходит для отладки на этапе разработки.
  • Чистый OTel + Grafana/Datadog — когда LLM-вызовы должны жить в общей наблюдаемости компании рядом с трейсами микросервисов.

Ключевой критерий на годы вперёд — совместимость с OTLP: если инструментирование говорит на OpenTelemetry, бэкенд можно сменить конфигом, без переписывания кода. Второй критерий — политика данных: если промпты нельзя отдавать наружу, остаются self-host Langfuse, Phoenix или собственный стек. И в любом случае инструментирование должно жить в коде приложения, а не в вендорском lock-in.

Practice

Агент под наблюдением

Возьмите мини-агента (один LLM-вызов плюс один инструмент, например поиск по документам) и инструментируйте его Langfuse или LangSmith: декоратор на функции агента и инструмента, обёртка клиента для generation-спанов, user_id и session_id на трейсе. Прогоните пять запросов и изучите трейсы в UI.

Deliverable

Ссылка или скриншот трейса с деревом минимум из трёх спанов (корень, инструмент, generation), где у generation заполнены модель, input/output tokens и латентность, а у трейса — user_id и session_id.

Checkpoint

Обоснуйте выбор способа инструментирования и стека наблюдаемости для своего текущего проекта.

  • ✓ Сопоставлены авто-инструментация, декораторы и ручной OTel API с их трейд-оффами.
  • ✓ Выбор бэкенда обоснован через политику данных (self-host/PII), фреймворк и OTLP-совместимость.
  • ✓ Описан путь спана: SDK → BatchSpanProcessor → OTLP exporter → бэкенд и зачем нужен batch.

Check yourself

Чем плоха «чистая» авто-инструментация без декораторов?

01Чем плоха «чистая» авто-инструментация без декораторов?

Зачем в проде используют BatchSpanProcessor вместо SimpleSpanProcessor?

02Зачем в проде используют BatchSpanProcessor вместо SimpleSpanProcessor?

Что даёт import OpenAI из langfuse.openai вместо обычного клиента?

03Что даёт import OpenAI из langfuse.openai вместо обычного клиента?

Почему в serverless-функции после работы SDK нужно вызывать flush()?

04Почему в serverless-функции после работы SDK нужно вызывать flush()?

Какой критерий сильнее всего защищает от vendor lock-in в наблюдаемости?

05Какой критерий сильнее всего защищает от vendor lock-in в наблюдаемости?

Sources

Instrumentation: OpenTelemetry, LangSmith, Langfuse · LLMOps and agent observability: debugging the black box · VORCL