Function Calling: AI가 함수를 실행하는 마법


Function Calling이란?

Function Calling은 LLM이 JSON 형식의 구조화된 데이터를 생성하고, 이를 통해 외부 함수를 호출할 수 있게 해주는 기능입니다. OpenAI GPT-3.5 Turbo와 GPT-4에서 지원합니다.

왜 중요한가?

일반 LLM 응답:

user: "서울의 날씨를 알려줘"
LLM: "죄송하지만 실시간 날씨 정보를 제공할 수 없습니다..."

Function Calling 사용:

user: "서울의 날씨를 알려줘"
LLM: {
    "function": "get_weather",
    "arguments": {"location": "서울"}
}
# → 실제 날씨 API 호출
# → "서울의 현재 날씨는 맑음, 20도입니다"

핵심 개념

1. 함수 정의

AI에게 사용 가능한 함수를 알려줍니다.

from openai import OpenAI

client = OpenAI()

functions = [
    {
        "name": "get_weather",
        "description": "특정 지역의 날씨 정보를 가져옵니다",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "도시 이름 (예: 서울, 부산)"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "온도 단위"
                }
            },
            "required": ["location"]
        }
    }
]

2. 함수 호출 요청

response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "서울 날씨 알려줘"}],
    functions=functions,
    function_call="auto"  # auto, none, {"name": "함수명"}
)

message = response.choices[0].message

3. 함수 실행

import json

if message.function_call:
    function_name = message.function_call.name
    arguments = json.loads(message.function_call.arguments)

    # 실제 함수 호출
    if function_name == "get_weather":
        result = get_weather(**arguments)

4. 결과 반환

# 함수 실행 결과를 다시 LLM에 전달
messages.append(message)  # LLM의 function_call 메시지
messages.append({
    "role": "function",
    "name": function_name,
    "content": json.dumps(result)
})

# 최종 응답 생성
final_response = client.chat.completions.create(
    model="gpt-4",
    messages=messages
)

print(final_response.choices[0].message.content)
# "서울의 현재 날씨는 맑음, 기온은 섭씨 20도입니다."

실전 예제

1. 날씨 조회 시스템

import requests
from openai import OpenAI

client = OpenAI()

def get_weather(location, unit="celsius"):
    """실제 날씨 API 호출"""
    api_key = "your-api-key"
    url = f"http://api.weatherapi.com/v1/current.json"
    params = {
        "key": api_key,
        "q": location
    }

    response = requests.get(url, params=params)
    data = response.json()

    temp = data['current']['temp_c'] if unit == "celsius" else data['current']['temp_f']
    condition = data['current']['condition']['text']

    return {
        "location": location,
        "temperature": temp,
        "unit": unit,
        "condition": condition
    }

# 함수 정의
functions = [{
    "name": "get_weather",
    "description": "Get the current weather in a location",
    "parameters": {
        "type": "object",
        "properties": {
            "location": {"type": "string", "description": "The city name"},
            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
        },
        "required": ["location"]
    }
}]

# 대화 처리
def chat(user_message):
    messages = [{"role": "user", "content": user_message}]

    response = client.chat.completions.create(
        model="gpt-4",
        messages=messages,
        functions=functions,
        function_call="auto"
    )

    message = response.choices[0].message

    # Function call이 있으면 실행
    if message.function_call:
        function_name = message.function_call.name
        arguments = json.loads(message.function_call.arguments)

        # 함수 실행
        function_response = get_weather(**arguments)

        # 결과를 메시지에 추가
        messages.append(message)
        messages.append({
            "role": "function",
            "name": function_name,
            "content": json.dumps(function_response)
        })

        # 최종 응답 생성
        second_response = client.chat.completions.create(
            model="gpt-4",
            messages=messages
        )

        return second_response.choices[0].message.content

    return message.content

# 사용
print(chat("서울과 부산의 날씨를 화씨로 알려줘"))

2. 데이터베이스 쿼리

import sqlite3

def query_database(query):
    """데이터베이스 쿼리 실행"""
    conn = sqlite3.connect('sales.db')
    cursor = conn.cursor()
    cursor.execute(query)
    results = cursor.fetchall()
    conn.close()
    return results

functions = [{
    "name": "query_database",
    "description": "SQL 쿼리를 실행하여 데이터베이스에서 정보를 조회합니다",
    "parameters": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "실행할 SQL SELECT 쿼리"
            }
        },
        "required": ["query"]
    }
}]

# 사용 예
user_query = "2024년 10월 총 매출액은 얼마인가요?"
# LLM이 자동으로 SQL 생성:
# SELECT SUM(amount) FROM sales WHERE YEAR(date) = 2024 AND MONTH(date) = 10

