Реализовать distributed tracing для агента с пропагацией trace_id через все tool calls

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать distributed tracing для агента с пропагацией trace_id через все tool calls

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

Научиться внедрять end-to-end distributed tracing в LLM-агента, который последовательно вызывает несколько инструментов (tools). Основной фокус — корректная пропагация единого trace_id через все шаги агента: от входящего запроса до каждого вызова внешнего инструмента и итогового ответа. В результате должен быть построен полный trace в системе визуализации (Jaeger или консоль), отображающий 10+ вложенных spans, соответствующих вызовам инструментов, с корректными родительско-дочерними связями.

Ключевой результат Рабочая трассировка, в которой один trace содержит не менее 10 spans, каждый спан соответствует одному вызову инструмента, и все spans принадлежат одному trace_id.


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

Что нужноОткуда взять
Код агента (Python)Ваш пет проект или пример из документации LangChain / CrewAI
Определения инструментов (tools)Те же зависимости агента (например, поиск, калькулятор, вызов API)
HTTP-интерфейс агента (если есть)Flask/FastAPI эндпоинт, принимающий запросы
OpenTelemetry SDKpip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation
Бэкенд трейсингаJaeger (рекомендуется) или Zipkin; можно запустить в Docker
Инструменты для тестированияcurl, Postman или pytest с requests
ДокументацияOpenTelemetry Python, LangChain/LlamaIndex tracing docs

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

  1. Напишите простой цикл агента в одном Python-файле: функция agent_step(state) вызывает один из 3-х инструментов (search, calculate, translate) в зависимости от состояния.
  2. Сделайте так, чтобы для получения ответа требовалось минимум 10 шагов (например, агент сначала ищет информацию, потом вычисляет, потом переводит, и повторяет несколько раз).
  3. Используйте стандартные HTTP-вызовы (например, к MockAPI или локальному Flask) как удалённые инструменты, чтобы проверить пропагацию traceparent header.

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

КомпонентИнструментыНазначение
Язык и агентPython 3.10+, LangChain / LlamaIndex / самописныйРеализация агента и инструментов
Distributed tracingOpenTelemetry SDK (opentelemetry-api, opentelemetry-sdk, opentelemetry-instrumentation-requests)Генерация, пропагация и экспорт трейсов
Бэкенд визуализацииJaeger (all-in-one) через DockerПриём и отображение трейсов
HTTP-пропагацияopentelemetry-propagator-jaeger или W3C Trace ContextПередача trace_id между сервисами
КонтейнеризацияDocker (docker-compose)Запуск Jaeger и агента
МониторингGrafana (опционально)Дашборды на данных трейсов (Jaeger datasource)

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

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

Действия

  1. Установите необходимые пакеты:
pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-requests opentelemetry-exporter-jaeger opentelemetry-propagator-jaeger
  1. Запустите Jaeger all-in-one в Docker:
docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14250:14250 \
  -p 14268:14268 \
  -p 14269:14269 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.53
  1. Убедитесь, что UI Jaeger доступен по адресу http://localhost:16686.

  2. Создайте файл tracing.py — модуль инициализации OpenTelemetry:

from opentelemetry import trace
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

def init_tracer(service_name: str = "agent"):
    resource = Resource(attributes={SERVICE_NAME: service_name})
    provider = TracerProvider(resource=resource)
    exporter = JaegerExporter(
        agent_host_name="localhost",
        agent_port=6831,
    )
    processor = BatchSpanProcessor(exporter)
    provider.add_span_processor(processor)
    trace.set_tracer_provider(provider)
    return trace.get_tracer(__name__)

