AI 파이프라인이 고장 났고, 대시보드는 이를 모릅니다.

발행: (2026년 6월 18일 PM 10:00 GMT+9)
10 분 소요

출처: The New Stack

최근에 우리 기업 고객 중 하나의 핵심 RAG 파이프라인이 오류나 실패를 알리지 않고 재무 수치에 대한 환각을 시작했습니다.
우리 대시보드는 시스템의 건강 상태가 초록색으로 표시되었고, 모든 테스트가 정상적으로 진행되었습니다.
시스템은 사용자들에게 특정 주식에 투자해야 한다고 확신적으로 권고했으며, 그들의 수익이 크게 상승할 것이라고 말했습니다. 불행하게도 그 보고서는 완전히 허구였습니다.

문제를 원인을 파악하는 데 우리는 3일간 고통스러운 시간을 보냈고, 그 결과 작은 프롬프트 템플릿 변경이 LLM이 문맥을 완전히 무시하고 사전 학습된 가중치에만 의존하게 만들었다는 것을 알게 되었습니다.
이 경험은 현대 AI 시스템이 기존 디버깅 방법에 매우 불친숙하다는 점을 보여주었습니다.

클래식 소프트웨어에서 문제가 발생하면 정확한 코드 위치와 줄 번호, 오류 메시지, 스택 트레이스, 심지어는 NullReferenceException까지 알 수 있습니다.
프로바빌리스틱 오류를 콘솔 로깅으로 해결할 수 있는 방법이 없고, 신경망의 내부 상태를 디버깅하기 위한 브레이크포인트는 존재하지 않습니다.

AI 기반 솔루션에서 문제가 발생하면 그 안에 버그가 존재하지 않으며 시스템은 여전히 완벽하게 작동합니다. 대신 완전히 허구인 내용을 내놓고, 추론의 중요한 단계를 건너뛰고, 무관한 출처를 선택하고, 잘못된 정보에 기반한 완벽한 논리를 구성합니다.
프로바빌리스틱 오류를 콘솔 로깅으로 해결할 수 있는 방법이 없고, 신경망의 내부 상태를 디버깅하기 위한 브레이크포인트는 존재하지 않습니다.

생성형 AI 시대를 헤쳐 나가기 위해서는 디버깅에 대한 새로운 개념화가 필요합니다.

패러다임 전환: 결정론적 vs. 확률적 버그

우리의 현재 도구가 왜 실패하는지 이해하려면 먼저 버그의 본질이 어떻게 변했는지를 파악해야 합니다.

특징전통적 디버깅생성 AI 디버깅****실패 모드
Binary (Pass/Fail, Crash, Exception)Gradients of wrong (Hallucination, Drift, Omission)Root causeLogical flaw, syntax error, bad statePoor retrieval, ambiguous prompt, model drift**ReproducibilityHigh (Given exact inputs, outputs match)Low (Same inputs can yield different outputs)Primary toolBreakpoints, Stack Traces, Unit TestsExecution Traces, Evals, Payload Logging

전통적인 소프트웨어에서 버그는 지시문에 결함이 있는 것입니다. 생성형 AI에서는 모델에 제공한 컨텍스트 환경에 결함이 있는 것입니다.
LLM 실패를 로직 버그처럼 대하면 실제 문제는 벡터 데이터베이스에 잘못 청크링된 PDF 때문인데, 이를 수정하기 위해 래퍼 코드를 몇 시간씩 다시 작성하는 시간을 낭비하게 됩니다.

대부분의 경우(90%), 버그는 그곳에 있습니다: 모델은 잘못된 컨텍스트([이 링크]에서) 를 받았습니다.

생성형 AI 시스템의 현대 디버깅 및 모니터링 접근법

생산 게이트를 통과한 시스템들은 AI 시스템을 마법 같은 함수 호출이 아니라 모든 무작위성과 예측 불가능성을 가진 I/O-제한 외부 서브시스템으로 보게 됩니다.
현대 엔지니어들이 확률적 코드 디버깅 과제를 어떻게 해결하는지 살펴보겠습니다.

스텝을 멈추고 비동기화

