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 Calling | LLM이 외부 함수 호출을 요청하는 메커니즘 |
| tools 스키마 | 함수 이름·설명·파라미터를 JSON으로 정의 |
| tool_calls | LLM 응답에 포함된 함수 호출 요청 목록 |
| 함수 실행 | 개발자 코드에서 직접 실행 후 결과 반환 |
| Structured Output | Pydantic 모델로 응답 JSON 스키마 강제 |
다음 편에서는 RAG 구현 — ChromaDB를 활용해 외부 문서를 LLM에 연결하는 실습을 합니다.