RAG с distributed tracing

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: RAG с distributed tracing

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

Научиться внедрять distributed tracing в RAG-систему, чтобы проследить путь каждого запроса от пользователя до LLM и обратно. Связать все компоненты (эмбеддинг, retrieval, промпт, вызов LLM) единым trace_id для быстрой локализации узких мест и ошибок в production-окружении.

Ключевой результат Работоспособный прототип RAG-системы, где каждое обращение пользователя порождает сквозной trace, видимый в Jaeger/UI, с временными метками каждого шага.

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

Что нужноОткуда взять
Рабочая RAG-система (простейшая: query → embed → search → build prompt → LLM → answer)Собственный пет-проект или готовый шаблон (например, на LangChain/LlamaIndex)
Базовый код на Python (FastAPI или Flask) для REST-интерфейсаСтартовый репозиторий (можно из предыдущего pet-проекта)
Docker / Docker ComposeЛокальная установка
OpenTelemetry Python SDKpip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation
Jaeger (или любой OpenTelemetry-совместимый бэкенд)docker run jaegertracing/all-in-one:latest
Пример векторного хранилища (Qdrant / Chroma / FAISS)Локально или через Docker

Если нет реального инструмента — симулируем:

  1. Развернуть Jaeger all-in-one через docker-compose.yml (порт 16686 — UI, 4318 — OTLP gRPC/HTTP).
  2. Собрать минимальную RAG-функцию на Python, которая принимает query, вызывает embedding model (sentence-transformers), ищет похожие чанки, формирует промпт и вызывает любой LLM (локальный через Ollama или OpenAI API).
  3. Убедиться, что код запускается и отвечает на запросы.

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

КомпонентИнструментыНазначение
Backend RAGPython + FastAPI + UvicornREST-сервер, хостящий RAG
Tracing SDKOpenTelemetry Python SDKГенерация и экспорт trace/spans
Tracing backendJaeger (all-in-one)Хранение и визуализация trace
Экспорт трассOTLP (gRPC)Передача трасс из приложения в Jaeger
Векторная БДQdrant / Chroma / FAISSХранилище эмбеддингов
LLMOllama (local) / OpenAI APIГенерация ответа
КонтейнеризацияDocker + Docker ComposeЗапуск всех сервисов одной командой
МониторингJaeger UI (веб-интерфейс)Просмотр trace и waterfall-диаграмм

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

Этап 1: Развёртывание инфраструктуры tracing (30 минут)

Действия

  1. Создать docker-compose.yml:

    version: '3.8'
    services:
      jaeger:
        image: jaegertracing/all-in-one:latest
        environment:
          - COLLECTOR_OTLP_ENABLED=true
        ports:
          - "16686:16686"   # UI
          - "4318:4318"     # OTLP HTTP
          - "4317:4317"     # OTLP gRPC
        networks:
          - tracing-net
    
      app:
        build: .
        ports:
          - "8000:8000"
        environment:
          - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318
          - OTEL_TRACES_EXPORTER=otlp
          - SERVICE_NAME=rag-service
        depends_on:
          - jaeger
        networks:
          - tracing-net
    
    networks:
      tracing-net:
        driver: bridge
    
  2. Запустить Jaeger отдельно (для тестирования): docker run -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest.

  3. Проверить, что Jaeger UI открывается на http://localhost:16686.

Ожидаемый результат этапа Работающий Jaeger в Docker, доступный по HTTP, с включённым OTLP collector.

Этап 2: Интеграция OpenTelemetry в RAG-код (1.5–2 часа)

Действия

  1. Установить необходимые пакеты:

    pip install opentelemetry-api opentelemetry-sdk \
                opentelemetry-instrumentation-flask \
                opentelemetry-exporter-otlp-proto-http \
                opentelemetry-instrumentation-requests \
                opentelemetry-instrumentation-openai \
                opentelemetry-instrumentation-qdrant
    
  2. Создать файл tracer_setup.py, инициализирующий TracerProvider и экспорт в Jaeger:

    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
    from opentelemetry.instrumentation.flask import FlaskInstrumentor
    from opentelemetry.instrumentation.requests import RequestsInstrumentor
    from opentelemetry.instrumentation.openai import OpenAIInstrumentor
    from opentelemetry.instrumentation.qdrant import QdrantInstrumentor
    import os
    
    def setup_tracing(app=None):
        provider = TracerProvider(
            resource=Resource.create({"service.name": os.getenv("SERVICE_NAME", "rag-service")})
        )
        exporter = OTLPSpanExporter(
            endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces")
        )
        processor = BatchSpanProcessor(exporter)
        provider.add_span_processor(processor)
        trace.set_tracer_provider(provider)
        
        if app:
            FlaskInstrumentor().instrument_app(app)
        RequestsInstrumentor().instrument()
        OpenAIInstrumentor().instrument()
        QdrantInstrumentor().instrument()
    
  3. Вызвать setup_tracing(app) при старте FastAPI/Flask приложения.

