Логування агентів

Логи фіксують дії агента, prompts і виклики інструментів.
На цій сторінці
  1. Ідея за 30 секунд
  2. Основна проблема
  3. Як це працює
  4. Які події логувати в першу чергу
  5. Коли використовувати
  6. Приклад реалізації
  7. Типові помилки
  8. Логується тільки фінальна відповідь
  9. Немає стабільних ідентифікаторів (run_id, trace_id)
  10. Логуються raw prompts або raw args без редагування
  11. Не логуються tool_result і stop_reason
  12. Самоперевірка
  13. FAQ
  14. Пов'язані сторінки

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

Логування агентів відповідає на просте питання: що саме сталося під час run.

Для цього потрібні структуровані події з кореляцією через run_id і trace_id.

Без цього в інциденті зазвичай видно лише фінальну відповідь, але не видно шляху до неї.

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

У звичайному бекенді часто достатньо кількох логів на запит.

В агентних системах один запит може містити reasoning, tool calls, retries і кілька кроків моделі. Якщо логувати лише фінал, то важко зрозуміти, де саме система зламалася.

У production це зазвичай виглядає так:

  • користувач скаржиться на неправильну відповідь;
  • витрати або latency ростуть хвилями;
  • у логах є окрема помилка, але без контексту run.

Саме тому агентам потрібні не випадкові логи, а структуроване логування подій по всьому життєвому циклу run.

Як це працює

Базова ідея проста: кожен важливий крок записується як окрема структурована подія.

Мінімум для кожної події:

  • run_id і trace_id для кореляції;
  • event (що сталося);
  • timestamp;
  • status (ok / error) де це релевантно;
  • ключові поля кроку (tool, latency, stop_reason тощо).

Які події логувати в першу чергу

ПодіяЩо важливо зафіксувати
run_startedrun_id, trace_id, request_id, user_id
agent_stepstep_type, step_index, tool
tool_calltool_name, args_hash
tool_resulttool_name, latency_ms, status, error_class
llm_resultmodel, token usage, latency_ms, status
run_finishedstop_reason, total_steps, total_latency_ms

У production системах raw prompts і raw tool args зазвичай не пишуть у логи без редагування. Частіше зберігають hash або анонімізовану версію.

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

Глибоке логування не завжди потрібне.

Для простого single-shot сценарію інколи вистачає мінімальних логів request -> response.

Але як тільки є tools, retries, кілька кроків або високі витрати, без структурованого логування стає складно:

  • дебажити інциденти;
  • пояснювати витрати;
  • стабільно налаштовувати алерти.

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

Нижче — спрощений приклад structured logging у runtime і tool gateway. У прикладі raw args не пишуться в логи: зберігається args_hash. У прикладі нижче agent_step фіксує сам факт кроку агента, а tool_call і tool_result деталізують окремо початок і результат виклику інструмента.

PYTHON
import hashlib
import json
import logging
import time
import uuid

logger = logging.getLogger("agent")


def stable_hash(value):
    payload = json.dumps(
        value,
        sort_keys=True,
        ensure_ascii=False,
        default=str,  # для datetime та складних типів; у критичних системах краще використовувати стабільний формат (наприклад ISO 8601)
    ).encode("utf-8")
    return hashlib.sha256(payload).hexdigest()


def log_event(event, **fields):
    logger.info(event, extra={"event": event, **fields})


