Function Call: AI의 실질적 행동 능력 부여

AI의 한계: 말만 하고 행동하지 못하는 이유

대부분의 생성형 AI(예: ChatGPT)는 자연어 이해와 생성에 능숙하지만, 실제 환경과의 상호작용에는 본질적 제약이 존재합니다. 사용자가 "내일 서울에서 부산까지 KTX 예매해줘"라고 요청하면, AI는 열차 시간표를 검색하거나 예매 시스템에 접근할 수 없어 단순히 "KTX 예매 방법을 안내해드립니다" 수준의 응답만 제공합니다.

Function Call은 이러한 한계를 극복하기 위해 등장한 메커니즘으로, 외부 도구와 서비스를 AI의 추론 체계에 통합합니다.

작동 원리: 의도 분석부터 실행까지

Function Call의 흐름은 다음과 같이 구성됩니다:

  1. 의도 파악: 사용자 입력에서 외부 도구 호출 필요성을 식별
  2. 매개변수 추출: 도구 실행에 필요한 인자를 구조화된 형태로 조립
  3. 도구 실행: 실제 API 호출 또는 함수 실행 (AI 외부에서 수행)
  4. 결과 통합: 도출된 데이터를 자연어로 재구성하여 응답

실제 구현 예시

Python 환경에서의 Function Call 구현을 살펴보겠습니다:

import json
from typing import Callable, Dict, Any

# 외부 도시 등록
tool_registry: Dict[str, Callable] = {}

def register_tool(func: Callable) -> Callable:
    tool_registry[func.__name__] = func
    return func

@register_tool
def fetch_exchange_rate(base: str, target: str) -> Dict[str, Any]:
    """환율 조회 API 호출 (실제로는 외부 서비스 연동)"""
    # 모의 응답
    mock_rates = {"USD": {"KRW": 1320.50, "JPY": 149.20}}
    rate = mock_rates.get(base, {}).get(target, 0)
    return {"base": base, "target": target, "rate": rate}

@register_tool
def compute_budget(amount: float, days: int, currency: str) -> Dict[str, Any]:
    """일별 예산 계산"""
    daily = round(amount / days, 2)
    return {"daily_limit": daily, "total": amount, "currency": currency}

# AI 응답 시뮬레이션
def process_user_query(query: str, available_tools: list) -> str:
    # LLM이 생성한 가상의 tool_calls 응답
    simulated_response = {
        "tool_calls": [
            {
                "tool_name": "fetch_exchange_rate",
                "arguments": {"base": "USD", "target": "KRW"}
            },
            {
                "tool_name": "compute_budget",
                "arguments": {"amount": 2000, "days": 5, "currency": "USD"}
            }
        ]
    }
    
    results = []
    for call in simulated_response["tool_calls"]:
        tool = tool_registry.get(call["tool_name"])
        if tool:
            output = tool(**call["arguments"])
            results.append(f"[{call['tool_name']}] 결과: {output}")
    
    return "\n".join(results)

# 실행
print(process_user_query("5일간 여행 예산 2000달러, 환율 알려줘", []))

스키마 정의의 중요성

AI가 정확한 매개변수를 생성하려면, 각 도구의 입력 스키마를 명확히 명시해야 합니다:

{
  "tools": [
    {
      "identifier": "search_accommodation",
      "description": "특정 지역의 숙소 정보 검색",
      "parameters": {
        "type": "object",
        "required": ["location", "check_in", "check_out"],
        "properties": {
          "location": {
            "type": "string",
            "description": "도시 또는 구체적 주소"
          },
          "check_in": {
            "type": "string",
            "format": "date",
            "description": "입실일 (YYYY-MM-DD)"
          },
          "check_out": {
            "type": "string",
            "format": "date", 
            "description": "퇴실일 (YYYY-MM-DD)"
          },
          "guests": {
            "type": "integer",
            "minimum": 1,
            "default": 1,
            "description": "투숙 인원"
          },
          "max_price": {
            "type": "number",
            "description": "1박당 최대 금액 (현지 통화)"
          }
        }
      }
    }
  ]
}

복합 시나리오: 여행 플래너 구현

다중 도구 연계를 통한 실용적인 예시입니다:

from datetime import datetime, timedelta
import random

