AI의 한계: 말만 하고 행동하지 못하는 이유
대부분의 생성형 AI(예: ChatGPT)는 자연어 이해와 생성에 능숙하지만, 실제 환경과의 상호작용에는 본질적 제약이 존재합니다. 사용자가 "내일 서울에서 부산까지 KTX 예매해줘"라고 요청하면, AI는 열차 시간표를 검색하거나 예매 시스템에 접근할 수 없어 단순히 "KTX 예매 방법을 안내해드립니다" 수준의 응답만 제공합니다.
Function Call은 이러한 한계를 극복하기 위해 등장한 메커니즘으로, 외부 도구와 서비스를 AI의 추론 체계에 통합합니다.
작동 원리: 의도 분석부터 실행까지
Function Call의 흐름은 다음과 같이 구성됩니다:
- 의도 파악: 사용자 입력에서 외부 도구 호출 필요성을 식별
- 매개변수 추출: 도구 실행에 필요한 인자를 구조화된 형태로 조립
- 도구 실행: 실제 API 호출 또는 함수 실행 (AI 외부에서 수행)
- 결과 통합: 도출된 데이터를 자연어로 재구성하여 응답
실제 구현 예시
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 시스템은 정적인 지능에서 동적인 행위자로 전환됩니다.