k
korAI
고급 전체
🔥 고급2026-09-236~8분

멀티 에이전트 장애 격리: 서킷 브레이커 패턴으로 연쇄 실패 차단하기

에이전트 체인에서 하위 에이전트 하나가 무너질 때 전체 파이프라인이 타임아웃되는 문제를 서킷 브레이커와 헬스체크로 격리하고, 복구 경로를 자동화하는 운영 패턴을 다룬다.

multi-agentresilienceproduction

왜 멀티 에이전트에서 연쇄 실패가 위험한가

단일 에이전트 대비 멀티 에이전트 시스템은 실패 경로가 O(n²)으로 증가한다. 에이전트 A가 B를 호출하고, B가 C를 호출하는 3단 체인에서 C가 30초 타임아웃에 걸리면 A·B까지 스레드를 점유한 채 대기한다. 실측 사례: 4개 에이전트 체인에서 말단 에이전트 오류율 5%가 전체 파이프라인 P99 레이턴시를 12초 → 48초로 4배 올린 케이스가 보고된다.

핵심 실패 모드 세 가지:

  • Slow failure: 에이전트가 죽지 않고 응답만 느려짐 → 타임아웃 전까지 리소스 점유
  • Poison response: 잘못된 JSON을 반환해 상위 에이전트 파싱 폭탄 유발
  • Retry storm: 상위 에이전트가 재시도를 반복하며 API 할당량 소진

서킷 브레이커 구현 (Anthropic SDK 연동)

import anthropic
import time
from dataclasses import dataclass, field
from enum import Enum

class CircuitState(Enum):
    CLOSED = "closed"      # 정상
    OPEN = "open"          # 차단
    HALF_OPEN = "half_open" # 탐색

@dataclass
class CircuitBreaker:
    failure_threshold: int = 3      # 연속 실패 N회 → OPEN
    recovery_timeout: float = 30.0  # OPEN 후 N초 뒤 HALF_OPEN
    success_threshold: int = 2      # HALF_OPEN에서 성공 N회 → CLOSED
    state: CircuitState = CircuitState.CLOSED
    failure_count: int = 0
    success_count: int = 0
    opened_at: float = 0.0

    def call_allowed(self) -> bool:
        if self.state == CircuitState.OPEN:
            if time.time() - self.opened_at >= self.recovery_timeout:
                self.state = CircuitState.HALF_OPEN
                return True
            return False
        return True

    def record_success(self):
        self.failure_count = 0
        if self.state == CircuitState.HALF_OPEN:
            self.success_count += 1
            if self.success_count >= self.success_threshold:
                self.state = CircuitState.CLOSED
                self.success_count = 0

    def record_failure(self):
        self.failure_count += 1
        self.success_count = 0
        if self.failure_count >= self.failure_threshold:
            self.state = CircuitState.OPEN
            self.opened_at = time.time()

client = anthropic.Anthropic()
breaker = CircuitBreaker()

def sub_agent_call(prompt: str) -> str:
    if not breaker.call_allowed():
        raise RuntimeError(f"Circuit OPEN — 폴백 응답 반환")
    try:
        resp = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=512,
            messages=[{"role": "user", "content": prompt}],
            timeout=10.0,  # 반드시 명시적 타임아웃
        )
        breaker.record_success()
        return resp.content[0].text
    except Exception as e:
        breaker.record_failure()
        raise

트레이드오프: recovery_timeout=30초는 보수적이다. 빠른 복구가 필요하면 10초로 줄이되, HALF_OPEN 중 실패 시 다시 OPEN으로 복귀하므로 success_threshold를 1로 낮추면 플래핑(flapping) 위험이 생긴다. 운영 환경에서는 30/2 조합을 권장한다.

운영 체크리스트

  • [ ] 모든 에이전트 호출에 timeout 파라미터 명시 (기본값 의존 금지, SDK 기본 600초)
  • [ ] 서킷 상태를 메트릭으로 노출: circuit_state{agent="sub_b"} 1|0
  • [ ] OPEN 상태 진입 시 알림 → Slack/PagerDuty 연동
  • [ ] 폴백 응답 품질 정의: 빈 문자열 vs 캐시된 마지막 정상 응답 vs 하드코딩된 안전 응답
  • [ ] failure_threshold 값을 에이전트별로 다르게 설정 (핵심 에이전트 5, 보조 에이전트 2)
  • [ ] Retry storm 방지: 상위 에이전트에 exponential backoff + jitter 적용, 최대 재시도 3회
  • [ ] 매주 서킷 브레이커 발동 이력을 리뷰해 근본 원인 제거