def run_agent(agent, task, user_id=None, request_id=None):
    run_id = str(uuid.uuid4())
    trace_id = str(uuid.uuid4())
    started_at = time.time()
    steps = 0
    stop_reason = "max_steps"
    run_status = "ok"

    log_event(
        "run_started",
        run_id=run_id,
        trace_id=trace_id,
        user_id=user_id,
        request_id=request_id,
        task_hash=stable_hash(task),
    )

    try:
        for step in agent.iter(task):  # step: reasoning або tool execution
            steps += 1
            step_started_at = time.time()
            step_type = step.type
            tool_name = getattr(step, "tool_name", None)

            log_event(
                "agent_step",
                run_id=run_id,
                trace_id=trace_id,
                step_index=steps,
                step_type=step_type,
                tool=tool_name,
            )

            if step_type == "tool_call":
                args = getattr(step, "args", {})
                log_event(
                    "tool_call",
                    run_id=run_id,
                    trace_id=trace_id,
                    tool=tool_name,
                    args_hash=stable_hash(args),
                )

            try:
                result = step.execute()
                latency_ms = int((time.time() - step_started_at) * 1000)

                if step_type == "tool_call":
                    log_event(
                        "tool_result",
                        run_id=run_id,
                        trace_id=trace_id,
                        tool=tool_name,
                        latency_ms=latency_ms,
                        status="ok",
                    )
                else:
                    token_usage = getattr(result, "token_usage", None)
                    log_event(
                        "llm_result",
                        run_id=run_id,
                        trace_id=trace_id,
                        step_type=step_type,
                        model=getattr(step, "model", None),
                        token_usage=token_usage,
                        latency_ms=latency_ms,
                        status="ok",
                    )
            except Exception as error:
                latency_ms = int((time.time() - step_started_at) * 1000)
                result_event = "tool_result" if step_type == "tool_call" else "llm_result"
                log_event(
                    result_event,
                    run_id=run_id,
                    trace_id=trace_id,
                    step_type=step_type,
                    tool=tool_name,
                    model=getattr(step, "model", None),
                    latency_ms=latency_ms,
                    status="error",
                    error_class=type(error).__name__,
                    error_message=str(error),
                )
                run_status = "error"
                stop_reason = "tool_error" if step_type == "tool_call" else "step_error"
                raise

            if result.is_final:
                stop_reason = "completed"
                break
    finally:
        log_event(
            "run_finished",
            run_id=run_id,
            trace_id=trace_id,
            status=run_status,
            stop_reason=stop_reason,
            total_steps=steps,
            total_latency_ms=int((time.time() - started_at) * 1000),
        )

У production такі події зазвичай відправляються в centralized logging систему (наприклад, ELK, Datadog або ClickHouse) і далі використовуються для побудови dashboard і алертів.

Цього прикладу достатньо, щоб:

  • знайти проблемний tool call;
  • порахувати latency по кроках;
  • зрозуміти, чому run зупинився.

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

JSON
{
  "timestamp": "2026-03-21T15:17:00Z",
  "event": "tool_result",
  "run_id": "run_9fd2",
  "trace_id": "tr_9fd2",
  "tool": "search_docs",
  "latency_ms": 410,
  "status": "ok"
}

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

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

Логується тільки фінальна відповідь

Без проміжних подій не видно, як агент прийшов до результату. У такому режимі навіть простий інцидент розбирається занадто довго.

Немає стабільних ідентифікаторів (run_id, trace_id)

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

Логуються raw prompts або raw args без редагування

Це прямий ризик витоку персональних або чутливих даних. У логах краще зберігати hash, редаговані поля або анонімізовані версії.

Не логуються tool_result і stop_reason

Якщо немає tool_result і stop_reason, складно зрозуміти, що саме зламалось. Такі прогалини часто маскують збій інструмента або ранню фазу спаму інструментами.

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

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

Прогрес: 0/9

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

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

FAQ

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

Q: Що логувати першим, якщо логування майже немає?
A: Почни з бази: run_id, trace_id, run_started, tool_call, tool_result, run_finished, stop_reason. Це вже дає основу для дебагу.

Q: Чи можна логувати prompts повністю?
A: За замовчуванням краще ні. У production prompts часто містять чутливі дані. Безпечніше логувати hash або редаговану версію.

Q: Як зрозуміти, що логування вже достатнє?
A: Якщо ти можеш за 5-10 хвилин відновити послідовність подій одного проблемного run і знайти точку збою, базовий рівень логування вже працює.

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

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

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

Автор

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

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

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


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

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

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