LLMLLM API 개발 · 3중급

Function Calling — LLM이 외부 함수를 호출하게 만들기

LLMOpenAIFunctionCallingToolUse에이전트Python

LLM의 한계를 외부 도구로 극복하기

LLM은 지식 컷오프 이후 정보를 모르고, 정확한 계산을 못하고, 실시간 데이터가 없습니다.

Function Calling은 LLM이 이런 한계를 만났을 때 직접 외부 함수를 호출할 수 있게 합니다.

flowchart LR
    USER["'서울 오늘 날씨 어때?'"] --> LLM["LLM"]
    
    LLM -->|"날씨 API 호출 필요\n→ get_weather('서울') 요청"| FUNC["get_weather(city)"]
    FUNC -->|"{'temp': 18, 'sky': '맑음'}"| LLM
    LLM -->|"도구 결과 + 자연어 생성"| ANS["서울은 현재 18도로\n맑은 날씨입니다."]

Function Calling 전체 흐름

flowchart TB
    S1["① 사용자 질문\n+ 사용 가능한 함수 목록 전달"]
    S2["② LLM 판단\n'이 질문엔 get_weather가 필요하다'"]
    S3["③ 함수 호출 요청\n{'name': 'get_weather', 'args': {'city': '서울'}}"]
    S4["④ 실제 함수 실행\n개발자 코드에서 직접 실행"]
    S5["⑤ 결과를 LLM에 전달\n{'temp': 18, 'condition': 'clear'}"]
    S6["⑥ 최종 자연어 응답 생성"]

    S1 --> S2 --> S3 --> S4 --> S5 --> S6

LLM은 함수를 직접 실행하지 않습니다. 호출해야 할 함수와 인자를 알려주면, 개발자 코드가 실제로 실행하고 결과를 돌려줍니다.


기본 구현: 날씨 봇

1단계: 함수 스키마 정의

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"]
            }
        }
    }
]

2단계: 실제 함수 구현

import json

def get_weather(city: str, unit: str = "celsius") -> dict:
    # 실제 구현에서는 날씨 API 호출
    # 여기서는 목 데이터 사용
    mock_data = {
        "서울": {"temp": 18, "condition": "맑음", "humidity": 60},
        "부산": {"temp": 22, "condition": "구름 조금", "humidity": 75},
    }
    data = mock_data.get(city, {"temp": 20, "condition": "데이터 없음", "humidity": 50})
    
    if unit == "fahrenheit":
        data["temp"] = data["temp"] * 9/5 + 32
    
    return {**data, "city": city, "unit": unit}

3단계: LLM + 함수 호출 루프

from openai import OpenAI

client = OpenAI()

def chat_with_tools(user_message: str) -> str:
    messages = [{"role": "user", "content": user_message}]
    
    # 1차 호출: LLM이 도구 사용 여부 판단
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        tools=tools,
        tool_choice="auto"  # 자동 판단 (또는 "none", "required")
    )
    
    message = response.choices[0].message
    
    # 도구 호출이 없으면 바로 반환
    if not message.tool_calls:
        return message.content
    
    # 도구 호출 처리
    messages.append(message)  # assistant 메시지 추가
    
    for tool_call in message.tool_calls:
        func_name = tool_call.function.name
        func_args = json.loads(tool_call.function.arguments)
        
        # 함수 실행
        if func_name == "get_weather":
            result = get_weather(**func_args)
        else:
            result = {"error": f"Unknown function: {func_name}"}
        
        # 결과를 메시지에 추가
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result, ensure_ascii=False)
        })
    
    # 2차 호출: 도구 결과로 최종 응답 생성
    final_response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages
    )
    
    return final_response.choices[0].message.content

# 테스트
print(chat_with_tools("서울 날씨 어때?"))
# 출력: 서울의 현재 날씨는 맑음이며, 기온은 18°C, 습도는 60%입니다.

