Ідея за 30 секунд
Tool usage metrics показують не «чи працює агент», а як саме він використовує інструменти і де ламається tool-layer.
Вони допомагають зрозуміти, які інструменти викликаються найчастіше, де росте latency, і де починаються повторні або помилкові виклики.
Без цих метрик складно вчасно помітити перевантаження tool-layer і зростання витрат.
Основна проблема
Загальні метрики run не показують, що саме відбувається на рівні інструментів.
Два run можуть мати схожу загальну latency, але в одному випадку проблема у повільному search, а в іншому — у повторних викликах fetch.
Без метрик по tools це важко побачити до інциденту.
Далі розберемо, як читати ці сигнали і знаходити проблеми.
У production це зазвичай виглядає так:
- один tool непомітно стає «гарячою точкою»;
- retries ростуть, але причина неочевидна;
- частина run витрачає забагато кроків саме на інструменти;
- команда бачить проблему тільки коли зростає error rate або бюджет.
Саме тому tool-layer варто моніторити окремо, а не лише через загальні run-метрики.
Як це працює
Метрики використання інструментів (tool usage metrics) будуються навколо подій tool_call і tool_result.
Tool metrics поділяються на:
- infra metrics (
tool_latency_p95,tool_error_rate); - behavior metrics (
repeated_tool_calls,tool_calls_per_run,unique_tools_per_run).
Ці метрики відповідають на питання «як поводиться tool-layer у часі». Логи й трейсинг потрібні, щоб пояснити конкретний проблемний run.
Retries зазвичай виникають не на рівні коду, а на рівні runtime: агент отримує помилку tool як observation і пробує ще раз. Retries — це не просто повторні виклики, а сигнал того, що агент намагається адаптуватись до помилки інструмента.
Типові production-метрики по tools
| Метрика | Що показує | Навіщо потрібна |
|---|---|---|
| tool_calls_total | загальна кількість викликів tools | контроль навантаження на tool-layer |
| tool_calls_per_run | скільки tool calls припадає на один run | виявлення зайвих або циклічних викликів |
| unique_tools_per_run | скільки різних tools використовує run | оцінка складності workflow |
| tool_error_rate | частка помилкових tool calls | раннє виявлення нестабільних інструментів |
| tool_latency_p50 / p95 | типова й «довга» затримка по tools | локалізація повільних залежностей |
| repeated_tool_calls | виклики, що повторюють попередній виклик того самого tool з тими самими args | виявлення спаму інструментами |
| tool_cost_per_run | приблизна вартість tools у межах одного run | контроль бюджету і пошук дорогих інструментів |
Щоб метрики були практичними, їх зазвичай сегментують за tool, release і, за потреби, model.
Важливо: не додавай у labels висококардинальні поля (run_id, request_id, args_hash), інакше сховище метрик швидко перевантажиться.
Як читати tool-layer
Що викликається → як агент поводиться → що відбувається у часі. Це три рівні, які завжди потрібно дивитися разом.
Важливо дивитися на тренди у часі і різницю між релізами, а не на одиничні значення.
Далі дивимось на комбінації сигналів:
tool_error_rate↑ +repeated_tool_calls↑ → tool нестабільний, агент ретраїтьtool_latency_p95↑ +tool_cost_per_run↑ → деградація дорогого інструментаtool_calls_per_run↑ +unique_tools_per_run↑ → надмірна складність workflow
Коли використовувати
Повний набір tool-метрик не завжди потрібен.
Для простого агента з 1-2 інструментами інколи вистачає tool_calls_total і tool_error_rate.
Але детальні метрики використання tools стають критичними, коли:
- агент активно використовує зовнішні API або БД;
- у системі часто виникають retries;
- важливо контролювати витрати на інструменти;
- треба виявляти tool spam до того, як він вплине на користувачів.
Приклад реалізації
Нижче — спрощений приклад інструментації метрик використання tools у стилі Prometheus. Приклад показує базовий контроль: кількість викликів, latency, error classes, повтори й навантаження на run.
import hashlib
import json
import time
from prometheus_client import Counter, Histogram
TOOL_CALL_TOTAL = Counter(
"agent_tool_call_total",
"Total tool calls",
["tool", "status", "release"],
)
TOOL_ERROR_TOTAL = Counter(
"agent_tool_error_total",
"Total tool errors by class",
["tool", "error_class", "release"],
)
TOOL_LATENCY_MS = Histogram(
"agent_tool_latency_ms",
"Tool latency in milliseconds",
["tool", "release"],
buckets=(20, 50, 100, 250, 500, 1000, 2000, 5000),
)
TOOL_CALLS_PER_RUN = Histogram(
"agent_tool_calls_per_run",
"Number of tool calls per run",
["release"],
buckets=(0, 1, 2, 4, 8, 12, 16, 24, 32),
)
UNIQUE_TOOLS_PER_RUN = Histogram(
"agent_unique_tools_per_run",
"Number of unique tools used in run",
["release"],
buckets=(0, 1, 2, 3, 4, 6, 8, 12),
)
REPEATED_TOOL_CALL_TOTAL = Counter(
"agent_repeated_tool_call_total",
"Repeated tool calls with same tool+args signature",
["tool", "release"],
)
TOOL_COST_USD_TOTAL = Counter(
"agent_tool_cost_usd_total",
"Estimated total tool cost in USD",
["tool", "release"],
)
STEP_ERROR_TOTAL = Counter(
"agent_step_error_total",
"Total non-tool step errors by type and class",
["step_type", "error_class", "release"],
)
def stable_hash(value):
# default=str дає базову сумісність;
# у критичних системах краще явна серіалізація (наприклад ISO 8601)
payload = json.dumps(value, sort_keys=True, ensure_ascii=False, default=str).encode("utf-8")
return hashlib.sha256(payload).hexdigest()
def run_agent(agent, task, release="2026-03-21"):
tool_calls = 0
unique_tools = set()
seen_signatures = set()
try:
for step in agent.iter(task):
step_type = step.type
result = None
if step_type != "tool_call":
try:
result = step.execute()
except Exception as error:
STEP_ERROR_TOTAL.labels(
step_type=step_type,
error_class=type(error).__name__,
release=release,
).inc()
raise
if result and result.is_final:
break
continue
tool_name = getattr(step, "tool_name", "unknown")
args = getattr(step, "args", {})
tool_calls += 1
unique_tools.add(tool_name)
signature = (tool_name, stable_hash(args))
if signature in seen_signatures:
REPEATED_TOOL_CALL_TOTAL.labels(tool=tool_name, release=release).inc()
else:
seen_signatures.add(signature)
started_at = time.time()
try:
result = step.execute()
TOOL_CALL_TOTAL.labels(tool=tool_name, status="ok", release=release).inc()
cost_usd = getattr(result, "cost_usd", None)
if cost_usd:
TOOL_COST_USD_TOTAL.labels(tool=tool_name, release=release).inc(cost_usd)
except Exception as error:
TOOL_CALL_TOTAL.labels(tool=tool_name, status="error", release=release).inc()
TOOL_ERROR_TOTAL.labels(
tool=tool_name,
error_class=type(error).__name__,
release=release,
).inc()
# Цей приклад робить raise.
# У реальних агентах помилку часто передають у LLM як observation для retry.
raise
finally:
TOOL_LATENCY_MS.labels(tool=tool_name, release=release).observe(
(time.time() - started_at) * 1000
)
if result and result.is_final:
break
finally:
TOOL_CALLS_PER_RUN.labels(release=release).observe(tool_calls)
UNIQUE_TOOLS_PER_RUN.labels(release=release).observe(len(unique_tools))
# tool_cost_per_run зазвичай рахується на рівні дашборду:
# sum(agent_tool_cost_usd_total) / run_count
Ось як ці метрики виглядають разом у реальному дашборді:
| Tool | calls/min | error_rate | p95 latency | Статус |
|---|---|---|---|---|
| search_docs | 320 | 6.8% | 1.9s | critical: alert |
| fetch_url | 180 | 1.4% | 680ms | warning: p95 росте |
| db_lookup | 95 | 0.3% | 120ms | ok |
Для error_class краще використовувати нормалізований словник значень, щоб уникати зайвої кардинальності.
Investigation
Коли алерт спрацьовує:
- знайти tool з аномалією через метрики;
- подивитись конкретні runs у трейсингу;
- перевірити аргументи й відповіді в логах;
- знайти root cause (tool, логіка агента або зовнішній API).
Типові помилки
Навіть коли tool metrics уже додані, вони часто не працюють як треба через типові помилки нижче.
Є загальна кількість викликів, але немає розбиття по tool
tool_calls_total без розбиття за конкретним інструментом майже не допомагає в інциденті.
У такій ситуації складно швидко знайти джерело збою інструмента.
Не відстежуються повторні виклики
Без метрики repeated_tool_calls важко побачити, що агент викликає той самий tool з тими самими аргументами.
Це часто маскує ранню фазу спаму інструментами.
Немає p95 latency по інструментах
Система може виглядати стабільною, поки частина користувачів уже чекає 5+ секунд.
Для tool-layer мінімум — p50 і p95.
Висококардинальні labels
Додавання run_id, request_id або args_hash у labels швидко перевантажує бекенд метрик.
Краще тримати ці дані в логах, а не в labels.
Немає алертів по tool-layer
Без алертів метрики залишаються пасивною телеметрією. Через це легко пропустити перші сигнали вибуху бюджету через надмірні виклики зовнішніх API.
Самоперевірка
Нижче — короткий checklist базових tool usage metrics перед релізом.
Прогрес: 0/9
⚠ Бракує базової observability
Систему буде складно дебажити в production. Почніть з run_id, structured logs і tracing tool calls.
FAQ
Q: Чим tool usage metrics відрізняються від загальних метрик агента?
A: Загальні метрики показують стан системи в цілому. Tool usage metrics показують, що відбувається саме на рівні інструментів.
Q: Який мінімум tool-метрик потрібен на старті?
A: Почни з tool_calls_total, tool_error_rate, tool_latency_p95 і tool_calls_per_run.
Q: Чи треба додавати args_hash у labels?
A: Ні. Це майже завжди створює високу кардинальність. Для таких даних краще використати структуровані логи.
Q: Як відрізнити разовий збій від системної проблеми tool-layer?
A: Перевір, чи проблема стабільно повторюється для конкретного tool у кількох run і релізах. Якщо повторюються ті самі сигнали (error_class, latency, repeated_tool_calls) — це системна проблема, а не разовий збій.
Пов'язані сторінки
Далі за темою:
- Метрики агентів — загальна модель метрик для агентних систем.
- Логування агентів — події, які потрібні для розбору інцидентів.
- Трейсинг агента — шлях одного run по кроках.
- Семантичне логування агентів — стабільний словник подій для аналітики.
- Моніторинг витрат AI-агентів — контроль витрат у production.