멀티 스텝 에이전트 워크플로 (예: Query → Retrieve → Tool Call → Synthesize) 동안 합성 단계에서 발생하는 고장은 가장 가능성이 높게는 그 전 단계인 3단계 이전의 불량 검색 때문입니다.
코드를 스텝으로 따라가면 도움이 되지 않으며, 대신 트레이스 그래프를 만들어야 합니다. 모든 상호작용은 전체 페이로드를 캡처해야 합니다. LLM 호출은 네트워크 의존적이며 몇 초가 걸리므로 비동기 방식으로 추적을 수행해 이벤트 루프를 차단하지 않아야 합니다.

아래는 시스템을 비동기 추적으로 감싸면서 FastAPI 애플리케이션을 방해하지 않고 stdout에 잘 구성된 JSON을 출력해 Datadog, CloudWatch 또는 OpenTelemetry로进一步 사용하도록 하는 예시입니다.

import time
import json
import logging
from string import Template 
from typing import Callable, Dict, Any, List

# Configure structured logging for production ingestion
logger  = logging.getLogger(__name__) 
logger.setLevel(logging.INFO) 
async def trace_llm_execution(
    step_name: str, 
    async_llm_callable: Callable, 
    prompt_template: str, 
    context: List[str], 
    user_query: str
) -> Dict[str, Any]:
    
# 1. Use string.Template for safer hydration to avoid KeyError on user input containing '{}'
template = Template(prompt_template)
hydrated_prompt = template.safe_substitute(
context="\n".join(context),
query=user_query
) 
    
    start_time = time.perf_counter()
    error = None
    raw_response = None

    try:
         # 2. Await the probabilistic call to keep the thread unblocked
        raw_response = await async_llm_callable(hydrated_prompt)
    except Exception as e:
        error = str(e)

    latency_ms = round((time.perf_counter() - start_time) * 1000, 2)

    # 3. Create an immutable artifact of the exact state
    trace_artifact = {
         "event": "llm_trace",
         "step": step_name,
         "latency_ms": latency_ms,
         "hydrated_prompt": hydrated_prompt,  # Crucial: What did the model actually see?
         "raw_context_chunks": context,       # Crucial: Did the DB return garbage?
         "raw_response": raw_response,
         "error": error
    }

    # 4. Emit to stdout for observability platforms
    logger.info(json.dumps(trace_artifact))

    if error:
        raise RuntimeError(f"Step {step_name} failed: {error}")

    return raw_response

“context bugs”와 “reasoning bugs” 구분

AI 파이프라인이 환각을 시작하면 개발자들은 프롬프트를 수정하려 서둘러 합니다. 이는 게으른 접근 방식입니다. 먼저 문제가 어디서 발생했는지를 파악해야 합니다:

컨텍스트 버그: 벡터 데이터베이스가 무관한 청unks를 반환합니다. 답변이 잘못된 이유는 모델이 적절한 컨텍스트를 충분히 받지 못했기 때문입니다 (해결책: 임베딩과 청크 크기 튜닝, 하이브리드 BM25 검색)

추론 버그: 벡터 데이터베이스는 가장 관련성이 높은 청unks를 반환했지만, 모델은 이를 적절히 사용하지 못했거나 오해했으며, 형식 드리프트로 인해 문제를 일으켰습니다. (해결책: 모델 업그레이드, 온도 낮추기, few-shot 예시 사용)

LLM에게 시스템 프롬프트를 obey 하라고 소리치는 것은 추론 버그를 수정하는 방법이 될 수 없습니다.

Pydantic를 이용한 현대 데이터 타입 스키마 검증

기업 시스템은 검증을 피할 수 없습니다. 수동적인 정규식과 json.loads()만으로는 충분하지 않습니다. 확률적 출력은 스키마에 맞춰야 합니다.
Python에서는 Pydantic가 이 문제를 깔끔하게 해결합니다.

import time
import json
import logging
from typing import Callable, Dict, Any, List

# Configure structured logging for production ingestion
logger = logging.getLogger(__name__) 
logger.setLevel(logging.INFO) 
async def trace_llm_execution(
    step_name: str, 
    async_llm_callable: Callable, 
    prompt_template: str, 
    context: List[str], 
    user_query: str
) -> Dict[str, Any]:
    
    # 1. Hydrate the prompt exactly as the model will see it
    hydrated_prompt = prompt_template.format(
        context="\n".join(context), 
        query=user_query
    )

By dumping structured traces to standard output, you allow your observability stack to index the exact hydrated_prompt. If the output is wrong, you don’t guess; you query your logs. 90% of the time, the bug is right there: the model was fed the wrong context.

0 조회
Back to Blog

관련 글

더 보기 »