Agent tracing: як відстежувати рішення агента

Agent tracing записує кожен крок агента у вигляді trace і spans, показує reasoning, tool calls і допомагає дебажити проблемні run у production.
На цій сторінці
  1. Ідея за 30 секунд
  2. Основна проблема
  3. Як це працює
  4. Як виглядає трейс одного run
  5. Коли використовувати
  6. Приклад реалізації
  7. Типові помилки
  8. Трейс лише на рівні run, без спанів
  9. Відсутній trace_id у частині подій
  10. Не трасуються виклики інструментів
  11. Немає stop_reason і статусу span
  12. Самоперевірка
  13. FAQ
  14. Пов'язані сторінки

Ідея за 30 секунд

Agent tracing показує повний шлях виконання одного run.

Трейс складається зі спанів (spans): кожен спан — це окремий крок, наприклад reasoning, tool call або LLM-генерація.

Це дає видимість на рівні кроків і значно спрощує debugging у production.

Основна проблема

У багатьох системах логують лише початок і завершення run.

Для агентів цього недостатньо: між стартом і фінальною відповіддю можуть бути десятки кроків. Без трейсингу важко зрозуміти, що саме зробив агент і на якому кроці виникла проблема.

Один і той самий запит може пройти по-різному: інша кількість кроків, інші інструменти, інша затримка (latency).

Без трейсингу складно відповісти навіть на базові питання:

  • Який крок був найповільніший?
  • Чому агент повторно викликав інструмент?
  • Де саме з'явилася помилка?
  • Чому виросли токени в конкретному run?

Саме тому трейсинг важливий: він показує повний шлях виконання run, а не лише фінальний результат.

Як це працює

У трейсингу є дві базові сутності:

  • trace — увесь шлях одного run
  • span — один крок усередині цього trace

На практиці step у runtime часто відповідає одному span, але не завжди. Складний крок може містити вкладені spans — наприклад, виклик інструмента, який усередині робить кілька HTTP-запитів або звернень до бази.

Кожен span зазвичай має базові поля:

  • trace_id і run_id для кореляції
  • span_id (і за потреби parent_span_id)
  • step_type (reasoning, tool_call, llm_generate)
  • latency_ms і status (ok / error)

Ця структура (trace_id, span_id) базується на стандарті OpenTelemetry (OTel), який є фундаментом для більшості сучасних систем моніторингу. Поле parent_span_id — частина OTel-моделі ієрархічних спанів, яка дозволяє будувати дерево виконання (trace tree). Існують спеціалізовані інструменти для трейсингу агентів (наприклад LangSmith, Langfuse, Arize Phoenix), але ці принципи однакові незалежно від платформи.

Як виглядає трейс одного run

Найпростіше зрозуміти трейсинг на прикладі одного запиту:

У реальних системах кожен span-event містить trace_id, span_id і часто parent_span_id. У прикладі нижче ці поля опущені для простоти.

TEXT
trace_id: tr_9fd2
run_id: run_9fd2
user_query: "Find recent research about battery recycling"

span 1  llm_reasoning          320ms   status=ok
span 2  tool_call: search      410ms   status=ok
span 3  llm_reasoning          180ms   status=ok
span 4  tool_call: fetch       260ms   status=error

stop_reason: tool_error

Такий трейс одразу показує:

  • які кроки пройшов агент;
  • які інструменти були викликані;
  • скільки часу зайняв кожен крок;
  • де саме виникла затримка або помилка.

Трейси корисні не лише для debugging. Вони також важливі для evaluations і автоматичної перевірки проміжних кроків: без них складно перевірити, чи агент діяв правильно, а не лише чи дав правильну фінальну відповідь.

Коли використовувати

Трейсинг не завжди потрібен.

Для простого сценарію — один виклик LLM без tools і без циклу виконання — часто вистачає базового логування.

Але якщо run містить кілька кроків, виклики інструментів або повторні ітерації, без трейсингу стає складно:

  • дебажити поведінку агента;
  • контролювати latency і витрати;
  • пояснювати, чому система прийняла конкретне рішення.

Приклад реалізації

Нижче — спрощений приклад runtime instrumentation для trace і spans. Такий підхід використовують у LangGraph, CrewAI і в кастомних agent runtime. У цьому прикладі весь run також оформлений як root span, а окремі кроки агента логуються як вкладені spans.

PYTHON
import contextvars
import logging
import time
import uuid

logger = logging.getLogger("agent")
trace_id_ctx = contextvars.ContextVar("trace_id", default=None)


def start_span(run_id, step_type, tool=None, parent_span_id=None):
    span_id = str(uuid.uuid4())
    started_at = time.time()
    logger.info(
        "span_started",
        extra={
            "trace_id": trace_id_ctx.get(),
            "run_id": run_id,
            "span_id": span_id,
            "parent_span_id": parent_span_id,
            "step_type": step_type,
            "tool": tool,
        },
    )
    return span_id, started_at


def finish_span(
    run_id,
    span_id,
    step_type,
    started_at,
    status,
    tool=None,
    parent_span_id=None,
    error=None,
):
    logger.info(
        "span_finished",
        extra={
            "trace_id": trace_id_ctx.get(),
            "run_id": run_id,
            "span_id": span_id,
            "parent_span_id": parent_span_id,
            "step_type": step_type,
            "tool": tool,
            "status": status,
            "latency_ms": int((time.time() - started_at) * 1000),
            "error": error,
        },
    )