Ожидаемый результат этапа Jaeger запущен, консоль показывает, что spans принимаются (проверьте через http://localhost:16686/api/services — пустой список допустим, пока нет данных).


Этап 2: Инструментирование цикла агента (1–1.5 часа)

Действия

  1. Добавьте вызов init_tracer() на старте приложения.

  2. Оберните каждый вызов инструмента в отдельный спан. Используйте менеджер контекста tracer.start_as_current_span("tool.call.<tool_name>"). Пример для агента с циклом:

from opentelemetry import trace
from tracing import init_tracer

tracer = trace.get_tracer(__name__)

def execute_tool(tool_name: str, input_data: dict) -> dict:
    with tracer.start_as_current_span(f"tool.call.{tool_name}") as span:
        span.set_attribute("tool.input", str(input_data)[:1000])
        # реальный вызов инструмента (например, HTTP-запрос)
        result = call_tool_external(tool_name, input_data)
        span.set_attribute("tool.result", str(result)[:1000])
        return result
  1. Создайте главный спан для всего эпизода агента (один запрос):
def handle_request(user_query: str):
    with tracer.start_as_current_span("agent.session") as main_span:
        main_span.set_attribute("user.query", user_query)
        state = initialize_state(user_query)
        steps = 0
        while not state.is_finished and steps < 15:
            with tracer.start_as_current_span(f"agent.step.{steps}") as step_span:
                tool = select_tool(state)
                step_span.set_attribute("tool.name", tool.name)
                result = execute_tool(tool.name, state)
                state.update(result)
            steps += 1
  1. Если инструменты вызываются удалённо (HTTP), настройте пропагацию: при HTTP-запросе добавьте заголовок traceparent, используя opentelemetry.propagate.inject. Пример:
import requests
from opentelemetry import propagate

def call_tool_external(tool_name: str, payload: dict):
    headers = {}
    propagate.inject(headers)  # добавляет traceparent, tracestate
    resp = requests.post(f"http://mock-tool/{tool_name}", json=payload, headers=headers)
    return resp.json()
  1. Если инструменты выполняются локально (простой вызов функции), контекст OpenTelemetry автоматически распространяется через contextvars внутри одного потока. Ничего дополнительно делать не нужно.

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


Этап 3: Настройка экспорта и проверка базовой трассировки (30 минут)

Действия

  1. Убедитесь, что spans отправляются в Jaeger. Добавьте временное логирование для проверки:
from opentelemetry.sdk.trace import SpanProcessor

class LoggingSpanProcessor(SpanProcessor):
    def on_start(self, span, parent_context):
        print(f"Span started: {span.name}, trace_id={span.get_span_context().trace_id}")

    def on_end(self, span):
        print(f"Span ended: {span.name}")

Подключите его перед BatchSpanProcessor, чтобы видеть в консоли.

  1. Запустите агента один раз с тестовым запросом (например, "найди погоду и переведи в цельсии"). Проверьте вывод: должны появиться сообщения о старте/завершении.

  2. В Jaeger UI нажмите "Search", выберите сервис agent укажите временной диапазон. Должен появиться один trace с несколькими spans. Откройте его и проверьте вложенность.

  3. Исправьте ошибки, если spans не отображаются (проверьте экспортёр, порты Jaeger, переменные окружения).

Ожидаемый результат этапа Один trace в Jaeger, в нём не менее 3-4 spans (шаги агента + вызовы инструментов).


Этап 4: Усложнение сценария до 10+ шагов (1 час)

Действия

  1. Модифицируйте агента так, чтобы он выполнял минимум 10-12 шагов перед завершением. Например, агент решает задачу, требующую последовательного поиска и расчётов:
шаг 1: поиск "население Москвы"
шаг 2: поиск "население Нью-Йорка"
шаг 3: вычисление разницы
шаг 4: перевод результата на французский
шаг 5: поиск "население Токио"
шаг 6: вычисление суммы всех трёх
шаг 7: перевод на немецкий
шаг 8: поиск "самый густонаселённый город"
шаг 9: вычисление процента от населения планеты
шаг 10: вывод окончательного ответа
  1. Убедитесь, что в коде нет фиксации только одного вызова инструмента. Каждый tool call должен оборачиваться в спан.

  2. Добавьте атрибуты spans для диагностики: agent.step.number, tool.name, duration (автоматически), input.hash (при необходимости).

  3. Запустите агента и выполните целевой запрос.

Ожидаемый результат этапа В Jaeger отображается trace, содержащий минимум 10 дочерних spans (или 10+ вместе с agent.step.*). Все spans имеют один trace_id, родительские связи идут от agent.session к agent.step.*, от них к tool.call.*.


Этап 5: Проверка пропагации через HTTP и финальное тестирование (30 минут)

Действия

  1. Если инструменты являются внешними HTTP-сервисами (например, свой микросервис на Flask), настройте их приём заголовка traceparent и создание дочернего спан.

    • Создайте минимальный Flask-сервис с OpenTelemetry, который читает входящий контекст и создаёт спан.
    • Запустите его как ещё один контейнер (или отдельный процесс).
  2. Проверьте, что в Jaeger появляется объединённый trace, соединяющий запрос агента и вызовы микросервиса (cross-service trace).

  3. Если инструменты не внешние, проверьте через логи: traceparent передаётся в заголовке (для имитации можно поднять простой HTTP-сервер с отладкой).

  4. Выполните итоговый тест с замером времени: зафиксируйте количество spans в Jaeger (>=10). Сделайте скриншот трейса.

Ожидаемый результат этапа Полностью рабочий distributed tracing, trace отображается в Jaeger с корректными parent-child отношениями, количество spans >=10.


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

  • Все вызовы инструментов агента обёрнуты в отдельные spans (OpenTelemetry).
  • Каждый спан содержит атрибуты: tool.name, tool.input (первые 1000 символов), tool.result.
  • Все spans одного запроса объединены единым trace_id.
  • В Jaeger UI виден один trace, в нём минимум 10 spans (включая спаны шагов и инструментов).
  • Иерархия spans корректна: agent.sessionagent.step.Ntool.call.toolname.
  • HTTP-вызовы инструментов (при наличии) передают traceparent заголовок, и принимающая сторона создаёт дочерний спан.
  • Агент выполняет запрос до получения финального ответа (сценарий с 10+ шагами).
  • Документация (файл README или встроенные комментарии) описывает, как запустить трассировку.

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

  • Основной артефакт Папка agent-tracing/ с кодом:
    • agent.py — основной файл агента (цикл, инструменты, вызовы).
    • tracing.py — инициализация OpenTelemetry и экспортёр.
    • trace_showcase.png — скриншот Jaeger UI с трассировкой (10+ spans).
  • Содержание кода Полная инструментация, возможность легко менять число шагов (например, через константу MAX_STEPS).
  • Опционально
    • docker-compose.yml для совместного запуска Jaeger + агента + инструментов.
    • test_trace.py — скрипт, автоматически отправляющий запрос и ждущий появления trace в Jaeger (через API Jaeger).

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

СложностьРешение
Spans не отображаются в JaegerПроверить, что JaegerExporter использует правильный порт (6831/udp) и Jaeger запущен. Временно использовать консольный экспортёр ConsoleSpanExporter.
Context propagation теряется в асинхронном коде (asyncio)Использовать opentelemetry.instrumentation.asyncio или передавать контекст вручную через contextvars; для LangChain — уже встроена поддержка OpenTelemetry.
Слишком много шагов, trace не влезает в Jaeger UIУстановить параметр max_length в BatchSpanProcessor или увеличить -DJAEGER_QUERY_MAX_TRACE_DURATION (опция Jaeger).
Внешний инструмент не принимает traceparentЕсли инструмент не поддерживает OpenTelemetry, можно добавить middleware (Flask) или симулировать приём контекста в локальном HTTP-сервисе.
LangChain/LlamaIndex уже имеют встроенный трейсингИспользовать их настройки (например, LangChainTracer), но задача — сделать руками на OpenTelemetry, чтобы понять механику.
Атрибуты spans становятся слишком большими из-за tool.inputУкоротить запись до первых 1000 символов.

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

ЭтапВремя
Этап 1: Подготовка окружения30 мин
Этап 2: Инструментирование цикла агента1–1,5 ч
Этап 3: Настройка экспорта и базовая проверка30 мин
Этап 4: Усложнение сценария до 10+ шагов1 ч
Этап 5: Проверка пропагации через HTTP и финальное тестирование30 мин
Итого3,5–4,5 часа

Примечание Время дано для первого прохождения. При наличии опыта с OpenTelemetry возможно сократить до 2 часов.


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

ВопросТема
101Основы OpenTelemetry: трассы, spans, метрики
202Пропагация контекста (traceparent, W3C Trace Context)
303Интеграция Jaeger с Python-приложениями
404Инструментирование LLM-агентов (LangChain, LlamaIndex)
505Distributed tracing для микросервисов
606Мониторинг производительности агентов с помощью трейсов
707Работа с BatchSpanProcessor и экспортёрами
808Отладка потери spans в асинхронном коде
909Создание дашбордов Grafana на основе Jaeger данных

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

  • Я установил OpenTelemetry SDK и Jaeger, запустил Jaeger через Docker.
  • Я добавил init_tracer() и обернул каждый вызов инструмента в start_as_current_span.
  • Я настроил HTTP-пропагацию (если используются внешние инструменты).
  • Я создал сценарий с минимум 10 шагами агента.
  • Я проверил в Jaeger UI, что один trace содержит 10+ spans с единым trace_id.
  • Я проверил, что все дочерние spans корректно вложены.
  • Я сделал скриншот трассировки для отчёта.
  • Я задокументировал процесс запуска и конфигурацию.