도구 사용(Tool use)


도구 사용(흔히 함수 호출이라고도 합니다)은 언어 모델이 자신의 텍스트 입출력 범위를 벗어나 무언가를 할 수 있게 해주는 방법입니다. 모델에게 실행을 요청할 수 있는 함수 목록을 건네주면, 모델은 언제 그 함수를 사용할지 스스로 판단합니다. 모델은 그 무엇도 직접 실행하지 않습니다. 오직 호출을 요청할 뿐이며, 실제 작업은 여러분의 코드가 수행합니다.
모델 혼자서는 상자 안에 밀봉되어 있는 것과 같습니다. 오늘의 날씨를 확인할 수도 없고, 여러분의 데이터베이스에서 사용자를 조회할 수도 없고, 이메일을 보낼 수도 없습니다. 학습 시점에 멈춰버린, LLM은 어떻게 동작하는가에서 본 것과 똑같은 그림 그대로, 모델은 오직 학습한 내용을 바탕으로 텍스트를 예측할 뿐입니다. 도구 사용은 모델이 그 상자 밖으로 손을 뻗을 수 있게 해주는 방법이며, 에이전트가 만들어지는 토대이기도 합니다.
기억해야 할 단어는 "요청"입니다. 모델은 어떤 함수를 원하는지, 어떤 인자와 함께 원하는지를 알려줄 뿐입니다. 여러분의 코드가 그것을 실행하고 결과를 돌려줍니다. 통제권은 항상 여러분에게 있습니다.
루프
도구 사용은 한 번의 호출이 아니라 **주고받는 과정**입니다.
- 사용자의 메시지와 모델이 사용할 수 있는 도구 목록을 함께 보냅니다.
- 모델은 두 가지 방식 중 하나로 응답합니다. 일반적인 답변이거나, 원하는 인자와 함께 도구 호출을 요청하는 것입니다.
- 도구를 요청했다면, 여러분의 코드가 해당 함수를 실행하고 그 결과를 모델에게 돌려줍니다.
- 모델은 그 결과를 이용해 최종 답변을 작성합니다.
이것이 전체 패턴입니다. 모델이 요청하면, 여러분의 코드가 실행합니다. 모델이 여러 도구를 필요로 한다면 2단계와 3단계가 반복될 수 있는데, 이것이 몇 장 뒤에 나올 에이전트의 씨앗입니다.
실제로 벌어지는 일
도구 사용은 마치 모델이 새로운 능력을 얻은 것처럼 느껴질 수 있지만, 실제로는 그렇지 않습니다. 겉으로 드러나지 않는 내부에서 모델은 늘 하던 그 한 가지 일, 즉 텍스트 예측을 하고 있을 뿐입니다. 모델이 갇혀 있는 그 상자 자체는 변한 것이 없습니다.
요청에 도구 정의를 포함시키면, 그것을 모델이 예측의 근거로 삼는 컨텍스트에 추가하는 것입니다. 모델은 이런 도구들이 주어졌을 때, 올바른 다음 내용이 일반적인 문장이 아니라 "도시 오슬로로 get_weather를 호출하라"는 의미의 특별한 형식의 메시지인 경우가 있다는 것을 학습된 예시들을 통해 익혔습니다. 그래서 여러분의 질문이 어떤 도구를 유용해 보이게 만들면, 가장 가능성 높은 다음 출력은 그 구조화된 **도구 호출 메시지**가 되고, API는 이를 tool_calls라는 형태로 여러분에게 보여줍니다.
모델이 밖으로 손을 뻗어 무언가를 실행한 것이 아닙니다. 도구 호출이 올바른 다음 수라고 예측하고, 여러분의 코드가 읽을 수 있는 형식으로 그 요청을 작성한 것입니다.
그다음, 모델이 아니라 여러분이 그 함수를 실행합니다. 실제 결과를 가져와서 메시지에 추가 컨텍스트로 다시 넣으면, 모델은 그 확장된 컨텍스트로부터 최종 답변을 예측합니다. 이 기능 전체는 그 사이에서 여러분의 코드가 실제 작업을 수행하는, 두 번의 평범한 예측일 뿐입니다.
여러분은 모델의 텍스트 상자와 현실 세계를 잇는 다리입니다. 이 그림을 마음에 새기면, 에이전트를 포함해 이 장의 나머지 내용이 더 이상 신비롭게 느껴지지 않을 것입니다. AI가 취하는 모든 행동은 예측된 요청이며, 그것을 실행할지는 여러분의 코드가 선택하는 것입니다.
tool_calls라는 형태로 여러분에게 넘겨줍니다. 모델은 결코 무언가를 실행하지 않고, 요청을 예측할 뿐입니다. 여러분의 코드가 함수를 실행하고, 그 결과를 다음 예측을 위한 추가 컨텍스트로 다시 넘겨주므로, 여러분이 모델과 현실 세계를 잇는 다리입니다. 도구 정의하기
각 도구를 모델에게 설명해야 합니다. 이름, 무엇을 하는지, 어떤 인자를 받는지를 말입니다. 이 설명은 여러분을 위한 문서가 아니라 언제 이 도구를 사용해야 하는지에 대해 모델에게 주는 지시이므로, 그것이 곧 프롬프트임을 염두에 두고 작성해야 합니다.
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city. Use when the user asks about weather.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "The city name, e.g. 'Seoul'"},
},
"required": ["city"],
},
},
},
]parameters 블록은 **스키마**이며, 구조화된 출력과 같은 개념입니다. 즉 모델이 생성해야 하는 인자를 정의합니다. 모델이 get_weather를 호출하기로 결정하면, 이 형태에 맞는 city 인자를 돌려보냅니다. 모호한 설명("날씨를 가져온다")은 모델이 엉뚱한 순간에 도구를 사용하게 만듭니다. 명확한 설명("사용자가 날씨에 대해 물을 때 사용")은 모델을 잘 안내합니다.
parameters 스키마로 구성됩니다. 설명은 사실 프롬프트입니다. 언제 도구를 사용해야 하는지 모델에게 알려주므로 신경 써서 작성해야 합니다. parameters 스키마는 구조화된 출력과 동일한 메커니즘으로, 모델이 보내는 인자를 제약합니다. 호출 처리하기
모델이 도구를 원할 때, 응답에는 최종 답변 대신 tool_calls가 들어 있습니다. 요청된 함수와 인자를 읽고, 실제 함수를 실행하고, 결과를 tool 메시지로 돌려보냅니다. 그런 다음 모델을 다시 호출해서 마무리하게 합니다.
import json
# 도구의 실제 구현
def get_weather(city):
# 실제 앱에서는 날씨 API를 호출하지만, 여기서는 가짜로 만듭니다
return {"city": city, "tempC": 18, "condition": "cloudy"}
messages = [{"role": "user", "content": "서울 날씨가 어때?"}]
# 1. 첫 번째 호출: 도구를 제공합니다
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
tool_calls = response.choices[0].message.tool_calls
tool_call = tool_calls[0] if tool_calls else None
if tool_call:
# 2. 모델이 전달한 인자로 요청된 함수를 실행합니다
args = json.loads(tool_call.function.arguments)
result = get_weather(args["city"])
# 3. 모델의 요청과 결과를 다시 보냅니다
messages.append(response.choices[0].message) # 어시스턴트의 도구 요청
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result),
})
# 4. 모델이 결과를 이용해 답변할 수 있도록 다시 호출합니다
response = client.chat.completions.create(model=MODEL, messages=messages)
print(response.choices[0].message.content)
# "서울은 현재 18도이고 흐립니다."인자는 **JSON 문자열**로 도착하므로, json.loads로 실제 객체로 변환한 다음 사용하기 전에 확인합니다. 히스토리에는 두 개의 메시지를 추가합니다. 모델의 도구 요청과 여러분의 tool 결과이며, 이 둘은 tool_call_id로 연결됩니다. 마지막 호출은 원시 날씨 데이터를 자연스러운 문장으로 바꿔줍니다.
tool_calls가 담겨 있습니다. JSON 문자열에서 인자를 파싱하고, 실제 함수를 실행하고, 두 개의 메시지를 돌려보냅니다. 모델의 요청과 여러분의 결과이며, tool_call_id로 연결됩니다. 마지막 모델 호출이 여러분의 원시 결과를 자연스러운 답변으로 바꿔줍니다. 실전에서
같은 흐름을 재사용 가능한 함수로 만든 것입니다. 모델과 여러분의 시스템 사이를 가로막고 있는 유일한 것이 여러분이 작성하고 통제하는 코드라는 점에 주목하세요.
import json
tool_impls = {"get_weather": lambda args: get_weather(args["city"])}
def answer_with_tools(user_text):
messages = [{"role": "user", "content": user_text}]
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
calls = response.choices[0].message.tool_calls
if not calls:
return response.choices[0].message.content # 모델이 직접 답변함
call = calls[0]
args = json.loads(call.function.arguments)
result = tool_impls[call.function.name](args)
messages.append(response.choices[0].message)
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
response = client.chat.completions.create(model=MODEL, messages=messages)
return response.choices[0].message.content이 코드는 하나의 도구 호출만 처리합니다. 모델은 한 번에 여러 도구를 요청할 수도 있는데, 이를 **병렬 도구 호출**이라고 부르며, 이는 tool_calls의 모든 항목을 순회하는 방식으로 처리하면 됩니다. 그리고 모델이 끝날 때까지 매번 스스로 다음 단계를 결정하며 루프 안에서 계속 도구를 호출하게 두면, 바로 에이전트가 되며, 이것이 다음 장에서 다룰 내용입니다.

