Claude Agent SDK 한국어 완전 가이드: 자율 AI 에이전트 만들기
Claude Agent SDK란 별도의 패키지가 아니라, 표준 anthropic Python SDK에 내장된 Tool Use 기능을 활용해 자율 에이전트를 구축하는 패턴입니다. 에이전트의 핵심 구조는 단순합니다: 메시지를 보내면 Claude가 도구를 호출하고, 도구 실행 결과를 돌려주면 Claude가 다음 판단을 내립니다. stop_reason == "end_turn"이 될 때까지 이 루프를 반복합니다. pip install anthropic 하나로 시작할 수 있으며, 이 가이드에서는 기본 설치부터 멀티에이전트 오케스트레이션, 프로덕션 에러 처리까지 실전 Python 코드와 함께 단계별로 설명합니다.
40가지 프로덕션 에이전트 패턴을 한 권에?
Agent SDK Cookbook ($49)은 이 가이드의 모든 패턴에 더해 30개 이상의 완성 예제 — 재시도 로직, 멀티에이전트 오케스트레이션, 비용 제한, 결정론적 테스트 — 를 Python 코드와 함께 수록했습니다.
SDK 설치
pip install anthropic
설치 후 API 키를 환경변수로 설정합니다:
export ANTHROPIC_API_KEY=sk-ant-api03-여기에키입력
프로젝트에서는 .env 파일을 사용하고 python-dotenv로 로드하는 방식을 권장합니다:
pip install python-dotenv
# agent.py
import anthropic
import os
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
client 인스턴스는 모듈 최상단에서 한 번만 생성합니다. 요청마다 새로 만들면 커넥션 오버헤드가 발생합니다.
기본 에이전트 구조 — 에이전틱 루프
에이전트의 핵심은 while True 루프입니다. Claude가 "end_turn"을 반환할 때까지 도구를 호출하고 결과를 누적합니다.
import anthropic
import json
client = anthropic.Anthropic()
def run_agent(tools: list, tool_executor, initial_message: str) -> str:
"""
범용 에이전틱 루프.
tool_executor: (tool_name, tool_input) -> str 형태의 콜백
반환값: Claude의 최종 텍스트 응답
"""
messages = [{"role": "user", "content": initial_message}]
while True:
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
tools=tools,
messages=messages
)
# 완료 — 텍스트 반환
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
return block.text
return ""
# Claude가 도구 호출을 요청함
if response.stop_reason == "tool_use":
tool_calls = [b for b in response.content if b.type == "tool_use"]
tool_results = []
for call in tool_calls:
result = tool_executor(call.name, call.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": call.id,
"content": str(result)
})
# 어시스턴트 응답과 도구 결과를 히스토리에 추가
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
else:
break # 예상치 못한 stop_reason
return ""
messages 배열에 assistant 응답과 tool_result를 교대로 추가하는 것이 핵심입니다. 순서가 틀리면 API에서 400 에러가 발생합니다.
도구(Tools) 정의
도구는 JSON Schema 형식으로 정의합니다. Claude는 이 스키마를 보고 언제, 어떤 인자로 도구를 호출할지 판단합니다.
기본 도구
SEARCH_TOOL = {
"name": "search_docs",
"description": "문서에서 관련 정보를 검색합니다",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "검색어"
},
"max_results": {
"type": "integer",
"description": "최대 결과 수",
"default": 5
}
},
"required": ["query"]
}
}
Enum 타입 도구
TICKET_TOOL = {
"name": "update_ticket_status",
"description": "지원 티켓의 상태를 업데이트합니다",
"input_schema": {
"type": "object",
"properties": {
"ticket_id": {"type": "string"},
"status": {
"type": "string",
"enum": ["open", "in_progress", "resolved", "closed"]
},
"note": {"type": "string", "description": "선택 메모"}
},
"required": ["ticket_id", "status"]
}
}
description 필드는 Claude가 도구를 올바르게 사용하는 데 결정적인 역할을 합니다. "무엇을 하는 도구인지"를 명확히 적을수록 정확도가 높아집니다.
멀티에이전트 패턴
복잡한 작업은 하나의 에이전트에 맡기지 않고 오케스트레이터와 서브에이전트로 분리합니다.
오케스트레이터 + 서브에이전트 패턴
import asyncio
async def run_agent_async(tools, tool_executor, message):
"""비동기 에이전틱 루프 (asyncio.gather()와 함께 사용)"""
from anthropic import AsyncAnthropic
async_client = AsyncAnthropic()
messages = [{"role": "user", "content": message}]
while True:
response = await async_client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
tools=tools,
messages=messages
)
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
return block.text
return ""
if response.stop_reason == "tool_use":
tool_calls = [b for b in response.content if b.type == "tool_use"]
tool_results = []
for call in tool_calls:
result = await tool_executor(call.name, call.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": call.id,
"content": str(result)
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
else:
break
return ""
async def parallel_research(topics: list[str]) -> list[str]:
"""여러 토픽을 병렬로 리서치하는 서브에이전트 패턴"""
tasks = [
run_agent_async(
tools=SEARCH_TOOLS,
tool_executor=async_search_executor,
message=f"다음 토픽을 리서치하고 핵심 내용을 요약하세요: {topic}"
)
for topic in topics
]
# 모든 서브에이전트를 동시에 실행
results = await asyncio.gather(*tasks)
return results
asyncio.gather()를 사용하면 각 서브에이전트가 독립적으로 실행되므로, 5개 토픽을 리서치할 때 순차 실행 대비 시간을 80% 이상 단축할 수 있습니다.
실전 예제 — 고객 지원 에이전트
실제 업무에 바로 적용 가능한 고객 지원 에이전트입니다. 티켓 조회, 상태 업데이트, 이메일 발송 세 가지 도구를 갖추고 있습니다.
import anthropic
import json
client = anthropic.Anthropic()
# 도구 정의
SUPPORT_TOOLS = [
{
"name": "get_ticket",
"description": "티켓 ID로 지원 티켓 정보를 조회합니다",
"input_schema": {
"type": "object",
"properties": {
"ticket_id": {"type": "string"}
},
"required": ["ticket_id"]
}
},
{
"name": "update_ticket",
"description": "티켓 상태와 메모를 업데이트합니다",
"input_schema": {
"type": "object",
"properties": {
"ticket_id": {"type": "string"},
"status": {
"type": "string",
"enum": ["open", "in_progress", "resolved"]
},
"internal_note": {"type": "string"}
},
"required": ["ticket_id", "status"]
}
},
{
"name": "send_reply",
"description": "고객에게 이메일 답변을 발송합니다",
"input_schema": {
"type": "object",
"properties": {
"ticket_id": {"type": "string"},
"message": {"type": "string", "description": "고객에게 보낼 메시지"}
},
"required": ["ticket_id", "message"]
}
}
]
# 실제 도구 구현 (데모용 스텁)
def execute_support_tool(name: str, input: dict) -> str:
if name == "get_ticket":
# 실제로는 DB 조회
return json.dumps({
"id": input["ticket_id"],
"customer": "김철수",
"issue": "결제 오류 발생 — 카드가 2회 청구됨",
"status": "open"
})
elif name == "update_ticket":
return json.dumps({"success": True, "status": input["status"]})
elif name == "send_reply":
# 실제로는 이메일 API 호출
return json.dumps({"sent": True, "ticket_id": input["ticket_id"]})
else:
return json.dumps({"error": f"알 수 없는 도구: {name}"})
SYSTEM_PROMPT = """당신은 전문 고객 지원 에이전트입니다.
티켓을 조회하고, 문제를 분석하고, 적절한 조치를 취한 후 고객에게 답변하세요.
항상 공감하는 태도로 명확하고 간결하게 소통합니다."""
def handle_support_request(ticket_id: str) -> str:
return run_agent(
tools=SUPPORT_TOOLS,
tool_executor=execute_support_tool,
initial_message=f"티켓 {ticket_id}를 처리해주세요. 문제를 파악하고 해결책을 제시한 후 고객에게 답변하세요."
)
# 실행 예시
# result = handle_support_request("TICKET-1234")
# print(result)
Claude API + FastAPI 연동 가이드를 참고하면 이 에이전트를 웹 API로 감싸 서비스화할 수 있습니다.
에러 처리
에이전트에서 도구 실행이 실패할 때는 예외를 던지지 않고 에러를 도구 결과로 반환합니다. Claude가 에러를 인식하고 재시도하거나 다른 방법을 선택합니다.
도구 에러를 결과로 반환
def execute_tool(name: str, input: dict) -> str:
try:
if name == "search_docs":
results = search_database(input["query"])
return json.dumps(results)
elif name == "update_ticket":
success = update_ticket(input["ticket_id"], input["status"])
return json.dumps({"success": success})
else:
return json.dumps({"error": f"알 수 없는 도구: {name}"})
except Exception as e:
# 예외를 올리지 않고 에러 정보를 도구 결과로 반환
return json.dumps({"error": str(e), "tool": name})
재시도 + 최대 턴 제한
import time
from anthropic import RateLimitError, APITimeoutError, APIConnectionError
def run_agent_with_retry(
tools: list,
tool_executor,
message: str,
max_retries: int = 3,
max_turns: int = 20
) -> str:
messages = [{"role": "user", "content": message}]
turns = 0
while turns < max_turns:
# API 에러 재시도 로직
for attempt in range(max_retries):
try:
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
tools=tools,
messages=messages,
timeout=30.0
)
break
except RateLimitError:
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt) # 지수 백오프: 1초, 2초, 4초
except (APITimeoutError, APIConnectionError):
if attempt == max_retries - 1:
raise
time.sleep(1)
turns += 1
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
return block.text
return ""
if response.stop_reason == "tool_use":
tool_calls = [b for b in response.content if b.type == "tool_use"]
tool_results = [
{
"type": "tool_result",
"tool_use_id": call.id,
"content": str(tool_executor(call.name, call.input))
}
for call in tool_calls
]
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
raise RuntimeError(f"에이전트가 최대 턴 수({max_turns})를 초과했습니다")
max_turns 제한은 필수입니다. 도구가 무한 루프에 빠지거나 Claude가 같은 도구를 반복 호출하는 상황을 방지합니다. 실제 프로젝트에서는 20턴이 대부분의 복잡한 작업에 충분합니다.
Claude Code Hooks 한국어 가이드에서는 에이전트 실행 전후로 자동으로 트리거되는 훅(hook) 패턴을 다룹니다.
토큰 비용 추적
에이전트는 여러 번의 API 호출을 반복하므로, 비용 추적이 특히 중요합니다.
from dataclasses import dataclass, field
@dataclass
class AgentStats:
input_tokens: int = 0
output_tokens: int = 0
turns: int = 0
@property
def estimated_cost_usd(self) -> float:
# claude-sonnet-4-5 기준 (2026년 5월)
input_cost = self.input_tokens * 3.0 / 1_000_000
output_cost = self.output_tokens * 15.0 / 1_000_000
return input_cost + output_cost
def __str__(self):
return (
f"턴: {self.turns} | "
f"입력: {self.input_tokens:,} 토큰 | "
f"출력: {self.output_tokens:,} 토큰 | "
f"예상 비용: ${self.estimated_cost_usd:.4f}"
)
def run_agent_with_stats(tools, tool_executor, message) -> tuple[str, AgentStats]:
stats = AgentStats()
messages = [{"role": "user", "content": message}]
while True:
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
tools=tools,
messages=messages
)
stats.input_tokens += response.usage.input_tokens
stats.output_tokens += response.usage.output_tokens
stats.turns += 1
if response.stop_reason == "end_turn":
result = next(
(b.text for b in response.content if hasattr(b, "text")), ""
)
return result, stats
if response.stop_reason == "tool_use":
tool_calls = [b for b in response.content if b.type == "tool_use"]
tool_results = [
{
"type": "tool_result",
"tool_use_id": c.id,
"content": str(tool_executor(c.name, c.input))
}
for c in tool_calls
]
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
# 사용 예시
# result, stats = run_agent_with_stats(tools, executor, "데이터 파이프라인을 분석하세요")
# print(f"결과: {result[:100]}...")
# print(stats)
# 출력: 턴: 4 | 입력: 8,432 토큰 | 출력: 1,204 토큰 | 예상 비용: $0.0433
10턴 이상 실행되는 에이전트는 턴당 평균 비용을 계산해 max_turns와 모델 선택을 최적화하세요. 단순 검색·요약 작업은 claude-haiku-4-5로 교체하면 비용을 80% 절감할 수 있습니다.
프로덕션 적용 체크리스트
에이전트를 실제 서비스에 배포하기 전에 아래 항목을 확인하세요:
-
max_turns제한 설정 — 무한 루프 방지 - 모든 도구에 에러 처리 추가 — 예외 대신 에러 JSON 반환
- Rate Limit 재시도 로직 (지수 백오프) 구현
- 요청별 토큰/비용 로깅 — 이상 감지용
-
ANTHROPIC_API_KEY를 환경변수로만 주입 — 코드에 하드코딩 절대 금지 - 시스템 프롬프트에 에이전트 역할과 제약 조건 명시
40가지 프로덕션 에이전트 패턴이 필요하다면?
Agent SDK Cookbook에는 고객 지원 에이전트, 데이터 파이프라인 에이전트, 코드 리뷰 에이전트, DevOps 모니터링 에이전트, 영업 자동화 에이전트의 완성 코드가 수록되어 있습니다. 에러 처리, 테스트, 배포 설정까지 포함한 프로덕션 레디 코드입니다.
→ Agent SDK Cookbook 구매하기 ($49)
30일 환불 보장. 구매 즉시 다운로드.
Frequently Asked Questions
Claude Agent SDK는 별도로 설치해야 하나요?
아닙니다. "Claude Agent SDK"는 별도 패키지가 아닙니다. pip install anthropic으로 설치하는 표준 Anthropic Python SDK에 Tool Use 기능이 내장되어 있으며, 이를 활용해 에이전트 루프를 구현합니다. LangChain, AutoGPT 같은 외부 프레임워크 없이 순수 anthropic SDK만으로 완전한 에이전트를 만들 수 있습니다.
에이전트가 도구를 몇 번까지 호출할 수 있나요?
API 자체에는 턴 수 제한이 없습니다. 하지만 턴마다 컨텍스트가 누적되므로 200K 토큰 한계에 가까워질 수 있습니다. 긴 작업에는 max_turns=20 수준의 가드를 설정하고, 필요하면 이전 도구 결과를 요약해 컨텍스트를 압축하는 전략을 사용하세요.
여러 에이전트를 동시에 실행할 수 있나요?
네, asyncio.gather()로 여러 에이전트를 병렬 실행할 수 있습니다. 각 에이전트는 독립된 messages 배열을 가지므로 상태가 격리됩니다. 병렬 실행 시 Rate Limit(분당 토큰 수)에 주의하세요. 동시에 10개 이상의 에이전트를 실행한다면 토큰 소비가 급증할 수 있습니다.
claude-sonnet과 claude-opus 중 어떤 모델이 에이전트에 적합한가요?
대부분의 에이전트 작업(검색, 요약, 분류, 코드 생성)은 claude-sonnet-4-5로 충분합니다. claude-opus-4-5는 모호한 지시사항을 해석하거나 복잡한 다단계 추론이 필요한 경우에만 사용하세요. Opus는 Sonnet 대비 약 5배 비싸므로, 먼저 Sonnet으로 구축하고 품질이 부족한 특정 에이전트만 Opus로 업그레이드하는 전략을 권장합니다.
에이전트 실행 중 도구가 실패하면 어떻게 되나요?
도구 실행 실패 시 예외를 던지지 않고 {"error": "에러 메시지"} 형태의 JSON을 tool_result로 반환하세요. Claude는 에러 내용을 읽고 재시도하거나 다른 방법을 선택하거나 사용자에게 상황을 설명합니다. 예외를 그대로 올리면 에이전트 루프 전체가 종료되므로, 도구 레벨의 에러는 반드시 결과로 처리해야 합니다.