Логи AI‑агентів (що логувати, що редагувати, на що алертити)

Логи, які реально рятують під час інцидентів: trace IDs, події tool calls, stop reasons, редакція, алерти. Є Python + JS приклади.
На цій сторінці
  1. Проблема (чому ти тут)
  2. Чому це ламається в проді
  3. Діаграма: мінімальний пайплайн подій
  4. Реальний код: інструментуй tool gateway (Python + JS)
  5. Реальний фейл (з цифрами)
  6. Компроміси
  7. Коли НЕ робити так
  8. Copy‑paste чекліст
  9. Safe default config (YAML)
  10. Implement in OnceOnly (опційно)
  11. FAQ (3–5)
  12. Пов’язані сторінки (3–6 лінків)

Проблема (чому ти тут)

У dev агент “працює”.

У проді раз на ~200 run’ів стається щось дивне:

  • користувач пише “він відправив не те”
  • витрати стрибають на 15 хвилин
  • він лупиться на flaky API й падає по timeout

А ти маєш… майже нічого:

  • одну “фінальну відповідь”
  • пару випадкових логів
  • інколи помилку tool’а без контексту

І далі починається найгірше: дебаг навмання, але з підключеною кредиткою.

Ця сторінка — як зробити логи так, щоб інциденти знову стали нудними.

Чому це ламається в проді

Агенти ламаються як distributed systems, бо вони ними і є:

  • модель — ненадійний planner
  • tools — це side effects (HTTP/DB/тікети/email)
  • retries + timeouts створюють “магію” з поганим фіналом

Якщо ти не логуваєш луп, ти не відповіси на базові питання інциденту:

  • які tool calls були і в якому порядку?
  • які args (або хоча б args_hash)?
  • що повернув tool (або що ми відредагували)?
  • чому run зупинився (stop_reason)?
  • яка заявка/юзер це тригернув?

Якщо ти не логуваєш stop_reason, ти не “спостерігаєш”. Ти збираєш вайби.

Діаграма: мінімальний пайплайн подій

Реальний код: інструментуй tool gateway (Python + JS)

Почни з межі. Tools — це місце, де живуть витрати й шкода.

Логуємо:

  • run_id, trace_id, tool_name
  • args_hash (raw args за замовчуванням не зберігаємо)
  • latency + статус
  • error_class (нормалізована)

І все ганяємо через gateway, щоб не було “ой, я забув залогувати”.

PYTHON
import hashlib
import json
import time
from dataclasses import dataclass
from typing import Any, Callable, Dict, Optional


def stable_hash(obj: Any) -> str:
  raw = json.dumps(obj, sort_keys=True, ensure_ascii=False).encode("utf-8")
  return hashlib.sha256(raw).hexdigest()


@dataclass(frozen=True)
class RunCtx:
  run_id: str
  trace_id: str
  user_id: Optional[str] = None
  request_id: Optional[str] = None


class Logger:
  def event(self, name: str, fields: Dict[str, Any]) -> None: ...


class ToolGateway:
  def __init__(self, *, impls: dict[str, Callable[..., Any]], logger: Logger):
      self.impls = impls
      self.logger = logger

  def call(self, ctx: RunCtx, name: str, args: Dict[str, Any]) -> Any:
      fn = self.impls.get(name)
      if not fn:
          self.logger.event("tool_call", {
              "run_id": ctx.run_id,
              "trace_id": ctx.trace_id,
              "tool": name,
              "args_hash": stable_hash(args),
              "ok": False,
              "error_class": "unknown_tool",
          })
          raise RuntimeError(f"unknown tool: {name}")

      t0 = time.time()
      self.logger.event("tool_call", {
          "run_id": ctx.run_id,
          "trace_id": ctx.trace_id,
          "tool": name,
          "args_hash": stable_hash(args),
      })

      try:
          out = fn(**args)
          self.logger.event("tool_result", {
              "run_id": ctx.run_id,
              "trace_id": ctx.trace_id,
              "tool": name,
              "latency_ms": int((time.time() - t0) * 1000),
              "ok": True,
          })
          return out
      except TimeoutError:
          self.logger.event("tool_result", {
              "run_id": ctx.run_id,
              "trace_id": ctx.trace_id,
              "tool": name,
              "latency_ms": int((time.time() - t0) * 1000),
              "ok": False,
              "error_class": "timeout",
          })
          raise
      except Exception as e:
          self.logger.event("tool_result", {
              "run_id": ctx.run_id,
              "trace_id": ctx.trace_id,
              "tool": name,
              "latency_ms": int((time.time() - t0) * 1000),
              "ok": False,
              "error_class": type(e).__name__,
          })
          raise
JAVASCRIPT
import crypto from "node:crypto";

export function stableHash(obj) {
const raw = JSON.stringify(obj);
return crypto.createHash("sha256").update(raw).digest("hex");
}

export class ToolGateway {
constructor({ impls = {}, logger }) {
  this.impls = impls;
  this.logger = logger;
}

call(ctx, name, args) {
  const fn = this.impls[name];
  const argsHash = stableHash(args);

  if (!fn) {
    this.logger.event("tool_call", {
      run_id: ctx.run_id,
      trace_id: ctx.trace_id,
      tool: name,
      args_hash: argsHash,
      ok: false,
      error_class: "unknown_tool",
    });
    throw new Error("unknown tool: " + name);
  }

  const t0 = Date.now();
  this.logger.event("tool_call", {
    run_id: ctx.run_id,
    trace_id: ctx.trace_id,
    tool: name,
    args_hash: argsHash,
  });

  try {
    const out = fn(args);
    this.logger.event("tool_result", {
      run_id: ctx.run_id,
      trace_id: ctx.trace_id,
      tool: name,
      latency_ms: Date.now() - t0,
      ok: true,
    });
    return out;
  } catch (e) {
    this.logger.event("tool_result", {
      run_id: ctx.run_id,
      trace_id: ctx.trace_id,
      tool: name,
      latency_ms: Date.now() - t0,
      ok: false,
      error_class: e?.name || "Error",
    });
    throw e;
  }
}

Комбінуй це з:

