LLM의 답변이 틀렸을 때, 추적(Trace)을 확인해야 하는 이유. 이를 쉽게 도와주는 도구들
요약
LLM 서비스 운영 중 발생하는 환각 현상을 디버깅하기 위해 요청 전체의 추적(Trace)이 왜 필수적인지 설명합니다. OpenTelemetry(OTel) 네이티브 방식의 중요성과 Helicone, LangSmith 등 다양한 도구의 특성을 다룹니다.
핵심 포인트
- LLM 오류는 모델 자체보다 잘못된 청크, 프롬프트, 도구 호출에서 발생하는 경우가 많음
- 효율적인 디버깅을 위해 검색된 청크, 프롬프트, 도구 인자 등을 포함한 전체 추적이 필요함
- OpenTelemetry 네이티브 방식은 서비스 전체의 통합된 타임라인 확인을 가능하게 함
- Helicone은 프록시 방식으로 설정이 매우 간편하지만 세밀한 단계 추적에는 한계가 있음
운영 환경(prod)에서 사용자가 환각(hallucination) 답변을 보고했습니다. 이를 수정하려면 해당 요청의 전체 추적(trace)이 필요하며, 이를 얼마나 빨리 가져올 수 있는지는 몇 달 전에 설정해 둔 추적(tracing) 방식에 전적으로 달려 있습니다.
티켓(The ticket)
한 지원 사용자가 스크린샷을 붙여넣었습니다. 우리의 에이전트가 환불 가능 기간이 90일이라고 안내한 것입니다. 실제 정책은 30일입니다. 잘못된 답변이 확신에 차서 이미 전송되었습니다. 티켓에는 응답 헤더(response headers)에 포함된 요청 ID(request id) 외에는 아무것도 없었습니다.
그 시점에서 유일하게 유용한 질문은 이것입니다: 해당 요청 내부에서 실제로 어떤 일이 일어났는가? 검색(retrieval) 과정에서 어떤 청크(chunks)를 가져왔는가? 템플릿화(templating) 이후 모델이 본 정확한 프롬프트(prompt)는 무엇인가? 각 도구 호출(tool call)은 무엇을 반환했는가? 잘못된 답변은 모델이 창의력을 발휘해서 발생하는 경우가 거의 없습니다. 대개는 잘못된 청크, 오래된 문서, 잘못된 행을 반환한 도구, 또는 잘못 조립된 프롬프트 때문입니다. 출력값만으로는 이 중 그 어떤 것도 볼 수 없습니다. 해당 요청 ID에 대한 추적(trace)을 열고 스팬(spans)을 읽어야 합니다.
중요한 축(The axis that matters)
단일 오류 출력을 디버깅할 때, 저는 두 가지를 중요하게 생각합니다. 첫째, 요청 ID가 주어졌을 때 검색된 청크(retrieved chunks), 템플릿화된 프롬프트(templated prompt), 도구 인자 및 반환값(tool args and returns), 토큰 수(token counts), 스팬별 지연 시간(per-span latency)을 포함한 해당 요청의 전체 추적(complete trace)을 얼마나 빨리 가져올 수 있는가입니다. 둘째, 추적(tracing)이 OpenTelemetry(OTel) 네이티브인가 하는 점입니다. 그래야 전용 SDK와 별도의 대시보드에 종속되지 않고, 이미 실행 중인 컬렉터(collector)와 백엔드(backend)로 스팬(spans)이 바로 들어갈 수 있기 때문입니다.
두 번째 포인트는 단순히 미적인 문제가 아닙니다. 추적(tracing)이 OTel 네이티브라면, LLM 스팬(span)이 HTTP 핸들러(HTTP handler), 벡터 DB 호출(vector DB call), 그리고 다운스트림 서비스(downstream service)와 동일한 추적(trace) 내에 위치하게 됩니다. 하나의 추적 ID(trace id)로 요청부터 응답까지 이어집니다. 반면 독점적인(proprietary) 방식이라면, LLM 부분은 별도의 도구에서 따로 존재하게 되어 새벽 2시에 수동으로 타임라인을 이어 붙여야 합니다.
캡처 방식에 따른 6가지 도구
순위가 아닌, 앱에서 스팬(spans)을 추출하는 방식에 따라 정렬되었습니다.
Helicone (github.com/Helicone/helicone)은 설정이 가장 빠릅니다. 모델의 베이스 URL (base URL)을 이들의 프록시 (proxy)로 지정하기만 하면 모든 호출이 기록되며, 스팬 (span)마다 별도의 계측 (instrumentation)을 할 필요가 없습니다. 단 한 줄의 통합이 가장 큰 매력입니다. 트레이드오프 (tradeoff)는 세밀함 (granularity)입니다. 게이트웨이 (gateway)는 프록시하는 요청과 응답을 볼 수 있으므로, 검색 (retrieval) 단계나 내부 도구 호출 (프록시에 도달하지 않는 경우)은 별도로 계측하지 않는 한 스팬으로 나타나지 않습니다. "모델에 무엇이 전송되었는가"를 확인하기에는 훌륭하지만, 문제를 일으킨 청크 (chunk)를 파악하기에는 정보가 부족할 수 있습니다.
LangSmith (smith.langchain.com)는 LangChain 또는 LangGraph 환경을 사용한다면 단일 요청에 대해 가장 풍부한 뷰 (view)를 제공합니다. 체인 (chains), 도구 노드 (tool nodes), 검색 단계 (retriever steps)가 이미 구조화되어 나타나며, 단일 실행에 대한 워터폴 (waterfall) 차트는 잘못된 출력을 읽어내는 데 진정으로 유용합니다. 단점은 트레이싱 (tracing)이 상당히 독점적 (proprietary)이라는 점입니다. SDK를 통해 그들의 백엔드 (backend)로 데이터를 전송하며, 해당 스팬들을 사용자의 OpenTelemetry (OTel) 컬렉터 (collector)로 가져오는 것은 기본 경로가 아닙니다.
Langfuse (github.com/langfuse/langfuse)는 오픈 소스 (open source)이며 OTel을 인식합니다. OpenTelemetry 엔드포인트 (endpoint)를 노출하므로 OTel 스팬이 그곳에 도달할 수 있으며, 자체 SDK와 데코레이터 (decorators)도 갖추고 있습니다. 데이터를 자신의 인프라 (infra)에 두고 싶다면 셀프 호스팅 (self-hostable)이 가능합니다. 하나의 요청을 읽는다는 것은 ID로 트레이스 (trace)를 열고 관찰 트리 (observation tree)를 따라가는 것을 의미하며, 이를 통해 프롬프트 (prompt), 기록된 검색 컨텍스트 (retrieved context), 단계별 지연 시간 (latency) 및 토큰 (tokens)을 확인할 수 있습니다.
Future AGI (github.com/future-agi/future-agi)는 트레이싱을 평가 (evaluation), 프롬프트 작업 (prompt work), 가드레일 (guardrails)까지 아우르는 더 넓은 플랫폼의 한 측면으로 접근합니다. 트레이싱 라이브러리는 OpenTelemetry 네이티브 (native)이므로, 스팬이 표준 OTel 경로를 통해 사용자의 스택 (stack)으로 지정할 수 있는 백엔드로 흐릅니다. 디버깅 작업 측면에서 유용한 점은, 잘못된 답변의 트레이스가 검색된 컨텍스트와 도구 IO (tool IO)를 동일한 트레이스 ID (trace id) 상의 스팬으로 포함한다는 것입니다. 이는 티켓에 요청 ID (request id)만 있을 때 열어볼 수 있는 핵심 정보입니다.
Braintrust (braintrust.dev)는 그 로깅 (logging)의 중심을 Eval 객체에 둡니다. Trace는 일급 객체 (first-class)이며, 이 워크플로우 (workflow)의 강력한 버전은 다음과 같습니다: 잘못된 출력을 포착하고, 해당 요청을 동일한 도구 내의 테스트 케이스 (test case)로 전환하는 것입니다. 만약 당신의 루프 (loop)가 '디버깅 후 Eval로 고정'하는 방식이라면, 이러한 긴밀한 결합이 도움이 됩니다. 만약 Eval 추상화 (abstraction)와 분리된 순수한 요청 레벨의 트레이싱 (tracing)만을 원한다면, 일반적인 OTel 백엔드 (backend)보다는 더 주관적인 (opinionated) 성향을 띱니다.
Arize Phoenix (github.com/Arize-ai/phoenix)는 오픈 소스 (open source)이며 OpenInference를 통해 OpenTelemetry (OTel) 네이티브 (native)로 작동하므로, LLM, 리트리버 (retriever), 그리고 도구 스팬 (tool spans)이 표준 OTel 시맨틱 (semantics)을 사용하며 당신의 콜렉터 (collector)로 흘러 들어갑니다. 단일 디버깅 세션을 위해 로컬 (locally)에서 실행하거나 지속적인 백엔드 (backend)를 대상으로 실행할 수 있습니다. 하나의 요청을 여는 것은 해당 trace id로 필터링하고 스팬 트리 (span tree)를 읽는 것을 의미하며, 이때 검색된 문서 (retrieved documents)와 도구 호출 (tool calls)이 스팬 속성 (span attributes)으로 첨부됩니다.
이들 중 세 가지 (Langfuse, Phoenix, Future AGI)는 오픈 소스 (open source)이며 멀티 서피스 (multi-surface)입니다. 여섯 가지 중 두 가지 (Phoenix, Future AGI)는 설계 단계부터 OTel 네이티브 (OTel-native)입니다. Langfuse는 자체 SDK와 함께 OTel을 지원합니다. 위의 모든 내용은 2026년 중반 기준이며, 이 분야는 매우 빠르게 발전하므로 도입하기 전에 현재의 문서 (docs)를 확인하십시오.
하나의 요청 읽기
백엔드 (backend)가 무엇이든 방식은 동일합니다: (로그에 남긴 request id로부터) trace id를 가져오고, trace를 불러온 뒤, 스팬 (spans)을 따라가며, 검색 (retrieval)과 도구 I/O를 먼저 확인하십시오. 여기서는 순수 OpenTelemetry (OpenTelemetry) 측면의 계측 (instrumentation)을 다루므로, LLM 스팬 (LLM span)에는 새벽 2시에 실제로 필요하게 될 속성 (attributes)들이 포함됩니다.
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
...
매번 문제를 해결해 주는 속성은 템플릿 자체가 아니라 완전히 템플릿화된 문자열을 담고 있는 llm.prompt입니다. 환불 버그의 경우도 마찬가지였습니다. 검색된 청크가 오래된 정책 문서에서 왔고, retrieval.chunk_ids는 그곳을 직접 가리켰으며, prompt 스팬은 모델이 '90일'을 컨텍스트로 받았음을 보여주었습니다. 모델이 환각(hallucination)을 일으킨 것이 아니었습니다. 단지 오래된 청크를 충실하게 반복했을 뿐입니다. 요청 ID부터 근본 원인을 파악하기까지 총 시간이 약 7분이 걸렸고, 그중 6분은 저희 자체 로그에서 해당 요청 ID를 찾는 데 사용되었습니다.
만약 스팬에 검색된 청크 ID와 템플릿화된 프롬프트가 포함되어 있지 않다면, 어떤 백엔드도 여러분을 구해주지 못할 것입니다. 흥미로운 부분이 빠진 입력과 출력을 보고 있게 될 겁니다.
제가 가장 먼저 확인할 것들
- 요청 ID로 트레이스를 가져와서 retrieval 스팬부터 엽니다. 잘못된 청크가 들어가면 당연히 잘못된 답변이 나오고, 이것이 가장 흔한 원인입니다.
- llm.prompt를 템플릿 자체가 아니라 완전히 템플릿화된 문자열로 읽습니다. 잘못 조립된 프롬프트는 코드상으로는 괜찮아 보이지만 스팬에서는 명백합니다.
- LLM 스팬이 HTTP 스팬 및 벡터 DB 스팬과 하나의 트레이스 ID를 공유하는지 확인합니다. 만약 LLM 부분이 별도의 독점 도구에 존재한다면, 여러분은 수동으로 타임라인을 연결하고 있는 것이며, 이는 다음 사고가 발생하기 전에 수정해야 할 설정 문제입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기