Strands 에러 처리
실패 모드, 패턴별 에러 처리, 복구 전략
멀티에이전트 에러 처리와 복원력
실패 모드, 패턴별 에러 처리, 복구 전략
멀티에이전트를 프로덕션에서 운영한다
에이전트 간 통신·에러 복원·안전 보장 — 운영의 3축
에러 처리와 복원력
5가지 실패 모드 · 내장 안전장치 · 복구 전략 · 노드 훅
에러 처리와 복원력
분산 에이전트 고유의 실패 모드와 단계적 복구 — 장애 격리의 원칙
에러 처리와 복원력
단일 에이전트 장애가 전체 시스템으로 전파되기 전에 — 감지하고, 차단하고, 대체하고, 에스컬레이션합니다
- 5가지 실패 모드 — 타임아웃·환각·도구 실패·핸드오프 유실·무한 루프
- SDK 내장 안전장치 — 타임아웃·핸드오프 상한·핑퐁 감지
- 복구 4단계 — Retry · Circuit Breaker · Fallback · Supervisor Escalation
- Strands Hooks — 도구 훅(BeforeToolCallEvent) + 노드 훅(BeforeNodeCallEvent)
- 알림 연동 — EventBridge → SNS
실패를 없앨 수는 없습니다 — 실패가 전파되지 않게 격리하고 자동 복구하는 구조가 복원력입니다.
멀티에이전트 5가지 실패 모드
타임아웃 연쇄·환각 전파·도구 실패·핸드오프 유실·무한 루프 — 격리가 필요한 이유
타임아웃 연쇄
에이전트 A가 B를 기다리고, B가 C를 기다리는 체인
하나의 지연이 전체 파이프라인을 블로킹
환각 전파
에이전트 A의 환각 출력을 B가 사실로 받아들여 증폭
멀티에이전트에서 환각이 기하급수적으로 확대
도구 실패
외부 API 장애·스로틀링으로 도구 호출 실패
에이전트가 대체 전략 없이 무한 재시도
핸드오프 유실
에이전트 간 작업 인계 시 컨텍스트 손실
수신 에이전트가 불완전한 정보로 잘못된 판단
무한 루프
에이전트가 동일 도구를 반복 호출하거나 A↔B 핑퐁
max_iterations 없으면 비용 폭발
다섯 모드의 공통점은 번진다는 것입니다 — 타임아웃은 체인으로, 환각은 증폭으로, 루프는 비용으로. 그래서 대응의 핵심은 격리입니다.
SDK 내장 안전장치 파라미터
Swarm 생성자·Graph 빌더에 이미 들어 있는 실행 상한 — 직접 구현 전의 1차 방어선
| 대상 | 파라미터 | 기본값 | 막는 실패 모드 |
|---|---|---|---|
| Swarm | execution_timeout | 900.0초 | 전체 실행 폭주 — 타임아웃 연쇄를 상위에서 차단 |
| Swarm | node_timeout | 300.0초 | 단일 노드 응답 지연 — 노드만 실패 처리하고 격리 |
| Swarm | max_handoffs · max_iterations | 각 20 | 무한 루프 — 핸드오프·반복 횟수 상한 |
| Swarm | repetitive_handoff_detection_window · repetitive_handoff_min_unique_agents | 0 (비활성) | A↔B 핑퐁 — 최근 N회 핸드오프 창의 고유 에이전트 수 검사 |
| GraphBuilder | set_execution_timeout() · set_node_timeout() | 미설정 | Graph 전체·노드별 시간 상한 |
| GraphBuilder | set_max_node_executions() | 미설정 | Graph 노드 실행 횟수 폭주 |
직접 구현 전에 내장부터 — Circuit Breaker를 짜기 전에 생성자 파라미터 한 줄로 막히는 실패인지 먼저 확인합니다.
복구 전략 4단계
실패 감지부터 Supervisor Escalation까지 단계적 복구 흐름
-
1
Retry (재시도)
Exponential backoff로 일시적 장애 극복. 모델 호출 스로틀링 재시도는 SDK 기본 내장 — Agent(retry_strategy=ModelRetryStrategy(...)), 기본 max_attempts=6·초기 지연 4초·최대 지연 128초(Python 기준)이며 None을 주면 꺼집니다. is_retryable() 오버라이드로 재시도 대상 예외를 확장할 수 있습니다. 도구 재시도는 AfterToolCallEvent 훅의 retry 플래그로 트리거합니다.
-
2
Circuit Breaker (차단)
연속 N회 실패 시 회로 차단 → 빠른 실패 반환. half-open 상태에서 주기적으로 복구 확인. 장애가 다른 에이전트로 전파되는 것을 방지합니다.
-
3
Fallback (대체)
주 경로 실패 시 대체 에이전트 또는 간소화된 응답 반환. 예: GPT 실패 → Claude로 전환, 검색 실패 → 캐시된 결과 사용.
-
4
Supervisor Escalation
모든 자동 복구 실패 시 Supervisor 에이전트가 개입. 작업을 재분배하거나 사람에게 에스컬레이션. EventBridge → SNS 알림 연동.
순서가 곧 전략입니다 — 재시도·차단·대체까지는 자동으로 버티고, 전부 실패했을 때만 사람에게 에스컬레이션합니다.
Retry + Circuit Breaker 구현
Strands Hooks 기반의 자동 복구 패턴 코드
핵심 포인트
- HookProvider
- 훅 클래스의 베이스 — register_hooks(registry)로 이벤트별 콜백 등록 (필수)
- BeforeToolCallEvent
- 도구 호출 직전 — 회로가 열려 있으면 event.cancel_tool 설정으로 도구만 취소 (예외 전파 없이 루프 지속)
- AfterToolCallEvent
- 도구 호출 직후 — event.exception으로 실패 판별, failures에 횟수·시각 집계 (코드에선 등록만 표시)
- hooks=[...]
- Agent에 훅 주입 — 이 한 줄로 자동 차단 활성화
코드
import time
from strands import Agent
from strands.hooks import BeforeToolCallEvent, AfterToolCallEvent, HookProvider
class CircuitBreakerHook(HookProvider):
def __init__(self, threshold=3, reset_timeout=60):
self.failures, self.th, self.reset = {}, threshold, reset_timeout
def register_hooks(self, registry, **kwargs):
registry.add_callback(BeforeToolCallEvent, self.on_before)
registry.add_callback(AfterToolCallEvent, self.on_after) # 실패 집계
def on_before(self, event): # 임계 초과면 차단 (reset 경과 후 half-open)
s = self.failures.get(event.tool_use["name"], {})
if s.get("count", 0) >= self.th and time.time() - s["last"] < self.reset:
event.cancel_tool = "circuit open" # 도구만 취소 — 루프는 지속
agent = Agent(hooks=[CircuitBreakerHook()])
노드 레벨 훅 — 멀티에이전트 이벤트 5종
단일 에이전트 훅 8종에 더해 노드 실행을 가로채는 전용 이벤트 — 노드 단위 승인·차단·계측
핵심 포인트
- MultiAgentInitializedEvent
- Swarm·Graph 초기화 완료 시 1회 — 오케스트레이션 시작점 계측
- NodeCallEvent 쌍
- Before/AfterNodeCallEvent — 노드(에이전트) 실행 전후, event.node_id로 어느 노드인지 식별하는 위임 추적의 정본 지점
- MultiAgentInvocationEvent
- Before/After 쌍 — 멀티에이전트 호출 전체의 전후, 실행 단위 비용·시간 집계
- cancel_node
- BeforeNodeCallEvent에서 설정 — 해당 노드만 취소. event.interrupt()로 실행 전 사람 승인 대기도 가능
- hooks=[...]
- Swarm·Graph 생성자에 그대로 주입 — 단일 에이전트 훅과 같은 등록 방식
코드
from strands import Agent
from strands.multiagent import Swarm
from strands.hooks import HookProvider, BeforeNodeCallEvent
class NodeApprovalHook(HookProvider):
def register_hooks(self, registry, **kwargs):
registry.add_callback(BeforeNodeCallEvent, self.on_node)
def on_node(self, event):
# 배포 노드만 실행 전 검사 — 거부되면 그 노드만 취소
if event.node_id == "deploy" and not approved(event.node_id):
event.cancel_node = "승인 거부 — deploy 노드 차단"
# 단일 에이전트와 같은 hooks= — Swarm/Graph 생성자 지원
swarm = Swarm(nodes=[research, write, deploy],
hooks=[NodeApprovalHook()])
운영 장면 — 장애에서 복구까지
리서치 Swarm의 요약 노드 타임아웃부터 session_manager 재개까지 — 안전장치가 개입하는 순서
research-swarm — 실행 로그 (예시 장면)
execution_timeout 900s · node_timeout 300s
14:02:11
WARN
summarizer 노드 응답 지연
대형 문서 요약에서 반복 무응답 — Swarm 전체가 이 노드를 기다리며 블로킹
14:07:11
AUTO
node_timeout=300.0 발동
SDK가 노드 실행을 강제 종료하고 실패로 기록 — 직접 짠 감시 코드 없이 개입
14:07:38
STOP
핑퐁 감지 — 안전 종료
researcher ↔ summarizer 왕복이 감지 창(detection_window) 안에서 고유 에이전트 2개뿐 — 재시도 루프 차단
14:09:40
RESUME
session_manager로 중단 지점 재개
serialize_state가 영속화한 상태·실행 이력을 deserialize_state로 복원 — 완료된 리서치 결과는 다시 돌리지 않음, shared context도 직렬화/역직렬화 시 보존
상한은 생성자 파라미터가 지키고, 재개는 session_manager가 맡습니다 — 장애 대응 코드보다 상태 영속화가 먼저입니다
복구의 핵심은 처음부터 다시가 아니라 중단 지점부터입니다 — 상태가 영속화되어 있어야 상한 파라미터가 안심하고 실행을 끊을 수 있습니다.
통신 · 에러 · 안전장치 — 세 축을 잡아야 프로덕션에서 살아남습니다