Skip to main content

Function Calling / Tool Use

Function Calling은 LLM이 외부 도구나 API를 호출할 수 있게 하는 핵심 패턴입니다. 모델이 직접 데이터를 조회하거나 계산하는 것이 아니라, 적절한 함수를 선택하고 인자를 생성하면 애플리케이션이 실행한 뒤 결과를 다시 모델에 전달합니다.

실습

1

도구 정의 (JSON Schema)

Function Calling의 첫 단계는 LLM에게 사용 가능한 도구를 JSON Schema로 정의하는 것입니다.
# 도구 정의 - OpenAI 형식
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "지정된 도시의 현재 날씨 정보를 조회합니다.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "날씨를 조회할 도시 이름 (예: 서울, 부산)"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "온도 단위"
                    }
                },
                "required": ["city"],  # 필수 파라미터
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_database",
            "description": "사내 데이터베이스에서 고객 정보를 검색합니다.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "검색 쿼리 문자열"
                    },
                    "limit": {
                        "type": "integer",
                        "description": "최대 검색 결과 수",
                        "default": 5
                    }
                },
                "required": ["query"],
            }
        }
    }
]
JSON Schema 작성 요령:
항목설명
name함수 이름 (영문, 스네이크 케이스)명확하고 구체적인 동사 사용
description함수의 용도 설명LLM이 언제 이 도구를 선택할지 결정하는 핵심
parameters입력 파라미터 스키마각 파라미터에 description 필수
required필수 파라미터 목록선택적 파라미터는 default 값 설정
description이 도구 선택의 핵심입니다. LLM은 이 설명을 읽고 어떤 도구를 호출할지 결정합니다. “데이터를 가져옵니다”보다 “사내 PostgreSQL 데이터베이스에서 고객 이름, 이메일, 가입일 기준으로 검색합니다”처럼 구체적으로 작성하세요.
2

OpenAI Function Calling

OpenAI API에서 도구를 등록하고 호출 결과를 처리하는 전체 흐름입니다.
import json
from openai import OpenAI

client = OpenAI()

# 실제 도구 함수 구현
def get_weather(city: str, unit: str = "celsius") -> dict:
    """날씨 API를 호출합니다. (여기서는 시뮬레이션)"""
    # 실제 구현에서는 OpenWeatherMap 등 외부 API 호출
    weather_data = {
        "서울": {"temp": 15, "condition": "맑음", "humidity": 45},
        "부산": {"temp": 18, "condition": "구름 많음", "humidity": 60},
    }
    data = weather_data.get(city, {"temp": 20, "condition": "정보 없음", "humidity": 50})
    if unit == "fahrenheit":
        data["temp"] = data["temp"] * 9 / 5 + 32
    return data

def search_database(query: str, limit: int = 5) -> list:
    """데이터베이스를 검색합니다. (여기서는 시뮬레이션)"""
    return [
        {"name": "김민수", "email": "minsu@example.com", "joined": "2024-01-15"},
        {"name": "이지영", "email": "jiyoung@example.com", "joined": "2024-03-22"},
    ][:limit]

# 도구 이름 -> 함수 매핑
tool_functions = {
    "get_weather": get_weather,
    "search_database": search_database,
}

# Step 1: LLM에 메시지와 도구 전달
messages = [
    {"role": "user", "content": "서울 날씨가 어때요?"}
]

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    tools=tools,
    tool_choice="auto",  # "auto" | "required" | "none" | 특정 도구 지정
)

assistant_message = response.choices[0].message

# Step 2: 도구 호출 여부 확인 및 실행
if assistant_message.tool_calls:
    # 어시스턴트 메시지를 히스토리에 추가
    messages.append(assistant_message)

    for tool_call in assistant_message.tool_calls:
        function_name = tool_call.function.name
        function_args = json.loads(tool_call.function.arguments)

        print(f"도구 호출: {function_name}({function_args})")

        # 도구 실행
        result = tool_functions[function_name](**function_args)

        # 도구 실행 결과를 메시지에 추가
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result, ensure_ascii=False),
        })

    # Step 3: 도구 결과를 포함하여 최종 응답 요청
    final_response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
    )
    print(final_response.choices[0].message.content)
