툴 콜링
모델이 함수를 호출하게 한다
툴 콜(함수 호출)은 모델이 스스로 갖고 있지 않은 정보나 동작을 요청할 수 있게 해줍니다. 모델은 아무것도 직접 실행하지 않습니다 — 어떤 함수를 어떤 인자로 부르고 싶은지 tool_calls 배열로 알려주면, 그 함수를 여러분이 직접 실행하고 결과를 다음 요청에 실어 보냅니다. 모델은 그 결과를 받아 최종 답을 만듭니다.
요청·응답 형식은 OpenAI Chat Completions 의 툴 콜링 규격을 그대로 씁니다. 이미 OpenAI 나 OpenAI 호환 게이트웨이로 이 방식을 쓰고 있다면, base URL 을 https://openrouter.myip.co.kr/api/v1 로, 모델을 우리가 서빙하는 모델로 바꾸는 것만으로 그대로 동작합니다.
호출 전에 지원 여부 확인하기
GET /models 의 supported_parameters 가 유일한 진실이고, 쿼리 필터로도 씁니다.
curl "https://openrouter.myip.co.kr/api/v1/models?supported_parameters=tools"지원하지 않는 모델로 조용히 넘어가는 대신 요청 자체를 막고 싶다면 provider{} 에 require_parameters: true 를 추가하세요. Provider 라우팅 을 보세요.
세 단계로 이루어지는 교환
1단계 — 요청에 툴을 함께 보낸다
{
"model": "google/gemma-4-26b-a4b",
"messages": [
{ "role": "user", "content": "지금 부산 날씨 어때?" }
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "특정 도시의 현재 날씨를 가져온다",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "도시 이름, 예: Busan" }
},
"required": ["city"]
}
}
}
]
}모델이 툴이 필요하다고 판단하면, 메시지 본문 대신 finish_reason: "tool_calls" 와 tool_calls 배열로 응답합니다.
{
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_8f2a",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Busan\"}" }
}
]
},
"finish_reason": "tool_calls",
"native_finish_reason": "tool_calls"
}
]
}2단계 — 툴을 직접 실행한다
우리는 어떤 함수도 대신 실행하지 않습니다. function.arguments 는 객체가 아니라 JSON 문자열이므로 파싱한 뒤 여러분의 코드를 호출하세요.
const args = JSON.parse(toolCall.function.arguments) as { city: string };
const result = await getWeather(args.city);3단계 — 결과를 다시 보낸다
어시스턴트의 툴 콜 메시지와 결과를 담은 role: "tool" 메시지를 이어 붙이고, 같은 tools 배열과 함께 다시 호출합니다.
{
"model": "google/gemma-4-26b-a4b",
"messages": [
{ "role": "user", "content": "지금 부산 날씨 어때?" },
{
"role": "assistant",
"content": null,
"tool_calls": [
{ "id": "call_8f2a", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"Busan\"}" } }
]
},
{ "role": "tool", "tool_call_id": "call_8f2a", "content": "{\"tempC\":21,\"condition\":\"clear\"}" }
],
"tools": [ /* 1단계와 같은 툴 정의 */ ]
}두 번째 응답은 finish_reason: "stop" 인 평범한 텍스트 메시지입니다.
전체 예제
import json
import os
from openai import OpenAI
client = OpenAI(base_url="https://openrouter.myip.co.kr/api/v1", api_key=os.environ["MYIP_API_KEY"])
MODEL = "google/gemma-4-26b-a4b"
def get_weather(city: str) -> dict:
return {"tempC": 21, "condition": "clear"} # 실제 구현으로 교체
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "특정 도시의 현재 날씨를 가져온다",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
messages = [{"role": "user", "content": "지금 부산 날씨 어때?"}]
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
message = response.choices[0].message
messages.append(message.model_dump())
for call in message.tool_calls or []:
args = json.loads(call.function.arguments)
result = get_weather(**args)
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
final = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
print(final.choices[0].message.content)tool_choice
{ "tool_choice": "auto" } // 기본값: 모델이 판단
{ "tool_choice": "none" } // 툴 호출 금지
{ "tool_choice": "required" } // 무조건 어떤 툴이든 호출
{ "tool_choice": { "type": "function", "function": { "name": "get_weather" } } } // 이 툴 강제tool_choice 는 우리 라우팅 키 목록에 없으므로 보낸 그대로 업스트림에 전달됩니다. 모든 변형을 다 지키는지는 우리가 아니라 그 엔진의 구현에 달려 있습니다.
parallel_tool_calls
일부 엔진은 한 턴에 여러 툴을 동시에 요청할 수 있습니다. parallel_tool_calls: false 를 보내면 한 번에 하나씩만 요청하게 만들 수 있습니다. 라우팅 대상이 아닌 다른 파라미터와 마찬가지로 그대로 전달될 뿐이라, 서빙 엔진이 실제로 구현한 경우에만 효과가 있습니다.
스트리밍에서의 툴 콜
툴 콜은 배열 인덱스로 구분되는 delta.tool_calls 청크로 나뉘어 옵니다. finish_reason: "tool_calls" 가 나올 때까지 function.arguments 문자열을 이어 붙이세요.
const toolCallsByIndex: Record<number, { id: string; name: string; args: string }> = {};
for await (const chunk of stream) {
for (const delta of chunk.choices[0]?.delta.tool_calls ?? []) {
const slot = (toolCallsByIndex[delta.index] ??= { id: '', name: '', args: '' });
if (delta.id) slot.id = delta.id;
if (delta.function?.name) slot.name = delta.function.name;
if (delta.function?.arguments) slot.args += delta.function.arguments;
}
if (chunk.choices[0]?.finish_reason === 'tool_calls') break;
}SSE 청크의 전체 형식(마지막 usage 청크, 스트림 도중의 오류 처리 방식 포함)은 스트리밍 을 보세요.
최소한의 에이전트 루프
몇 개의 툴을 정의하든, 모델이 더 이상 툴을 요청하지 않을 때까지 반복하는 구조는 같습니다.
def run(messages, tools, max_turns=10):
for _ in range(max_turns):
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
message = response.choices[0].message
messages.append(message.model_dump())
if not message.tool_calls:
return message.content
for call in message.tool_calls:
result = TOOL_MAP[call.function.name](**json.loads(call.function.arguments))
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
raise RuntimeError("max_turns exceeded")턴 수는 항상 제한하세요. 모델이 계속 툴을 요청하는 상황은 가상의 시나리오가 아니라, 통합 코드가 예상치 못한 비용을 만들어내는 가장 흔한 원인입니다.
좋은 툴 정의
- 이름을 구체적으로 짓습니다.
weather가 아니라get_weather_forecast처럼요. - 설명을 문서처럼 씁니다. 모델은
description에 적힌 것만 압니다 — 단위, 형식, 예외 상황을 적어두세요("도시 이름, 우편번호, 또는 'lat,lng'"). - 스키마를 엄격하게 유지합니다.
"additionalProperties": false와required목록은 잘못된 인자를 줄입니다. - 툴이 서로 연결되도록 설계합니다.
search_products→get_product_details→check_inventory처럼 이어지는 흐름은 각 툴의 역할을 이해한 모델에게 자연스럽습니다.
툴 콜링 관련 오류
| 상황 | 결과 |
|---|---|
지원하지 않는 모델/후보에 tools 를 보냄 | 엔진마다 다릅니다: 대개 그 필드를 조용히 무시하고 안 보낸 것처럼 응답합니다 |
모델이 만든 arguments 가 JSON 으로 잘못됨 | 우리가 검사하지 않습니다 — JSON.parse 전에 직접 검증하세요 |
후속 호출에서 tools 를 다시 보내지 않음 | 모델이 같은 툴을 다시 요청하거나, 이전 결과를 쓰지 않고 답할 수 있습니다 |
일반적인 오류 형식은 오류와 디버깅 을 보세요.
마지막 수정 2026. 9. 5.