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

안전한 Tool 실행: 샌드박스 격리·타임아웃·멱등 재시도 설계

에이전트가 tool을 반복 호출할 때 가장 큰 위험은 부분 실행 후 실패로 인한 상태 불일치다. 타임아웃 계층, 멱등 키, 실패 시 tool_result 오류 피드백 패턴으로 안전망을 구성한다.

tool-useagent-safetyreliability

왜 Tool 실행은 위험한가

Claude가 tool_use 블록을 반환한 뒤 실제 실행은 개발자 코드에서 일어난다. 세 가지 실패 모드가 존재한다.

  1. 부분 실행 후 타임아웃 — DB write 절반 완료 후 네트워크 단절. 에이전트는 성공 여부를 모른 채 재시도해 중복 실행.
  2. 무한 루프 — 에이전트가 오류 결과를 받고 동일 tool을 반복 호출. 비용과 사이드이펙트 누적.
  3. 권한 상승bash tool처럼 범용 도구에서 프롬프트 인젝션으로 의도치 않은 시스템 명령 실행.

멱등 키 + 타임아웃 계층 설계

모든 상태 변경 tool 호출에 멱등 키를 부여한다. 키는 tool_use.id를 그대로 사용하면 Claude가 같은 블록을 재전송해도 중복 실행을 방지할 수 있다.

import anthropic, asyncio, uuid
from typing import Any

client = anthropic.Anthropic()

_executed: set[str] = set()  # 프로덕션에서는 Redis TTL 키로 대체

def execute_tool(tool_name: str, tool_input: dict, idempotency_key: str) -> Any:
    if idempotency_key in _executed:
        return {"status": "already_executed", "idempotency_key": idempotency_key}
    # 실제 실행 (예: DB write, API 호출)
    result = {"status": "ok", "data": f"{tool_name} completed"}
    _executed.add(idempotency_key)
    return result

def run_agent_loop(user_message: str, max_iterations: int = 5) -> str:
    messages = [{"role": "user", "content": user_message}]
    tools = [{"name": "write_record", "description": "DB에 레코드 저장",
              "input_schema": {"type": "object", "properties": {"data": {"type": "string"}},
                               "required": ["data"]}}]
    for iteration in range(max_iterations):
        response = client.messages.create(
            model="claude-opus-4-5", max_tokens=1024,
            tools=tools, messages=messages
        )
        if response.stop_reason == "end_turn":
            return next(b.text for b in response.content if hasattr(b, 'text'))
        tool_results = []
        for block in response.content:
            if block.type != "tool_use":
                continue
            try:
                # tool_use.id를 멱등 키로 재사용
                result = execute_tool(block.name, block.input, block.id)
                tool_results.append({"type": "tool_result", "tool_use_id": block.id,
                                     "content": str(result)})
            except Exception as e:
                # 실패를 에이전트에게 명시적으로 피드백
                tool_results.append({"type": "tool_result", "tool_use_id": block.id,
                                     "content": f"ERROR: {e}", "is_error": True})
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": tool_results})
    return "max_iterations reached"

타임아웃 계층: tool 실행 자체에 asyncio.wait_for(coro, timeout=10.0), 전체 에이전트 루프에 timeout=120s 상위 타임아웃을 중첩한다. 단일 타임아웃만 두면 느린 외부 API가 루프 전체를 점거한다.

실패 피드백과 운영 체크리스트

is_error: true로 tool_result를 반환하면 Claude는 오류 내용을 분석해 다른 전략을 시도한다. 오류를 숨기거나 빈 문자열로 반환하면 에이전트는 성공으로 오해하고 다음 단계를 진행해 상태 불일치가 생긴다.

트레이드오프: 멱등 키를 Redis에 저장할 때 TTL을 24시간으로 설정하면 재시작 후에도 안전하지만, 장기 실행 워크플로에서 동일 tool_use.id가 재사용될 수 없어 인간 리뷰가 필요한 케이스에서 재승인 UX가 복잡해진다.

운영 체크리스트

  • [ ] 모든 상태 변경 tool에 멱등 키 적용 (tool_use.id 활용)
  • [ ] tool 실행 타임아웃 ≤ 10초, 루프 전체 타임아웃 ≤ 120초
  • [ ] 실패 시 is_error: true + 구체적 메시지 반환
  • [ ] max_iterations 하드 캡 설정 (권장: 10 이하)
  • [ ] bash/shell 계열 tool은 whitelist 명령어만 허용하는 래퍼 레이어 추가
  • [ ] tool 호출 횟수·성공률·평균 실행 시간 메트릭 수집