3. 이메일 발송

import smtplib
from email.mime.text import MIMEText

def send_email(to, subject, body):
    """이메일 발송"""
    msg = MIMEText(body)
    msg['Subject'] = subject
    msg['To'] = to
    msg['From'] = 'your@email.com'

    with smtplib.SMTP('smtp.gmail.com', 587) as server:
        server.starttls()
        server.login('your@email.com', 'password')
        server.send_message(msg)

    return {"status": "sent", "to": to}

functions = [{
    "name": "send_email",
    "description": "이메일을 발송합니다",
    "parameters": {
        "type": "object",
        "properties": {
            "to": {"type": "string", "description": "수신자 이메일"},
            "subject": {"type": "string", "description": "메일 제목"},
            "body": {"type": "string", "description": "메일 본문"}
        },
        "required": ["to", "subject", "body"]
    }
}]

# 사용
chat("홍길동(hong@example.com)에게 내일 회의 일정 리마인더 메일 보내줘")
# LLM이 자동으로 적절한 제목과 본문 생성

4. 다중 함수 사용

# 여러 함수를 동시에 제공
functions = [
    {
        "name": "get_weather",
        "description": "날씨 조회",
        "parameters": {...}
    },
    {
        "name": "search_flights",
        "description": "항공편 검색",
        "parameters": {...}
    },
    {
        "name": "book_hotel",
        "description": "호텔 예약",
        "parameters": {...}
    }
]

# LLM이 자동으로 적절한 함수 선택
user_query = "다음주 월요일 서울에서 제주도 가는 항공편 찾아주고, 제주 날씨도 알려줘"
# → search_flights와 get_weather 둘 다 호출

고급 패턴

1. 체인 함수 호출

함수 결과를 바탕으로 또 다른 함수를 호출합니다.

def run_conversation(user_message):
    messages = [{"role": "user", "content": user_message}]

    # 최대 5번까지 함수 호출 가능
    for _ in range(5):
        response = client.chat.completions.create(
            model="gpt-4",
            messages=messages,
            functions=functions,
            function_call="auto"
        )

        message = response.choices[0].message

        if not message.function_call:
            # 함수 호출이 없으면 최종 응답
            return message.content

        # 함수 실행
        function_name = message.function_call.name
        arguments = json.loads(message.function_call.arguments)
        result = execute_function(function_name, arguments)

        # 메시지에 추가하고 계속 진행
        messages.append(message)
        messages.append({
            "role": "function",
            "name": function_name,
            "content": json.dumps(result)
        })

    return "최대 함수 호출 횟수 초과"

# 사용 예
chat("서울 날씨 확인하고, 비가 오면 우산 추천 상품 검색해줘")
# 1. get_weather(location="서울")
# 2. search_products(query="우산") - 날씨 결과에 따라

2. 구조화된 데이터 추출

Function Calling을 JSON 파서로 활용합니다.

# 함수를 실제로 실행하지 않고 JSON만 생성
extract_schema = {
    "name": "extract_contact_info",
    "description": "텍스트에서 연락처 정보를 추출합니다",
    "parameters": {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "email": {"type": "string"},
            "phone": {"type": "string"},
            "company": {"type": "string"}
        },
        "required": ["name"]
    }
}

text = """
안녕하세요, 저는 김철수입니다.
제 이메일은 kim@example.com이고,
전화번호는 010-1234-5678입니다.
ABC 회사에서 일하고 있습니다.
"""

response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": f"Extract contact info: {text}"}],
    functions=[extract_schema],
    function_call={"name": "extract_contact_info"}  # 강제 호출
)

data = json.loads(response.choices[0].message.function_call.arguments)
print(data)
# {
#     "name": "김철수",
#     "email": "kim@example.com",
#     "phone": "010-1234-5678",
#     "company": "ABC"
# }

3. 병렬 함수 호출

# GPT-4-turbo 이상에서 지원
response = client.chat.completions.create(
    model="gpt-4-turbo-preview",
    messages=[{
        "role": "user",
        "content": "서울, 부산, 제주의 날씨를 모두 알려줘"
    }],
    tools=[{  # functions 대신 tools 사용
        "type": "function",
        "function": weather_function_schema
    }],
    tool_choice="auto"
)

# 여러 함수 호출이 동시에 반환됨
for tool_call in response.choices[0].message.tool_calls:
    function_name = tool_call.function.name
    arguments = json.loads(tool_call.function.arguments)
    # 병렬 처리 가능

실전 애플리케이션

1. AI 어시스턴트

