k
korAI
고급 전체
🔥 고급2026-07-186~8분

멀티 에이전트 환경에서 안전한 Tool 실행: 격리 레이어와 재시도 예산 설계

여러 에이전트가 동일 tool을 병렬 호출할 때 발생하는 경쟁 조건과 무한 재시도를 막기 위한 실전 아키텍처와 코드 패턴을 정리한다.

multi-agenttool-executionreliability

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

단일 에이전트는 tool 결과를 순차적으로 처리하지만, 오케스트레이터-서브에이전트 구조에서는 동일 tool이 동시에 N회 호출된다. 대표적 장애 시나리오:

  • 파일 쓰기 경쟁: 두 서브에이전트가 동시에 write_file(path) 호출 → 마지막 쓰기가 이전 결과 덮어쓰기
  • 외부 API 중복 실행: 결제 API가 2회 호출되어 이중 청구
  • 무한 재시도 루프: 에이전트가 tool 실패를 받아 자체 판단으로 재시도 → 오케스트레이터도 재시도 → 지수적 폭발

실측 사례: 10개 서브에이전트 구조에서 재시도 예산 미설정 시 단일 작업에 API 호출 340회 발생, 비용 $12 초과.

격리 레이어와 재시도 예산 구현

핵심 설계 원칙: tool 실행 레이어를 에이전트 외부로 분리하고, 중앙 레지스트리에서 호출 횟수·락·예산을 관리한다.

import anthropic
import asyncio
from collections import defaultdict
from dataclasses import dataclass, field

@dataclass
class ToolBudget:
    max_calls: int = 5
    calls_made: int = 0
    lock: asyncio.Lock = field(default_factory=asyncio.Lock)

class SafeToolExecutor:
    def __init__(self):
        self.budgets: dict[str, ToolBudget] = defaultdict(ToolBudget)
        self.client = anthropic.Anthropic()

    async def execute(self, tool_name: str, tool_input: dict) -> dict:
        budget = self.budgets[tool_name]
        async with budget.lock:
            if budget.calls_made >= budget.max_calls:
                return {"error": f"budget_exceeded: {tool_name} called {budget.calls_made} times"}
            budget.calls_made += 1
            call_id = budget.calls_made

        try:
            # 실제 tool 로직 (예: 파일 쓰기, API 호출)
            result = await self._dispatch(tool_name, tool_input)
            return {"result": result, "call_id": call_id}
        except Exception as e:
            # 예외 시 카운트 롤백하지 않음 → 실패도 예산 소모
            return {"error": str(e), "call_id": call_id}

    async def _dispatch(self, name: str, inp: dict) -> str:
        # 실제 구현체 연결
        if name == "write_file":
            return f"written: {inp.get('path')}"
        return "ok"

executor = SafeToolExecutor()
executor.budgets["write_file"] = ToolBudget(max_calls=3)
executor.budgets["call_payment_api"] = ToolBudget(max_calls=1)

트레이드오프:

  • max_calls=1 (멱등하지 않은 API): 안전하지만 에이전트가 오류로 판단해 다른 경로 탐색 가능성
  • max_calls=3~5 (읽기/분석 tool): 유연성과 안전성 균형
  • 락 경합 오버헤드: 10개 에이전트 기준 ~2ms 추가 지연, 무시 가능

실패 모드와 운영 체크리스트

실패 모드 3가지:

  1. 에이전트가 budget_exceeded를 재시도 신호로 해석 → tool result에 "retry": false 명시적 포함 필수
  2. 오케스트레이터와 서브에이전트 각각 재시도 로직 보유 → 지수 백오프 레이어를 오케스트레이터 단 하나에만 구현
  3. tool 예산 초기화 타이밍 → 작업(task) 단위로 리셋, 요청(request) 단위 아님

운영 체크리스트:

  • [ ] 모든 외부 부작용 tool에 max_calls 명시적 설정
  • [ ] tool 호출 로그에 agent_id + call_id 포함 (디버깅 필수)
  • [ ] 멱등하지 않은 API는 max_calls=1 + 고유 idempotency key 주입
  • [ ] 재시도 로직은 오케스트레이터 레이어 1곳에만 존재하도록 아키텍처 문서화
  • [ ] 작업 완료 후 calls_made 지표를 모니터링 대시보드에 적재