Интегрировать OpenTelemetry в RAG

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Интегрировать OpenTelemetry в RAG

1. Цель задачи

Освоить инструментирование RAG-пайплайна с помощью OpenTelemetry (OTel) для трассировки ключевых шагов: retrieval, reranking, генерация ответа. Реализовать сбор трейсов, экспорт их в Jaeger и визуализацию полной цепочки вызовов, чтобы на практике убедиться в прозрачности и диагностируемости системы.

Ключевой результат В Jaeger отображается целостный trace с тремя вложенными span’ами (retrieve, rerank, generate), по каждому из которых видны длительность, атрибуты (число найденных документов, модель reranker, модель LLM и т.д.) и корректная вложенность.


2. Исходные данные

Перед началом необходимо иметь:

Что нужноОткуда взять
RAG-система (минимальная)Пет-проект из предыдущих занятий или любой open‑source пример (LangChain RAG, LlamaIndex RAG)
OpenTelemetry SDK для Pythonpip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp
JaegerDocker)docker run -d -p 16686:16686 -p 4317:4317 jaegertracing/all-in-one:latest
Пример запросов и документовЛюбая коллекция текстов (например, статьи Wikipedia или датасет SQuAD)

Если нет реальной RAG-системы — симулируем:

  1. Напишите простую RAG-функцию, которая:
    • использует sentence-transformers для эмбеддингов,
    • faiss (или in-memory list) для поиска,
    • заглушку для reranker (например, sorted()[:k] по score),
    • любой LLM (OpenAI API, или dummy-функцию, возвращающую "Generated answer").
  2. Заверните три шага в отдельные функции retrieve(query), rerank(docs), generate(context, query).

3. Технологический стек

КомпонентИнструментыНазначение
ТрассировкаOpenTelemetry SDK (Python)Создание и управление спанами
ЭкспортOTLP gRPC exporterОтправка трейсов в Jaeger
Бэкенд трассировкиJaeger (all-in-one)Приём, хранение, визуализация трейсов
RAG пайплайнLangChain / LlamaIndex / собственный кодИсполнение retrieve → rerank → generate
Векторный поискFAISS / Qdrant (in-memory)Поиск релевантных документов
LLMOpenAI / Anthropic / mock-функцияГенерация ответа
Контейнеризация (опционально)DockerЗапуск Jaeger

4. Этапы выполнения

Этап 1: Подготовка окружения (15 минут)

Действия

  1. Установите необходимые пакеты:
    pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp
    pip install opentelemetry-instrumentation-openai  # если используете OpenAI
    
  2. Запустите Jaeger в Docker:
    docker run -d --name jaeger \
      -p 16686:16686 \
      -p 4317:4317 \
      jaegertracing/all-in-one:latest
    
  3. Убедитесь, что веб-интерфейс Jaeger доступен: http://localhost:16686.

Ожидаемый результат этапа Jaeger запущен, стартовый экран виден в браузере.

Этап 2: Инициализация OpenTelemetry Tracer Provider (20 минут)

Действия

  1. Создайте файл tracing.py с конфигурацией OTel:
    from opentelemetry import trace
    from opentelemetry.sdk.trace import TracerProvider
    from opentelemetry.sdk.trace.export import BatchSpanProcessor
    from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
    
    provider = TracerProvider()
    processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="localhost:4317", insecure=True))
    provider.add_span_processor(processor)
    trace.set_tracer_provider(provider)
    
    tracer = trace.get_tracer(__name__)
    
  2. В главном файле rag.py импортируйте tracer:
    from tracing import tracer
    

Ожидаемый результат этапа Код инициализации трассировки готов, программа не зависает при запуске.

Этап 3: Инструментирование retrieval (20 минут)

Действия

  1. Создайте функцию retrieve(query, top_k=5).
  2. Внутри функции создайте span:
    with tracer.start_as_current_span("retrieve") as span:
        # логика поиска документов
        docs = vector_store.search(query, top_k)
        span.set_attribute("db.system", "faiss")
        span.set_attribute("top_k", top_k)
        span.set_attribute("num_docs_retrieved", len(docs))
    
  3. Добавьте обработку ошибок: при исключении запишите span.set_status(Status(StatusCode.ERROR)).

Ожидаемый результат этапа При вызове retrieve() в Jaeger появляется span с именем retrieve и заданными атрибутами.

Этап 4: Инструментирование rerank и generate (25 минут)

Действия

  1. Аналогично создайте span для rerank:
    with tracer.start_as_current_span("rerank") as span:
        reranked = reranker.rerank(docs, query)
        span.set_attribute("model", "cross-encoder/ms-marco-MiniLM-L2-v2")
        span.set_attribute("num_input_docs", len(docs))
        span.set_attribute("num_output_docs", len(reranked))
    
  2. Для generate:
    with tracer.start_as_current_span("generate") as span:
        answer = llm.generate(context, query)
        span.set_attribute("llm.model", "gpt-4")
        span.set_attribute("input_tokens", count_tokens(context))
        span.set_attribute("output_tokens", count_tokens(answer))
    
  3. Убедитесь, что все три span’а вложены в родительский span (например, rag_query):
    with tracer.start_as_current_span("rag_query") as root_span:
        docs = retrieve(query)
        reranked = rerank(docs)
        answer = generate(reranked, query)
    

