中文翻译暂不可用,显示俄语原文。
Как организовать 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 + Jaeger | Apache 2.0 | Да | Нет (нужны кастомные spans) | Полная гибкость |
| LangSmith | Проприетарная | Да (через SDK) | Да (автоматически) | Только через LangChain |
| Langfuse | MIT (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:
- Всегда передавайте trace_id между сервисами (через заголовки или параметры).
- Создавайте spans для каждого значимого шага: не только для вызовов LLM, но и для планирования, принятия решений, рефлексии, записи в память.
- Используйте атрибуты для хранения ключевой информации: имя агента, номер итерации, выбранное действие, код ошибки.
- Настройте сэмплирование (например,
sampling.ratio=0.1для production,1.0для тестовой среды). - Храните трассировки в системе с поиском (Jaeger позволяет фильтровать по тегам, временному диапазону).
- Добавьте мониторинг ошибок: если 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).
Шаги:
- Настройте Jaeger локально.
- Реализуйте агент: принимает вопрос, строит план (один вызов LLM), выполняет шаги (retrieval + чтение документа, если нужно уточнение — делает рефлексию и ещё один retrieval).
- Интегрируйте OpenTelemetry: создайте корневой span для запроса, spans для
plan,execute,tool.retrieve,llm.response,reflect. Передавайте trace_id в вызовы LLM через заголовки (если провайдер поддерживает). - Отправьте несколько тестовых запросов (в том числе с ошибкой, например, отключив поиск).
- Зайдите в Jaeger UI (http://localhost:16686) и найдите свои traces. Проанализируйте, сколько времени занял каждый шаг, где возникла ошибка.
Ожидаемый результат:
- В Jaeger видно полное дерево spans для каждого запроса.
- Можно увидеть, что рефлексия вызвала дополнительный retrieval (два
tool.retrieveподряд). - Можно добавить сценарий с параллельным вызовом (например, два инструмента одновременно) и увидеть наложение временных отрезков в трассировке.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 408 | Введение в distributed tracing для ML |
| 823 | Мониторинг и логирование agent pipeline |
| 825 | Отладка ошибок мультиагентных систем |
| 822 | Роли агентов и handoff |
| 801 | Архитектура agentic RAG |
| 730 | Observability в LLM-приложениях |
Навигация
- Предыдущий: 823
- Следующий: 825
- Индекс: 00. Индекс разборов