에이전트가 무엇을 했는지 확인하기 — NVIDIA NIM을 활용한 트레이싱 (Tracing) 및 관측성 (Observability)
요약
AI 에이전트의 비결정론적 동작과 지연 시간을 디버깅하기 위해 NVIDIA NIM을 활용한 트레이싱 및 관측성 구현 방법을 설명합니다. 순수 Python을 사용하여 매 턴의 실행 과정을 JSONL 형식으로 기록하는 경량화된 방식을 제안합니다.
핵심 포인트
- 에이전트의 이상 동작 원인을 파악하기 위한 트레이스(Trace)의 중요성
- 순수 Python과 JSONL 형식을 활용한 경량 관측성 구현
- 모델 호출, 도구 호출, 지연 시간 등 핵심 데이터 구조 설계
- 복잡한 프레임워크 없이 파일 I/O만으로 구현하는 디버깅 전략
우리가 구축한 에이전트와 스무 번째 정도 대화를 나누다 보면, 에이전트가 이상한 행동을 할 때가 있을 것입니다. 아무 이유 없이 도구 (tool)를 두 번 호출하거나, 어제는 답변했던 질문을 거부하기도 합니다. 평소 2초면 끝날 일을 9초 동안 붙잡고 있기도 하죠. 그러면 여러분은 운영 환경에서 가장 중요한 단 하나의 질문을 던지게 될 것입니다: 왜 그랬을까?
다시 실행해 보는 것만으로는 답을 알 수 없습니다. 모델은 결정론적 (deterministic)이지 않으며, 그 순간은 이미 지나갔기 때문입니다. 파트 6에서 사용했던 디버깅용 프린트 문들은 터미널과 함께 스크롤되어 사라집니다. 여러분에게 필요한 것은 **트레이스 (trace)**입니다. 즉, 실제로 일어난 일에 대한 지속적인 기록입니다. 인자 (arguments)와 결과가 포함된 모든 도구 호출 (tool call), 지연 시간 (latency)이 포함된 모든 모델 호출 (model call), JSON 답변의 수정 필요 여부, 그리고 에이전트가 최종적으로 말한 내용까지 말이죠.
이 포스트에서는 표준 라이브러리 외에 다른 것은 전혀 사용하지 않고, 순수 Python을 사용하여 바로 그 기능을 추가합니다. 매 턴마다 파일에 **한 줄의 JSON (one JSON line)**을 추가합니다. 턴당 한 줄이라는 이 구조는 의도된 것입니다. 다음 포스트(평가, evals)에서 한 줄을 로드하기만 하면, 재그룹화 과정 없이 어설션 (assertion)에 필요한 모든 것—입력값, 도구 경로, 그리고 파트 9의 검증된 6개 키 답변—을 얻을 수 있기 때문입니다. 관측성 (Observability)은 설치하는 제품이 아니라, 여러분이 작성하는 파일입니다.
저는 USC의 NVIDIA Developer Champion인 B Torkian입니다. 시리즈의 파트 10입니다.
추가되는 내용
Workshop 9: 턴 발생 → 답변 반환 → 세부 정보가 영원히 사라짐
Workshop 10: 턴 발생 → 답변 반환 → 한 줄의 JSON이 생존함:
{user_message, steps: [model calls, tool calls, validation], final, latency}
Workshop 9의 루프는 변경되지 않습니다. 작은 JsonlTracer가 다섯 가지 자연스러운 접점(seams)에 연결됩니다: 턴 시작, 각 모델 호출, 각 도구 호출, 검증 결과, 그리고 최종 답변입니다.
1단계 — 한 턴의 기록에 무엇을 담을지 결정하기
{
"schema_version": "ws10.turn.v1",
"trace_id": "a3f9c2e81b04",
...
"왜"라는 질문에 필요한 모든 정보는 단 한 줄에 들어 있습니다. 모델이 days_until_weekday를 요청했고(따라서 메모리가 _"그것"_을 정확하게 해결했습니다), 도구(tool)는 1밀리초 미만으로 실행되었으며, 두 번째 모델 호출은 첫 시도에 깨끗한 JSON을 생성했고, 전체 턴(turn)은 2초가 걸렸습니다. 그중 대부분은 두 번째 모델 호출에 소요되었습니다.
schema_version 필드는 오늘날 비용이 들지 않으며, 데이터 구조(shape)가 진화할 때 유용합니다. 미래의 툴링(tooling)이 오래된 라인과 새로운 라인을 구분할 수 있게 해줍니다.
2단계 — 트레이서 (plain file I/O)
class JsonlTracer:
SCHEMA_VERSION = "ws10.turn.v1"
...
로깅 프레임워크(logging framework), 데코레이터(decorators), 전역 변수(globals)는 없습니다. 세션(session)이 트레이서(tracer)를 소유하고, 트레이서는 파일을 소유합니다.
("왜 OpenTelemetry를 사용하지 않나요?" 9부의 "왜 Pydantic을 사용하지 않나요?"와 동일한 답변입니다: OpenTelemetry — 그리고 이를 기반으로 구축된 LLM 관측성 (LLM-observability) 플랫폼 — 는 프로덕션 팀이 스팬(spans), 익스포터(exporters), 대시보드(dashboards)를 사용하여 정확히 이 작업을 수행할 때 사용하는 것입니다. 우리가 트레이서를 직접 구현(hand-roll)하는 이유는 해당 도구들이 무엇을 기록하는지, 그리고 왜 그렇게 하는지를 직접 볼 수 있게 하기 위함입니다. 이 JSONL 파일이 이해되기 시작하면, OTel 스팬은 표준 스키마를 가진 이 기록과 더 나은 저장 공간을 갖춘 형태일 뿐입니다.)
3단계 — 루프의 이음새(seams)에 훅(hook) 걸기
세션은 __init__에서 한 줄의 코드 — self.tracer = JsonlTracer(trace_path) — 를 추가하며, 루프는 각 이음새(seam)에서 타이밍 래퍼(timing wrapper)를 얻습니다:
def _run_turn(self, user_message: str, stream: bool) -> dict:
self.messages.append({"role": "user", "content": user_message})
self.tracer.begin_turn(user_message, mode="stream" if stream else "chat")
...
(축약된 스니펫이 숨기고 있는 한 가지 세부 사항: 실제 파일에서 _run_turn은 이 루프를 try/except로 감싸며, 예외를 다시 발생시키기 전에 tracer.abort_turn(...)을 호출합니다. 따라서 턴 중간에 충돌(crash)이 발생하더라도 부분적인 트레이스(trace)를 기록합니다. 충돌은 바로 기록이 가장 필요한 순간입니다.)
두 번의 작은 리팩터링(refactor)이 이를 깔끔하게 만들며, 이들은 솔직히 이름을 붙일 가치가 있습니다:
_complete가 이제usage도 반환합니다 — 최선의 노력을 다한 토큰 계정 (token accounting) 방식입니다. 비스트리밍 (Non-streaming) NIM 응답에는 보통response.usage(프롬프트, 완료, 총 토큰)가 포함되지만, 스트리밍 (streamed) 응답에는 보통 포함되지 않습니다. (일부 OpenAI 호환 엔드포인트는 마지막 청크에서 사용량을 보고하기 위해stream_options={"include_usage": true}를 허용하지만, 지원 여부가 제각각이므로 추측하기보다는null을 기록하는 것입니다.) 측정하지 않은 0을 절대 기록하지 마세요. 0은 측정된 값처럼 보이지만,null은 진실을 말해줍니다._finalize_json이 이제(data, validation_meta)를 반환합니다 — 이를 통해 파싱 (parsing) 코드가 트레이스 (trace)의 존재를 알지 못해도, 트레이서 (tracer)가 파싱 실패 여부와 복구 호출 (repair call) 실행 여부를 기록할 수 있습니다.
4단계 — 실행한 다음, 파일에 질문하기
session = ChatSession(verbose=True)
for q in ["When does the USC AI Club meet?",
"How many days until that?",
...
네 번의 턴 (turn), traces/campus_assistant.jsonl에 네 줄이 기록되었습니다. 이제 보상을 받을 차례입니다. 추측 대신 단 몇 줄의 분석으로 확인이 가능합니다 (리포지토리 버전에는 빈 파일에 대한 방어 코드가 추가되어 있습니다):
def analyze_traces(path=TRACE_PATH):
turns = [json.loads(line) for line in Path(path).read_text().splitlines()]
slowest = max(turns, key=lambda t: t["total_latency_ms"])
...
어떤 턴이 가장 느렸는지, 그리고 그것이 모델 때문이었는지 아니면 도구 (tool) 때문이었는지? 비교 질문이 정말로 days_until_weekday를 두 번 호출했는지? JSON 복구가 얼마나 자주 필요한지? 트레이스는 에이전트를 다시 실행하지 않고도 이 모든 것에 답을 줍니다 — 이것이 바로 핵심입니다.
사이드바 — 두 번째 레이어: 셀프 호스팅 NIM의 서버 메트릭 (metrics)
위의 모든 내용은 애플리케이션 측 (app-side) 관측성 (observability)이며, 호스팅된 API Catalog와 로컬 NIM 컨테이너 모두에서 동일하게 작동합니다. 만약 NIM을 셀프 호스팅한다면 (4부 참조), 무료로 두 번째 레이어를 얻게 됩니다. 컨테이너는 HTTP 포트를 통해 GPU 사용률 (utilization), 첫 번째 토큰까지의 시간 (time-to-first-token), 진행 중인 요청 (requests in flight) 등의 **Prometheus 메트릭 (metrics)**을 노출합니다:
# 로컬 NIM 컨테이너 전용입니다. 호스팅된 엔드포인트는 이를 노출하지 않습니다.
curl -s http://localhost:8000/v1/metrics | head -20
경로에 주의하세요: 메트릭 (metrics)은 추론 경로와 함께 /v1 아래에 존재합니다. 즉, :8000/metrics가 아니라 :8000/v1/metrics입니다. 앱 트레이스 (App traces)는 당신의 에이전트가 무엇을 했는지 알려주며, 서버 메트릭 (server metrics)은 모델 서버가 무엇을 했는지 알려줍니다. 프로덕션 환경에서는 두 가지를 모두 사용합니다. 문서: https://docs.nvidia.com/nim/large-language-models/latest/reference/logging-and-observability.html
단계 5 — 나중에 당신을 구해줄 규칙: 트레이스에는 사용자 데이터가 포함됩니다
우리가 무엇을 로깅 (logging)하고 있는지 살펴보세요: 사용자의 메시지, 도구 결과 (tool results), 최종 답변입니다. 이 데모에서는 클럽 일정입니다. 실제 배포 환경에서는 이름, 이메일, 학생 ID 등 사람들이 어시스턴트에게 입력하는 모든 것이 될 수 있습니다. 따라서 첫날부터 세 가지 습관을 지키세요:
- 비밀 정보(secrets)를 절대 로깅하지 마세요. API 키, 요청 헤더 (request headers), 환경 변수 (environment) 등을 포함하지 마세요. (트레이서 (tracer)가
client나os.environ에 전혀 접근하지 않는 점에 주목하세요.) messages배열 전체를 절대 로깅하지 마세요. 이는 매 턴마다 전체 대화 내용을 다시 축적합니다. 이는 재앙으로 이어질 수 있는 유출 위험이 있으며, 어차피 불필요합니다. 턴별 기록 (per-turn records)이 이미 이를 재구성할 수 있기 때문입니다.- 트레이스 파일을 git에서 제외하세요. 이 저장소의
.gitignore에는traces/가 포함되어 있습니다. 트레이스 파일은 코드가 아니라 데이터입니다.
단계 6 — 당신이 실제로 구축한 것
- 워크숍 1–9에서는 검색 (retrieves), 거부 (refuses), 계획 (plans), 기억 (remembers), 스트리밍 (streams), 그리고 검증된 JSON을 반환하는 에이전트를 구축했습니다.
- 워크숍 10에서는 이를 **관측 가능 (observable)**하게 만들었습니다. 모든 턴은 사후에
Repo: github.com/torkian/nvidia-nim-workshop
One-click Colab: Open part10_traces.ipynb
Local Python: 리포지토리 내의 part10_traces.py (pip install -r requirements.txt 실행 후 python3 part10_traces.py).
MIT 라이선스입니다. 저는 USC(University of Southern California)에서 이 프로젝트를 운영하고 있습니다. 이를 포크(fork)하여 지식 베이스 (knowledge base)와 도구 (tools)를 여러분의 학교, 클럽, 또는 프로젝트에 맞게 교체하여 사용해 보세요.
전체 시리즈
-
Part 2: 수동 RAG에서 실제 검색으로 — NVIDIA NIM을 활용한 임베딩 기반 RAG (Embedding-Based RAG)
-
Part 4: 자신의 GPU에서 NVIDIA NIM 실행하기
-
Part 6: 단일 도구에서 계획으로 — NVIDIA NIM을 활용한 다단계 에이전트 (Multi-Step Agents)
-
Part 7: 에이전트에게 메모리 부여하기 — NVIDIA NIM을 활용한 다회차 대화 (Multi-Turn Conversations)
-
Part 8: 에이전트가 실시간처럼 느껴지게 만들기 — NVIDIA NIM을 활용한 스트리밍 (Streaming)
-
Part 9: 에이전트가 산문이 아닌 데이터를 반환하게 만들기 — NVIDIA NIM을 활용한 구조화된 출력 (Structured Outputs)
-
Part 10 (본 포스트): 에이전트가 무엇을 했는지 확인하기 — NVIDIA NIM을 활용한 트레이싱 (Tracing) 및 관측성 (Observability)
전체 시리즈를 한 번에 읽고 싶은 분들을 위해 Medium에 통합된 롱폼 (Long-form) 버전이 게시되어 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기