LLMLLM API 개발강의 5개 중 3번째난이도 중급

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

발행 읽는 시간 약 2 분

  • LLM
  • OpenAI
  • FunctionCalling
  • ToolUse
  • 에이전트
  • Python

강의 소개

OpenAI Function Calling으로 LLM이 날씨 API, 데이터베이스, 계산기 등 외부 도구를 직접 호출하는 AI 에이전트를 만드는 방법을 배웁니다.

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

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

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

Diagram · Mermaid

Function Calling 전체 흐름#

Diagram · Mermaid

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


기본 구현: 날씨 봇#

1단계: 함수 스키마 정의#

Python
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단계: 실제 함수 구현#

Python
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 + 함수 호출 루프#

Python
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%입니다.

다중 도구: 계산기 + 날씨#

Python
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는 필요하면 여러 함수를 동시에 호출합니다.

Diagram · Mermaid
Python
# 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 스키마로 강제하는 방법도 있습니다.

Python
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)  # ['빠른 배송', '좋은 품질']

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

Python
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에 연결하는 실습을 합니다.

Follow · Ask · Work

이 글이 도움이 됐다면

짧은 작업 기록과 데모는 인스타그램에 올립니다. 만들고 싶은 것이 있다면 프로젝트 의뢰나 협업 제안도 편하게 보내 주세요.

글쓴이

파란여우

직접 만든 AI 앱·제품의 개발·배포를 공개하는 실전형 테크 크리에이터

파란여우 소개 Instagram @bluefox._.hi

이어서 보기

만들고 싶은 것이 있다면 프로젝트 의뢰로 알려 주세요.