Как организовать distributed tracing для agent pipeline?

Краткий тезис

Distributed tracing (распределённая трассировка) для agent pipeline — это метод отслеживания полного пути выполнения запроса через множество вызовов LLM, инструментов, внутренних переходов между агентами и циклов рефлексии. Ключевые элементы: propagation уникального trace_id через все сервисы, создание иерархических spans (например, agent.plan, tool.call.*, agent.reflect) и визуализация временной шкалы в инструментах вроде Jaeger или LangSmith. Главная сложность — агенты могут создавать ветвления, циклы и параллельные execution, что требует поддержки направленных ациклических графов (DAG) в трейсинге.


1. Термин: Distributed Tracing (распределённая трассировка)

Distributed tracing — это техника мониторинга и отладки распределённых систем, при которой каждому внешнему запросу присваивается уникальный идентификатор — trace_id. Все компоненты, участвующие в обработке этого запроса, создают spans (единицы работы), аннотированные временем начала, окончания, атрибутами и связями с родительским span.

Для agent pipeline это означает: один пользовательский запрос может инициировать несколько шагов у одного агента, вызовы инструментов (API, базы данных, калькуляторы), а также передачу управления другому агенту (handoff). Без distributed tracing невозможно понять, где произошла задержка или ошибка, особенно при ветвлениях и циклах.


2. Зачем нужен distributed tracing в agent pipeline?

Традиционный мониторинг (логи, метрики) не даёт полной картины, потому что агенты:

  • Многократно вызывают LLM: каждый вызов — это отдельный сетевой запрос с задержкой 1–30 секунд.
  • Обращаются к инструментам: вызовы внешних API, поиск в векторной базе, чтение файлов — всё это может падать или тормозить.
  • Создают циклы: план → действие → наблюдение → перепланирование. Без трассировки сложно увидеть, сколько итераций потребовалось и какая из них была лишней.
  • Ветвятся: один агент может делегировать подзадачи другим агентам параллельно, а затем агрегировать результаты. Нужно видеть временную шкалу каждого подпроцесса.
  • Ошибки распространяются: исключение в одной подзадаче может привести к неверному решению на верхнем уровне. Трассировка помогает найти корень проблемы.

Distributed tracing даёт:

  • Визуализацию полного потока запроса: от входа до выхода.
  • Локализацию узких мест: какой span длиннее всего? Какой инструмент не отвечает?
  • Поиск ошибок: в каком span возникла ошибка? Есть ли повторные попытки?
  • Сравнение сессий: для одного запроса trace можно сравнить с эталонным или с предыдущими версиями агента.

3. Ключевые компоненты distributed tracing для агентов

3.1 Propagation (распространение контекста)

Trace_id и родительский span_id должны передаваться между всеми компонентами pipeline:

  • В вызовы LLM: при отправке промпта провайдеру (OpenAI, Anthropic) в заголовках HTTP (OpenTelemetry propagation).
  • В вызовы инструментов: если инструмент — это внешнее API, trace_id передаётся в метаданные запроса.
  • Меж-агентские сообщения: когда один агент передаёт управление другому (через очередь, RPC или gRPC), trace_id должен быть встроен в протокол.
  • Внутренние шаги агента: планирование, рефлексия, перепланирование — даже если они выполняются в одном процессе, нужно явно создавать spans с правильным parent.

Механизмы propagation:

  • W3C Trace-Context: HTTP-заголовки traceparent и tracestate (стандарт для большинства инструментов).
  • Baggage: дополнительные метаданные, передаваемые с trace_id (например, ID пользователя, версия агента).

3.2 Spans (единицы работы)

Каждый значимый этап обработки должен быть обёрнут в span. Для agent pipeline мы выделяем следующие типы spans (иерархия):

Имя spanОписаниеТип
agent.requestВходной запрос пользователя (корневой span)root
agent.planПостроение плана действий (первый вызов LLM)child of request
agent.executeЦикл выполнения шагов плана (один span на итерацию)child of request
tool.call.{tool_name}Вызов конкретного инструмента (поиск, калькулятор, API)child of execute
llm.completionВызов LLM (включая промпт, ответ, токены)child of plan/execute
agent.reflectСаморефлексия — анализ результата и принятие решения (дальше/завершить)child of execute
delegation.handoffПередача управления другому агенту (создаётся новый подграф)child of execute
memory.writeЗапись в память (например, в векторную базу)child of execute
memory.readЧтение из памяти (например, из истории диалога)child of plan

Важно: span может быть активным (выполняется) и завершённым. Инструменты трассировки автоматически измеряют длительность.


4. Типичная архитектура distributed tracing для agent pipeline

Рассмотрим на примере агента, который отвечает на вопрос, используя поиск в документации и вызов API погоды. Предположим, он может перепланировать, если первый инструмент не сработал.

