🔥 고급2026-07-046~8분
안전한 Tool 실행: 샌드박스 격리·타임아웃·멱등 재시도 설계
에이전트가 tool을 반복 호출할 때 가장 큰 위험은 부분 실행 후 실패로 인한 상태 불일치다. 타임아웃 계층, 멱등 키, 실패 시 tool_result 오류 피드백 패턴으로 안전망을 구성한다.
tool-useagent-safetyreliability
왜 Tool 실행은 위험한가
Claude가 tool_use 블록을 반환한 뒤 실제 실행은 개발자 코드에서 일어난다. 세 가지 실패 모드가 존재한다.
- 부분 실행 후 타임아웃 — DB write 절반 완료 후 네트워크 단절. 에이전트는 성공 여부를 모른 채 재시도해 중복 실행.
- 무한 루프 — 에이전트가 오류 결과를 받고 동일 tool을 반복 호출. 비용과 사이드이펙트 누적.
- 권한 상승 —
bashtool처럼 범용 도구에서 프롬프트 인젝션으로 의도치 않은 시스템 명령 실행.
멱등 키 + 타임아웃 계층 설계
모든 상태 변경 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 호출 횟수·성공률·평균 실행 시간 메트릭 수집