관측성과 디버깅
에이전트가 “왜 이렇게 답했는가” 를 추적할 수 없으면 운영은 불가능하다. , , 가 어둠을 밝힌다.
관측성의 세 기둥
은 단순한 모니터링이 아니다. 시스템 내부 상태를 외부 신호로부터 추론할 수 있는 성질이다. 메트릭·· 세 기둥 위에 선다. 에이전트에선 트레이스가 압도적으로 중요하다.
“왜” 를 묻는 순간, 트레이스가 없으면 손가락이 멈춘다.
이 챕터는 트레이스 중심으로 본다.
트레이스와 스팬
는 한 요청이 시스템 안에서 만들어내는 작업의 트리다. 은 그 트리의 노드 하나 — 시작 시각, 끝 시각, 속성을 가진다. 가 트레이스를 외부 서비스까지 잇는다.
에이전트 한 사이클은 보통 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초가 사라졌다” 가 시각적으로 드러난다.
리플레이
는 과거 를 그대로 다시 실행하는 기능이다. 입력·모델·시드를 재현해 회귀 여부를 본다. 으로 묶어 자동화하는 게 다음 장의 주제다.
운영의 대부분은 “다시 한 번 돌려보기” 다. 리플레이가 없으면 그게 안 된다.
리플레이 가능한 시스템을 만드는 핵심은 입력을 결정적으로 저장하는 것이다.
비용 가시성
에 토큰 수와 비용을 속성으로 박아두면, 한 사용자·기능별 비용을 분 단위로 본다. 의 부산물 중 가장 직접적인 가치다. 에 입출력 토큰을 매번 찍는 습관을 들인다.
다음 장으로
트레이스로 보았다면, 그것을 점수로 바꾸는 게 평가다. 다음 장은 ··로 회귀 테스트를 자동화한다.