# AI observability: чому агент витратив $8 і все одно викликав не той tool

> Спостережуваність AI-агентів на практиці: анатомія трейсу, correlation IDs, taxonomy збоїв, бюджети токенів, версіонування промптів і vendor-neutral TypeScript-схема трейсингу з middleware.

- Автор: Юра Скиба (https://cookiesoftware.io)
- Опубліковано: 2026-10-01
- Категорія: AI в роботі розробника
- Canonical: https://cookiesoftware.io/blog/ai-agent-observability

---

**Коротка відповідь.** Спостережуваність агента – це не логи «request completed», а трейс усього ланцюжка рішень: кожен виклик моделі й кожен tool call – окремий span зі входом, виходом, токенами, вартістю і версією промпта, зшиті одним correlation ID. Додай taxonomy збоїв (wrong tool, bad args, timeout, hallucinated state), бюджети на сесію і чіткі privacy-межі логування – і «агент знову дивний» перетворюється на конкретний span, де видно, що модель отримала і чому вирішила саме так. Нижче – vendor-neutral схема на TypeScript, яку можна прикрутити до будь-якого стека.

## Чому звичайний APM тут сліпий

Класичний моніторинг бачить: `POST /api/agent` → 200, 14 секунд. Усе «добре». А всередині цих 14 секунд модель викликала пошуковий tool із порожнім запитом, отримала сміття, «виправилась», викликала не той tool, отримала помилку, переформулювала – і видала користувачу впевнену нісенітницю. Шість LLM-викликів, $8 токенів, нуль корисності – і жодного сліду в логах.

Проблема структурна: одиниця роботи агента – не HTTP-запит, а **ланцюжок рішень**. Спостережуваність має відповідати цій одиниці.

## Анатомія трейсу агента

Поняттєвий апарат – стандартний (OpenTelemetry): **trace** – усе опрацювання одного звернення, **span** – окремий крок усередині. Для агента кроки такі:

```
trace: "допоможи оформити повернення замовлення #1234"
├── span: model_call #1        (вирішує: потрібен lookup)
├── span: tool_call orders.get (args: {id:"1234"} → 200, 1.2s)
├── span: model_call #2        (вирішує: створити повернення)
├── span: tool_call refunds.create (→ 422: вікно повернення минуло)
└── span: model_call #3        (формулює відповідь користувачу)
```

Кожен span відповідає на чотири питання: **що отримав** на вході, **що віддав**, **скільки коштував** (токени, мілісекунди, гроші) і **під якими версіями** працював (модель, промпт, схеми tool-ів). Correlation ID пронизує все – від першого повідомлення до останнього tool-а, через усі черги й сервіси.

## Vendor-neutral схема на TypeScript

Мінімальна модель, якої вистачає на 90% питань «що сталося»:

```ts
type AgentSpan = {
  traceId: string;
  spanId: string;
  parentSpanId?: string;
  kind: 'model_call' | 'tool_call';
  name: string;                  // 'claude-...' або 'orders.get'
  startedAt: string;
  durationMs: number;

  // Версії – без них не поясниш «чому вчора працювало»
  versions: {
    model?: string;
    promptId?: string;           // ідентифікатор+версія промпта, НЕ сам текст
    toolSchemaHash?: string;
  };

  usage?: { inputTokens: number; outputTokens: number; costUsd?: number };

  // Вміст – свідомо обрізаний і керований політикою (див. privacy)
  inputDigest: string;           // хеш або санітизований прев'ю
  outputDigest: string;

  outcome: 'ok' | AgentFailure;
};

type AgentFailure = {
  category:
    | 'wrong_tool'          // викликав не той інструмент
    | 'bad_args'            // той, але з невалідними аргументами
    | 'tool_error'          // tool впав сам (5xx, timeout)
    | 'timeout'
    | 'budget_exceeded'     // вибив ліміт токенів/вартості
    | 'hallucinated_state'; // послався на дані, яких не отримував
  detail: string;
};
```

І middleware, який робить трейсинг неуникним – замість «не забудь залогувати»:

```ts
function traced<TArgs, TResult>(
  kind: AgentSpan['kind'],
  name: string,
  fn: (args: TArgs) => Promise<TResult>
) {
  return async (args: TArgs, ctx: TraceContext): Promise<TResult> => {
    const span = startSpan(ctx, { kind, name });
    try {
      const result = await fn(args);
      endSpan(span, { outcome: 'ok', output: result });
      return result;
    } catch (error) {
      endSpan(span, { outcome: classifyFailure(error), output: error });
      throw error;
    }
  };
}

// Кожен tool загортається один раз – і жоден виклик не пройде повз трейс
const getOrder = traced('tool_call', 'orders.get', ordersApi.get);
```

`classifyFailure` – це і є твоя taxonomy в коді: Zod-помилка валідації аргументів → `bad_args`, 5xx від tool-а → `tool_error`, перевищення лічильника → `budget_exceeded`. Категорія `wrong_tool` – єдина, яку не визначиш автоматично в момент виклику: вона ставиться пізніше – ручним розбором або [LLM-judge/eval-ами](/blog/ai-agent-evals), і для швидких типізованих вердиктів тут природно лягає [модель на кшталт Jev](/blog/jev-ai-typesafe).

## Що НЕ можна логувати

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

- Логуй **digest** (хеш + санітизоване прев'ю з редагованими PII), а не сирий вміст.
- Промпт – через `promptId@version`, текст живе у версіонованому сховищі з контрольованим доступом.
- Для глибокого дебагу тримай окремий режим повного семплінгу: явно увімкнений, обмежений у часі, з аудитом доступу.
- Аргументи tool-ів – найпідступніше місце: `orders.get({email})` уже містить PII. Фільтруй по білому списку полів.

## Бюджети, алерти, семплінг

Бюджет – це запобіжник, а не метрика пост-фактум: лічильник токенів/вартості **всередині** циклу агента, який обриває сесію з `budget_exceeded` замість рахунку-сюрпризу наприкінці місяця.

Алерти вішай на taxonomy, а не на «error rate»: сплеск `bad_args` після деплою = зламали схему tool-а; ріст `wrong_tool` після зміни промпта = регресія роутингу; `hallucinated_state` – завжди привід для ручного розбору. Семплінг: 100% для помилок і дорогих сесій, відсоток – для успішних, інакше сховище трейсів з'їсть більше, ніж самі моделі.

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

1. **Логування «на совісті» кожного tool-а.** Один пропущений виклик – і трейс бреше. Middleware робить трейсинг структурно неуникним.
2. **Повний текст розмов у логах.** Див. privacy – це не «потім почистимо», це дизайн-рішення дня першого.
3. **Немає версій промптів.** «Чому вчора працювало?» без `promptId@version` – археологія по git blame.
4. **Метрики без трейсів.** Середня вартість сесії виросла на 40% – а чому? Без span-ів відповіді немає.
5. **Алерт «агент помилився».** Без категорії це шум, який усі навчаться ігнорувати за тиждень.

## Практичне завдання

Візьми один реальний (чи навчальний) агентний флоу і намалюй його очікуваний трейс на папері: усі model/tool span-и, їхні входи-виходи, де межі бюджету. Потім влаштуй йому «поганий день»: уяви по одному збою кожної категорії з taxonomy і перевір – чи дозволить твоя намальована схема за 2 хвилини відрізнити `bad_args` від `tool_error` і `wrong_tool`? Якщо ні – ти щойно знайшов, якого поля бракує в span-і. Це дешевше, ніж знаходити його о другій ночі в production.