class TravelOrchestrator:
    def __init__(self):
        self.session_data = {}
    
    def plan_trip(self, destination: str, duration: int, 
                  departure: str, budget: int) -> Dict:
        
        itinerary = []
        remaining_fund = budget
        
        # 도구 1: 교통편 조회
        transport = self._query_transport(departure, destination)
        transport_cost = transport["lowest_fare"]
        remaining_fund -= transport_cost
        
        itinerary.append({
            "day": 0,
            "activity": f"{departure} → {destination} ({transport['mode']})",
            "cost": transport_cost
        })
        
        # 도구 2: 날씨 기반 일정 조정
        forecast = self._fetch_weather(destination, duration)
        
        # 도구 3: 동적 일정 생성
        for day_offset in range(duration):
            current_date = datetime.strptime(departure, "%Y-%m-%d") + \
                          timedelta(days=day_offset)
            weather = forecast[day_offset]
            
            activities = self._suggest_by_condition(weather["condition"])
            selected = random.choice(activities[:2])
            
            expense = min(selected["estimated_cost"], 
                         remaining_fund // (duration - day_offset))
            remaining_fund -= expense
            
            itinerary.append({
                "day": day_offset + 1,
                "date": current_date.strftime("%m/%d"),
                "weather": weather["condition"],
                "schedule": selected["name"],
                "cost": expense
            })
        
        return {
            "summary": {
                "destination": destination,
                "total_days": duration,
                "initial_budget": budget,
                "remaining": round(remaining_fund, 2)
            },
            "daily_plan": itinerary
        }
    
    def _query_transport(self, origin: str, dest: str) -> dict:
        # 실제로는 항공/철도 API 호출
        return {"mode": "KTX", "lowest_fare": 59800, "duration_min": 137}
    
    def _fetch_weather(self, city: str, days: int) -> list:
        patterns = ["맑음", "흐림", "비", "구름 많음"]
        return [{"condition": random.choice(patterns)} for _ in range(days)]
    
    def _suggest_by_condition(self, condition: str) -> list:
        mapping = {
            "맑음": [
                {"name": "야외 전망대 방문", "estimated_cost": 15000},
                {"name": "공원 산책", "estimated_cost": 0}
            ],
            "비": [
                {"name": "실내 박물관 관람", "estimated_cost": 4000},
                {"name": "쇼핑몰 탐방", "estimated_cost": 50000}
            ]
        }
        return mapping.get(condition, mapping["맑음"])

# 사용
planner = TravelOrchestrator()
result = planner.plan_trip("부산", 3, "2025-07-15", 300000)
print(json.dumps(result, indent=2, ensure_ascii=False))

에러 처리와 안전성

Production 환경에서는 다음 사항을 고려해야 합니다:

  • 타임아웃 관리: 외부 API 응답 지연 시 폴백 응답 제공
  • 파라미터 검증: AI 생성 값의 범위 및 형식 사전 확인
  • 권한 분리: 민감 작업(결제, 예약 확정)은 사용자 재확인 단계 추가
  • 호출 추적: 도구 실행 로그 및 재시도 메커니즘 구현
from functools import wraps
import time

def resilient_call(max_retry: int = 2, timeout: float = 5.0):
    def decorator(func: Callable):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retry + 1):
                try:
                    start = time.time()
                    result = func(*args, **kwargs)
                    elapsed = time.time() - start
                    
                    if elapsed > timeout:
                        raise TimeoutError(f"응답 시간 {elapsed:.2f}s 초과")
                    return {"status": "success", "data": result}
                    
                except Exception as e:
                    if attempt == max_retry:
                        return {
                            "status": "failed", 
                            "error": str(e),
                            "fallback": "수동 처리가 필요합니다"
                        }
                    time.sleep(0.5 * (attempt + 1))
        return wrapper
    return decorator

@resilient_call(max_retry=2)
def unstable_api_call(endpoint: str):
    # 간헐적 실패를 시뮬레이션
    if random.random() < 0.6:
        raise ConnectionError("서버 응답 없음")
    return {"endpoint": endpoint, "result": "ok"}

아키텍처적 고려사항

Function Call을 시스템에 통합할 때의 구조적 패턴:

구성 요소책임구현 예시
Tool Router의도 분류 및 적절한 도기 매핑임베딩 기반 유사도 검색
Parameter Builder스키마 준수하는 인자 조립Pydantic 검증 모델
Execution Sandbox격리된 도구 실행 환경Docker 컨테이너 / WASM
Result Aggregator다중 도구 출력 통합LLM 기반 요약 체인

이러한 계층적 설계를 통해 AI 시스템은 정적인 지능에서 동적인 행위자로 전환됩니다.

태그: Function Calling LLM integration API Orchestration AI Agents Tool Use

9월 30일 20:52에 게시됨