def run_agent(agent, task):
    trace_id = str(uuid.uuid4())
    run_id = str(uuid.uuid4())  # у multi-agent системах один trace_id може містити кілька різних run_id
    token = trace_id_ctx.set(trace_id)

    logger.info("trace_started", extra={"trace_id": trace_id, "run_id": run_id, "task": task})

    stop_reason = "max_steps"
    step_count = 0
    root_span_id, root_started_at = start_span(run_id, "run", parent_span_id=None)

    try:
        # у цьому прикладі всі кроки — дочірні до root span (без глибокої вкладеності)
        for step in agent.iter(task):  # step: reasoning або tool execution
            step_count += 1
            step_type = step.type  # reasoning | tool_call | llm_generate
            tool_name = getattr(step, "tool_name", None)

            span_id, started_at = start_span(
                run_id,
                step_type,
                tool=tool_name,
                parent_span_id=root_span_id,
            )

            try:
                result = step.execute()
                finish_span(
                    run_id,
                    span_id,
                    step_type,
                    started_at,
                    status="ok",
                    tool=tool_name,
                    parent_span_id=root_span_id,
                )
            except Exception as error:
                finish_span(
                    run_id,
                    span_id,
                    step_type,
                    started_at,
                    status="error",
                    tool=tool_name,
                    parent_span_id=root_span_id,
                    error=str(error),
                )
                stop_reason = "tool_error"
                raise

            if result.is_final:
                stop_reason = "completed"
                break

    finally:
        if stop_reason == "completed":
            root_status = "ok"
        elif stop_reason == "max_steps":
            root_status = "error"  # у цьому прикладі вважаємо як error для простоти
        else:
            root_status = "error"
        finish_span(
            run_id,
            root_span_id,
            "run",
            root_started_at,
            status=root_status,
            error=None if root_status == "ok" else stop_reason,
        )

        logger.info(
            "trace_finished",
            extra={
                "trace_id": trace_id,
                "run_id": run_id,
                "steps": step_count,
                "stop_reason": stop_reason,
            },
        )
        trace_id_ctx.reset(token)

У реальних системах trace_id і run_id потрібно прокидати через увесь ланцюг викликів. У Python для цього часто використовують contextvars, щоб не передавати ідентифікатор у кожну функцію вручну.

Наприклад, один span у структурованому логу може виглядати так:

JSON
{
  "timestamp": "2026-03-21T15:17:00Z",
  "event": "span_finished",
  "trace_id": "tr_9fd2",
  "run_id": "run_9fd2",
  "span_id": "sp_21ab",
  "parent_span_id": "sp_root_01",
  "step_type": "tool_call",
  "tool": "search_docs",
  "latency_ms": 410,
  "status": "ok"
}

Типові помилки

Навіть якщо трейсинг уже додано, системи часто залишаються складними для діагностики через типові помилки нижче.

Трейс лише на рівні run, без спанів

Якщо логується тільки старт і фініш run, трейс фактично втрачає цінність: проміжні кроки не видно, а затримку або помилку майже неможливо локалізувати.

Відсутній trace_id у частині подій

Коли частина логів не має trace_id або run_id, події не складаються в один ланцюг. Через це debugging займає більше часу навіть для простих інцидентів.

Не трасуються виклики інструментів

Інструменти часто є найповільнішою частиною run. Якщо tool calls не потрапляють у trace, важко знайти причину затримки і повторів. У production це може маскувати збій інструмента або спам інструментами.

Немає stop_reason і статусу span

Без stop_reason і status складно зрозуміти, run завершився успішно чи зупинився через обмеження або помилку. У результаті важко відтворити інцидент і правильно налаштувати алерти.

Самоперевірка

Нижче — короткий checklist базового трейсингу перед релізом.

Прогрес: 0/9

⚠ Бракує базової observability

Систему буде складно дебажити в production. Почніть з run_id, structured logs і tracing tool calls.

FAQ

Q: Чим трейс відрізняється від звичайних логів?
A: Логи відповідають на питання «що сталося». Трейс показує послідовність кроків одного run і допомагає зрозуміти, «як саме це сталося».

Q: Що варто впровадити першим для трейсингу агента?
A: Мінімум: trace_id, run_id, span_id, тип кроку, latency, status і stop_reason. Цього вже достатньо для базового debugging.

Q: Чи обов'язково одразу підключати зовнішній tracing-інструмент?
A: Ні. Можна почати з власного instrumentation і JSON-логів. Зовнішні платформи стають особливо корисними, коли росте кількість run і команд.

Q: Коли повний трейсинг може бути зайвим?
A: Для простих single-shot сценаріїв без tools і без циклу виконання часто вистачає базового логування. Повний трейсинг стає особливо корисним, коли run містить кілька кроків, зовнішні інструменти або повторні ітерації.

Пов'язані сторінки

Далі за темою:

⏱️ 7 хв читанняОновлено 21 березня 2026 р.Складність: ★★★
Інтегровано: продакшен-контрольOnceOnly
Додай guardrails до агентів з tool-calling
Зашип цей патерн з governance:
  • Бюджетами (кроки / ліміти витрат)
  • Дозволами на інструменти (allowlist / blocklist)
  • Kill switch та аварійна зупинка
  • Ідемпотентність і dedupe
  • Audit logs та трасування
Інтегрована згадка: OnceOnly — контрольний шар для продакшен агент-систем.

Автор

Микола — інженер, який будує інфраструктуру для продакшн AI-агентів.

Фокус: патерни агентів, режими відмов, контроль рантайму та надійність систем.

🔗 GitHub: https://github.com/mykolademyanov


Редакційна примітка

Ця документація підготовлена з допомогою AI, із людською редакторською відповідальністю за точність, ясність і продакшн-релевантність.

Контент базується на реальних відмовах, постмортемах та операційних інцидентах у розгорнутих AI-агентних системах.