본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 06. 14:35

에이전트 시리즈 (14): 에이전트 관측성(Agent Observability) — 모든 의사결정 추적 및 블랙박스 투명화

요약

에이전트의 의사결정 과정을 가시화하는 '에이전트 관측성(Agent Observability)'의 중요성과 구현 방법을 다룹니다. LangChain의 BaseCallbackHandler를 활용하여 LLM 호출, 도구 사용, 지연 시간을 추적하는 구체적인 기술적 접근법을 제시합니다.

핵심 포인트

  • 에이전트의 블랙박스 문제를 해결하기 위한 관측성 개념 정의
  • 개발, 분석, 운영 단계별 세 가지 관측성 수준 제시
  • LangChain의 BaseCallbackHandler를 이용한 트레이서 구현 방법
  • ReAct 에이전트의 도구 호출 시 발생하는 특이사항 설명

에이전트 블랙박스 (The Agent Black Box)

에이전트에게 요청을 보냅니다. 6초 후, 답변을 받습니다.

그 6초 동안 무슨 일이 일어났을까요?

  • LLM은 몇 번이나 생각했을까요?
  • 어떤 도구(tools)가 어떤 인자(arguments)와 함께 호출되었고, 무엇을 반환했을까요?
  • 지연 시간(latency)은 실제로 어디에서 발생했을까요?
  • 만약 답변이 틀렸다면, 어느 단계에서 문제가 발생했을까요?

단순한 agent.invoke()는 아무것도 알려주지 않습니다. 에이전트 관측성(Agent observability)은 이 문제를 해결합니다. 즉, 모든 의사결정과 그 비용을 가시화합니다.

세 가지 관측성 수준 (Three Observability Levels)

개발 (Development)   →  라이브 트레이스 (Live Trace)        (무슨 일이 일어나고 있는지 확인)
분석 (Analysis)      →  지연 시간 타임라인 (Latency Timeline)  (시간이 어디에 쓰이는지 확인)
운영 (Production)    →  감사 로그 (Audit Log)               (각 요청을 기록하고 재현 가능하게 함)

이 세 가지 패턴은 모두 하나의 기반을 공유합니다: LangChain의 BaseCallbackHandler입니다.

핵심: AgentTracer

BaseCallbackHandler는 LLM 호출 및 도구 호출(tool invocations)의 전체 생명주기에 대한 훅(hooks)을 제공합니다:

from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.outputs import LLMResult

...

중요한 세부 사항 하나: 채팅 모델(chat models)은 on_llm_start가 아니라 on_chat_model_start를 실행합니다. 모든 모델 유형을 완벽하게 커버하려면 두 가지를 모두 구현하십시오.

호출 시 config를 통해 트레이서(tracer)를 전달합니다:

