본문 바로가기

AI Agent

AI 에이전트 프로덕션 실패 7가지 패턴

반응형

데모에선 완벽했어요. 프로덕션에 올렸더니 망가졌어요.

AI 에이전트는 일반 소프트웨어와 다르게 실패해요.

일반 소프트웨어 실패:
→ 500 에러
→ 타임아웃
→ 명확한 스택 트레이스

AI 에이전트 실패:
→ 조용히 틀린 답 반환
→ 11일 동안 아무도 모르게 무한 루프
→ $47,000 청구서
→ 자신 있게 틀린 방향으로 달려감

실제 사례들이에요.

사례 1: Claude Code 서브에이전트
→ 무한 루프로 4.6시간 동안 2,700만 토큰 소비
→ GitHub Issue #15909에 보고됨

사례 2: AWS Kiro AI 에이전트
→ 프로덕션 환경 자율 삭제
→ 13시간 장애

사례 3: 멀티에이전트 리서치 툴
→ 에이전트 A가 B에게 리서치 요청
→ B가 A에게 검증 요청
→ A가 B에게 재확인 요청
→ 11일 동안 루프, $47,000 API 청구

실패 패턴 1 — 컨텍스트 오염 (Context Pollution)

증상:

초반: 답변 정확하고 빠름
30턴 후: 이상한 답변 시작
50턴 후: 초기 지시사항 무시

원인:

에이전트가 처리한 모든 정보가 컨텍스트에 쌓여요. 관련 없는 정보, 이전 에러 메시지, 실패한 시도들이 전부 남아서 노이즈가 됩니다.

컨텍스트 윈도우 상태 (50턴 후):
시스템 프롬프트:    1,500 토큰
툴 정의:           4,000 토큰
초반 대화:         8,000 토큰 ← 이미 잊혀진 정보
실패한 시도들:     12,000 토큰 ← 노이즈
현재 작업:         2,000 토큰
→ 실제 추론 공간: 거의 없음

실제 사례: RAG 에이전트가 50개의 "관련" 청크를 검색해서 컨텍스트에 넣었어요. 청크들이 서로 모순되는 정보를 담고 있었고, 에이전트는 자신 있게 틀린 답을 냈어요.

방어 코드:

from anthropic import Anthropic

client = Anthropic()

def run_with_compaction(messages: list, threshold_tokens: int = 20000) -> list:
    """컨텍스트가 임계값 초과 시 자동 압축"""

    # 토큰 수 추정
    estimated = sum(len(m["content"]) // 4 for m in messages)

    if estimated < threshold_tokens:
        return messages

    # 오래된 히스토리 압축
    old = messages[:-5]  # 최근 5개는 유지
    recent = messages[-5:]

    summary = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=500,
        messages=[{
            "role": "user",
            "content": f"다음 대화를 핵심 결정사항과 컨텍스트만 남겨서 압축해줘:\n{old}"
        }]
    ).content[0].text

    return [
        {"role": "user", "content": f"[이전 대화 요약]\n{summary}"},
        {"role": "assistant", "content": "이해했습니다."},
        *recent
    ]

실패 패턴 2 — 무한 루프

증상:

에이전트가 계속 실행 중 (멈추지 않음)
API 비용이 시간당 수백 달러씩 증가
같은 툴을 계속 호출하는 로그

원인: 에러 발생 → 재시도 → 같은 에러 → 재시도... 멈춤 조건이 없으면 영원히 반복해요.

무한 루프 4가지 패턴:
1. 같은 툴을 동일 파라미터로 계속 호출
2. 파라미터를 조금씩 바꿔가며 반복 (본질은 같음)
3. 에러 재시도가 게이트웨이 + 에이전트 양쪽에서 중복 발생
4. 멀티에이전트에서 A→B→A→B 순환

방어 코드:

import hashlib
import time