Ожидаемый результат этапа Каждый REST-запрос к RAG автоматически создаёт trace с spans для HTTP входа, вызовов Qdrant, OpenAI/LLM (если есть готовые инструментируемые библиотеки).

Этап 3: Ручная трассировка критических точек (1–1.5 часа)

Действия

  1. Для компонентов, которые не покрыты авто-инструментацией (например, вызов локального LLM через Ollama, кастомный retrieval), добавить ручные spans:

    from opentelemetry import trace
    tracer = trace.get_tracer(__name__)
    
    def retrieve_documents(query: str, top_k: int):
        with tracer.start_as_current_span("retrieve_documents") as span:
            span.set_attribute("query.length", len(query))
            span.set_attribute("top_k", top_k)
            # реальный вызов БД
            docs = vector_store.search(query, top_k)
            span.set_attribute("docs.count", len(docs))
            return docs
    
  2. Добавить span для этапа формирования промпта:

    with tracer.start_as_current_span("build_prompt") as span:
        prompt = template.format(context=docs, query=query)
        span.set_attribute("prompt.length", len(prompt))
    
  3. Добавить span для вызова LLM (если не покрыт автоматически):

    with tracer.start_as_current_span("llm_call") as span:
        answer = ollama.chat(model="llama3", messages=[{"role": "user", "content": prompt}])
        span.set_attribute("response_length", len(answer["message"]["content"]))
    
  4. Привязать пользовательский trace_id к ответу (например, вернуть в HTTP header X-Trace-ID):

    @app.get("/query")
    def query_handler(q: str):
        current_span = trace.get_current_span()
        trace_id = format(current_span.get_span_context().trace_id, '032x')
        answer = rag_pipeline(q)
        return {"answer": answer, "trace_id": trace_id}
    

Ожидаемый результат этапа Полный trace, отображающий все стадии RAG-процесса с именованными spans и атрибутами.

Этап 4: Верификация сквозного trace (45 минут)