tracer = AgentTracer(verbose=True)
result = agent.invoke(
    {"messages": [HumanMessage(query)]},
...

데모 1: 라이브 트레이스 (Live Trace)

질의: "베이징과 상하이의 날씨는 어떠한가요? 온도 차이를 계산해 주세요."

실제 출력:

  [LLM →] reasoning...
  [LLM ←] 1544ms  |                              ← 도구 호출 단계, 내용(content) 비어 있음
  [TOOL→] get_weather({'city': 'Beijing'})
...

중간 LLM 단계에서 output_preview가 비어 있는 이유는 무엇인가요?

ReAct 에이전트에서 LLM이 도구를 호출하기로 결정할 때마다, 텍스트가 아닌 tool_calls JSON을 출력합니다. 해당 JSON은 LangGraph 프레임워크에 의해 소비되며 content 텍스트 필드에는 나타나지 않습니다. 오직 마지막 LLM 단계(모델이 도구 호출을 중단하고 직접 답변하기로 결정하는 시점)에서만 텍스트 내용이 생성됩니다.

이것이 바로 트레이스 (trace)의 가치입니다. 트레이스가 없다면 여러분은 단지 "8초가 걸렸다"는 사실만 알게 됩니다. 하지만 트레이스가 있다면 4번의 LLM 호출과 3번의 도구 호출(tool calls)이 있었으며, 어떤 추론 단계(reasoning step)에서 가장 오랜 시간이 걸렸는지 알 수 있습니다.

데모 2: 단계별 타임라인 (Step Timeline)

쿼리: "WonderBot Pro의 가격을 알려주고, 연간 비용을 위해 299 * 12를 계산해줘."

백그라운드에서 실행한 후, tracer.steps를 분석하여 ASCII 타임라인을 생성합니다:

total_ms  = sum(s.duration_ms for s in tracer.steps)
bar_scale = 40 / total_ms

...

실제 출력:

단계별 상세 내역:

  단계 1  LLM 추론             [  2409ms]  ██████████████
...

LLM = 100%. 도구 (Tools) = 0%.

이것은 에이전트 성능 최적화를 위한 가장 중요한 통찰입니다: 도구 호출은 본질적으로 비용이 거의 들지 않습니다 (2ms). 모든 지연 시간(latency)은 LLM에서 발생합니다. 에이전트의 속도를 높이고 싶다면 LLM 호출 횟수를 줄여야 합니다. 즉, 추론 단계(reasoning steps)를 줄이거나, 요청을 병합하거나, 캐시된 응답(cached responses)을 사용하는 식입니다. 도구 구현을 최적화하는 것은 거의 아무런 도움이 되지 않습니다.

데모 3: 구조화된 감사 로그 (Structured Audit Log)

쿼리: "심천(Shenzhen)의 날씨는 어떻고, WonderBot Basic의 가격은 얼마인가요?"

실행 후, tracer.steps를 JSON으로 직렬화(serialize)합니다:

def build_audit_log(tracer: AgentTracer, query: str, answer: str) -> dict:
    steps_log = []
    for s in tracer.steps:
...

실제 출력:

{
  "trace_id": "6c8e4b64",
  "query": "What's the weather in Shenzhen and how much does WonderBot Basic cost?",
...

단계 4를 살펴보십시오: get_product_info{'product_name': 'Basic'}을 전달받았습니다. LLM이 "wonderbot" 접두사 없이 "Basic"만 전달했기 때문에 조회에 실패한 것입니다. 감사 로그(audit log)는 가공되지 않은 도구 인자(raw tool arguments)를 포착했습니다. 로그가 없었다면 최종 답변에서 보이는 것은 왜 그런지 알 수 없는 "사용할 수 없음"이라는 문구뿐이었을 것입니다. 로그가 있다면 LLM이 어떤 파라미터(parameter)를 전달하기로 선택했는지 정확히 확인할 수 있습니다.

이것이 바로 감사 로그가 단순히 도구 이름뿐만 아니라 도구 입력값(tool inputs)까지 기록해야 하는 이유입니다.

패턴 비교 (Pattern Comparison)

패턴 (Pattern)         사용 사례 (Use case)           성능 영향 (Perf impact)   출력 (Output)
──────────────────────────────────────────────────────────────────────
Live trace          개발, 디버깅 (Debugging)        있음 (I/O)               콘솔 이벤트 스트림 (Console event stream)
...

세 가지 패턴 모두 하나의 AgentTracer를 공유합니다. 개발 중 실시간 출력을 확인하려면 verbose=True로 설정하고, 프로덕션 (Production) 환경에서 조용히 기록하려면 verbose=False로 설정하세요.

디자인 체크리스트 (Design Checklist)

트레이서 구현 (Tracer Implementation)

  • on_llm_start뿐만 아니라 on_chat_model_start도 구현할 것 (더 안전한 방법: 둘 다 구현)
  • on_tool_start에서 도구 이름과 입력값을 저장하고, on_tool_end에서 저장된 해당 값들을 사용할 것
  • on_end에서 차이(delta)를 계산하는 대신 start_time / end_time을 사용할 것 — 중첩 호출 (Nested calls) 지원을 위함
  • 쉬운 직렬화 (Serialization) 및 후속 분석을 위해 StepRecord를 데이터 클래스 (Dataclass)로 만들 것

감사 로그 (Audit Log)

  • 요청당 고유한 trace_id를 생성할 것 (UUID의 앞 8자리로 충분함)
  • 도구 이름뿐만 아니라 가공되지 않은 도구 입력 인자 (Raw tool input arguments)를 기록할 것 — 버그는 바로 그곳에 숨어 있음
  • 중간 LLM 단계에서는 output_preview가 비어 있을 수 있음을 인지할 것: 도구 호출 (Tool-calling) 차례에는 텍스트 콘텐츠가 없음
  • 프로덕션 (Production) 환경에서는 감사 로그를 표준 출력 (stdout)이 아닌 데이터베이스나 로깅 시스템에 기록할 것

성능 분석 (Performance Analysis)

  • 메트릭 (Metrics)에서 llm_mstool_ms를 항상 분리할 것
  • 최적화 우선순위: 더 적은 LLM 호출 >> 더 빠른 도구 실행
  • 에이전트 (Agent) 변경 후 성능 저하 (Regressions)를 감지하기 위해 p50/p95 지연 시간 (Latency) 기준선을 설정할 것

요약 (Summary)

다섯 가지 핵심 요점:

  1. LangChain 콜백(Callbacks)은 에이전트 관측성(Agent observability)의 기초입니다: BaseCallbackHandler의 네 가지 메서드가 LLM 및 도구(Tool)의 전체 라이프사이클을 다룹니다.
  2. 채팅 모델(Chat models)은 on_chat_model_start를 실행합니다: 놓치기 쉬운 주의 사항 — ChatOpenAI의 경우 on_llm_start는 절대 실행되지 않습니다.
  3. 중간 LLM 단계는 콘텐츠가 비어 있습니다: 도구 호출(Tool-calling) 단계는 텍스트가 아닌 tool_calls JSON을 생성하며, 이는 정상적인 동작입니다.
  4. LLM = 에이전트 지연 시간(Latency)의 99-100%: 도구의 지연 시간은 1-2ms 범위입니다. 도구 구현이 아닌 LLM 호출 횟수를 줄임으로써 최적화하십시오.
  5. 감사 로그(Audit log)의 가치 = 도구 입력 파라미터: 최종 답변만으로는 충분하지 않습니다. 디버깅을 위해 필요한 것은 LLM이 전달하기로 선택한 원시 인자(Raw arguments)입니다.

다음 편: 고급 에이전트 메모리 (Advanced Agent Memory) — 단기 메모리, 장기 메모리, 요약 압축(Summary compression), 그리고 세션 전반에 걸쳐 유지되는 컨텍스트를 에이전트에게 제공하는 방법에 대해 다룹니다.

참고 문헌 (References)

실제 기업급 워크플로우에서 검증된 AI 에이전트와 기술의 큐레이션 마켓플레이스인 PrimeSkills를 확인해 보세요. 군더더기 없이 실제로 작동하는 것들만 제공합니다.

홈페이지에서 더 유용한 지식과 흥미로운 제품들을 찾아보세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0