  • бюджетами (/uk/governance/budget-controls)
  • dedupe проти tool spam (/uk/failures/tool-spam)
  • юніт‑тестами, які фіксують stop reasons (/uk/testing-evaluation/unit-testing-agents)

Реальний фейл (з цифрами)

Ми шипнули “read-only” research агента, який робив http.get.

Одного дня партнерський API почав повертати 200 з error‑payload’ом (так). Наш wrapper трактував “200 == ok” і логував тільки “success”.

Імпакт:

  • ~18% run’ів давали впевнено неправильні підсумки ~2 години
  • ~30 тікетів
  • онкол: ~4 години, щоб довести, що це не “модель галюцинує”

Фікс:

  1. логувати нормалізовану error_class + validation failures
  2. зберігати args_hash + latency, щоб знайти hot spots
  3. алерт: validation_fail_rate > 2% протягом 5 хвилин

Тобі не потрібні ідеальні логи. Тобі потрібні логи, які відповідають “що сталося?” за <10 хвилин.

Компроміси

  • Логувати raw args корисно і водночас це шлях до PII‑ліка. Default: args_hash.
  • Зберігати повні tool results зручно для дебагу і боляче для комплаєнсу. Sampling + редакція.
  • Забагато логів може стати окремим інцидентом. Почни з того, на що ти алертиш.

Коли НЕ робити так

  • Якщо агент бігає тільки локально в довіреному середовищі — можеш бути лінивішим (на якийсь час).
  • Якщо ти щодня ламаєш форму лупа — роби лог легким, але стабільним (IDs + stop reasons).
  • Не пиши свій tracing, якщо ти не готовий його оперувати. Бери щось нудне.

Copy‑paste чекліст

  • [ ] run_id, trace_id, request_id, user_id на кожній події
  • [ ] tool_call + tool_result (name, args_hash, latency, ok, error_class)
  • [ ] stop_reason + бюджети в кінці run’а
  • [ ] policy редакції (PII/secrets) + hash за замовчуванням
  • [ ] алерти: tool calls/run, timeouts, validation fails
  • [ ] одна “incident query” на кожен топ‑фейл (dashboard / saved search)

Safe default config (YAML)

YAML
logging:
  ids:
    run_id: required
    trace_id: required
    request_id: required
  tool_calls:
    enabled: true
    store_args: false
    store_args_hash: true
    store_results: "sampled"
    result_sample_rate: 0.01
  pii:
    redact_fields: ["email", "phone", "token", "authorization", "cookie"]
  stop_reasons:
    enabled: true
alerts:
  tool_calls_per_run_p95: { warn: 10, critical: 20 }
  timeout_rate: { warn: 0.02, critical: 0.05 }
  validation_fail_rate: { warn: 0.02, critical: 0.05 }

Implement in OnceOnly (опційно)

Implement in OnceOnly
Логуй tool calls з args_hash + stop reasons (safe by default).
Use in OnceOnly
# onceonly-python: governed audit logs + metrics
import os
from onceonly import OnceOnly

client = OnceOnly(api_key=os.environ["ONCEONLY_API_KEY"])
agent_id = "support-bot"

# Pull last 50 actions (includes args_hash + decisions)
for e in client.gov.agent_logs(agent_id, limit=50):
    print(e.ts, e.tool, e.decision, e.args_hash, e.spend_usd, e.reason)

# Rollups for dashboards/alerts
m = client.gov.agent_metrics(agent_id, period="day")
print("spend_usd=", m.total_spend_usd, "blocked=", m.blocked_actions)

FAQ (3–5)

Чи треба логувати raw args інструментів?
За замовчуванням — ні. Логуй args_hash + безпечні поля. Raw args вмикай тільки на коротке incident‑вікно з редакцією.
Яке поле найкорисніше?
Стабільний run_id/trace_id на кожній події. Без цього ти не відновиш run.
Як швидко ловити лупи?
Алерти на tool_calls/run і повтори (tool, args_hash) у межах run’а. Плюс stop_reason таксономія.
Чи потрібен distributed tracing?
Якщо tools ходять в інші сервіси — так. Почни з trace_id propagation + кілька span’ів навколо tool calls.

Q: Чи треба логувати raw args інструментів?
A: Default — ні. args_hash + safe поля. Raw args — тільки тимчасово під інцидент, з редакцією.

Q: Яке поле найкорисніше?
A: Стабільний run_id/trace_id на кожній події.

Q: Як швидко ловити лупи?
A: Алерт на tool_calls/run + повтори (tool, args_hash). Далі — /failures/tool-spam.

Q: Чи потрібен distributed tracing?
A: Якщо перетинаєш сервіси — так. Почни з Trace IDs + spans на tool calls.

Пов’язані сторінки (3–6 лінків)

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

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

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

Патерни та рекомендації базуються на постмортемах, режимах відмов і операційних інцидентах у розгорнутих системах, зокрема під час розробки та експлуатації governance-інфраструктури для агентів у OnceOnly.