Реализовать 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 SDK | pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation |
| Бэкенд трейсинга | Jaeger (рекомендуется) или Zipkin; можно запустить в Docker |
| Инструменты для тестирования | curl, Postman или pytest с requests |
| Документация | OpenTelemetry Python, LangChain/LlamaIndex tracing docs |
Если нет реального агента — симулируем:
- Напишите простой цикл агента в одном Python-файле: функция
agent_step(state)вызывает один из 3-х инструментов (search, calculate, translate) в зависимости от состояния. - Сделайте так, чтобы для получения ответа требовалось минимум 10 шагов (например, агент сначала ищет информацию, потом вычисляет, потом переводит, и повторяет несколько раз).
- Используйте стандартные HTTP-вызовы (например, к MockAPI или локальному Flask) как удалённые инструменты, чтобы проверить пропагацию
traceparentheader.
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Язык и агент | Python 3.10+, LangChain / LlamaIndex / самописный | Реализация агента и инструментов |
| Distributed tracing | OpenTelemetry 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 минут)
Действия
- Установите необходимые пакеты:
pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-requests opentelemetry-exporter-jaeger opentelemetry-propagator-jaeger
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
-
Убедитесь, что UI Jaeger доступен по адресу
http://localhost:16686. -
Создайте файл
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 часа)
Действия
-
Добавьте вызов
init_tracer()на старте приложения. -
Оберните каждый вызов инструмента в отдельный спан. Используйте менеджер контекста
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
- Создайте главный спан для всего эпизода агента (один запрос):
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
- Если инструменты вызываются удалённо (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()
- Если инструменты выполняются локально (простой вызов функции), контекст OpenTelemetry автоматически распространяется через
contextvarsвнутри одного потока. Ничего дополнительно делать не нужно.
Ожидаемый результат этапа Каждый вызов инструмента и каждый шаг агента порождают спан, все spans вложены в главный спан agent.session.
Этап 3: Настройка экспорта и проверка базовой трассировки (30 минут)
Действия
- Убедитесь, что 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, чтобы видеть в консоли.
-
Запустите агента один раз с тестовым запросом (например, "найди погоду и переведи в цельсии"). Проверьте вывод: должны появиться сообщения о старте/завершении.
-
В Jaeger UI нажмите "Search", выберите сервис
agentукажите временной диапазон. Должен появиться один trace с несколькими spans. Откройте его и проверьте вложенность. -
Исправьте ошибки, если spans не отображаются (проверьте экспортёр, порты Jaeger, переменные окружения).
Ожидаемый результат этапа Один trace в Jaeger, в нём не менее 3-4 spans (шаги агента + вызовы инструментов).
Этап 4: Усложнение сценария до 10+ шагов (1 час)
Действия
- Модифицируйте агента так, чтобы он выполнял минимум 10-12 шагов перед завершением. Например, агент решает задачу, требующую последовательного поиска и расчётов:
шаг 1: поиск "население Москвы"
шаг 2: поиск "население Нью-Йорка"
шаг 3: вычисление разницы
шаг 4: перевод результата на французский
шаг 5: поиск "население Токио"
шаг 6: вычисление суммы всех трёх
шаг 7: перевод на немецкий
шаг 8: поиск "самый густонаселённый город"
шаг 9: вычисление процента от населения планеты
шаг 10: вывод окончательного ответа
-
Убедитесь, что в коде нет фиксации только одного вызова инструмента. Каждый tool call должен оборачиваться в спан.
-
Добавьте атрибуты spans для диагностики:
agent.step.number,tool.name,duration(автоматически),input.hash(при необходимости). -
Запустите агента и выполните целевой запрос.
Ожидаемый результат этапа В Jaeger отображается trace, содержащий минимум 10 дочерних spans (или 10+ вместе с agent.step.*). Все spans имеют один trace_id, родительские связи идут от agent.session к agent.step.*, от них к tool.call.*.
Этап 5: Проверка пропагации через HTTP и финальное тестирование (30 минут)
Действия
-
Если инструменты являются внешними HTTP-сервисами (например, свой микросервис на Flask), настройте их приём заголовка
traceparentи создание дочернего спан.- Создайте минимальный Flask-сервис с OpenTelemetry, который читает входящий контекст и создаёт спан.
- Запустите его как ещё один контейнер (или отдельный процесс).
-
Проверьте, что в Jaeger появляется объединённый trace, соединяющий запрос агента и вызовы микросервиса (cross-service trace).
-
Если инструменты не внешние, проверьте через логи:
traceparentпередаётся в заголовке (для имитации можно поднять простой HTTP-сервер с отладкой). -
Выполните итоговый тест с замером времени: зафиксируйте количество 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.session→agent.step.N→tool.call.toolname. - HTTP-вызовы инструментов (при наличии) передают
traceparentзаголовок, и принимающая сторона создаёт дочерний спан. - Агент выполняет запрос до получения финального ответа (сценарий с 10+ шагами).
- Документация (файл README или встроенные комментарии) описывает, как запустить трассировку.
6. Ожидаемый результат
- Основной артефакт Папка
agent-tracing/с кодом:agent.py— основной файл агента (цикл, инструменты, вызовы).tracing.py— инициализация OpenTelemetry и экспортёр.trace_showcase.png— скриншот Jaeger UI с трассировкой (10+ spans).
- Содержание кода Полная инструментация, возможность легко менять число шагов (например, через константу
MAX_STEPS). - Опционально
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) |
| 505 | Distributed 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 корректно вложены.
- Я сделал скриншот трассировки для отчёта.
- Я задокументировал процесс запуска и конфигурацию.