class AIAssistant:
    def __init__(self):
        self.client = OpenAI()
        self.functions = self._register_functions()

    def _register_functions(self):
        return [
            {
                "name": "create_todo",
                "description": "할 일 추가",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "task": {"type": "string"},
                        "due_date": {"type": "string"}
                    },
                    "required": ["task"]
                }
            },
            {
                "name": "search_web",
                "description": "웹 검색",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "query": {"type": "string"}
                    },
                    "required": ["query"]
                }
            },
            {
                "name": "set_reminder",
                "description": "리마인더 설정",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "message": {"type": "string"},
                        "time": {"type": "string"}
                    },
                    "required": ["message", "time"]
                }
            }
        ]

    def chat(self, message):
        # Function calling 로직...
        pass

# 사용
assistant = AIAssistant()
assistant.chat("내일 오후 3시에 회의 리마인더 설정해줘")
assistant.chat("AI 최신 뉴스 검색해줘")

2. 고객 지원 봇

functions = [
    {
        "name": "get_order_status",
        "description": "주문 상태 조회",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string"}
            }
        }
    },
    {
        "name": "initiate_refund",
        "description": "환불 신청",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string"},
                "reason": {"type": "string"}
            }
        }
    },
    {
        "name": "track_shipment",
        "description": "배송 추적",
        "parameters": {
            "type": "object",
            "properties": {
                "tracking_number": {"type": "string"}
            }
        }
    }
]

# 고객: "주문번호 12345 환불하고 싶어요. 사이즈가 안 맞아서요"
# → initiate_refund(order_id="12345", reason="사이즈 불일치")

모범 사례

1. 명확한 함수 설명

# ❌ 나쁜 예
{
    "name": "func",
    "description": "데이터 가져오기",
    "parameters": {...}
}

# ✅ 좋은 예
{
    "name": "get_user_profile",
    "description": "사용자 ID를 입력받아 프로필 정보(이름, 이메일, 가입일)를 반환합니다. 사용자가 존재하지 않으면 null을 반환합니다.",
    "parameters": {...}
}

2. 타입과 설명 명시

"parameters": {
    "type": "object",
    "properties": {
        "date": {
            "type": "string",
            "description": "날짜 (YYYY-MM-DD 형식)",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
        },
        "priority": {
            "type": "string",
            "enum": ["low", "medium", "high"],
            "description": "우선순위 (low: 낮음, medium: 보통, high: 높음)"
        }
    }
}

3. 에러 처리

def execute_function(name, arguments):
    try:
        if name == "get_weather":
            return get_weather(**arguments)
        # ... 다른 함수들
    except Exception as e:
        return {
            "error": str(e),
            "message": "함수 실행 중 오류가 발생했습니다"
        }

4. 보안 고려

# 민감한 작업은 확인 절차 추가
def execute_function_safely(name, arguments):
    # 위험한 작업
    dangerous_functions = ["delete_data", "send_money", "update_password"]

    if name in dangerous_functions:
        # 사용자 확인 요청
        if not confirm_with_user(name, arguments):
            return {"error": "User cancelled the operation"}

    return execute_function(name, arguments)

한계와 해결책

1. 환각 문제

LLM이 존재하지 않는 함수를 호출하려 할 수 있습니다.

해결책:

valid_functions = {"get_weather", "search_web", "send_email"}

if message.function_call:
    if message.function_call.name not in valid_functions:
        return "요청하신 작업을 수행할 수 없습니다."

2. 부정확한 파라미터

해결책:

from pydantic import BaseModel, ValidationError

class WeatherParams(BaseModel):
    location: str
    unit: str = "celsius"

try:
    params = WeatherParams(**arguments)
    result = get_weather(params.location, params.unit)
except ValidationError as e:
    return {"error": "Invalid parameters", "details": str(e)}

3. 비용

Function calling은 일반 chat보다 토큰을 더 사용합니다.

최적화:

  • 꼭 필요한 함수만 제공
  • 함수 설명을 간결하게
  • 캐싱 활용

결론

Function Calling은 LLM을 실세계와 연결하는 다리입니다. 이를 통해:

  • 🌐 외부 API 통합: 날씨, 검색, 결제 등
  • 📊 구조화된 데이터 추출: JSON 파싱
  • 🤖 AI Agent 구축: 자율적으로 작업 수행
  • 💼 업무 자동화: 이메일, 문서 작성 등

시작하기:

  1. 간단한 함수 1-2개로 시작
  2. 함수 설명을 명확하게 작성
  3. 에러 처리 철저히
  4. 점진적으로 복잡한 시스템 구축

Function Calling을 마스터하면 단순한 챗봇을 넘어 진정한 AI 어시스턴트를 만들 수 있습니다!