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

안전한 Tool 실행: 샌드박스 격리·재시도 예산·부분 실패 복구 설계

에이전트가 tool을 실행할 때 무한 재시도, 사이드이펙트 중복, 권한 확대가 실제 운영 사고의 주원인이다. 재시도 예산 관리, 멱등성 보장, 실패 시 부분 복구 전략을 코드와 함께 설명한다.

tool-useagent-designreliability

실패 모드 세 가지

① 무한 재시도 루프: 모델이 tool 오류를 받으면 다시 호출하는 전략을 스스로 선택한다. 오케스트레이터가 재시도 횟수를 제한하지 않으면 토큰 비용이 선형이 아닌 지수로 증가한다. 실제 운영 사례에서 단일 에이전트 세션이 재시도 루프로 $12 이상 과금된 케이스가 보고된다.

② 사이드이펙트 중복: 결제·이메일 발송·DB write 같은 non-idempotent tool이 네트워크 타임아웃 후 재시도되면 중복 실행된다. 멱등성 키(idempotency key)를 tool 호출 레이어에서 주입하지 않으면 모델은 이 문제를 인식하지 못한다.

③ 권한 확대: tool 스키마에 admin: true 파라미터가 노출되어 있으면 모델이 해당 파라미터를 사용하는 호출을 생성할 수 있다. 화이트리스트 검증을 모델에 위임하면 안 된다.

재시도 예산과 멱등성 설계

import anthropic
import uuid
from dataclasses import dataclass, field
from typing import Any

@dataclass
class RetryBudget:
    max_tool_calls: int = 10
    used: int = 0
    idempotency_keys: dict = field(default_factory=dict)

    def acquire(self, tool_name: str) -> str:
        if self.used >= self.max_tool_calls:
            raise RuntimeError(f"Tool call budget exhausted ({self.max_tool_calls})")
        self.used += 1
        key = f"{tool_name}-{uuid.uuid4()}"
        self.idempotency_keys[key] = True
        return key

def execute_tool_safely(tool_name: str, tool_input: dict, budget: RetryBudget) -> Any:
    idem_key = budget.acquire(tool_name)
    ALLOWED_PARAMS = {"send_email": {"to", "subject", "body"}}
    allowed = ALLOWED_PARAMS.get(tool_name, set())
    blocked = set(tool_input.keys()) - allowed
    if blocked:
        return {"error": f"Blocked params: {blocked}", "idempotency_key": idem_key}
    # 실제 실행 (멱등성 키 헤더 전달)
    return {"status": "ok", "idempotency_key": idem_key}

client = anthropic.Anthropic()
budget = RetryBudget(max_tool_calls=10)

tools = [{"name": "send_email", "description": "이메일 발송",
          "input_schema": {"type": "object",
                          "properties": {"to": {"type": "string"},
                                         "subject": {"type": "string"},
                                         "body": {"type": "string"}},
                          "required": ["to", "subject", "body"]}}]

response = client.messages.create(
    model="claude-opus-4-5", max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "user@example.com에 가입 확인 메일 보내줘"}]
)

if response.stop_reason == "tool_use":
    for block in response.content:
        if block.type == "tool_use":
            result = execute_tool_safely(block.name, block.input, budget)
            print(f"budget_used={budget.used}/{budget.max_tool_calls} result={result}")

부분 실패 복구와 운영 체크리스트

tool 체인 중간에서 실패가 나면 전체를 재시작하지 않고 체크포인트 직전 상태부터 재개해야 한다. 각 tool 실행 결과를 외부 스토어(Redis, DynamoDB)에 기록하고, 세션 재시작 시 완료된 tool은 결과를 주입하고 건너뛴다. 이때 멱등성 키가 체크포인트 ID 역할을 한다.

트레이드오프: 멱등성 키 저장소 추가 → 레이턴시 +5~15ms, 비용은 Redis 기준 무시 가능. 재시도 예산을 너무 낮게 설정(≤3)하면 정상적인 다단계 작업도 실패한다. 도메인별로 평균 tool 호출 수를 측정 후 95 percentile의 1.5배를 예산으로 설정하는 것을 권장한다.

  • [ ] 모든 non-idempotent tool에 멱등성 키 레이어 강제
  • [ ] tool 파라미터 화이트리스트를 모델 외부(오케스트레이터)에서 검증
  • [ ] 세션당 최대 tool 호출 수 설정 및 초과 시 graceful error 반환
  • [ ] tool 실행 결과를 외부 스토어에 기록하여 체크포인트 복구 지원
  • [ ] admin, sudo, override 패턴 파라미터는 스키마에서 완전 제거
  • [ ] tool 호출 수 / 세션을 메트릭으로 수집하여 예산 재조정 주기 운영