class LoopDetector:
    def __init__(self, max_iter: int = 50, max_cost_usd: float = 10.0):
        self.max_iter = max_iter
        self.max_cost = max_cost_usd
        self.iteration = 0
        self.total_tokens = 0
        self.recent_actions = []  # 최근 10개 액션 해시

    def check(self, action: str, params: dict, tokens: int) -> dict:
        self.iteration += 1
        self.total_tokens += tokens
        cost = (self.total_tokens / 1_000_000) * 15  # Sonnet 기준

        # 반복 횟수 초과
        if self.iteration > self.max_iter:
            return {"halt": True, "reason": f"최대 반복 {self.max_iter}회 초과"}

        # 비용 초과
        if cost > self.max_cost:
            return {"halt": True, "reason": f"비용 한도 ${self.max_cost} 초과: ${cost:.2f}"}

        # 같은 액션 반복 감지
        action_hash = hashlib.md5(f"{action}{params}".encode()).hexdigest()
        if action_hash in self.recent_actions[-5:]:  # 최근 5개에서 중복
            return {"halt": True, "reason": "동일 액션 반복 감지 — 루프 의심"}

        self.recent_actions.append(action_hash)
        if len(self.recent_actions) > 10:
            self.recent_actions.pop(0)

        return {"halt": False}

실패 패턴 3 — 툴 선택 실패 (Tool Misuse)

증상:

비싼 툴(웹 검색)을 단순 질문에도 사용
툴 호출 3~15%가 조용히 실패
잘못된 파라미터로 API 호출 → 부분 데이터 반환

원인: 툴 설명이 모호하거나, 언제 어떤 툴을 쓸지 명시 안 되어 있을 때 발생해요.

# 나쁜 툴 정의 — 에이전트가 언제나 웹 검색 사용
tools = [
    {
        "name": "web_search",
        "description": "정보를 검색합니다",
        # ← 언제 쓰면 안 되는지 없음
    }
]

# 좋은 툴 정의 — 명확한 사용 조건
tools = [
    {
        "name": "web_search",
        "description": """인터넷에서 최신 정보를 검색합니다.
        사용 조건: 실시간 데이터, 최신 뉴스, 현재 가격이 필요할 때만.
        사용 금지: 이미 컨텍스트에 있는 정보, 코드 생성, 계산 작업.
        비용: 호출당 높음 — 꼭 필요할 때만 사용하세요."""
    },
    {
        "name": "calculate",
        "description": """수학 계산을 수행합니다.
        사용 조건: 수치 계산이 필요한 모든 경우.
        웹 검색보다 항상 먼저 시도하세요."""
    }
]

툴 호출 실패 안전하게 처리:

import json

class SafeToolExecutor:
    def __init__(self, tools: dict, max_retries: int = 3):
        self.tools = tools
        self.max_retries = max_retries

    def execute(self, tool_name: str, params: dict) -> dict:
        if tool_name not in self.tools:
            return {
                "error": f"존재하지 않는 툴: {tool_name}",
                "available": list(self.tools.keys())
            }

        for attempt in range(self.max_retries):
            try:
                result = self.tools[tool_name](**params)

                # 결과 스키마 검증
                if not self._validate_output(tool_name, result):
                    return {
                        "error": "툴 결과 형식 오류",
                        "raw": str(result)[:500]
                    }

                return {"success": True, "result": result}

            except Exception as e:
                if attempt == self.max_retries - 1:
                    return {
                        "error": f"툴 실행 실패 ({self.max_retries}회 시도): {str(e)}",
                        "suggestion": "다른 방법을 시도하거나 사용자에게 알리세요"
                    }
                time.sleep(2 ** attempt)  # 지수 백오프

    def _validate_output(self, tool_name: str, result) -> bool:
        # 툴별 예상 출력 형식 검증
        schemas = {
            "web_search": lambda r: isinstance(r, list),
            "calculate": lambda r: isinstance(r, (int, float)),
        }
        validator = schemas.get(tool_name, lambda r: True)
        return validator(result)

실패 패턴 4 — 목표 표류 (Goal Drift)