else:
    # 도구 호출 없이 직접 응답
    print(assistant_message.content)
출력 예시:
도구 호출: get_weather({'city': '서울'})
서울의 현재 날씨는 기온 15도에 맑은 상태입니다. 습도는 45%입니다.
3

Anthropic Tool Use

Anthropic Claude의 Tool Use는 개념은 동일하지만 API 구조가 다릅니다.
import json
import anthropic

client = anthropic.Anthropic()

# Anthropic 형식의 도구 정의
anthropic_tools = [
    {
        "name": "get_weather",
        "description": "지정된 도시의 현재 날씨 정보를 조회합니다.",
        "input_schema": {  # OpenAI의 "parameters" 대신 "input_schema"
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "날씨를 조회할 도시 이름"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "온도 단위"
                }
            },
            "required": ["city"],
        }
    }
]

# Step 1: 메시지 전송
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=anthropic_tools,
    messages=[
        {"role": "user", "content": "부산 날씨를 알려주세요."}
    ],
)

# Step 2: 응답에서 도구 호출 확인
if response.stop_reason == "tool_use":
    # tool_use 블록 찾기
    tool_use_block = next(
        block for block in response.content
        if block.type == "tool_use"
    )

    function_name = tool_use_block.name
    function_args = tool_use_block.input
    tool_use_id = tool_use_block.id

    print(f"도구 호출: {function_name}({function_args})")

    # 도구 실행
    result = tool_functions[function_name](**function_args)

    # Step 3: 도구 결과를 포함하여 최종 응답 요청
    final_response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        tools=anthropic_tools,
        messages=[
            {"role": "user", "content": "부산 날씨를 알려주세요."},
            {"role": "assistant", "content": response.content},
            {
                "role": "user",
                "content": [
                    {
                        "type": "tool_result",
                        "tool_use_id": tool_use_id,
                        "content": json.dumps(result, ensure_ascii=False),
                    }
                ],
            },
        ],
    )
    print(final_response.content[0].text)
OpenAI vs Anthropic Tool Use 비교:
항목OpenAIAnthropic
도구 스키마 키parametersinput_schema
도구 호출 감지message.tool_calls 존재 여부stop_reason == "tool_use"
결과 전달role: "tool" 메시지type: "tool_result" 콘텐츠 블록
다중 도구 호출한 응답에 여러 tool_calls한 응답에 여러 tool_use 블록
4

멀티턴 도구 호출

실제 애플리케이션에서는 하나의 요청에서 여러 도구를 순차적으로 호출하는 경우가 많습니다.
import json
from openai import OpenAI

client = OpenAI()

def run_conversation(user_message: str, tools: list, tool_functions: dict) -> str:
    """멀티턴 도구 호출을 자동으로 처리하는 루프입니다."""
    messages = [
        {"role": "system", "content": "도구를 적극 활용하여 정확한 정보를 제공하세요."},
        {"role": "user", "content": user_message},
    ]

    max_turns = 5  # 무한 루프 방지
    for turn in range(max_turns):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            tools=tools,
            tool_choice="auto",
        )

        assistant_message = response.choices[0].message

        # 도구 호출이 없으면 최종 응답
        if not assistant_message.tool_calls:
            return assistant_message.content

        # 도구 호출 처리
        messages.append(assistant_message)

        for tool_call in assistant_message.tool_calls:
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)

            print(f"  [턴 {turn + 1}] {function_name}({function_args})")

            # 도구 실행
            try:
                result = tool_functions[function_name](**function_args)
            except Exception as e:
                result = {"error": str(e)}

            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result, ensure_ascii=False),
            })

    return "최대 턴 수를 초과했습니다."

# 실행 예시 - 여러 도구가 필요한 복합 질문
result = run_conversation(
    user_message="서울 날씨를 확인하고, '김민수' 고객 정보도 검색해 주세요.",
    tools=tools,
    tool_functions=tool_functions,
)
print(result)
출력 예시:
  [턴 1] get_weather({'city': '서울'})
  [턴 1] search_database({'query': '김민수'})