User Request (trace_id = abc123)
  └─ agent.plan (span_id = 1)
      └─ llm.completion (планирование шага 1 и 2)
  └─ agent.execute (iter 1)
       ├─ tool.call.search_docs (span_id = 2)
       │   └─ llm.completion (краткое изложение документа)
       ├─ tool.call.get_weather (span_id = 3)
       │   └─ http.request (к API погоды)
       └─ agent.reflect (span_id = 4)
           └─ llm.completion (оценка: нужно ли дополнительное действие?)
  └─ agent.execute (iter 2) — рефлексия решила сделать ещё один шаг
       ├─ tool.call.search_docs (уточнённый запрос)
       └─ agent.reflect (всё хорошо, завершить)
  └─ agent.response (финальный ответ пользователю)

Визуализация в Jaeger покажет временную шкалу с вложенными блоками. Если get_weather занял 10 секунд из-за тайм-аута, мы это сразу увидим.


5. Инструменты для distributed tracing

5.1 OpenTelemetry (OTel) + Jaeger

OpenTelemetry — open-source стандарт для сбора трассировок и метрик. Позволяет инструментировать код на Python, JavaScript, Java и др. Jaeger — визуализатор, который принимает spans по протоколу OTLP (OpenTelemetry Protocol).

Преимущества:

  • Стандартизирован, переносим между инфраструктурами.
  • Поддерживает propagation, baggage, контексты.
  • Можно развернуть самостоятельно (self-hosted) или использовать облачные аналоги.

Недостатки:

  • Требует ручного создания spans для каждого вызова LLM и инструмента.
  • Нет встроенной поддержки специфических для агентов типов spans (например, «рефлексия»).

5.2 LangSmith

LangSmith — платформа от создателей LangChain, специально заточенная под LLM-приложения и агентов. Автоматически создаёт spans для вызовов LLM, инструментов, цепочек и ручных шагов.

Преимущества:

  • Из коробки понимает структуру агента (создаёт группы «run» для шагов).
  • Есть аннотации для feedback, метрики, сравнение сессий.
  • Не требует написания кода для базовых случаев.

Недостатки:

  • Закрытая платформа (облачная/enterprise).
  • Может не хватать гибкости для кастомных архитектур, не основанных на LangChain.

5.3 Langfuse

Langfuse — open-source платформа для observability LLM. Поддерживает OpenTelemetry, создаёт spans для вызовов LLM, RAG, агентов. Есть встроенная поддержка трассировки для LangChain, LlamaIndex, OpenAI SDK.

Преимущества:

  • Бесплатный self-hosted вариант.
  • Поддерживает traces для агентов с шагами и инструментами.
  • Интегрируется с OpenTelemetry.

5.4 Сравнительная таблица

ИнструментЛицензияПропагацияСпецифика агентовСамописные интеграции
OpenTelemetry + JaegerApache 2.0ДаНет (нужны кастомные spans)Полная гибкость
LangSmithПроприетарнаяДа (через SDK)Да (автоматически)Только через LangChain
LangfuseMIT (open-core)Да (OTel)ЧастичноДа (через OTel)
Datadog APMПроприетарнаяДаНетДа (через их SDK)

6. Особенности трассировки для ветвлений, циклов и параллельного execution

Агенты редко выполняются линейно. Рассмотрим три ключевые сложности.

6.1 Ветвления (conditional execution)

После agent.reflect агент может выбрать один из нескольких путей (например, вызвать инструмент A или B). В трассировке это должно выглядеть как выбор между двумя поддеревьями. Проблема: если оба пути не выполняются, span для невыбранного пути не создаётся. Визуализация не покажет «возможность».

Решение: создавать span decision с атрибутом decision.result, в который записать, какой путь был выбран. Если агент параллельно запускает несколько агентов, каждый получает свой subtrace, объединённый общим родительским span.

6.2 Циклы и саморефлексия

Агент может повторять шаги, пока не достигнет условия (например, пока не получит корректный ответ). Каждую итерацию нужно создавать отдельный span (например, agent.execute_iteration). Временная шкала может содержать много повторяющихся блоков. Лучшая практика: начиная со второй итерации, добавлять атрибут iteration, чтобы различать.

6.3 Параллельное выполнение

Современные агенты (например, crewai, AutoGen) могут запускать несколько подзадач одновременно. В трассировке это выглядит как несколько дочерних spans, которые перекрываются во времени. OpenTelemetry поддерживает такую модель: spans с одними и теми же start/end могут быть вложены, если они от разных процессов. Важно правильно установить parent_id.

Пример: агент-оркестратор создаёт span parallel_dispatch, внутри которого несколько дочерних spans sub_agent_1.run, sub_agent_2.run, которые выполняются параллельно. Каждый из них может иметь свои под-агенты и инструменты.


7. Практическая реализация на Python с OpenTelemetry

Ниже пример кода, иллюстрирующий, как инструментировать простой agent pipeline с помощью OpenTelemetry. Предполагается, что агент использует библиотеку langchain и openai, но мы вручную создаём spans.

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

# Настройка OTel
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

tracer = trace.get_tracer(__name__)

