AI 애플리케이션이 엉뚱한 답변을 내놓았습니다. 사용자는 불만을 터뜨립니다. 팀원들은 어떤 프롬프트가 전송되었는지, 어떤 컨텍스트가 검색되었는지, 어떤 도구가 호출되었는지, 혹은 모델이 왜 그런 결정을 내렸는지에 대한 단서 없이 그저 “모델이 응답을 반환함”이라고 적힌 로그만 멍하니 바라보고 있습니다.
이것이 바로 블랙박스 문제입니다. 그리고 옵저버빌리티(Observability, 관측 가능성) 없이 배포된 모든 LLM 애플리케이션이 마주하는 기본 상태이기도 합니다.
비결정론이 만드는 간극
전통적인 소프트웨어는 결정론적(deterministic)입니다. 동일한 입력이 주어지면 REST API는 동일한 출력을 반환합니다. 버그를 재현할 수 있고, 어설션(assertion)을 작성할 수 있으며, 기대값과 실제값의 차이(diff)를 비교할 수 있습니다.
하지만 LLM 애플리케이션은 이러한 가정을 송두리째 무너뜨립니다. 동일한 프롬프트라도 호출할 때마다 다른 출력이 생성될 수 있습니다. 검색 증강 생성(RAG, Retrieval-Augmented Generation)은 인덱스 상태에 따라 서로 다른 컨텍스트 청크를 가져옵니다. 도구를 사용하는 에이전트는 모호한 지시를 모델이 어떻게 해석하느냐에 따라 매번 다른 결정을 내립니다. 여기에 온도(temperature), top-p, 샘플링 전략이 제어된 무작위성을 더합니다.
이러한 환경에서 옵저버빌리티 없는 디버깅은 엔지니어링이 아닙니다. 번거로운 과정만 늘어난 ’찍기(guesswork)’에 불과합니다.
애플리케이션 트레이싱이 실제로 기록하는 것
애플리케이션 트레이싱(Application Tracing)은 요청이 시스템을 거쳐 흐르는 전체 라이프사이클을 기록합니다. 단순히 “모델이 호출되었다”는 사실뿐만 아니라, 전송된 정확한 프롬프트, 모델의 응답, 토큰 사용량, 지연 시간(latency), 비용, 그리고 그 사이에 일어난 모든 도구 호출 및 검색 단계를 포착합니다.
잘 구조화된 트레이스는 다음과 같은 정보를 제공합니다:
┌─────────────────────────────────────────────────────────────────┐
│ TRACE: user-query-12345 │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ GENERATION: generate-response 1.2s $0.003 │ │
│ │ model: gpt-4o tokens: 340 in / 180 out │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ SPAN: retrieve-context 0.4s │ │ │
│ │ │ ┌──────────────────────────────────────────────┐ │ │ │
│ │ │ │ TOOL: vector-search 0.3s │ │ │ │
│ │ │ │ query: "GxP validation steps" │ │ │ │
│ │ │ │ results: 5 chunks │ │ │ │
│ │ │ └──────────────────────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ TOOL: database-query 0.1s │ │ │
│ │ │ table: user_preferences │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
이 트리 구조는 무슨 일이 일어났는지 명확하게 보여줍니다. 먼저 검색 단계가 실행되어 5개의 청크를 가져왔고, 그 후 LLM이 해당 컨텍스트와 데이터베이스 조회 결과를 활용하여 응답을 생성했습니다. 만약 답변이 잘못되었다면, 검색이 실패했는지(품질이 낮은 청크), 프롬프트가 잘못 구성되었는지, 아니면 단순히 모델이 잘못된 선택을 내렸는지를 명확히 짚어낼 수 있습니다.
3단계 데이터 모델
모든 LLM 옵저버빌리티 시스템에는 세 가지 수준의 그룹화 계층이 필요합니다:
**옵저베이션(Observations, 관측 단위)**은 개별 단계(LLM 호출, 도구 실행, 검색 작업 등)를 의미합니다. 애플리케이션의 실제 구조를 반영하여 계층적으로 중첩(nest)됩니다. 예를 들어 3개의 도구 호출을 트리거하는 단일 LLM 호출은 부모-자식 트리 구조에서 총 4개의 옵저베이션을 생성합니다.
**트레이스(Traces)**는 하나의 완전한 요청을 나타냅니다. 사용자가 메시지를 보내고, 애플리케이션이 컨텍스트를 검색하고, 모델을 호출하고, 도구를 실행하고, 모델을 다시 호출하여 응답을 반환합니다. 이 전체 체인이 trace_id로 식별되는 하나의 트레이스입니다.
**세션(Sessions)**은 연관된 여러 트레이스를 하나로 묶습니다. 멀티턴(multi-turn) 챗봇 대화는 사용자 턴마다 하나씩, 여러 트레이스를 포함하는 하나의 세션이 됩니다. 대화가 언제 끝날지 사전에 알 수 없으므로, 턴 단위 모델을 사용해야 트레이스를 작고 탐색하기 쉽게 유지할 수 있습니다.
┌─────────────────────────────────────────────────────────┐
│ DATA MODEL │
│ │
│ Session ──────── contains ──────── N Traces │
│ │ │
│ contains │
│ │ │
│ N Observations │
│ (nested tree) │
│ │
│ Trace-level attributes propagate to all observations: │
│ user_id, session_id, tags, metadata │
└─────────────────────────────────────────────────────────┘
OpenTelemetry: 기본 토대
현대 LLM 옵저버빌리티에서 가장 현명한 아키텍처적 결정은 OpenTelemetry(OTEL)를 기반으로 구축하는 것입니다. 이는 세 가지 이유에서 결정적입니다:
벤더 락인(Vendor Lock-in) 방지. OTEL은 오픈 표준입니다. 인스트루멘테이션(계측) 코드는 동일한 코드베이스에서 모든 OTEL 호환 백엔드로 스팬(span)을 전송할 수 있습니다. LLM 전용 분석을 위해서는 Langfuse로, 인프라 모니터링을 위해서는 Datadog으로, 분산 트레이싱을 위해서는 Jaeger로 데이터를 전송할 수 있습니다.
프레임워크 비종속성(Framework-agnostic). OpenAI SDK, LangChain, LlamaIndex, Vercel AI SDK, 순수 HTTP 호출 등 무엇을 사용하든 OTEL은 통합된 스팬 모델을 제공합니다. 프레임워크를 교체하더라도 트레이싱 코드를 다시 작성할 필요가 없습니다.
프로덕션에서 검증된 안정성. OTEL은 쿠버네티스(Kubernetes)에 이어 CNCF에서 두 번째로 활발한 프로젝트입니다. 스팬 수집, 배치 처리, 내보내기(export) 인프라는 수백만 건의 프로덕션 배포를 통해 철저하게 검증되었습니다.
통합 스펙트럼: 구현 방식의 선택지
실질적인 질문은 이것입니다: “코드를 얼마나 많이 작성해야 하는가?” 그 답은 사용하는 기술 스택에 따라 제로 코드 수준부터 보통 수준까지 다양합니다.
드롭인 래퍼 (Zero Code Changes)
이미 OpenAI Python SDK를 사용 중이라면, import 문 단 한 줄만 바꾸면 됩니다:
# Before
from openai import openai
# After
from langfuse.openai import openai
이것으로 끝입니다. 이제 모든 openai.chat.completions.create() 호출이 프롬프트, 모델, 응답, 토큰, 지연 시간, 비용과 함께 백그라운드에서 Langfuse로 자동 트레이싱됩니다. 애플리케이션 코드는 전혀 수정할 필요가 없습니다.
JavaScript/TypeScript의 경우 클라이언트를 래핑하여 동일하게 구성할 수 있습니다:
import OpenAI from "openai";
import { observeOpenAI } from "@langfuse/openai";
const openai = observeOpenAI(new OpenAI());
// 이제 모든 호출이 자동으로 트레이싱됩니다
프레임워크 콜백 (One Line)
LangChain은 바로 이러한 목적을 위해 설계된 콜백 시스템을 제공합니다. 핸들러를 생성한 후 체인에 전달하기만 하면 됩니다:
from langfuse.langchain import CallbackHandler
langfuse_handler = CallbackHandler()
response = chain.invoke(
{"topic": "cats"},
config={"callbacks": [langfuse_handler]}
)
해당 호출 내에서 실행되는 모든 LLM 호출, 도구 실행, 체인 단계가 중첩된 옵저베이션으로 기록됩니다.
수동 계측 (Full Control)
자체 구축한 애플리케이션이나 기본 통합이 없는 프레임워크의 경우, SDK가 제공하는 컨텍스트 매니저와 데코레이터를 사용합니다:
from langfuse import get_client
langfuse = get_client()
with langfuse.start_as_current_observation(
as_type="span", name="process-request"
) as span:
with langfuse.start_as_current_observation(
as_type="generation", name="llm-response", model="gpt-4o"
) as generation:
# LLM 호출 코드 위치
generation.update(output="Generated response")
span.update(output="Processing complete")
langfuse.flush() # 단명(short-lived) 애플리케이션에서 필수
통합 방식 비교
| 통합 방식 | 언어 | 설정 공수 | 트레이싱 대상 |
|---|---|---|---|
| OpenAI SDK 래퍼 | Python | import 문 1줄 변경 | 모든 모델 호출, 토큰, 비용 |
| OpenAI SDK 래퍼 | JS/TS | 클라이언트 래핑 + OTEL 초기화 | 모든 모델 호출, 토큰, 비용 |
| Vercel AI SDK | JS/TS | OTEL + 텔레메트리 등록 | 모든 AI SDK 호출 |
| LangChain 콜백 | Python | 핸들러 1개 생성 후 체인에 전달 | 모든 체인 단계, LLM 호출, 도구 |
| LangChain 콜백 | JS/TS | OTEL + 핸들러 | 모든 체인 단계 |
| Python SDK | Python | 컨텍스트 매니저 / 데코레이터 | 계측(instrument)한 모든 항목 |
| JS/TS SDK | JS/TS | startActiveObservation | 계측(instrument)한 모든 항목 |
| 순수 OpenTelemetry | 모든 언어 | OTEL SDK 설정 | 방출(emit)한 모든 스팬 |
백그라운드 처리 모델
가장 중요한 아키텍처적 설계 원칙 중 하나는 트레이싱이 애플리케이션의 응답 속도를 늦춰서는 안 된다는 점입니다.
아키텍처는 직관적입니다:
- 애플리케이션이 트레이스를 생성하거나 이벤트를 기록합니다.
- SDK가 데이터를 로컬 큐에 넣습니다(넌블로킹, non-blocking).
- 애플리케이션은 지연 없이 요청을 계속 처리하고 사용자에게 응답합니다.
- 백그라운드 익스포터(exporter)가 큐에 쌓인 이벤트를 배치(batch)로 묶어 백엔드로 전송합니다.
즉, 애플리케이션의 응답 시간은 트레이싱의 영향을 전혀 받지 않습니다. 데이터는 응답이 전송된 지 불과 몇 초 후에 Langfuse에 도달합니다.
결정적인 예외 사항: 스크립트, 배치 작업, 서버리스 함수(Serverless functions)와 같은 단명 애플리케이션(short-lived application)은 백그라운드 익스포터가 데이터를 플러시(flush)하기 전에 프로세스가 종료될 수 있습니다. 프로세스 종료 전에 flush()를 호출하지 않으면 데이터가 유실됩니다.
Short-lived app without flush():
Request → trace created → response → process exits → DATA LOST
Short-lived app with flush():
Request → trace created → response → flush() → data sent → process exits
이것이 LLM 옵저버빌리티 구축 시 가장 흔히 발생하는 실수입니다. 애플리케이션이 상시 실행되는 서버가 아니라면 flush() 호출은 필수적입니다.
좋은 트레이스를 만드는 조건
트레이스를 수집하는 것 자체는 필요조건일 뿐 충분조건이 아닙니다. 잘못 구성된 트레이스는 트레이스가 전혀 없는 것만큼이나 쓸모가 없습니다. 가치 있는 트레이스와 단순한 노이즈를 구분 짓는 기준은 다음과 같습니다.
단일 작업 단위(Unit of Work)로 트레이스 범위 한정하기
하나의 트레이스는 독립적인 단일 작업(챗봇의 1턴, 에이전트의 1회 실행, 파이프라인의 1회 처리)을 나타내야 합니다. 여러 턴으로 이루어진 대화 전체를 단일 트레이스에 억지로 밀어 넣지 마십시오. 연관된 트레이스들을 묶을 때는 세션(Session)을 활용해야 합니다.
API처럼 옵저베이션 명명하기
옵저베이션 이름은 평가기(evaluators), 대시보드, 저장된 필터 등에서 참조됩니다. 따라서 안정적인 식별자로 다루어야 합니다:
| 좋은 예 (Good) | 나쁜 예 (Bad) | 이유 |
|---|---|---|
classify-intent |
gpt-4o-classify |
모델을 교체할 때 모델명이 포함되어 있으면 관측 일관성이 깨짐 |
retrieve-context |
retrieve-context-retry-3 |
동적 값이 포함되면 그룹화 및 집계가 불가능함 |
generate-response |
step-4 |
서술적인 명칭이어야 필터링과 분석이 가능함 |
동사 우선(verb-first)의 능동태 언어를 사용하십시오. 트레이스 트리는 애플리케이션이 수행한 작업을 한눈에 읽을 수 있는 서술문처럼 구성되어야 합니다.
유의미한 입력과 출력 설정하기
루트 옵저베이션의 입력과 출력은 트레이싱 테이블에 직접 표시되며 평가기에 의해 읽힙니다. 챗봇의 경우 함수 인수의 원시 JSON 블롭 대신 사용자의 입력 메시지를 input으로, 어시스턴트의 답변을 output으로 지정해야 합니다. 디버깅을 위해 원시 페이로드가 필요하다면 메타데이터에 넣으십시오.
노이즈 제거하기
단순 HTTP 스팬, 데이터베이스 쿼리, 프레임워크 내부 동작은 종종 유용한 통찰 없이 화면을 어지럽히기만 합니다. 트레이스 트리에 47개의 옵저베이션이 표시되는데 그중 40개가 프레임워크 내부 배관(plumbing) 작업이라면 중요한 신호(signal)가 노이즈 속에 파묻히게 됩니다. 애플리케이션의 핵심 동작을 이해하는 데 도움이 되지 않는 옵저베이션은 필터링하여 제거하십시오.
어트리뷰트 계층
잘 설계된 트레이스는 필터링, 세분화(세그먼테이션), 분석을 가능하게 하는 풍부한 어트리뷰트(attributes, 속성)를 갖추고 있습니다:
환경(Environments): 프로덕션 데이터를 스테이징 및 개발 환경 데이터와 분리합니다. 이를 지정하지 않으면 테스트 트레이스가 프로덕션 대시보드와 평가 결과를 오염시키게 됩니다.
태그(Tags): 생성 시점에 설정되는 불변(immutable) 레이블입니다. 어떤 기능인지, 어떤 API 엔드포인트인지, 어떤 사용자 세그먼트인지 등 비즈니스 차원의 분류에 사용합니다. 불변 속성이므로 사후에 알게 되는 정보가 아니라 사전에 이미 알고 있는 항목에 적합합니다.
메타데이터(Metadata): 내부 요청 ID, API 라우트, 실험 변형(experiment variant), 검색 컨텍스트(데이터 소스, 청크 개수), 원시 페이로드 등 유용한 모든 정보를 담을 수 있는 유연한 키-값 저장소입니다. 태그와 달리 언제든지 설정하거나 업데이트할 수 있습니다.
사용자 ID(User IDs): 트레이스를 특정 최종 사용자와 연결하여 사용자별 비용 분석, 품질 비교, 사용 패턴 추적을 가능하게 합니다.
세션 ID(Session IDs): 멀티턴 상호작용에서 연관된 트레이스들을 하나로 묶어 세션 리플레이(session replay) 및 대화 단위 분석을 가능하게 합니다.
비용 추적
LLM 비용을 정확히 파악하려면 모든 generation 옵저베이션에 세 가지 속성이 포함되어야 합니다:
- 모델 이름(Model name) — Langfuse는 모델 가격 책정 테이블에서 요금을 조회합니다. 모델명이 정확히 일치하지 않으면 비용 계산이 오류 없이 조용히 실패(silent failure)합니다.
- 토큰 사용량(Token usage) — 입력 토큰, 출력 토큰, 그리고 선택적으로 캐시된 토큰을 포함합니다. 이는 대시보드의 토큰 사용량 뷰를 구동하는 핵심 데이터입니다.
- 명시적 비용(Explicit cost, 선택 사항) — 커스텀 계약 단가가 적용되거나 가격 테이블에 없는 모델의 경우 직접 비용을 지정합니다.
대부분의 프레임워크 통합 라이브러리는 이 세 가지를 자동으로 캡처하지만, 수동 계측을 수행할 때는 명시적으로 설정해주어야 합니다.
평가와의 유기적 연계
트레이싱은 종착점이 아니라 평가(Evaluation)를 위한 토대입니다. 트레이스 데이터가 지속적으로 수집되기 시작하면 다음과 같은 고도화가 가능해집니다:
- 이름과 타입을 기준으로 특정 옵저베이션을 타깃팅하고, 해당 입출력을 읽어 품질 점수를 자동으로 매기는 LLM-as-a-Judge 평가기 구축
- 옵저베이션 이름, 트레이스 속성, 시간 범위를 기준으로 지표를 필터링하고 집계하는 커스텀 대시보드 생성
- 애플리케이션 버전 간 트레이스 입출력을 비교하는 데이터셋 실험(Dataset experiments) 실행
- 특정 지표가 임계값을 초과할 때 즉각 발동하는 알림(Alerts) 구성
여기서 핵심 인사이트는 이 모든 고급 기능이 잘 구조화된 트레이스에 의존한다는 점입니다. 부적절한 옵저베이션 이름, 누락된 입출력, 일관성 없는 데이터 타입은 평가기와 대시보드를 소리 없이 망가뜨립니다. 초기 설계 단계에서 트레이스 구조를 올바르게 잡아야 추후 막대한 사후 수정 비용을 방지할 수 있습니다.
규제 환경에서의 관점
생명과학(Life Sciences), 금융, 국방 등 규제 대상 산업에서 AI를 구축하는 팀에게 옵저버빌리티는 단순한 편의 기능이 아닙니다. 이는 법적·제도적 컴플라이언스(규제 준수) 필수 요건입니다.
감사 추적(Audit trails): 모든 트레이스는 전송된 정확한 프롬프트, 모델 응답, 토큰 사용량, 타임스탬프를 기록합니다. 이는 ALCOA+ 데이터 무결성 요건인 귀속성(Attributable), 가독성(Legible), 동시성(Contemporaneous), 원본성(Original), 정확성(Accurate)에 직접 부합합니다.
버전 추적(Version tracking): 트레이스 데이터와 연동된 프롬프트 관리는 변경 제어(Change Control)를 완벽히 지원합니다. 어떤 프롬프트 버전이 어떤 출력을 생성했는지, 변경이 언제 이루어졌는지를 명확히 입증할 수 있습니다.
환경 분리(Environment separation): 프로덕션 및 밸리데이션 데이터는 개발 및 테스트 데이터와 엄격히 분리되어야 합니다. 환경 어트리뷰트는 데이터 계층에서 이러한 경계를 강제합니다.
불변 태그(Immutable tags): 생성 시점에 지정된 레이블은 사후에 수정할 수 없습니다. 이는 사건이 일어난 기록을 사후에 변조할 수 없도록 보장하는 데이터 무결성 원칙을 뒷받침합니다.
자체 호스팅(Self-hosting): 규제 산업의 데이터 주권(Data Sovereignty) 요건상 민감한 데이터를 서드파티 클라우드 서비스로 전송하는 것이 금지되는 경우가 많습니다. 자체 호스팅이 가능한 옵저버빌리티 솔루션은 모든 데이터를 자체 인프라 내에 안전하게 보관합니다.
결론
LLM 옵저버빌리티는 있으면 좋은 부가 기능(nice-to-have)이 아닙니다. AI 애플리케이션을 공학적으로 엔지니어링하는 것과 그저 잘 작동하기를 기도하는 것 사이의 결정적인 차이입니다.
아키텍처는 명쾌합니다: 데이터 수집을 위한 OpenTelemetry, 지연 시간 영향이 없는 비동기 백그라운드 처리, 체계적인 구조를 위한 3단계 데이터 모델(옵저베이션, 트레이스, 세션), 그리고 심층 필터링과 분석을 지원하는 풍부한 어트리뷰트 시스템입니다.
구현 역시 가볍습니다: 주요 프레임워크를 위한 드롭인 SDK 래퍼, LangChain을 위한 콜백 핸들러, 그 외 모든 환경을 위한 직관적인 수동 계측 인터페이스를 제공합니다.
그 효용은 즉각적입니다. 사용자가 잘못된 답변을 보고했을 때, “모델이 응답을 반환함”이라는 불투명한 로그를 보며 추측하는 대신 어떤 컨텍스트가 검색되었고 어떤 프롬프트가 주어졌으며 무엇이 반환되었는지를 정확한 트레이스로 확인하는 순간, 옵저버빌리티가 왜 필수적인지 실감하게 될 것입니다.
단 하나의 트레이스부터 시작하십시오. 구조를 올바르게 잡으십시오. 나머지는 자연스럽게 따라올 것입니다.
[[Langfuse-LLM-Observability-Application-Tracing-2026]]
Saram Consulting