멀티 에이전트 환경에서 안전한 Tool 실행과 재시도 전략
오케스트레이터-서브에이전트 구조에서 tool 호출의 실패 모드를 분류하고, 멱등성 보장·지수 백오프·인간 승인 게이트를 조합한 운영 가능한 재시도 파이프라인을 설명한다.
멀티 에이전트 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)