Ожидаемый результат этапа В Jaeger виден корневой span rag_query с тремя дочерними span’ами, каждый — со своими атрибутами.

Этап 5: Верификация и отладка (20 минут)

Действия

  1. Выполните несколько разных запросов к RAG-системе (минимум 3).
  2. Откройте Jaeger UI → Search → сервис your-service-name (по умолчанию __main__).
  3. Найдите трейсы, убедитесь, что:
    • каждый запрос отображается как отдельный trace,
    • в каждом trace три вложенных span’а,
    • тайминги логичны (retrieve < rerank < generate).
  4. Искусственно сломайте один из шагов (например, выключите векторную БД) и проверьте, что span помечается как ERROR.

Ожидаемый результат этапа Все трейсы отображаются корректно, ошибки видны.


5. Критерии приемки (Definition of Done)

  • В Jaeger UI есть хотя бы один trace, у которого корневой span называется rag_query.
  • Внутри корневого span присутствуют ровно три дочерних span’а: retrieve, rerank, generate.
  • Каждый span содержит как минимум два кастомных атрибута (например, num_docs_retrieved, llm.model).
  • Длительность span’ов не является нулевой или отрицательной.
  • При генерации исключения в одном из шагов span помечается статусом ERROR и содержит описание ошибки.
  • Код трассировки вынесен в отдельный модуль tracing.py и переиспользуется.
  • В requirements.txt добавлены все зависимости OpenTelemetry.
  • Для каждого запроса к RAG создаётся отдельный новый trace.

6. Ожидаемый результат

Основной артефакт Папка с файлами:

  • rag.py – основная RAG-логика с интеграцией OTel,
  • tracing.py – инициализация TracerProvider и экспортёра,
  • requirements.txt,
  • скриншот Jaeger UI, показывающий treeview с тремя span’ами.

Дополнительно

  • docker-compose.yml для запуска Jaeger (если используется).
  • Краткая документация (в комментариях к коду), какие атрибуты установлены и зачем.

7. Возможные сложности и их решение

СложностьРешение
Jaeger не видит трейсы (404 в UI)Проверьте, что Jaeger запущен на порту 4317 (gRPC) и экспортёр использует localhost. В Docker Desktop используйте host.docker.internal. Убедитесь, что insecure=True для gRPC.
Span’ы не вложенныеГарантируйте, что дочерние span’ы создаются внутри контекста родительского (используйте with tracer.start_as_current_span внутри такого же with для родителя).
opentelemetry-instrumentation-openai конфликтует с версией OpenAIУстановите конкретную версию: pip install openai==1.0.0 opentelemetry-instrumentation-openai==0.45b0.
Не видны атрибуты в JaegerУбедитесь, что атрибуты строковые или числовые (не списки/дикты). Для списков используйте span.set_attribute("key", str(value)).
Трейсы не отображаются после перезапуска JaegerJaeger in-memory теряет данные при перезапуске — используйте постоянное хранилище (Elasticsearch) или перезапускайте редко.

8. Бюджет времени (оценка)

ЭтапВремя
1. Подготовка окружения15 мин
2. Инициализация OTel20 мин
3. Инструментирование retrieval20 мин
4. Инструментирование rerank и generate25 мин
5. Верификация и отладка20 мин
Итого1 ч 40 мин

Примечание Время рассчитано на один подход без учёта дополнительного изучения документации. При первом выполнении заложите ещё 30–60 минут на чтение ошибок и настройку экспортёра.


9. Связанные вопросы из базы знаний

ВопросТема
42Как установить OpenTelemetry Python SDK
43Настройка экспортёра OTLP gRPC
44Разница между BatchSpanProcessor и SimpleSpanProcessor
45Как запустить Jaeger в Docker
46Создание вложенных спанов с помощью start_as_current_span
47Установка атрибутов и статусов спанов
48Обработка ошибок в OTel (RecordException)
49Интеграция OTel с LangChain (instrumentation)
50Визуализация трейсов в Jaeger UI
51Отладка: трейсы не отображаются

10. Чек-лист самопроверки

  • Я запустил Jaeger перед выполнением кода и проверил, что UI отвечает.
  • Я создал отдельный модуль tracing.py с TracerProvider и BatchSpanProcessor.
  • Я импортировал tracer в основной скрипт и использовал его для каждого span’а.
  • Все три основные функции (retrieve, rerank, generate) обёрнуты в span’ы.
  • Я добавил хотя бы два атрибута в каждый span (например, количество документов, модель LLM).
  • Я выполнил минимум 3 запроса и убедился, что в Jaeger появилось 3 отдельных трейса.
  • Я проверил, что при ошибке (например, выключение БД) span помечается красным в Jaeger.
  • Я зафиксировал скриншот UI с видимой вложенностью span’ов.
  • Я добавил все зависимости в requirements.txt.