1 왜 LLM Agent에 관측성이 더 중요한가
일반 web service의 관측성은 latency·error·throughput에 집중. LLM Agent는 그 위에 더 많은 차원이 있다.
| 일반 web service | LLM Agent 추가 |
|---|---|
| latency | + 단계별 latency (retrieval·rerank·LLM·tool) |
| error rate | + LLM-specific (rate limit·content filter·timeout) |
| throughput | + token throughput·cost throughput |
| - | LLM streaming progress (TTFT·TBT) |
| - | Tool call chain 깊이 |
| - | Prompt version·model version |
| - | Hallucination·refusal 신호 |
| - | Retrieval 품질 (citation count·diversity) |
이 모든 신호가 표준화된 방식으로 수집되어야 디버깅·SLO·optimization이 가능. OpenTelemetry가 그 토대.
2 3 Pillar — Metrics·Logs·Traces
[Metrics] 집계된 시계열 — 추세·SLO·alert
"p95 latency은 시간별 어떻게 변하는가"
"thumbs_up_rate가 일주일간 떨어졌는가"
[Logs] 개별 이벤트 — 디버깅·audit
"이 user가 11:23에 어떤 query를 보냈는가"
[Traces] 한 query의 전체 흐름 — 단계별 latency·실패 위치
"이 한 query가 retrieval에서 4초 걸렸는데 왜?"
세 pillar는 같은 사건의 다른 view — 같은 trace_id로 연결.
2.1 OpenTelemetry 통합
# app/observability/telemetry.py
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.metrics import MeterProvider
tracer = trace.get_tracer("minerva")
meter = metrics.get_meter("minerva")
# Counter — 누적 합 (요청 수·토큰 수·에러 수)
request_counter = meter.create_counter("requests", unit="1")
token_counter = meter.create_counter("tokens", unit="1")
# Histogram — 분포 (latency·token count per query)
latency_histogram = meter.create_histogram("latency_ms", unit="ms")
cost_histogram = meter.create_histogram("cost_per_query_usd", unit="USD")
# Gauge — 현재 값 (active connections·queue depth)
active_runs_gauge = meter.create_up_down_counter("active_runs", unit="1")C20 대화 로깅이 logs 측면, 본 편이 metrics·traces 측면.
3 Tracing — 단계별 흐름
# app/agents/qna_chatbot.py
async def run(query: Query) -> Response:
with tracer.start_as_current_span("agent.run") as root:
root.set_attribute("agent_id", "qna_chatbot")
root.set_attribute("user_id", query.user_id)
root.set_attribute("intent", query.intent)
with tracer.start_as_current_span("retrieval") as span:
docs = await retriever.search(query.text)
span.set_attribute("retrieval.count", len(docs))
with tracer.start_as_current_span("rerank") as span:
ranked = await reranker.rerank(query.text, docs)
span.set_attribute("rerank.model", "bge-large")
with tracer.start_as_current_span("llm.complete") as span:
response = await llm.complete(prompt(query, ranked))
span.set_attribute("llm.model", response.model)
span.set_attribute("llm.input_tokens", response.usage.input_tokens)
span.set_attribute("llm.output_tokens", response.usage.output_tokens)
span.set_attribute("llm.cost_usd", response.cost_usd)
return response각 단계가 별도 span — 단계별 latency·attribute가 trace에 기록. Jaeger·Tempo·Datadog APM 같은 도구로 시각.
3.1 Trace 분석 예
trace_id: abc-123
└─ agent.run [3.2s] {agent: qna_chatbot, user: u-42}
├─ retrieval [120ms] {count: 25}
├─ rerank [80ms] {model: bge-large}
└─ llm.complete [2.9s] {model: gpt-4o, in: 1500, out: 320, cost: 0.018}
└─ http.request [2.85s] {host: api.openai.com}
이 시각화로 즉시 — llm.complete가 90% 시간 차지, retrieval은 빠름, 최적화는 LLM 단계에 집중해야 한다는 결론.
4 LLM 특화 메트릭
4.1 시간 지표
# Time To First Token (TTFT) — 사용자 대기 체감
ttft_histogram = meter.create_histogram("ttft_ms", unit="ms")
# Time Between Tokens (TBT) — streaming 흐름
tbt_histogram = meter.create_histogram("tbt_ms", unit="ms")
# 단계별
stage_latency = meter.create_histogram("stage_latency_ms", unit="ms")
# attributes: {stage: "retrieval"|"rerank"|"llm"|"tool"|"output_guard"}TTFT는 사용자 체감 latency — 응답이 시작되는 시점. 전체 latency보다 중요한 경우 많음.
4.2 Token·Cost
# 입력·출력 분리
input_tokens = meter.create_counter("llm_input_tokens", unit="1")
output_tokens = meter.create_counter("llm_output_tokens", unit="1")
# 모델·세그먼트별 attribute
input_tokens.add(1500, {"model": "gpt-4o", "agent": "qna", "segment": "rnd"})cardinality 위험 — segment·user 같은 high-cardinality attribute는 별도 분석 storage로 (Prometheus는 cardinality에 민감).
4.3 Tool Call
tool_call_counter = meter.create_counter("tool_calls", unit="1")
tool_latency_histogram = meter.create_histogram("tool_latency_ms", unit="ms")
with tracer.start_as_current_span("tool.search") as span:
span.set_attribute("tool.name", "search")
span.set_attribute("tool.args", str(args)[:500]) # truncate
result = await tool.execute(args)
span.set_attribute("tool.success", True)C24 하네싱의 audit log와 같은 데이터 — observability는 metric·trace 형태.
4.4 LLM-specific Errors
# rate_limit·content_filter·timeout·context_too_long 별도
llm_error_counter = meter.create_counter("llm_errors", unit="1")
llm_error_counter.add(1, {"type": "rate_limit", "provider": "openai"})C25 Circuit Breaker trigger 결정의 토대.
5 Logs — Structured + Correlation
C20 대화 로깅을 OpenTelemetry로 emit:
import logging
from opentelemetry.instrumentation.logging import LoggingInstrumentor
LoggingInstrumentor().instrument(set_logging_format=True)
# → 모든 log에 trace_id·span_id 자동 주입
logger = logging.getLogger(__name__)
async def handle_query(query: Query):
logger.info("query received", extra={
"user_id": query.user_id,
"intent": query.intent,
"agent": "qna_chatbot",
})
# → trace_id가 자동 포함되어 trace 시각화에서 직접 jump 가능Engineering: structured logging 패턴 그대로 + trace_id 통합.
6 Sampling 전략
trace 100% 저장은 storage·비용 폭발. 대표성을 보장하면서 줄여야.
6.1 Head Sampling (시작 시점)
# 모든 trace 중 1%만 저장
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
sampler = TraceIdRatioBased(0.01)강점: 단순·결정적. 약점: 드문 사건 (에러·slow query)을 놓침.
6.2 Tail Sampling (종료 후)
전체 trace 받은 후 — 룰로 어느 것을 저장할지 결정:
def tail_sampler(trace) -> bool:
if trace.has_error:
return True # 모든 에러 trace 저장
if trace.duration_ms > 5000:
return True # slow trace 저장
if trace.user_segment == "vip":
return True # VIP 100%
return random.random() < 0.05 # 일반 5%강점: 의미 있는 trace 누락 X. 약점: 모든 trace를 일단 buffer해야 — 메모리 비용.
OpenTelemetry Collector가 tail sampler 구현 제공 — 별도 설치.
6.3 우선순위 Sampling
| 우선순위 | 저장률 |
|---|---|
| Error | 100% |
| Slow (> p95) | 100% |
| Hallucination flag | 100% |
| Bandit canary arm | 50% |
| 일반 active | 1~5% |
| Health check | 0% |
7 SLO·SLI·Error Budget
# config/slo.yaml
slos:
- name: availability
sli: requests_success_rate
target: 99.9 # 월 다운 ≤ 43분
window: 30d
- name: latency
sli: p95_e2e_latency_ms
target: 3000
window: 7d
- name: quality
sli: thumbs_up_rate
target: 0.55
window: 7d
- name: cost
sli: avg_cost_per_query_usd
target: 0.025
window: 30d7.1 Error Budget Burn Rate
budget_remaining = (1 - target/100) - (1 - actual/100)
= actual/100 - target/100
burn_rate = (recent_error_rate / (1 - target/100))
| burn rate | 의미 | 알람 |
|---|---|---|
| < 1 | 정상 흐름 | 없음 |
| 1~6 (24h) | 매월 지킬 수 있음 | warning |
| 6~14 (6h) | 매주 budget 소진 | critical (slack) |
| > 14 (1h) | 즉시 page | on-call page |
C25 alert 연동 그대로 — burn rate가 임계 위반 시 자동 page.
8 Dashboard 설계
8.1 4 Golden Signals (Google SRE)
1. Latency — p50·p95·p99
2. Traffic — requests per second
3. Errors — error rate (%)
4. Saturation — concurrent runs / capacity
8.2 LLM Extras
5. Token Usage — input·output tokens per second
6. Cost Rate — USD per minute, per user
7. TTFT — p50·p95
8. Citation Rate — 응답에 citation 포함 비율
9. Refusal Rate — refusal·content_filter %
10. Quality — thumbs_up_rate (실시간 7일 rolling)
8.3 Drilldown
[Top-level]
├── 4 Golden Signals (전체 · 최근 1h)
├── LLM 6 Extras
├── Active Experiments (C19) 상태
├── Bandit Arms (C16) 분포
└── SLO Burn Rate (모든 SLO)
[Per-Agent]
├── 같은 메트릭 + agent_id 필터
└── 단계별 latency breakdown
[Per-User] (PII 권한 있는 사람만)
├── 개별 사용자 query·thumbs·cost
└── (디버깅·incident 대응)
운영팀은 [Top-level]만 매일, [Per-Agent]는 alert 발생 시 drilldown.
9 Storage·Backend 선택
| 데이터 | 권장 backend |
|---|---|
| Metrics | Prometheus·Mimir·Datadog·Cloud Monitoring |
| Logs | Loki·Elasticsearch·CloudWatch·Datadog Logs |
| Traces | Tempo·Jaeger·Datadog APM·Honeycomb |
| Long-term | S3·BigQuery·ClickHouse (cold storage) |
OpenTelemetry는 SDK 표준 — backend는 Collector를 통해 어느 것이든 라우팅 가능.
10 C25·C33과 결합
10.1 C25 Kill Switch trigger
burn rate 임계 위반 시 — 자동으로 C25 Kill Switch trigger:
10.2 C33 지식 품질 결합
C33 5종 신호도 metrics로 emit:
gap_counter = meter.create_counter("knowledge_coverage_gaps")
freshness_gauge = meter.create_gauge("knowledge_stale_active_count")운영 dashboard에 같이 표시 — 응답 품질·지식 상태가 같은 view.
11 비용·성능 trade-off
| 결정 | 비용 | 가시성 |
|---|---|---|
| 100% trace 저장 | 매우 높음 | 완전 |
| Head 1% sampling | 낮음 | 일반 패턴만 |
| Tail sampling (error+slow+VIP) | 중간 | 의미 있는 케이스 100% |
| Metrics + 1% trace | 낮음~중간 | 통계 정확, drilldown 부족 |
MINERVA 권장: Tail sampling + Metrics 풀 — 비용·가시성 균형.
12 MINERVA 적용
app/observability/
├── tracing.py # OpenTelemetry tracer 초기화
├── metrics.py # counter·histogram·gauge 정의
├── logs.py # structured logging + trace_id 주입
├── llm_attrs.py # LLM-specific span attributes
├── sampler.py # tail sampling 룰
├── slo.py # SLO·SLI·burn rate 계산
└── dashboards/ # Grafana·Datadog 정의 (코드로 관리)
scripts/
├── slo_check.py # SLO·burn rate 계산 (cron)
├── trace_replay.py # 특정 trace 재현 (디버깅)
└── cost_audit.py # 비용 dashboard 생성
infra/
├── otel-collector.yaml # Collector 설정
├── prometheus.yaml # metrics scrape
├── loki.yaml # log aggregation
└── tempo.yaml # trace storage
Engineering: structured logging·Docker Compose 패턴이 토대 — OpenTelemetry stack 운영.
13 자주 발생하는 함정
13.1 Cardinality Explosion
user_id를 metric label로 → 사용자 수만큼 시계열 폭증 → Prometheus 메모리 cap.
해법: - high-cardinality는 trace·log에만 (metric에는 X) - segment·percentile 같은 aggregated label만 metric에 - Prometheus relabel rule로 cap 강제
13.2 Sampling Bias
head sampling 1%로 데이터 분석 → drift 못 잡거나 false signal.
해법: - 분석은 sampled metric이 아니라 logs (raw) 또는 BigQuery·ClickHouse - 의사결정용은 100% logs, dashboard는 sampled trace
13.3 Trace Storage Cost
trace 100% 저장 → 월 수천 USD. 그러나 1% sampling은 incident 디버깅에 부족.
해법: - Tail sampling (error·slow·VIP 100%, 일반 1%) - 일반 trace TTL 7일, 의미 있는 trace 30일·90일 - 비용 dashboard로 추세 모니터링
13.4 Alert Noise
모든 메트릭에 alert → 매일 10건 → 운영팀 무시.
해법: - Burn rate 기반 (특정 임계값보다 우선) - 등급별 채널 (warning은 daily digest, critical만 page) - alert 처리율 (acknowledge·resolved) 분석 — 무시율 ↑ 시 임계 재평가
13.5 Trace Context Loss
비동기 task·subprocess에서 trace 끊김 → 단편 데이터.
해법: - opentelemetry.context로 명시적 propagate - asyncio.create_task 시 context 전달 - Celery·Kafka 등 메시지 시스템에 trace_id header inject
13.6 Local Dev vs Production 분리
개발자 local에 production OTel endpoint 연결 → noise·비용.
해법: - OTEL_EXPORTER_OTLP_ENDPOINT env로 분리 (local은 console exporter) - staging·production tag 명시
13.7 메트릭 의존 의사결정
dashboard만 보고 의사결정 → 깊이 부족·context 결여.
해법: - 알람 발생 시 즉시 trace drilldown - weekly 회고에 trace·log sample 포함 - 신호 합산 점수(C33 패턴)로 단일 메트릭 의존 회피
14 정리
| 영역 | 핵심 |
|---|---|
| 3 Pillar | Metrics·Logs·Traces — 같은 사건의 다른 view |
| OpenTelemetry | SDK 표준, backend 무관 |
| LLM 특화 | TTFT·token·tool call·hallucination·refusal·quality |
| Sampling | Head(빠름·드문 사건 누락) vs Tail(meaningful 100%) |
| SLO·burn rate | 4 단계 etl(< 1·16·614·>14) |
| Dashboard | 4 Golden + LLM 6 Extras + drilldown |
| 결합 | C25 Kill Switch trigger·C33 지식 품질 통합 view |
| 함정 | cardinality·sampling bias·trace cost·alert noise·context loss |
15 응용 분야
| 시나리오 | 핵심 신호 |
|---|---|
| 단계별 latency 최적화 | trace 단계별 span |
| LLM provider 장애 대응 | error counter + circuit breaker trigger |
| 비용 spike 추적 | cost histogram + segment label |
| Hallucination 회귀 | hallucination flag + trace replay |
| 신규 모델 도입 영향 | model attribute로 비교 |
| Bandit 분포 검증 | arm label로 metric 분리 |
| Incident 사후 분석 | trace replay + log search |
16 관련 주제
선행 학습 (선수)
- C20 대화 로깅 — log 측면 토대
- C24 하네싱 audit — 결정 audit이 trace로 emit
- C25 실행 제어 SLO — burn rate alert 결합
- Engineering: Python structured logging — log 구현 토대
18-LangGraph 시리즈 cross-reference
- (Phase C-9는 일반 관측성 패턴 — LangGraph 특정 cross-ref 작음)
후속 (Phase C-9)
- C35 비용 최적화 — token·cost dashboard 활용
- C36 보안·접근 제어 — audit log·trace 보존 정책
Cross-reference
- 08-1 streaming observability — TTFT·TBT 측정 토대
- C19 실험 파이프라인 — 모니터링 결정 SLO 통합
- C33 지식 품질 — 5종 신호도 metric으로 emit
- Engineering: Docker Compose — OTel stack 운영