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

멀티 에이전트 환경에서 안전한 Tool 실행과 재시도 전략

오케스트레이터-서브에이전트 구조에서 tool 호출의 실패 모드를 분류하고, 멱등성 보장·지수 백오프·인간 승인 게이트를 조합한 운영 가능한 재시도 파이프라인을 설명한다.

multi-agenttool-safetyretry-strategy

멀티 에이전트 Tool 실행의 위험 구조

오케스트레이터가 서브에이전트에게 tool 실행을 위임할 때 두 가지 치명적 실패가 발생한다. 중복 실행(결제 2회, 이메일 2회 발송)과 부분 실행(DB 업데이트 성공 후 알림 실패로 상태 불일치). 이를 방지하려면 tool 실행을 세 범주로 분류해야 한다.

| 범주 | 예시 | 재시도 정책 | |------|------|------------| | 읽기 전용 | DB 조회, API GET | 즉시 최대 3회 | | 멱등 쓰기 | S3 업로드, upsert | 지수 백오프 3회 | | 비멱등 쓰기 | 결제, 이메일 발송 | 인간 승인 후 1회만 |

비멱등 tool에 자동 재시도를 적용하는 것은 가장 흔한 운영 사고 원인이다.

구현: 재시도 레이어와 인간 게이트

import anthropic, time, uuid
from enum import Enum

client = anthropic.Anthropic()

class ToolRisk(Enum):
    READ_ONLY = "read_only"
    IDEMPOTENT = "idempotent"
    NON_IDEMPOTENT = "non_idempotent"

TOOL_REGISTRY = {
    "search_db": ToolRisk.READ_ONLY,
    "upload_file": ToolRisk.IDEMPOTENT,
    "send_payment": ToolRisk.NON_IDEMPOTENT,
}

def execute_tool_with_policy(tool_name: str, tool_input: dict) -> dict:
    risk = TOOL_REGISTRY.get(tool_name, ToolRisk.NON_IDEMPOTENT)
    idempotency_key = str(uuid.uuid4())

    if risk == ToolRisk.NON_IDEMPOTENT:
        # 실제 환경: Slack/PagerDuty로 승인 요청 후 블로킹
        approved = request_human_approval(tool_name, tool_input)
        if not approved:
            return {"error": "인간 승인 거부", "status": "rejected"}
        return call_tool_once(tool_name, tool_input, idempotency_key)

    max_retries = 3 if risk == ToolRisk.READ_ONLY else 3
    for attempt in range(max_retries):
        try:
            return call_tool_once(tool_name, tool_input, idempotency_key)
        except TransientError as e:
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)  # 1s, 2s, 4s
    
def run_agent_loop(user_task: str):
    messages = [{"role": "user", "content": user_task}]
    tools = [{"name": t, "description": f"{t} 실행",
              "input_schema": {"type": "object", "properties": {}}} 
             for t in TOOL_REGISTRY]
    
    while True:
        response = client.messages.create(
            model="claude-opus-4-5", max_tokens=2048,
            tools=tools, messages=messages
        )
        if response.stop_reason == "end_turn":
            return response.content
        
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                result = execute_tool_with_policy(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(result)
                })
        
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": tool_results})

실패 모드와 트레이드오프

프롬프트 인젝션: 외부 데이터(웹 검색 결과, 사용자 파일)가 tool 입력을 조작할 수 있다. 서브에이전트가 실행할 tool 목록을 오케스트레이터가 화이트리스트로 제한해야 한다.

무한 루프: 에이전트가 tool 오류를 반복 재시도하며 루프에 빠진다. max_iterations=10 같은 하드 리밋과 함께 반복된 동일 tool 호출을 탐지하는 해시 기반 중복 제거가 필요하다.

트레이드오프: 인간 승인 게이트는 안전하지만 p99 레이턴시를 수분~수시간으로 늘린다. 비즈니스 크리티컬도가 낮은 비멱등 작업은 금액·수량 임계값(예: $100 미만 결제)으로 자동 승인 조건을 세분화할 수 있다.

운영 체크리스트

  • [ ] 모든 tool을 READ_ONLY / IDEMPOTENT / NON_IDEMPOTENT로 문서화
  • [ ] 멱등성 키를 tool 호출마다 생성·기록해 중복 실행 감사 로그 확보
  • [ ] 에이전트 루프에 max_iterations 하드 리밋 설정
  • [ ] 외부 입력을 포함하는 tool 입력값에 대한 스키마 검증 필수 적용
  • [ ] 비멱등 tool 호출 횟수를 별도 메트릭으로 알림 설정 (임계치 초과 시 PagerDuty)