반응형
데모에선 완벽했어요. 프로덕션에 올렸더니 망가졌어요.
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% 미만 출력
반응형
'AI Agent' 카테고리의 다른 글
| smolagents 시작 가이드 — HuggingFace 초경량 에이전트 30분에 완성 (0) | 2026.04.21 |
|---|---|
| Google ADK 실전 가이드 — 에이전트를 백엔드 시스템처럼 만드는 법 (0) | 2026.04.17 |
| LLM 모델 라우팅 완전 가이드 — 분류기, 캐스케이딩, 시맨틱 캐시 실전 (1) | 2026.04.15 |
| AI 에이전트 옵저버빌리티 완전 가이드 — 에이전트가 뭘 하는지 추적하는 법 (0) | 2026.04.15 |
| n8n으로 AI 워크플로우 자동화 — 코드 없이 Claude 에이전트 파이프라인 만들기 (0) | 2026.04.14 |