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 SDK | pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation |
| Jaeger (или любой OpenTelemetry-совместимый бэкенд) | docker run jaegertracing/all-in-one:latest |
| Пример векторного хранилища (Qdrant / Chroma / FAISS) | Локально или через Docker |
Если нет реального инструмента — симулируем:
- Развернуть Jaeger
all-in-oneчерез docker-compose.yml (порт 16686 — UI, 4318 — OTLP gRPC/HTTP). - Собрать минимальную RAG-функцию на Python, которая принимает query, вызывает embedding model (sentence-transformers), ищет похожие чанки, формирует промпт и вызывает любой LLM (локальный через Ollama или OpenAI API).
- Убедиться, что код запускается и отвечает на запросы.
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Backend RAG | Python + FastAPI + Uvicorn | REST-сервер, хостящий RAG |
| Tracing SDK | OpenTelemetry Python SDK | Генерация и экспорт trace/spans |
| Tracing backend | Jaeger (all-in-one) | Хранение и визуализация trace |
| Экспорт трасс | OTLP (gRPC) | Передача трасс из приложения в Jaeger |
| Векторная БД | Qdrant / Chroma / FAISS | Хранилище эмбеддингов |
| LLM | Ollama (local) / OpenAI API | Генерация ответа |
| Контейнеризация | Docker + Docker Compose | Запуск всех сервисов одной командой |
| Мониторинг | Jaeger UI (веб-интерфейс) | Просмотр trace и waterfall-диаграмм |
4. Этапы выполнения
Этап 1: Развёртывание инфраструктуры tracing (30 минут)
Действия
-
Создать 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 -
Запустить Jaeger отдельно (для тестирования): docker run -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest.
-
Проверить, что Jaeger UI открывается на http://localhost:16686.
Ожидаемый результат этапа Работающий Jaeger в Docker, доступный по HTTP, с включённым OTLP collector.
Этап 2: Интеграция OpenTelemetry в RAG-код (1.5–2 часа)
Действия
-
Установить необходимые пакеты:
pip install opentelemetry-api opentelemetry-sdk \ opentelemetry-instrumentation-flask \ opentelemetry-exporter-otlp-proto-http \ opentelemetry-instrumentation-requests \ opentelemetry-instrumentation-openai \ opentelemetry-instrumentation-qdrant -
Создать файл
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() -
Вызвать
setup_tracing(app)при старте FastAPI/Flask приложения.
Ожидаемый результат этапа Каждый REST-запрос к RAG автоматически создаёт trace с spans для HTTP входа, вызовов Qdrant, OpenAI/LLM (если есть готовые инструментируемые библиотеки).
Этап 3: Ручная трассировка критических точек (1–1.5 часа)
Действия
-
Для компонентов, которые не покрыты авто-инструментацией (например, вызов локального 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 -
Добавить 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)) -
Добавить 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"])) -
Привязать пользовательский 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 минут)
Действия
-
Отправить тестовый запрос к RAG-сервису:
curl http://localhost:8000/query?q="Какие преимущества у distributed tracing?" -
Из ответа получить trace_id.
-
Открыть Jaeger UI (http://localhost:16686), выбрать сервис
rag-service, найти trace по trace_id или времени. -
Убедиться, что в waterfall-диаграмме видны spans:
-
Проверить, что время каждого span реалистично, а атрибуты (длина промпта, количество документов) логируются.
-
Ввести намеренную задержку (например,
time.sleep(2)в retrieval) и убедиться, что она отображается в trace.
Ожидаемый результат этапа Подтверждённый сквозной trace с корректным временем и атрибутами на всех этапах.
Этап 5: Написание скрипта для автоматической проверки trace (1 час)
Действия
-
Создать скрипт
check_trace.py, который:- Отправляет запрос к RAG;
- Извлекает
trace_id; - Через Jaeger API (
http://localhost:16686/api/traces/{trace_id}) получает trace; - Проверяет наличие обязательных spans (retrieve, llm, build_prompt);
- Выводит сводку: длительность trace, количество spans, наличие ошибок.
-
Пример реализации:
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") -
Запустить скрипт несколько раз с разными запросами.
Ожидаемый результат этапа Скрипт корректно идентифицирует сквозной 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: Развёртывание инфраструктуры tracing | 0.5 |
| Этап 2: Интеграция OpenTelemetry в RAG | 2.0 |
| Этап 3: Ручная трассировка критических точек | 1.5 |
| Этап 4: Верификация сквозного trace | 0.75 |
| Этап 5: Скрипт автоматической проверки | 1.0 |
| Итого | 5.75 часов |
Примечание для первого раза: Рекомендуется заложить до 8 часов с учётом изучения документации и возможных проблем с Docker/Jaeger.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 101 | Основы OpenTelemetry |
| 245 | Observability в 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и пример запроса для проверки.