서울의 현재 날씨는 15도이며 맑습니다. 김민수 고객은 2024년 1월 15일에
가입하셨으며, 이메일은 minsu@example.com입니다.
LLM이 한 번의 응답에서 여러 도구를 동시에 호출할 수 있습니다 (Parallel Tool Calls). 이 경우 tool_calls 배열에 여러 항목이 포함되며, 각각에 대한 결과를 모두 메시지에 추가해야 합니다.
5

실전 예시: 날씨 API + DB 조회 통합

실제 외부 API와 데이터베이스를 연동하는 완전한 예시입니다.
import json
import httpx
from openai import OpenAI

client = OpenAI()

# 실전 도구 함수들
async def get_real_weather(city: str) -> dict:
    """OpenWeatherMap API로 실제 날씨를 조회합니다."""
    api_key = os.getenv("OPENWEATHER_API_KEY")
    url = f"http://api.openweathermap.org/data/2.5/weather"
    params = {
        "q": city,
        "appid": api_key,
        "units": "metric",
        "lang": "kr",
    }
    async with httpx.AsyncClient() as http_client:
        response = await http_client.get(url, params=params)
        data = response.json()

    return {
        "city": city,
        "temp": data["main"]["temp"],
        "condition": data["weather"][0]["description"],
        "humidity": data["main"]["humidity"],
        "wind_speed": data["wind"]["speed"],
    }

async def query_customer_db(query: str, limit: int = 5) -> list:
    """PostgreSQL에서 고객 정보를 검색합니다."""
    import asyncpg

    conn = await asyncpg.connect(os.getenv("DATABASE_URL"))
    try:
        rows = await conn.fetch(
            """
            SELECT name, email, created_at
            FROM customers
            WHERE name ILIKE $1 OR email ILIKE $1
            LIMIT $2
            """,
            f"%{query}%",
            limit,
        )
        return [dict(row) for row in rows]
    finally:
        await conn.close()

# 도구 정의
real_tools = [
    {
        "type": "function",
        "function": {
            "name": "get_real_weather",
            "description": "OpenWeatherMap API로 도시의 실시간 날씨를 조회합니다.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "도시 이름 (영문)"}
                },
                "required": ["city"],
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "query_customer_db",
            "description": "사내 고객 데이터베이스에서 이름 또는 이메일로 검색합니다.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "검색 키워드"},
                    "limit": {"type": "integer", "description": "최대 결과 수", "default": 5}
                },
                "required": ["query"],
            }
        }
    }
]
실전에서 도구 함수가 에러를 발생시킬 수 있습니다. 반드시 try/except로 감싸고, 에러 메시지를 LLM에 전달하여 사용자에게 안내하도록 합니다.

트러블슈팅

도구의 description이 충분히 구체적인지 확인하세요. LLM은 description을 읽고 도구 선택을 결정합니다.
  • 모호한 설명: “데이터를 가져옵니다” (X)
  • 구체적인 설명: “PostgreSQL DB에서 고객 이름, 이메일 기준으로 검색합니다” (O)
  • tool_choice="required"로 설정하면 반드시 도구를 호출합니다
LLM이 생성한 JSON이 올바르지 않은 경우가 드물게 있습니다.
  • json.loads() 전에 기본적인 검증을 추가하세요
  • 스트리밍 사용 시 arguments가 분할되어 올 수 있으므로 누적 후 파싱합니다
  • gpt-4o 이상 모델은 JSON 생성 정확도가 매우 높습니다
tool_call_id로 각 호출과 결과가 매칭되므로 순서는 중요하지 않습니다. 다만 모든 tool_calls에 대해 빠짐없이 tool 역할의 결과 메시지를 추가해야 합니다.
response.content를 순회하며 type == "tool_use"인 블록을 모두 처리하세요. 각 블록의 id에 대응하는 tool_result를 모두 포함하여 다음 요청을 보내야 합니다.

다음 단계