증상:

작업 중간에 다른 일을 시작함
원래 지시사항을 잊고 엉뚱한 방향으로 감
"더 나은 방법"을 혼자 결정해서 다른 걸 구현

원인: 장기 실행 중 컨텍스트가 오래된 대화로 가득 차면서 초기 목표를 잃어버려요. 또는 외부 입력(파일, API 응답)에 프롬프트 인젝션이 있을 때 발생해요.

실제 사례:
"인증 모듈 리팩토링해줘"
→ 에이전트가 리팩토링 중 보안 취약점 발견
→ 취약점 수정도 시작
→ 그 과정에서 DB 스키마 변경
→ 마이그레이션 스크립트 작성
→ 30분 후: 원래 요청과 전혀 다른 작업 진행 중

방어 코드:

SYSTEM_PROMPT = """
## 목표
{original_goal}

## 범위
다음만 수행하세요:
{scope}

## 범위 밖 작업 발견 시
범위 밖 작업이 필요하다고 판단되면:
1. 현재 작업을 중단하세요
2. 발견한 내용을 보고하세요
3. 사용자 승인을 받은 후에만 진행하세요

절대로 범위를 혼자 확장하지 마세요.
"""

def create_bounded_agent(goal: str, scope: list[str]):
    return SYSTEM_PROMPT.format(
        original_goal=goal,
        scope="\n".join(f"- {s}" for s in scope)
    )

실패 패턴 5 — 조용한 품질 저하 (Silent Quality Degradation)

가장 위험한 패턴이에요. 에러가 없어서 아무도 모르다가 나중에 큰 문제가 됩니다.

증상:

에러 없음
HTTP 200 정상 반환
근데 결과가 점점 엉터리

예시:
→ 금융 계산이 미묘하게 틀림
→ 번역 품질이 서서히 저하
→ 추천 시스템이 편향된 결과 반환
→ 3개월 후 데이터 분석에서 발견

방어 코드 — 온라인 평가:

from anthropic import Anthropic

client = Anthropic()

def evaluate_response(question: str, response: str, criteria: list) -> dict:
    """응답 품질을 LLM으로 자동 평가"""

    eval_prompt = f"""다음 응답을 평가해줘.

질문: {question}
응답: {response}

평가 기준:
{chr(10).join(f'- {c}' for c in criteria)}

각 기준에 대해 0-10 점수와 이유를 JSON으로 반환해줘.
형식: {{"criteria_name": {{"score": 0-10, "reason": "이유"}}}}"""

    result = client.messages.create(
        model="claude-haiku-4-5",  # 평가는 저렴한 모델로
        max_tokens=500,
        messages=[{"role": "user", "content": eval_prompt}]
    )

    import json
    scores = json.loads(result.content[0].text)
    avg_score = sum(v["score"] for v in scores.values()) / len(scores)

    if avg_score < 7.0:
        alert_team(f"품질 저하 감지: 평균 {avg_score:.1f}/10")

    return {"scores": scores, "avg": avg_score}

# 카나리 테스트 (5분마다 실행)
def canary_eval():
    test_cases = [
        {"q": "2+2는?", "expected_contains": "4"},
        {"q": "한국의 수도는?", "expected_contains": "서울"},
    ]
    for tc in test_cases:
        resp = agent.run(tc["q"])
        if tc["expected_contains"] not in resp:
            alert_team(f"카나리 실패: {tc['q']}")

실패 패턴 6 — 스키마 드리프트 (Schema Drift)

증상:

라이브러리 업데이트 후 갑자기 모든 툴 호출 실패
Anthropic API: "tools.0.custom.input_schema.type: Field required"
OpenAI API: "schema must be a JSON Schema of 'type: object'"

실제 사례: 2026년 2월 n8n v2.6.3 업데이트 후 Vector Store 툴의 JSON 스키마 생성 방식이 변경됨. 엔터프라이즈 전체 워크플로우 중단. 롤백만이 해결책.

방어 코드:

