Claude 함수 호출, 말로만 듣다가 직접 써보니 이렇더라
Claude 함수 호출(Function Calling)이 뭔지, 어떻게 설정하고 어디서 막히는지 실제로 써본 경험 그대로 정리했습니다. tools 파라미터부터 흔한 실수까지.
Claude API를 쓰다 보면 어느 순간 벽에 부딪힌다. 텍스트 주고받는 건 됐고, 이제 Claude가 내 서비스 기능을 직접 실행해줬으면 싶은 거다. "오늘 날씨 알려줘"라고 했을 때 그냥 말로만 대답하는 게 아니라, 실제로 날씨 API를 호출해서 데이터를 가져오는 것처럼.
그게 바로 함수 호출, 공식 용어로는 Tool Use다. 처음엔 저도 이름부터 겁먹었는데, 써보고 나니 구조 자체는 생각보다 단순하다. 다만 막히는 포인트가 꽤 명확하게 정해져 있어서, 그 부분만 알고 들어가면 훨씬 덜 헤맨다.
함수 호출이 필요해지는 순간
친구가 "Claude로 챗봇 만들었는데, 사용자가 재고 조회 요청하면 어떻게 처리해?"라고 물어본 적 있다. 그 친구는 Claude한테 그냥 "재고 조회해줘"라고 시키면 어떻게든 되는 줄 알았던 거다.
근데 Claude는 사내 데이터베이스를 모른다. 당연히. 그 친구 회사 ERP에 접근할 권한도 없고, 실시간 재고가 얼마인지 알 방법도 없다. Claude는 언어를 처리하는 모델이지, 외부 시스템에 연결된 만능 도구가 아니니까.
함수 호출은 이 갭을 메우는 방식이다. "사용자가 재고 조회를 원한다"는 의도를 Claude가 파악해서, 개발자가 미리 정의해둔 함수를 실행하라고 신호를 보내는 것. 실제 실행은 여전히 개발자 서버에서 한다. Claude는 "이 함수 써야 할 것 같아요, 파라미터는 이거예요"라고 말해주는 역할이다.
구조를 머릿속에 그려두면 코드가 쉬워진다
함수 호출의 흐름은 총 세 단계다. 짧게 말하면 이렇다.
- 개발자가 도구(tool)를 정의해서 Claude에게 넘긴다
- Claude가 응답 대신 "이 도구 써야겠다"는 신호를 보낸다
- 개발자가 실제 함수를 실행하고, 결과를 다시 Claude에 넣어준다
왕복이 두 번 일어난다는 게 핵심이다. 한 번의 API 호출로 끝나는 게 아니다. 이걸 모르고 "Claude가 함수 결과를 바로 줄 거야"라고 기대했다가 당황하는 경우가 꽤 많다. Claude API 첫 연결 단계를 이미 해봤다면, 그 위에 왕복 구조 하나를 얹는다고 생각하면 된다.
실제 코드, 날씨 조회로 한 번 짜보면
가장 많이 쓰이는 예제가 날씨 조회라서, 그걸로 설명한다. Python 기준이다.
먼저 도구 정의부터.
tools = [
{
"name": "get_weather",
"description": "특정 도시의 현재 날씨를 조회합니다.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "날씨를 조회할 도시 이름 (예: 서울, 부산)"
}
},
"required": ["city"]
}
}
]
description이 생각보다 중요하다. Claude가 이 설명을 읽고 "아, 이게 언제 쓰는 도구구나"를 판단하기 때문이다. 대충 써두면 엉뚱한 상황에서 도구를 호출하거나, 반대로 써야 할 때 안 쓰는 일이 생긴다.
이제 첫 번째 API 호출.
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=tools,
messages=[
{"role": "user", "content": "서울 날씨 알려줘"}
]
)
print(response.stop_reason)
# tool_use 출력됨
stop_reason이 tool_use면 Claude가 "함수 써야 해"라고 신호를 보낸 것이다. 이 시점에서 응답 내용을 보면 어떤 함수를 어떤 파라미터로 쓰라고 명시돼 있다.
# Claude의 tool_use 블록 확인
tool_use_block = next(
block for block in response.content
if block.type == "tool_use"
)
print(tool_use_block.name) # get_weather
print(tool_use_block.input) # {'city': '서울'}
이제 실제 날씨 API를 호출하고 결과를 다시 넣어주면 된다.
# 실제 날씨 함수 (여기선 더미 데이터)
def get_weather(city):
return {"city": city, "temperature": 18, "condition": "맑음"}
weather_result = get_weather(tool_use_block.input["city"])
# 두 번째 API 호출 - 결과 전달
final_response = client.messages.create(
model="claude-opus-4-5",
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": str(weather_result)
}
]
}
]
)
print(final_response.content[0].text)
# "서울의 현재 날씨는 맑음이고 기온은 18도입니다."
두 번째 호출할 때 messages 배열에 assistant 응답 전체와 tool_result를 같이 넣어주는 게 포인트다. 이 부분 빠뜨리면 Claude가 맥락을 잃는다.
여기서 대부분 한 번씩 막힌다
솔직히 말하면, 처음 함수 호출 짤 때 저도 같은 실수를 반복했다.
첫 번째 함정: tool_result를 user 역할로 넣어야 한다는 걸 모른다. 직관적으로 이상해 보이지만, Anthropic API 규칙이 그렇다. assistant가 도구를 요청하면, 그 결과는 user 메시지로 돌아가야 한다. role을 assistant로 쓰면 에러가 난다.
두 번째 함정: tool_use_id를 빠뜨린다. tool_result에 tool_use_id를 안 넣으면 어느 도구 호출에 대한 결과인지 매칭이 안 된다. 도구를 여러 개 동시에 호출할 수 있어서 이 ID가 꼭 필요하다.
세 번째 함정: stop_reason 체크를 안 한다. 항상 tool_use가 오는 게 아니다. 사용자 질문에 따라 Claude가 도구 없이 바로 답할 수도 있다. stop_reason == "tool_use"인지 먼저 확인하고 분기 처리해야 한다. 안 하면 tool_use 블록이 없는데 찾으려다 에러가 터진다.
참고로 함수 호출을 여러 번 반복하다 보면 토큰이 빠르게 쌓인다. messages 배열에 대화 이력이 계속 붙으니까. 토큰 관리에서 흔히 하는 실수들을 같이 읽어두면 나중에 비용 충격을 줄일 수 있다.
도구 여러 개, 동시 호출은 어떻게 되나
tools 배열에 함수를 여러 개 넣을 수 있다. Claude가 한 번의 응답에서 여러 도구를 동시에 요청하기도 한다. "서울이랑 부산 날씨 둘 다 알려줘"라고 하면 get_weather를 두 번 호출하는 식이다.
이럴 때 response.content에 tool_use 블록이 두 개 들어온다. 각각 tool_use_id가 다르니까, 루프 돌면서 전부 실행하고 결과를 tool_result 두 개로 묶어서 돌려줘야 한다.
tool_results = []
for block in response.content:
if block.type == "tool_use":
if block.name == "get_weather":
result = get_weather(block.input["city"])
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
})
하나라도 빠뜨리면 Claude가 결과를 못 받았다고 판단하고 이상하게 응답할 수 있다.
실제로 쓸 만한 상황이 따로 있다
함수 호출이 빛을 발하는 케이스는 꽤 구체적이다. 외부 API 연동(날씨, 주식, 지도), 데이터베이스 조회, 계산이 필요한 작업, 사용자 맞춤 데이터 retrieval 같은 것들.
반대로 단순 요약이나 번역, 감성 분류처럼 Claude 자체가 잘 하는 일에는 굳이 함수 호출을 끼워 넣을 이유가 없다. 왕복 API 호출이 두 번 일어나니까 레이턴시도 생기고, API 비용도 두 번 과금된다. 필요한 곳에만 써야 한다.
에이전트 구조로 확장할 때는 함수 호출이 거의 필수다. Claude가 도구 결과를 받고, 그걸 바탕으로 또 다른 도구를 호출하는 루프를 반복하면서 복잡한 작업을 처리하게 된다. 그 루프를 어떻게 설계하느냐가 에이전트의 핵심이기도 하다.
📌 한 줄 정리: Claude 함수 호출은 "Claude가 어떤 함수를 쓸지 결정하고, 실행은 개발자 서버가 하고, 결과를 다시 Claude에 넣어주는" 왕복 구조다. tools 정의의 description을 꼼꼼히 쓰고, tool_result를 user 역할로 넣고, stop_reason을 꼭 체크하는 것. 이 세 가지만 지키면 대부분의 삽질을 피할 수 있다.