def agent_pipeline(user_query: str):
    # Корневой span
    with tracer.start_as_current_span("agent.request") as root_span:
        root_span.set_attribute("query", user_query)
        
        # Планирование
        with tracer.start_as_current_span("agent.plan") as plan_span:
            plan = llm_plan(user_query)  # вызов LLM
            plan_span.set_attribute("plan", str(plan))
        
        for step in plan:
            with tracer.start_as_current_span("agent.execute", attributes={"step": step}) as exec_span:
                # Вызов инструмента
                with tracer.start_as_current_span(f"tool.call.{step.tool}") as tool_span:
                    result = call_tool(step)
                    tool_span.set_attribute("result", truncate(result, 512))
                
                # Рефлексия
                with tracer.start_as_current_span("agent.reflect") as reflect_span:
                    feedback = llm_reflect(result)
                    if feedback.need_retry:
                        # Создаём дополнительный цикл (можно добавить атрибут retry_count)
                        exec_span.set_attribute("retry", True)
                        continue
        
        # Финальный ответ
        with tracer.start_as_current_span("agent.response") as resp_span:
            response = build_response(plan, results)
            resp_span.set_attribute("response_len", len(response))
        
        root_span.set_status(trace.Status(trace.StatusCode.OK))
    return response

Важно: в реальном проекте нужно подумать о ручном или автоматическом получении trace_id в контексте (например, через trace.get_current_span()). Для агентов, которые вызывают другой сервис, используйте W3C propagation в HTTP-заголовках.


8. Проблемы и best practices

Проблемы:

  • Размер данных: атрибуты spans (промпты, ответы LLM, результаты инструментов) могут быть большими. Лучше сохранять их в отдельном хранилище (например, S3) и хранить ссылку в span.
  • Высокая загрузка: если агент делает 100 шагов, будет 100 spans (и это нормально). Но при параллельном выполнении количество spans может резко возрасти. Настройте сэмплирование (например, сохранять только 10% сессий или только те, где есть ошибка).
  • Контекст в асинхронном коде: при использовании asyncio нужно аккуратно пробрасывать контекст (OpenTelemetry поддерживает автоматическое связывание через ContextVars).
  • Соединение с LLM-провайдером: у OpenAI нет нативной поддержки OpenTelemetry, но можно обернуть вызовы в spans. LangChain SDK уже интегрирован с OTel.

Best practices:

  1. Всегда передавайте trace_id между сервисами (через заголовки или параметры).
  2. Создавайте spans для каждого значимого шага: не только для вызовов LLM, но и для планирования, принятия решений, рефлексии, записи в память.
  3. Используйте атрибуты для хранения ключевой информации: имя агента, номер итерации, выбранное действие, код ошибки.
  4. Настройте сэмплирование (например, sampling.ratio=0.1 для production, 1.0 для тестовой среды).
  5. Храните трассировки в системе с поиском (Jaeger позволяет фильтровать по тегам, временному диапазону).
  6. Добавьте мониторинг ошибок: если span завершился с ошибкой, укажите статус ERROR и запишите трассировку стека.

9. Связь с мониторингом LLM и observability

Distributed tracing — часть более широкой концепции observability (наблюдаемости). Для LLM-агентов обычно строят три уровня:

  • Метрики: latency каждой стадии, количество вызовов LLM, количество шагов, ошибки (rate).
  • Логи: каждый вызов LLM с полными промптами и ответами (но без связей между шагами).
  • Трассировки: связывает логи в единую временную линию.

Для агентов особенно важно совмещение: например, в Jaeger можно кликнуть на span llm.completion и перейти в лог с полным промптом. Инструменты вроде LangSmith автоматически это делают.


Пет-проект для закрепления

Задача: Разработать простой агент для ответа на вопросы по документации (векторная БД) с возможностью перепланирования, и добавить distributed tracing с OpenTelemetry + Jaeger.

Инструменты:

  • Python, LangChain (или кастомная реализация).
  • OpenTelemetry SDK, OTLP exporter.
  • Jaeger (через Docker: docker run -p 16686:16686 jaegertracing/all-in-one:latest).
  • Документация в формате Markdown (например, несколько файлов про Python).

Шаги:

  1. Настройте Jaeger локально.
  2. Реализуйте агент: принимает вопрос, строит план (один вызов LLM), выполняет шаги (retrieval + чтение документа, если нужно уточнение — делает рефлексию и ещё один retrieval).
  3. Интегрируйте OpenTelemetry: создайте корневой span для запроса, spans для plan, execute, tool.retrieve, llm.response, reflect. Передавайте trace_id в вызовы LLM через заголовки (если провайдер поддерживает).
  4. Отправьте несколько тестовых запросов (в том числе с ошибкой, например, отключив поиск).
  5. Зайдите в Jaeger UI (http://localhost:16686) и найдите свои traces. Проанализируйте, сколько времени занял каждый шаг, где возникла ошибка.

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

  • В Jaeger видно полное дерево spans для каждого запроса.
  • Можно увидеть, что рефлексия вызвала дополнительный retrieval (два tool.retrieve подряд).
  • Можно добавить сценарий с параллельным вызовом (например, два инструмента одновременно) и увидеть наложение временных отрезков в трассировке.

Связь с другими вопросами

ВопросТема
408Введение в distributed tracing для ML
823Мониторинг и логирование agent pipeline
825Отладка ошибок мультиагентных систем
822Роли агентов и handoff
801Архитектура agentic RAG
730Observability в LLM-приложениях

Навигация