Ідея за 30 секунд
Семантичне логування (semantic logging) для агентів означає, що події мають не лише JSON-формат, а й стабільний зміст.
Тобто однакові кроки у різних run логуються однаково: той самий event, ті самі ключові поля, ті самі статуси.
Це робить логи придатними для пошуку, алертів, аналітики та debugging у production.
Основна проблема
Багато команд уже пишуть структуровані логи, але цього часто недостатньо.
У різних сервісах та версіях агента одна і та сама подія може мати різні назви й поля:
tool_called, call_tool, tool.invoke.
У результаті логи наче є, але порівнювати run між собою складно.
Семантичне логування — це рішення на рівні дизайну подій, а не лише технічний шар поверх логів.
У production це зазвичай виглядає так:
- запит у лог-систему повертає занадто багато шуму;
- алерти поводяться нестабільно через різні назви подій;
- під час інциденту доводиться вручну зводити події з кількох форматів.
Тому для агентних систем важливі спільний словник подій і стабільна схема полів.
Як це працює
Семантичне логування спирається на три речі:
- узгоджений словник подій (
event taxonomy); - стабільні поля для кожної події;
- нормалізовані значення (
status,error_class,stop_reason).
event taxonomy — це контракт між runtime, логами, дашбордами й алертами.
Порушення цього контракту ламає observability.
status зазвичай має обмежений словник значень (наприклад: ok, error, timeout, cancelled; у цьому прикладі використовується спрощений варіант: ok / error).
Семантичне логування не замінює трейсинг, а доповнює його. Воно робить події не тільки видимими, а й порівнюваними між сервісами та релізами. Логування відповідає на «що сталося», трейсинг — «як це сталося», а semantic logging — «що це означає».
Мінімальний словник подій
| Подія | Семантичний зміст | Ключові поля |
|---|---|---|
| run_started | новий run почався | run_id, trace_id, request_id, task_hash |
| agent_step | агент перейшов до наступного кроку | step_index, step_type, actor |
| tool_call | початок виклику інструмента | tool_name, args_hash |
| tool_result | результат виклику інструмента | tool_name, latency_ms, status, error_class |
| llm_result | результат кроку моделі | model, token_usage, latency_ms, status |
| policy_decision | рішення policy/guardrails | rule_id, decision, reason_code |
| run_finished | run завершився | stop_reason, total_steps, total_latency_ms |
policy_decision дозволяє бачити не лише помилки, а й причини блокування та guardrail-рішень.
event_version дозволяє змінювати схему подій без поломки існуючих дашбордів і алертів.
Коли використовувати
Повне семантичне логування не завжди потрібне.
Для простого single-shot сценарію без tools і без циклу виконання часто вистачає базових логів.
Але семантичне логування стає критичним, коли:
- у системі кілька агентів або сервісів;
- потрібно стабільно будувати алерти та дашборди;
- важливо порівнювати поведінку між релізами;
- інциденти треба розбирати швидко, без ручного мапінгу подій.
Приклад реалізації
Нижче — спрощений приклад семантичного логування у runtime. Ідея проста: ми логуємо тільки події з узгодженого словника і нормалізуємо значення полів.
import hashlib
import json
import logging
import time
import uuid
from enum import StrEnum
logger = logging.getLogger("agent")
class EventName(StrEnum):
RUN_STARTED = "run_started"
AGENT_STEP = "agent_step"
TOOL_CALL = "tool_call"
TOOL_RESULT = "tool_result"
LLM_RESULT = "llm_result"
POLICY_DECISION = "policy_decision"
RUN_FINISHED = "run_finished"
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 normalize_status(ok):
return "ok" if ok else "error"
def normalize_error(error):
if error is None:
return None
return type(error).__name__
def log_semantic(event_name: EventName, **fields):
logger.info(
event_name.value,
extra={
"event": event_name.value,
"event_version": 1,
"timestamp_ms": int(time.time() * 1000),
**fields,
},
)
def run_agent(agent, task, request_id=None):
run_id = str(uuid.uuid4())
trace_id = str(uuid.uuid4())
started_at = time.time()
step_index = 0
stop_reason = "max_steps"
run_status = "ok"
log_semantic(
EventName.RUN_STARTED,
run_id=run_id,
trace_id=trace_id,
request_id=request_id,
task_hash=stable_hash(task),
)
try:
for step in agent.iter(task): # step: reasoning або tool execution
step_index += 1
step_started_at = time.time()
step_type = step.type
tool_name = getattr(step, "tool_name", None)
log_semantic(
EventName.AGENT_STEP,
run_id=run_id,
trace_id=trace_id,
step_index=step_index,
step_type=step_type,
actor=getattr(step, "actor", "agent_runtime"),
)
if step_type == "tool_call":
args = getattr(step, "args", {})
log_semantic(
EventName.TOOL_CALL,
run_id=run_id,
trace_id=trace_id,
step_index=step_index,
tool_name=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_semantic(
EventName.TOOL_RESULT,
run_id=run_id,
trace_id=trace_id,
step_index=step_index,
tool_name=tool_name,
latency_ms=latency_ms,
status=normalize_status(True),
error_class=None,
)
else:
log_semantic(
EventName.LLM_RESULT,
run_id=run_id,
trace_id=trace_id,
step_index=step_index,
model=getattr(step, "model", None),
token_usage=getattr(result, "token_usage", None),
latency_ms=latency_ms,
status=normalize_status(True),
)
# policy_decision фіксується після виконання кроку
# (коли відомий результат або помилка)
if getattr(step, "policy_decision", None) is not None:
decision = step.policy_decision
log_semantic(
EventName.POLICY_DECISION,
run_id=run_id,
trace_id=trace_id,
step_index=step_index,
rule_id=decision.rule_id,
decision=decision.value,
reason_code=decision.reason_code,
)
except Exception as error:
latency_ms = int((time.time() - step_started_at) * 1000)
run_status = "error"
if step_type == "tool_call":
stop_reason = "tool_error"
log_semantic(
EventName.TOOL_RESULT,
run_id=run_id,
trace_id=trace_id,
step_index=step_index,
tool_name=tool_name,
latency_ms=latency_ms,
status=normalize_status(False),
error_class=normalize_error(error),
)
else:
stop_reason = "step_error"
log_semantic(
EventName.LLM_RESULT,
run_id=run_id,
trace_id=trace_id,
step_index=step_index,
model=getattr(step, "model", None),
latency_ms=latency_ms,
status=normalize_status(False),
error_class=normalize_error(error),
)
if getattr(step, "policy_decision", None) is not None:
decision = step.policy_decision
log_semantic(
EventName.POLICY_DECISION,
run_id=run_id,
trace_id=trace_id,
step_index=step_index,
rule_id=decision.rule_id,
decision=decision.value,
reason_code=decision.reason_code,
)
raise
if result.is_final:
stop_reason = "completed"
break
finally:
log_semantic(
EventName.RUN_FINISHED,
run_id=run_id,
trace_id=trace_id,
status=run_status,
stop_reason=stop_reason,
total_steps=step_index,
total_latency_ms=int((time.time() - started_at) * 1000),
)
У production такі події зазвичай відправляються в centralized logging систему (наприклад, ELK, Datadog або ClickHouse), де на їх основі будуються запити, дашборди й алерти.
Наприклад, один semantic event у JSON може виглядати так:
{
"timestamp_ms": 1774106220000,
"event": "policy_decision",
"event_version": 1,
"run_id": "run_9fd2",
"trace_id": "tr_9fd2",
"step_index": 3,
"rule_id": "email_external_domain",
"decision": "deny",
"reason_code": "missing_user_confirmation"
}
Типові помилки
Навіть якщо структуровані логи вже є, семантичне логування часто ламається через типові помилки нижче.
Події названі по-різному в різних сервісах
Коли одна й та сама дія має різні event-назви, запити по логах стають нестабільними.
У результаті складніше вчасно помітити збій інструмента або ранню фазу спаму інструментами.
Вільний текст замість нормалізованих полів
Поля на кшталт "error": "something failed" майже не придатні для аналітики.
Краще мати окремі поля status, error_class, reason_code з фіксованими значеннями.
Немає event_version
Без версії події зміни схеми непомітно ламають дашборди, saved queries й алерти. Тому еволюцію схеми краще робити явно.
Логуються raw prompts або raw args без редагування
Це ризик для безпеки та відповідності вимогам. Безпечніше зберігати hash або анонімізовану версію полів.
Самоперевірка
Нижче — короткий checklist базового семантичного логування перед релізом.
Прогрес: 0/9
⚠ Бракує базової observability
Систему буде складно дебажити в production. Почніть з run_id, structured logs і tracing tool calls.
FAQ
Q: Чим semantic logging відрізняється від звичайного JSON-логування?
A: JSON-логування задає лише формат. Semantic logging задає зміст: стабільні назви подій, однакові поля та нормалізовані значення.
Q: Чи замінює semantic logging трейсинг?
A: Ні. Трейсинг показує шлях виконання, а semantic logging робить події цього шляху зрозумілими для пошуку, алертів і аналітики.
Q: Який мінімум semantic logging потрібен для першого production-релізу?
A: Базовий словник подій (run_started, tool_call, tool_result, run_finished), стабільні run_id/trace_id, status, error_class і stop_reason.
Q: Чи треба одразу переробляти всі старі логи?
A: Ні. Краще почати з нових подій і критичних run-шляхів, а потім поступово мігрувати старі формати.
Пов'язані сторінки
Далі за темою:
- Observability для AI-агентів — загальна модель трейсингу, логування і метрик.
- Логування агентів — які події фіксувати в runtime.
- Трейсинг агента — як бачити повний шлях одного run.
- Розподілений трейсинг агентів — як з'єднувати події між сервісами.
- Дебаг запусків агентів — практичний розбір інцидентів.