Claude API 함수 호출 사용 방법: AI 에이전트 구축을 위한 Tool Use 완전 가이드
함수 호출이 왜 중요한가
대규모 언어 모델은 텍스트 생성에 뛰어나지만, 그 자체로는 바깥 세상과 상호작용할 수 없다. 데이터베이스를 조회하지 못하고, API를 호출하지 못하며, 파일을 읽거나 쓰지 못한다. 이 한계를 넘어서는 열쇠가 바로 함수 호출(function calling), Anthropic 용어로는 tool use다.
Claude API의 tool use를 사용하면 개발자가 정의한 함수를 Claude가 직접 선택하고 파라미터를 생성한다. 모델이 “이 함수를 이 인자로 호출해달라”고 요청하면, 개발자의 코드가 실제 실행을 담당하고, 결과를 다시 Claude에 전달한다. 이 루프가 반복되면서 Claude는 외부 시스템과 대화하듯 작업을 수행하게 된다.
이 패턴을 올바르게 구현하면 다음과 같은 시스템을 만들 수 있다.
- 사내 데이터베이스에서 정보를 조회해 자연어로 답변하는 고객 지원 봇
- 캘린더 확인, 이메일 발송, 파일 생성을 조합하는 업무 자동화 에이전트
- 실시간 주가, 날씨, 뉴스 데이터를 가져와 분석하는 리서치 도우미
- 여러 마이크로서비스를 오케스트레이션하는 백엔드 워크플로우 엔진
이 글에서는 tool use의 기본 개념부터 멀티스텝 에이전트 루프, 프로덕션 수준의 에러 처리, 그리고 RAG와 액션 에이전트 같은 실전 디자인 패턴까지 빠짐없이 다룬다. 코드 예제는 Python과 Anthropic SDK를 기준으로 하되, 개념 자체는 언어에 관계없이 동일하게 적용할 수 있다.
Tool Use의 작동 원리
기본 흐름
Tool use의 전체 흐름은 네 단계로 구성된다.
첫째, 개발자가 사용 가능한 tool 목록을 API 요청에 포함한다. 각 tool은 이름, 설명, 파라미터 스키마를 가진다.
둘째, Claude가 사용자 메시지를 분석하고, 필요하다고 판단하면 하나 이상의 tool을 호출한다. 이때 Claude는 tool의 이름과 인자를 JSON으로 생성한다.
셋째, 개발자의 코드가 실제 함수를 실행하고 결과를 얻는다. 이 단계에서 데이터베이스 쿼리, 외부 API 호출, 파일 처리 등이 이루어진다.
넷째, 실행 결과를 tool_result 메시지로 Claude에 전달하면, Claude가 결과를 종합하여 최종 응답을 생성한다.
중요한 점은 Claude가 함수를 직접 실행하지 않는다는 것이다. Claude는 “이 함수를 호출하겠다”는 의도만 표현하고, 실제 실행은 전적으로 개발자의 코드에서 이루어진다. 이 설계 덕분에 보안 경계가 명확하게 유지된다. 개발자는 어떤 함수를 노출할지, 어떤 파라미터를 허용할지, 실행 전에 어떤 검증을 수행할지를 완전히 통제할 수 있다.
OpenAI function calling과의 차이
OpenAI의 function calling에 익숙하다면 Claude의 tool use와 몇 가지 차이를 알아둘 필요가 있다. 핵심 개념은 동일하지만, API 인터페이스와 응답 구조에 차이가 있다.
Claude는 응답의 content 배열 안에 tool_use 블록을 포함한다. 하나의 응답에 텍스트와 tool_use가 섞여 나올 수 있다. 반면 OpenAI는 별도의 tool_calls 필드를 사용한다.
Claude의 tool 결과 반환 방식도 다르다. tool_result라는 별도의 role을 가진 메시지로 결과를 보내며, 각 결과는 tool_use_id로 매칭된다.
이러한 차이는 구현 코드에 영향을 미치므로, 두 API를 모두 지원하는 시스템을 만든다면 추상화 계층을 하나 두는 것이 좋다.
Tool 정의 스키마 작성
기본 구조
Tool을 정의할 때는 세 가지 필수 요소를 제공해야 한다: name, description, input_schema.
tools = [
{
"name": "get_weather",
"description": "지정된 도시의 현재 날씨 정보를 조회합니다. 온도, 습도, 날씨 상태를 반환합니다.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "날씨를 조회할 도시 이름 (예: Seoul, Tokyo, New York)"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "온도 단위. 기본값은 celsius"
}
},
"required": ["city"]
}
}
]
name은 영문 소문자와 언더스코어로 구성하며, 함수의 역할을 직관적으로 나타내야 한다. 64자 이내로 유지하는 것이 좋다.
description은 Claude가 이 tool을 언제 사용해야 하는지 판단하는 핵심 근거다. “날씨를 조회한다” 같은 짧은 설명보다는 “지정된 도시의 현재 날씨 정보를 조회합니다. 온도, 습도, 날씨 상태를 반환합니다.”처럼 반환값까지 명시하는 것이 정확도를 높인다.
input_schema는 JSON Schema 형식을 따른다. 각 파라미터에 type, description을 명시하고, 선택적으로 enum, default, format 등을 추가할 수 있다. required 배열로 필수 파라미터를 지정한다.
복잡한 스키마 작성
실무에서는 단순한 문자열 파라미터만으로는 부족할 때가 많다. 중첩된 객체, 배열, 조건부 파라미터를 다루는 방법을 알아보자.
{
"name": "search_database",
"description": "사내 문서 데이터베이스에서 조건에 맞는 문서를 검색합니다. 키워드, 날짜 범위, 카테고리로 필터링할 수 있으며, 결과는 관련도 순으로 정렬됩니다.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "검색 키워드 또는 자연어 질의"
},
"filters": {
"type": "object",
"description": "검색 필터 조건",
"properties": {
"date_from": {
"type": "string",
"format": "date",
"description": "검색 시작 날짜 (YYYY-MM-DD)"
},
"date_to": {
"type": "string",
"format": "date",
"description": "검색 종료 날짜 (YYYY-MM-DD)"
},
"category": {
"type": "string",
"enum": ["policy", "technical", "hr", "finance"],
"description": "문서 카테고리"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "필터링할 태그 목록"
}
}
},
"limit": {
"type": "integer",
"description": "최대 반환 결과 수. 기본값 10, 최대 50",
"default": 10
}
},
"required": ["query"]
}
}
중첩 객체를 사용할 때는 각 레벨의 description을 빠뜨리지 않도록 한다. Claude는 description을 보고 어떤 값을 채워야 하는지 판단하기 때문이다.
Description 작성의 기술
Tool의 description은 Claude가 올바른 도구를 선택하는 데 결정적인 역할을 한다. 다음 원칙을 따르면 정확도를 크게 높일 수 있다.
첫째, 언제 이 tool을 사용해야 하는지 명시한다. “사용자가 특정 도시의 현재 날씨를 물어볼 때 사용합니다”처럼 사용 조건을 설명한다.
둘째, 무엇을 반환하는지 설명한다. Claude는 반환값을 모르면 tool 사용 여부를 결정하기 어렵다.
셋째, 한계를 언급한다. “과거 날씨는 조회할 수 없으며, 최대 7일 후까지의 예보만 제공합니다”처럼 이 tool로 할 수 없는 것을 명확히 하면 불필요한 호출을 줄인다.
넷째, 비슷한 tool이 여러 개 있을 때는 차이를 분명히 한다. get_current_weather와 get_weather_forecast가 모두 있다면, 각각의 description에서 “현재 날씨만 반환”과 “미래 예보를 반환”이라는 차이를 강조해야 한다.
Tool Use 요청 전송
첫 번째 요청
이제 실제로 API 요청을 보내보자. Anthropic Python SDK를 사용한다.
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=[
{"role": "user", "content": "서울 날씨 어때?"}
]
)
이 요청을 보내면 Claude는 두 가지 중 하나를 선택한다. 사용자의 질문이 tool과 관련이 없다면 일반 텍스트 응답을 반환한다. tool이 필요하다고 판단하면 tool_use 블록을 포함한 응답을 반환한다.
응답 구조 분석
tool_use 응답은 다음과 같은 구조를 가진다.
# response.stop_reason == "tool_use"
# response.content는 리스트 형태
for block in response.content:
if block.type == "tool_use":
print(f"Tool: {block.name}")
print(f"Input: {block.input}")
print(f"ID: {block.id}")
elif block.type == "text":
print(f"Text: {block.text}")
stop_reason이 “tool_use”인지 확인하는 것이 핵심이다. 이 값이 “end_turn”이면 Claude가 tool 없이 직접 답변한 것이고, “tool_use”이면 tool 실행을 요청한 것이다.
content 배열에는 텍스트 블록과 tool_use 블록이 섞여 있을 수 있다. 예를 들어 Claude가 “서울 날씨를 확인해보겠습니다”라는 텍스트와 함께 get_weather tool을 호출할 수 있다. 모든 블록을 순회하며 처리해야 한다.
tool_use 블록의 id는 이후 결과를 반환할 때 매칭에 사용하므로 반드시 보존해야 한다.
Tool 결과 반환
tool_result 메시지 구성
Tool을 실행한 후에는 결과를 Claude에 전달해야 한다. 이를 위해 대화 히스토리에 assistant 메시지(Claude의 tool_use 응답)와 user 역할의 tool_result 메시지를 추가한다.
# 1. 실제 함수 실행
def get_weather(city, unit="celsius"):
# 실제로는 날씨 API를 호출
return {
"city": city,
"temperature": 18,
"unit": unit,
"condition": "partly_cloudy",
"humidity": 62
}
# 2. tool_use 블록에서 파라미터 추출
tool_use_block = next(b for b in response.content if b.type == "tool_use")
result = get_weather(**tool_use_block.input)
# 3. 결과를 포함하여 다시 요청
import json
follow_up = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=[
{"role": "user", "content": "서울 날씨 어때?"},
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": json.dumps(result, ensure_ascii=False)
}
]
}
]
)
이 두 번째 요청에서 Claude는 tool 결과를 바탕으로 자연어 답변을 생성한다. “서울의 현재 기온은 18도이고 구름이 약간 낀 상태입니다. 습도는 62%입니다.”와 같은 응답이 돌아온다.
에러 결과 반환
Tool 실행이 실패할 수도 있다. 이때는 is_error 플래그를 사용한다.
{
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": "Error: 도시 이름 'Seuol'을 찾을 수 없습니다. 올바른 도시 이름을 입력해주세요.",
"is_error": True
}
에러 메시지를 받은 Claude는 사용자에게 상황을 설명하거나, 파라미터를 수정하여 다시 tool을 호출하기도 한다. 에러 메시지는 Claude가 이해하고 적절히 대응할 수 있도록 구체적으로 작성하는 것이 좋다.
멀티스텝 에이전트 루프
왜 루프가 필요한가
실제 업무에서는 하나의 tool 호출로 작업이 끝나는 경우가 드물다. “내일 서울 비 오면 우산 리마인더 설정해줘”라는 요청을 처리하려면 날씨 예보를 조회하고, 비가 온다면 리마인더를 생성해야 한다. 두 개의 tool을 순차적으로 호출하는 것이다.
이처럼 Claude가 stop_reason으로 “tool_use”를 반환하는 한 계속 루프를 돌며 tool을 실행하고 결과를 전달하는 패턴을 에이전트 루프(agent loop)라고 부른다.
구현
def run_agent(user_message, tools, system_prompt=None):
messages = [{"role": "user", "content": user_message}]
max_iterations = 10 # 무한 루프 방지
for i in range(max_iterations):
kwargs = {
"model": "claude-sonnet-4-20250514",
"max_tokens": 4096,
"tools": tools,
"messages": messages
}
if system_prompt:
kwargs["system"] = system_prompt
response = client.messages.create(**kwargs)
# assistant 응답을 대화 히스토리에 추가
messages.append({"role": "assistant", "content": response.content})
# tool_use가 없으면 최종 응답
if response.stop_reason == "end_turn":
final_text = ""
for block in response.content:
if hasattr(block, "text"):
final_text += block.text
return final_text
# tool_use 블록 처리
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result, ensure_ascii=False)
})
# tool 결과를 대화 히스토리에 추가
messages.append({"role": "user", "content": tool_results})
return "최대 반복 횟수에 도달했습니다."
핵심 포인트를 짚어보면 다음과 같다.
max_iterations는 반드시 설정해야 한다. Claude가 tool을 무한히 호출하는 상황을 방지한다. 프로덕션에서는 10~20 정도가 적당하다.
매 반복마다 assistant 응답 전체를 messages에 추가한다. content 배열을 그대로 넣어야 한다. 텍스트만 추출하면 tool_use 블록이 누락되어 대화 맥락이 깨진다.
하나의 응답에 여러 tool_use 블록이 있을 수 있다. Claude가 동시에 여러 tool을 호출하는 parallel tool use를 지원하기 때문이다. 모든 tool_use 블록에 대한 결과를 하나의 tool_results 메시지에 담아 반환해야 한다.
execute_tool 함수 설계
def execute_tool(name, input_data):
tool_map = {
"get_weather": get_weather,
"set_reminder": set_reminder,
"search_calendar": search_calendar,
"send_email": send_email,
}
func = tool_map.get(name)
if not func:
return {"error": f"알 수 없는 tool: {name}"}
try:
return func(**input_data)
except TypeError as e:
return {"error": f"파라미터 오류: {str(e)}"}
except Exception as e:
return {"error": f"실행 오류: {str(e)}"}
execute_tool은 tool 이름을 실제 함수에 매핑하는 디스패처 역할을 한다. try-except로 감싸서 어떤 에러가 발생하더라도 JSON 직렬화 가능한 결과를 반환하도록 한다. 이 함수가 예외를 던지면 전체 에이전트 루프가 중단되므로, 반드시 에러를 잡아서 결과로 변환해야 한다.
에러 처리와 안전장치
API 레벨 에러
Anthropic API는 몇 가지 흔한 에러를 반환할 수 있다.
rate_limit_error는 요청이 너무 많을 때 발생한다. exponential backoff으로 재시도한다.
import time
def api_call_with_retry(func, max_retries=3):
for attempt in range(max_retries):
try:
return func()
except anthropic.RateLimitError:
wait = 2 ** attempt
time.sleep(wait)
except anthropic.APIConnectionError:
time.sleep(1)
raise Exception("최대 재시도 횟수 초과")
overloaded_error는 서버 부하가 높을 때 발생한다. 역시 재시도로 해결 가능하다.
invalid_request_error는 요청 형식이 잘못되었을 때 발생한다. 이 에러는 재시도해도 해결되지 않으므로, 요청 데이터를 점검해야 한다.
Tool 실행 레벨 에러
Tool 함수 내부에서 발생하는 에러도 체계적으로 관리해야 한다.
def get_weather(city, unit="celsius"):
try:
response = requests.get(
f"https://api.weather.example.com/current",
params={"city": city, "unit": unit},
timeout=5
)
response.raise_for_status()
return response.json()
except requests.Timeout:
return {"error": "날씨 서비스 응답 시간 초과. 잠시 후 다시 시도해주세요."}
except requests.HTTPError as e:
if e.response.status_code == 404:
return {"error": f"'{city}' 도시를 찾을 수 없습니다. 도시 이름을 확인해주세요."}
return {"error": f"날씨 서비스 오류: {e.response.status_code}"}
에러 메시지는 Claude가 읽고 적절히 대응할 수 있도록 작성한다. “Error code 404”보다는 “해당 도시를 찾을 수 없습니다”가 Claude에게 더 유용한 정보다.
파라미터 검증
Claude가 생성하는 파라미터가 항상 올바르다고 가정하면 안 된다. 실행 전에 기본적인 검증을 수행한다.
def validate_and_execute(name, input_data):
# 허용된 tool인지 확인
if name not in ALLOWED_TOOLS:
return {"error": f"허용되지 않은 tool: {name}"}
# 위험한 작업은 추가 확인
if name in DANGEROUS_TOOLS:
if not input_data.get("confirmed"):
return {
"warning": "이 작업은 되돌릴 수 없습니다. confirmed=true를 포함하여 다시 호출해주세요.",
"requires_confirmation": True
}
return execute_tool(name, input_data)
특히 삭제, 결제, 이메일 발송 같은 되돌릴 수 없는 작업에 대해서는 확인 단계를 추가하는 것이 좋다.
디자인 패턴
패턴 1: RAG (Retrieval-Augmented Generation)
RAG는 tool use의 가장 대표적인 활용 사례다. 사용자 질문에 답하기 위해 먼저 관련 문서를 검색하고, 검색 결과를 근거로 응답을 생성한다.
rag_tools = [
{
"name": "search_knowledge_base",
"description": "사내 지식 기반에서 질문과 관련된 문서를 검색합니다. 벡터 유사도 기반으로 가장 관련성 높은 문서 조각을 반환합니다.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "검색할 자연어 질의"
},
"top_k": {
"type": "integer",
"description": "반환할 최대 문서 수",
"default": 5
}
},
"required": ["query"]
}
}
]
rag_system_prompt = """당신은 사내 지식 기반을 활용하여 직원들의 질문에 답변하는 AI 어시스턴트입니다.
답변 규칙:
1. 반드시 search_knowledge_base tool로 관련 문서를 검색한 후 답변하세요.
2. 검색된 문서에 근거하여 답변하고, 출처를 명시하세요.
3. 검색 결과에 관련 정보가 없으면 솔직하게 "해당 정보를 찾을 수 없습니다"라고 답하세요.
4. 추측하거나 지식 기반에 없는 정보를 만들어내지 마세요."""
RAG 패턴에서 가장 중요한 것은 시스템 프롬프트에서 “반드시 검색 후 답변하라”고 지시하는 것이다. 이 지시가 없으면 Claude가 자체 학습 지식으로 답변해버릴 수 있다.
패턴 2: 액션 에이전트
액션 에이전트는 정보 조회를 넘어 실제로 작업을 수행하는 패턴이다. 이메일 보내기, 일정 만들기, 주문 처리 같은 행위를 포함한다.
action_tools = [
{
"name": "search_contacts",
"description": "이름이나 이메일로 연락처를 검색합니다.",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "검색할 이름 또는 이메일"}
},
"required": ["query"]
}
},
{
"name": "send_email",
"description": "이메일을 발송합니다. 발송 전 확인이 필요합니다.",
"input_schema": {
"type": "object",
"properties": {
"to": {"type": "string", "description": "수신자 이메일"},
"subject": {"type": "string", "description": "제목"},
"body": {"type": "string", "description": "본문"}
},
"required": ["to", "subject", "body"]
}
},
{
"name": "create_calendar_event",
"description": "캘린더에 새 일정을 만듭니다.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start_time": {"type": "string", "format": "date-time"},
"end_time": {"type": "string", "format": "date-time"},
"attendees": {"type": "array", "items": {"type": "string"}}
},
"required": ["title", "start_time", "end_time"]
}
}
]
액션 에이전트는 읽기(search_contacts)와 쓰기(send_email, create_calendar_event)를 명확히 구분해야 한다. 쓰기 tool에는 실행 전 사용자 확인 절차를 반드시 넣는다.
패턴 3: 데이터 분석 에이전트
SQL 쿼리, 통계 계산, 차트 생성을 조합하는 패턴이다.
analysis_tools = [
{
"name": "run_sql_query",
"description": "읽기 전용 SQL 쿼리를 실행합니다. SELECT 문만 허용됩니다.",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "실행할 SQL SELECT 문"},
"database": {
"type": "string",
"enum": ["analytics", "sales", "hr"],
"description": "대상 데이터베이스"
}
},
"required": ["query", "database"]
}
},
{
"name": "calculate_statistics",
"description": "숫자 데이터에 대한 기술 통계를 계산합니다.",
"input_schema": {
"type": "object",
"properties": {
"data": {"type": "array", "items": {"type": "number"}},
"metrics": {
"type": "array",
"items": {"type": "string", "enum": ["mean", "median", "std", "percentile"]},
"description": "계산할 통계 지표"
}
},
"required": ["data", "metrics"]
}
}
]
SQL tool은 반드시 읽기 전용으로 제한하고, 쿼리를 실행하기 전에 위험한 키워드(DROP, DELETE, UPDATE 등)를 필터링하는 안전장치를 추가한다.
패턴 4: 멀티 에이전트 오케스트레이션
하나의 에이전트가 다른 에이전트를 호출하는 패턴도 가능하다. 오케스트레이터 에이전트가 사용자 요청을 분석하고, 전문 에이전트에 작업을 위임하는 방식이다.
{
"name": "delegate_to_specialist",
"description": "전문 에이전트에게 하위 작업을 위임합니다.",
"input_schema": {
"type": "object",
"properties": {
"specialist": {
"type": "string",
"enum": ["researcher", "writer", "analyst", "coder"],
"description": "위임할 전문 에이전트"
},
"task": {"type": "string", "description": "수행할 작업 설명"},
"context": {"type": "string", "description": "작업에 필요한 배경 정보"}
},
"required": ["specialist", "task"]
}
}
이 패턴은 복잡한 워크플로우를 모듈화할 수 있다는 장점이 있지만, 각 에이전트 호출마다 API 비용이 발생하므로 비용 관리에 주의해야 한다.
프로덕션 배포 시 고려사항
비용 최적화
Tool use는 일반 대화보다 토큰 소모가 크다. tool 정의 자체가 입력 토큰에 포함되기 때문이다. tool이 10개이고 각 정의가 200토큰이라면, 매 요청마다 2,000토큰이 추가된다.
비용을 줄이는 방법은 다음과 같다.
필요한 tool만 포함한다. 사용자의 의도를 먼저 분류하고, 해당 카테고리에 필요한 tool만 선택적으로 전달한다.
tool description을 간결하게 유지한다. 정확도를 해치지 않는 범위에서 설명을 줄인다.
캐싱을 활용한다. 동일한 tool 호출은 결과를 캐시하여 반복 요청을 줄인다.
경량 모델을 1차 필터로 사용한다. Haiku로 tool 선택만 수행하고, 실제 응답 생성은 Sonnet으로 하는 2단계 구조도 효과적이다.
로깅과 모니터링
프로덕션 환경에서는 모든 tool 호출을 로깅해야 한다.
import logging
from datetime import datetime
logger = logging.getLogger("agent")
def execute_tool_with_logging(name, input_data, request_id):
start_time = datetime.now()
logger.info(f"[{request_id}] Tool 호출: {name}, Input: {json.dumps(input_data)}")
result = execute_tool(name, input_data)
duration = (datetime.now() - start_time).total_seconds()
logger.info(f"[{request_id}] Tool 완료: {name}, Duration: {duration}s")
if "error" in result:
logger.warning(f"[{request_id}] Tool 에러: {name}, Error: {result['error']}")
return result
request_id를 사용하면 하나의 사용자 요청에 속한 여러 tool 호출을 묶어서 추적할 수 있다. 이 로그는 디버깅뿐 아니라 비용 분석, 성능 모니터링, 사용 패턴 파악에도 활용된다.
테스트 전략
Tool use 시스템은 세 가지 수준에서 테스트해야 한다.
단위 테스트: 개별 tool 함수가 올바르게 동작하는지 확인한다. 정상 입력, 경계값, 에러 케이스를 모두 포함한다.
통합 테스트: tool_use 응답을 모킹하여 에이전트 루프가 정상적으로 작동하는지 확인한다. 멀티스텝 시나리오, 에러 복구, 최대 반복 횟수 도달 등을 테스트한다.
엔드투엔드 테스트: 실제 API를 호출하여 전체 흐름을 검증한다. 비용이 발생하므로 CI에서는 선택적으로 실행하고, 릴리스 전에는 반드시 수행한다.
실전 팁과 흔한 실수
tool_choice 파라미터 활용
기본적으로 Claude는 tool을 사용할지 여부를 자율적으로 판단한다. 하지만 tool_choice 파라미터로 이 동작을 제어할 수 있다.
# Claude가 자유롭게 판단 (기본값)
tool_choice = {"type": "auto"}
# 반드시 tool을 사용하도록 강제
tool_choice = {"type": "any"}
# 특정 tool을 강제로 사용하도록 지정
tool_choice = {"type": "tool", "name": "search_knowledge_base"}
RAG 시스템에서는 검색을 반드시 수행해야 하므로 tool_choice를 “any”나 특정 tool로 지정하는 것이 좋다. 반면 자유로운 대화가 필요한 에이전트에서는 “auto”를 유지한다.
흔한 실수 모음
첫째, tool_use_id를 잘못 매칭하는 실수다. 병렬 tool use에서 여러 tool이 동시에 호출되면 각 결과를 올바른 ID에 매칭해야 한다. ID가 틀리면 API가 에러를 반환한다.
둘째, assistant 메시지를 수정하는 실수다. Claude의 tool_use 응답을 messages에 추가할 때 content를 변경하면 안 된다. 그대로 넣어야 대화 맥락이 유지된다.
셋째, tool 정의를 너무 많이 포함하는 실수다. Tool이 20개를 넘으면 Claude의 선택 정확도가 떨어질 수 있다. 10개 이내로 유지하거나, 라우팅 로직으로 필요한 tool만 선별한다.
넷째, 에러를 무시하는 실수다. tool_result에서 에러 정보를 제대로 전달하지 않으면 Claude가 잘못된 가정을 바탕으로 답변을 만들어낸다.
다섯째, 토큰 한도를 고려하지 않는 실수다. 대화가 길어지면 tool 정의와 대화 히스토리가 모두 입력 토큰에 포함되어 한도를 초과할 수 있다. 오래된 대화를 요약하거나 잘라내는 로직을 추가한다.
마무리
Claude API의 tool use는 단순한 텍스트 생성 모델을 실제로 행동하는 에이전트로 변환시키는 핵심 기능이다. 이 글에서 다룬 내용을 정리하면 다음과 같다.
tool 정의에서 description이 가장 중요하다. Claude가 올바른 도구를 선택하려면 언제, 왜, 무엇을 반환하는지가 명확해야 한다.
에이전트 루프는 while 루프에 max_iterations를 설정하는 것이 전부다. 복잡해 보이지만 기본 구조는 단순하다.
에러 처리를 소홀히 하면 프로덕션에서 반드시 문제가 생긴다. API 에러, tool 실행 에러, 파라미터 검증을 모두 체계적으로 다뤄야 한다.
RAG, 액션 에이전트, 데이터 분석, 오케스트레이션 등의 패턴은 서로 조합할 수 있다. 한 시스템이 검색도 하고 액션도 수행하는 것이 일반적이다.
비용 최적화와 모니터링을 처음부터 설계에 포함하면 운영 단계에서의 부담이 크게 줄어든다.
이 가이드의 코드를 그대로 복사하기보다는, 각 패턴의 원리를 이해한 후 자신의 시스템에 맞게 변형하는 것을 권한다. Tool use의 진정한 가치는 특정 코드 스니펫이 아니라, “모델이 외부 세상과 상호작용하는 안전한 인터페이스를 설계한다”는 사고방식에 있다.