다중 도구: 계산기 + 날씨

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "도시의 현재 날씨 조회",
            # ... (위와 동일)
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "수학 계산을 수행합니다",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "계산할 수식 (예: '25 * 4 + 100')"
                    }
                },
                "required": ["expression"]
            }
        }
    }
]

def calculate(expression: str) -> dict:
    try:
        result = eval(expression)  # 실제 서비스에서는 안전한 파서 사용
        return {"result": result, "expression": expression}
    except Exception as e:
        return {"error": str(e)}

# 함수 라우터
FUNCTION_MAP = {
    "get_weather": get_weather,
    "calculate": calculate,
}

def execute_tool(name: str, args: dict):
    if name in FUNCTION_MAP:
        return FUNCTION_MAP[name](**args)
    return {"error": f"Unknown tool: {name}"}

병렬 함수 호출

GPT-4는 필요하면 여러 함수를 동시에 호출합니다.

flowchart TD
    Q["'서울과 부산의 날씨를\n비교해줘'"]
    
    Q --> LLM["LLM"]
    LLM -->|"두 호출 동시 요청"| T1["get_weather('서울')"]
    LLM --> T2["get_weather('부산')"]
    
    T1 & T2 -->|"결과 반환"| LLM2["LLM\n비교 응답 생성"]
# message.tool_calls에 여러 개가 담겨옴
for tool_call in message.tool_calls:
    # 각각 처리
    result = execute_tool(
        tool_call.function.name,
        json.loads(tool_call.function.arguments)
    )
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(result, ensure_ascii=False)
    })

Structured Output: JSON 스키마 강제

Function Calling 외에, 응답 자체를 JSON 스키마로 강제하는 방법도 있습니다.

from pydantic import BaseModel
from typing import Literal

class ReviewAnalysis(BaseModel):
    sentiment: Literal["positive", "negative", "neutral"]
    score: float  # 0.0 ~ 1.0
    key_phrases: list[str]
    summary: str

response = client.beta.chat.completions.parse(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "리뷰를 분석하세요."},
        {"role": "user", "content": "이 제품 정말 좋아요! 배송도 빠르고 품질도 최고입니다."}
    ],
    response_format=ReviewAnalysis
)

analysis = response.choices[0].message.parsed
print(analysis.sentiment)    # positive
print(analysis.score)        # 0.95
print(analysis.key_phrases)  # ['빠른 배송', '좋은 품질']

실전 예시: 데이터베이스 조회 에이전트

tools = [{
    "type": "function",
    "function": {
        "name": "query_database",
        "description": "고객 데이터베이스를 조회합니다",
        "parameters": {
            "type": "object",
            "properties": {
                "customer_id": {"type": "string"},
                "query_type": {
                    "type": "string",
                    "enum": ["orders", "profile", "payments"]
                }
            },
            "required": ["customer_id", "query_type"]
        }
    }
}]

def query_database(customer_id: str, query_type: str) -> dict:
    # 실제 DB 조회 로직
    mock_db = {
        "C001": {
            "profile": {"name": "김철수", "email": "kim@example.com"},
            "orders": [{"id": "O123", "item": "노트북", "status": "배송완료"}],
            "payments": [{"amount": 1200000, "date": "2024-03-15"}]
        }
    }
    customer = mock_db.get(customer_id, {})
    return customer.get(query_type, {"error": "데이터 없음"})

정리

개념내용
Function CallingLLM이 외부 함수 호출을 요청하는 메커니즘
tools 스키마함수 이름·설명·파라미터를 JSON으로 정의
tool_callsLLM 응답에 포함된 함수 호출 요청 목록
함수 실행개발자 코드에서 직접 실행 후 결과 반환
Structured OutputPydantic 모델로 응답 JSON 스키마 강제

다음 편에서는 RAG 구현 — ChromaDB를 활용해 외부 문서를 LLM에 연결하는 실습을 합니다.

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.