Ідея за 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_started | run_id, trace_id, request_id, user_id |
| agent_step | step_type, step_index, tool |
| tool_call | tool_name, args_hash |
| tool_result | tool_name, latency_ms, status, error_class |
| llm_result | model, token usage, latency_ms, status |
| run_finished | stop_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 деталізують окремо початок і результат виклику інструмента.
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-логу може виглядати так:
{
"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 і знайти точку збою, базовий рівень логування вже працює.
Пов'язані сторінки
Далі за темою:
- Observability для AI-агентів — загальна модель traces, logs і metrics.
- Трейсинг агента — як бачити шлях одного run по кроках.
- Розподілений трейсинг агентів — як з'єднувати події між сервісами.
- Дебаг запусків агентів — практичний розбір інцидентів.
- Метрики агентів — які показники потрібні для стабільної експлуатації.