LLM 앱 개발 실전 #6 툴 콜링으로 외부 기능 연결
5편까지는 Claude가 텍스트나 구조화된 데이터를 “답"으로 돌려줬습니다. 그런데 Claude는 혼자서는 오늘 날씨를 모르고, 우리 데이터베이스도 들여다보지 못합니다. 학습한 지식 안에서만 답할 뿐입니다. 이번 글에서는 Claude가 우리가 정의한 함수를 직접 호출하게 해서, 바깥 세상과 연결하는 툴 콜링을 다룹니다.
툴 콜링이란 #
흐름은 이렇습니다. 우리가 Claude에게 쓸 수 있는 도구(함수)의 목록과 각 도구의 설명을 알려줍니다. Claude는 질문에 답하다가 도구가 필요하다고 판단하면, 직접 함수를 실행하는 대신 “get_weather 도구를 city=서울로 불러줘"라고 요청합니다. 그 요청을 받아 우리 코드가 실제로 함수를 실행하고, 결과를 Claude에게 돌려줍니다. Claude는 그 결과를 보고 최종 답을 만듭니다.
핵심은 Claude가 함수를 직접 실행하지는 않는다는 점입니다. Claude는 “이 도구를 이 인자로 부르고 싶다"는 의사만 표현하고, 실행은 우리 몫입니다.
도구를 정의하고 호출하기 #
도구는 이름, 설명, 입력 스키마로 정의합니다. 입력 스키마는 5편에서 본 JSON 스키마와 같은 형식입니다.
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "get_weather",
"description": "특정 도시의 현재 날씨를 가져온다.",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "도시 이름"},
},
"required": ["city"],
},
}
]
def get_weather(city: str) -> str:
# 실제로는 날씨 API를 호출한다. 여기서는 예시 값을 돌려준다.
return f"{city}의 현재 날씨는 맑음, 기온 22도입니다."
messages = [{"role": "user", "content": "서울 날씨 어때?"}]
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
print(response.stop_reason) # tool_usedescription은 단순한 설명이 아닙니다. Claude는 이 설명을 보고 언제 이 도구를 쓸지 판단합니다. 그래서 도구가 무엇을 하는지뿐 아니라 어떤 상황에 쓰는지를 분명히 적는 것이 중요합니다.
날씨를 물었으니 Claude는 직접 답하지 않고 도구를 부르려 합니다. 이때 stop_reason이 tool_use로 나오고, 응답에 tool_use 블록이 담깁니다.
결과를 돌려주고 마무리하기 #
tool_use 블록에는 도구 이름(name)과 인자(input), 그리고 식별자(id)가 들어 있습니다. 우리는 그 인자로 함수를 실행하고, 결과를 tool_result로 돌려줍니다. 이때 tool_use_id로 어느 호출에 대한 결과인지 짝을 맞춰야 합니다.
while response.stop_reason == "tool_use":
# 도구 호출이 담긴 어시스턴트 응답을 먼저 히스토리에 추가
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
if block.name == "get_weather":
result = get_weather(**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
# 도구 결과는 user 역할로 돌려준다
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
print(next(b.text for b in response.content if b.type == "text"))while 문으로 감싼 이유가 있습니다. Claude는 도구를 한 번만 쓰고 끝내지 않을 수 있습니다. 날씨를 확인한 뒤 다른 도구를 또 부를 수도 있습니다. 그래서 stop_reason이 tool_use인 동안 계속 돌면서, Claude가 도구를 그만 부르고 자연스럽게 답을 끝낼 때(end_turn)까지 반복합니다.
한 가지 빠뜨리기 쉬운 부분은, 도구 호출이 담긴 어시스턴트 응답을 반드시 히스토리에 다시 넣어야 한다는 점입니다(messages.append). 그래야 Claude가 자기가 무슨 도구를 불렀는지 기억하고 결과와 연결합니다.
도구 러너로 간단하게 #
이 루프를 매번 손으로 짜는 대신, SDK의 도구 러너를 쓰면 호출과 실행, 반복을 자동으로 처리해 줍니다. @beta_tool 데코레이터로 함수를 정의하면 입력 스키마도 함수 시그니처에서 자동으로 만들어집니다.
from anthropic import beta_tool
@beta_tool
def get_weather(city: str) -> str:
"""특정 도시의 현재 날씨를 가져온다.
Args:
city: 도시 이름.
"""
return f"{city}의 현재 날씨는 맑음, 기온 22도입니다."
runner = client.beta.messages.tool_runner(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=[get_weather],
messages=[{"role": "user", "content": "서울 날씨 어때?"}],
)
for message in runner:
print(message)흔히 걸려 넘어지는 곳 #
tool_use_id를 안 맞춘다 —tool_result의tool_use_id는 그 결과가 응답하는tool_use블록의id와 정확히 같아야 합니다. 도구를 여러 개 부르면 각각 짝을 맞춥니다.- 어시스턴트 응답을 히스토리에 안 넣는다 — 도구 호출이 담긴 응답을
messages에 다시 넣지 않으면, 다음 호출에서 Claude가 자기 호출과 결과를 연결하지 못해 오류가 납니다. - 설명이 부실하다 —
description이 모호하면 Claude가 도구를 엉뚱한 때 부르거나 아예 안 부릅니다. 무엇을 하는 도구인지, 언제 써야 하는지를 분명히 적습니다.
마무리 #
이번 글에서는 Claude를 외부 기능과 연결하는 툴 콜링을 다뤘습니다.
- 우리가 도구를 정의하면, Claude는 도구를 부르겠다는 의사를
tool_use로 표현하고, 실행은 우리가 합니다. - 실행 결과를
tool_result로tool_use_id에 맞춰 돌려주고,stop_reason이tool_use인 동안 루프를 돌립니다. - 도구 러너를 쓰면 이 루프를 자동으로 처리할 수 있습니다.
이제 Claude가 외부 기능을 쓸 수 있게 됐습니다. 다음 글인 “LLM 앱 개발 실전 #7 임베딩과 벡터 검색"에서는 방향을 바꿔, 우리 문서를 Claude가 검색할 수 있는 형태로 바꾸는 임베딩을 다루겠습니다. 이는 다음 편의 RAG로 이어지는 준비 단계입니다.