관측성과 디버깅

에이전트가 “왜 이렇게 답했는가” 를 추적할 수 없으면 운영은 불가능하다. , , 가 어둠을 밝힌다.


관측성의 세 기둥

은 단순한 모니터링이 아니다. 시스템 내부 상태를 외부 신호로부터 추론할 수 있는 성질이다. 메트릭·· 세 기둥 위에 선다. 에이전트에선 트레이스가 압도적으로 중요하다.

“왜” 를 묻는 순간, 트레이스가 없으면 손가락이 멈춘다.

이 챕터는 트레이스 중심으로 본다.


트레이스와 스팬

는 한 요청이 시스템 안에서 만들어내는 작업의 트리다. 은 그 트리의 노드 하나 — 시작 시각, 끝 시각, 속성을 가진다. 가 트레이스를 외부 서비스까지 잇는다.

다이어그램 로딩…

에이전트 한 사이클은 보통 LLM 호출 + 도구 호출 + 메모리 조회로 3~5개 스팬을 만든다.


LangSmith — 가장 빠른 시작

는 환경변수만 켜면 LangChain·LangGraph 호출을 자동 로 잡아준다. 임의 함수는 @traceable 로 감싼다. 트리는 웹 UI에서 그대로 펼쳐진다.

# Verified against: https://docs.langchain.com/langsmith/observability-quickstart
# Verified at: 2026-06-02
# LANGSMITH_TRACING=true, LANGSMITH_API_KEY=... 필요
from openai import OpenAI
from langsmith.wrappers import wrap_openai
from langsmith import traceable

client = wrap_openai(OpenAI())

@traceable(run_type="tool")
def get_context(q: str) -> str:
  return "사용자는 한국어를 선호한다"

@traceable
def assistant(q: str) -> str:
  ctx = get_context(q)
  r = client.chat.completions.create(
      model="gpt-4o-mini",
      messages=[
          {"role": "system", "content": "컨텍스트: " + ctx},
          {"role": "user", "content": q},
      ],
  )
  return r.choices[0].message.content

print(assistant("어떤 언어로 답해야 하나요?"))

Langfuse — 오픈소스 대안

는 자체 호스팅 가능한 오픈소스다. Python에서는 get_client()로 받아 컨텍스트 매니저로 을 시작한다. 데이터를 자기 인프라에 보관해야 하는 팀에 적합하다.

# Verified against: https://langfuse.com/docs/get-started
# Verified at: 2026-06-02
# LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY 필요
from langfuse import get_client

langfuse = get_client()

with langfuse.start_as_current_observation(as_type="span", name="agent-cycle") as span:
  with langfuse.start_as_current_observation(
      as_type="generation",
      name="llm-call",
      model="claude-sonnet-4-6",
  ) as gen:
      # 실제로는 Anthropic 호출
      gen.update(output="한국어로 답해드리겠다")

langfuse.flush()

OpenTelemetry — 벤더 중립

는 표준이다. 에이전트 자체 과 외부 의존성(DB, HTTP)을 한 로 묶고, 익스포터만 바꿔 LangSmith·Langfuse·Jaeger 어디로든 보낸다.

# Verified against: https://opentelemetry.io/docs/languages/python/getting-started/
# Verified at: 2026-06-02
# pip install opentelemetry-distro && opentelemetry-bootstrap -a install
from opentelemetry import trace

tracer = trace.get_tracer("agent.tracer")

def call_llm(prompt: str) -> str:
  with tracer.start_as_current_span("llm-call") as span:
      span.set_attribute("model", "claude-sonnet-4-6")
      span.set_attribute("prompt.len", len(prompt))
      # 실제 LLM 호출
      return "답변"

with tracer.start_as_current_span("agent-cycle"):
  call_llm("안녕")

트레이스 한 사이클을 보는 법

한 에이전트 사이클을 로 분해하면 단위의 단계가 보인다. 의 가장 직접적인 효용이다.

각 스팬의 시간을 보면 병목이 어디 있는지가 즉시 드러난다. 가 없으면 평균 지연만 보고 추측해야 한다.


구조화 로그

자유 텍스트 로그는 검색이 어렵다. 는 JSON으로 찍어 필드 기반 쿼리를 허용한다. 를 모든 로그 줄에 박아 와 로그를 잇는 게 핵심이다.

# Verified against: structlog 표준 패턴
# Verified at: 2026-06-02
import structlog

log = structlog.get_logger()
log.info("agent_step", trace_id="abc123", step="llm_call",
       model="claude-sonnet-4-6", tokens_in=120, tokens_out=80)

디버깅 워크플로

이슈가 들어오면 검색이 첫걸음이다. 로 사용자 입력에서 응답까지의 모든 스팬을 한 화면에 펼친다. 로 누가·언제·무엇을 호출했는지가 같이 보여야 한다.

다이어그램 로딩…

스팬을 시간순으로 정렬하면 “여기서 5초가 사라졌다” 가 시각적으로 드러난다.


리플레이

는 과거 를 그대로 다시 실행하는 기능이다. 입력·모델·시드를 재현해 회귀 여부를 본다. 으로 묶어 자동화하는 게 다음 장의 주제다.

운영의 대부분은 “다시 한 번 돌려보기” 다. 리플레이가 없으면 그게 안 된다.

리플레이 가능한 시스템을 만드는 핵심은 입력을 결정적으로 저장하는 것이다.


비용 가시성

에 토큰 수와 비용을 속성으로 박아두면, 한 사용자·기능별 비용을 분 단위로 본다. 의 부산물 중 가장 직접적인 가치다. 에 입출력 토큰을 매번 찍는 습관을 들인다.


다음 장으로

트레이스로 보았다면, 그것을 점수로 바꾸는 게 평가다. 다음 장은 ··로 회귀 테스트를 자동화한다.