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
Sources