Коротка відповідь. Спостережуваність агента – це не логи «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% питань «що сталося»:
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, який робить трейсинг неуникним – замість «не забудь залогувати»:
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-ами, і для швидких типізованих вердиктів тут природно лягає модель на кшталт Jev.
Що НЕ можна логувати
Найкоротший розділ і найдорожчий у разі ігнорування. У вмісті розмов живуть email-и, токени, медичні подробиці – і повний текст діалогу в логах перетворює твою систему спостережуваності на найбільший витік компанії.
- Логуй digest (хеш + санітизоване прев'ю з редагованими PII), а не сирий вміст.
- Промпт – через
promptId@version, текст живе у версіонованому сховищі з контрольованим доступом. - Для глибокого дебагу тримай окремий режим повного семплінгу: явно увімкнений, обмежений у часі, з аудитом доступу.
- Аргументи tool-ів – найпідступніше місце:
orders.get({email})уже містить PII. Фільтруй по білому списку полів.
Бюджети, алерти, семплінг
Бюджет – це запобіжник, а не метрика пост-фактум: лічильник токенів/вартості всередині циклу агента, який обриває сесію з budget_exceeded замість рахунку-сюрпризу наприкінці місяця.
Алерти вішай на taxonomy, а не на «error rate»: сплеск bad_args після деплою = зламали схему tool-а; ріст wrong_tool після зміни промпта = регресія роутингу; hallucinated_state – завжди привід для ручного розбору. Семплінг: 100% для помилок і дорогих сесій, відсоток – для успішних, інакше сховище трейсів з'їсть більше, ніж самі моделі.
Типові помилки
- Логування «на совісті» кожного tool-а. Один пропущений виклик – і трейс бреше. Middleware робить трейсинг структурно неуникним.
- Повний текст розмов у логах. Див. privacy – це не «потім почистимо», це дизайн-рішення дня першого.
- Немає версій промптів. «Чому вчора працювало?» без
promptId@version– археологія по git blame. - Метрики без трейсів. Середня вартість сесії виросла на 40% – а чому? Без span-ів відповіді немає.
- Алерт «агент помилився». Без категорії це шум, який усі навчаться ігнорувати за тиждень.
Практичне завдання
Візьми один реальний (чи навчальний) агентний флоу і намалюй його очікуваний трейс на папері: усі model/tool span-и, їхні входи-виходи, де межі бюджету. Потім влаштуй йому «поганий день»: уяви по одному збою кожної категорії з taxonomy і перевір – чи дозволить твоя намальована схема за 2 хвилини відрізнити bad_args від tool_error і wrong_tool? Якщо ні – ти щойно знайшов, якого поля бракує в span-і. Це дешевше, ніж знаходити його о другій ночі в production.