import jsonschema

def validate_tool_schema(tool_definition: dict) -> bool:
    """툴 정의가 Anthropic API 스펙과 맞는지 검증"""

    required_fields = ["name", "description", "input_schema"]
    for field in required_fields:
        if field not in tool_definition:
            raise ValueError(f"필수 필드 없음: {field}")

    schema = tool_definition["input_schema"]
    if schema.get("type") != "object":
        raise ValueError(f"input_schema.type은 'object'여야 함, 현재: {schema.get('type')}")

    if "properties" not in schema:
        raise ValueError("input_schema에 properties 필드 필요")

    return True

# CI/CD 파이프라인에서 배포 전 검증
def pre_deploy_check(tools: list) -> None:
    for tool in tools:
        try:
            validate_tool_schema(tool)
        except ValueError as e:
            raise RuntimeError(f"툴 스키마 검증 실패 [{tool.get('name')}]: {e}")

    print("✅ 모든 툴 스키마 검증 통과")

실패 패턴 7 — 멀티에이전트 캐스케이딩 실패

증상:

서브에이전트 하나의 에러가 전체 파이프라인 실패로 확산
에이전트 A의 잘못된 출력이 에이전트 B로 전달
B가 잘못된 입력으로 작업해서 C로 전달
최종 출력이 처음부터 틀려있지만 아무도 모름

원인: 에이전트 간 출력이 검증 없이 다음 에이전트에게 그대로 전달됩니다.

단계별 정확도가 85%라면:
1단계: 85%
2단계: 85% × 85% = 72%
3단계: 72% × 85% = 61%
4단계: 61% × 85% = 52%
5단계: 52% × 85% = 44%

5단계 파이프라인 = 완료 성공률 44%

방어 코드:

from dataclasses import dataclass
from typing import Optional

@dataclass
class AgentOutput:
    success: bool
    data: dict
    confidence: float  # 0.0 ~ 1.0
    errors: list[str]

def run_pipeline_with_validation(stages: list) -> AgentOutput:
    """각 단계 출력을 검증하면서 파이프라인 실행"""

    context = {}
    for i, stage in enumerate(stages):
        result = stage["agent"].run(context)

        # 각 단계 출력 검증
        if not result.success:
            return AgentOutput(
                success=False,
                data={},
                confidence=0.0,
                errors=[f"단계 {i+1} ({stage['name']}) 실패: {result.errors}"]
            )

        # 신뢰도 임계값 체크
        if result.confidence < 0.7:
            # 신뢰도 낮으면 사람에게 확인 요청
            human_approval = request_human_review(
                stage=stage["name"],
                output=result.data,
                confidence=result.confidence
            )
            if not human_approval:
                return AgentOutput(
                    success=False,
                    data={},
                    confidence=0.0,
                    errors=[f"단계 {i+1}: 낮은 신뢰도({result.confidence:.0%})로 사람이 거부"]
                )

        context.update(result.data)

    return AgentOutput(success=True, data=context, confidence=1.0, errors=[])

요약 — 실패 패턴 체크리스트

배포 전 점검:
□ 컨텍스트 압축 로직 있는가? (20K 토큰 기준)
□ 무한 루프 감지기 있는가? (반복 + 비용 한도)
□ 툴 정의에 "언제 쓰면 안 되는지" 있는가?
□ 범위 밖 작업 발견 시 중단 지시가 있는가?
□ 카나리 평가 테스트 있는가? (5분 주기)
□ 툴 스키마 검증이 CI에 포함됐는가?
□ 멀티에이전트 단계별 출력 검증 있는가?

모니터링:
□ 세션당 토큰 사용량 추적
□ 비용 이상 알림 (평소의 3배 이상)
□ 툴 호출 성공률 추적
□ 응답 품질 점수 추적

인간 승인 게이트 (필수):
□ 삭제 작업
□ 전송/발행 작업
□ 결제/청구 작업
□ 신뢰도 70% 미만 출력

 

반응형