Действия

  1. Отправить тестовый запрос к RAG-сервису:

    curl http://localhost:8000/query?q="Какие преимущества у distributed tracing?"
    
  2. Из ответа получить trace_id.

  3. Открыть Jaeger UI (http://localhost:16686), выбрать сервис rag-service, найти trace по trace_id или времени.

  4. Убедиться, что в waterfall-диаграмме видны spans:

    • HTTP GET /query
    • retrieve_documents
    • build_prompt
    • llm_call
    • (возможно) DB query (если есть инструментирование Qdrant)
  5. Проверить, что время каждого span реалистично, а атрибуты (длина промпта, количество документов) логируются.

  6. Ввести намеренную задержку (например, time.sleep(2) в retrieval) и убедиться, что она отображается в trace.

Ожидаемый результат этапа Подтверждённый сквозной trace с корректным временем и атрибутами на всех этапах.

Этап 5: Написание скрипта для автоматической проверки trace (1 час)

Действия

  1. Создать скрипт check_trace.py, который:

    • Отправляет запрос к RAG;
    • Извлекает trace_id;
    • Через Jaeger API (http://localhost:16686/api/traces/{trace_id}) получает trace;
    • Проверяет наличие обязательных spans (retrieve, llm, build_prompt);
    • Выводит сводку: длительность trace, количество spans, наличие ошибок.
  2. Пример реализации:

    import requests, json, sys
    
    host = "http://localhost:8000"
    jaeger_api = "http://localhost:16686/api/traces"
    
    resp = requests.get(f"{host}/query?q=test")
    trace_id = resp.json()["trace_id"]
    trace_resp = requests.get(f"{jaeger_api}/{trace_id}")
    if trace_resp.status_code != 200:
        print(f"Trace {trace_id} not found")
        sys.exit(1)
    trace = trace_resp.json()
    spans = trace["data"][0]["spans"]
    span_names = {s["operationName"] for s in spans}
    required = {"POST /query", "retrieve_documents", "llm_call"}
    missing = required - span_names
    if missing:
        print(f"Missing spans: {missing}")
        sys.exit(1)
    print(f"Trace {trace_id}: {len(spans)} spans, duration={trace['data'][0]['startTime']} ns")
    
  3. Запустить скрипт несколько раз с разными запросами.

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

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

  • При запуске Jaeger и RAG-сервиса в Docker Compose все контейнеры стартуют без ошибок.
  • На каждый HTTP-запрос к RAG генерируется уникальный trace_id, возвращаемый в ответе.
  • В Jaeger UI отображается хотя бы один полный trace со spans: GET /query, retrieve_documents, build_prompt, llm_call.
  • Каждый span содержит атрибуты, указанные в коде (длина запроса, количество документов и т.п.).
  • При искусственной задержке в любом компоненте задержка видна на соответствующем span.
  • Скрипт check_trace.py успешно выполняется и находит все обязательные spans.
  • Код интегрирован с OpenTelemetry без ручной настройки ресурсов (Resource создаётся программно).
  • В README проекта описаны шаги для развёртывания и проверки.

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

Основной артефакт Репозиторий с кодом RAG-приложения, docker-compose.yml и tracer_setup.py.

Содержание

  • FastAPI-приложение, принимающее GET /query?q=...;
  • Полная интеграция OpenTelemetry с Jaeger;
  • Ручные spans для всех этапов RAG-пайплайна;
  • Возврат trace_id клиенту;
  • Скрипт check_trace.py для автоматической верификации.

Дополнительные результаты

  • Демонстрация waterfall-диаграммы в Jaeger (скриншот в README);
  • Анализ узких мест (например, какой этап занимает больше всего времени).

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

СложностьРешение
OpenTelemetry SDK не видит переменные окруженияЯвно передать OTEL_EXPORTER_OTLP_ENDPOINT в коде, а не полагаться на стандартные переменные, или проверить имя переменной (без опечаток).
Jaeger не принимает трассы (пустой дашборд)Проверить, что сервис app может достучаться до jaeger:4318 внутри сети Docker. Использовать curl из контейнера. Убедиться, что выставлен COLLECTOR_OTLP_ENABLED=true.
Автоматическая инструментация OpenTelemetry не захватывает кастомные библиотекиДобавить ручные spans для всех критических функций, где автоинструментация не применяется (Ollama, кастомный поиск).
Трассы теряются при высоком RPSВключить BatchSpanProcessor с разумным таймаутом (по умолчанию 5 секунд). Для пет-проекта достаточно.
Сложность отладки ошибок в tracing-кодеВременно отключать отправку трасс, проверять spans на уровне print() до интеграции с бэкендом.

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

ЭтапВремя (часы)
Этап 1: Развёртывание инфраструктуры tracing0.5
Этап 2: Интеграция OpenTelemetry в RAG2.0
Этап 3: Ручная трассировка критических точек1.5
Этап 4: Верификация сквозного trace0.75
Этап 5: Скрипт автоматической проверки1.0
Итого5.75 часов

Примечание для первого раза: Рекомендуется заложить до 8 часов с учётом изучения документации и возможных проблем с Docker/Jaeger.

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

ВопросТема
101Основы OpenTelemetry
245Observability в RAG-системах
312Настройка Jaeger для микросервисов
401Мониторинг вызовов LLM
501Трассировка запросов в FastAPI
603Экспорт трасс в OTLP
708Продвинутые атрибуты и семантические соглашения
819Поиск и фильтрация trace в Jaeger
888Интеграция OpenTelemetry с Qdrant

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

  • Я развернул Jaeger и убедился, что его UI открывается.
  • Я инициализировал TracerProvider и экспортёр в коде приложения до обработки любого запроса.
  • Я добавил хотя бы один ручной span для каждого этапа RAG (retrieve, prompt build, LLM).
  • Я убедился, что trace_id возвращается клиенту и отображается в Jaeger.
  • Я протестировал скрипт check_trace.py и он проходит без ошибок.
  • Я проверил, что при задержке в любом этапе waterfall-диаграмма корректно показывает увеличение времени.
  • Я удалил отладочные print и использовал атрибуты spans для логирования.
  • В README описана команда docker-compose up и пример запроса для проверки.