본문 바로가기

LLM

[기초] LLM이 도구를 직접 호출한다 — Function Calling 원리와 구현 완전 정리

반응형

AI 에이전트를 만들다 보면 이런 상황이 생깁니다.

"LLM한테 날씨 알려달라고 했는데, 학습 데이터에 없는 오늘 날씨를 어떻게 알려주지?"

LLM은 학습 데이터 기반으로만 답하기 때문에 실시간 정보나 외부 시스템과 연동이 안 돼요. 이걸 해결하는 게 Function Calling입니다. 이번 글에서는 Function Calling이 뭔지, 어떻게 동작하는지, 실제로 어떻게 구현하는지 정리해 드릴게요.


Function Calling이란?

Function Calling은 LLM이 응답을 생성할 때 "이 질문은 내가 직접 답하는 게 아니라 이 함수를 호출해야 한다" 고 판단해서 함수 호출 정보를 반환하는 기능이에요.

중요한 건 LLM이 함수를 직접 실행하는 게 아니라는 점이에요. LLM은 "어떤 함수를 어떤 인자로 호출하면 된다"는 정보만 반환하고, 실제 실행은 개발자 코드에서 합니다.

사용자: "서울 날씨 알려줘"
LLM: "get_weather 함수를 city='서울'로 호출하세요" (함수 실행 X, 정보만 반환)
개발자 코드: get_weather(city='서울') 실행
결과를 다시 LLM에 전달
LLM: "서울 현재 날씨는 맑음, 기온 15도입니다"

동작 원리

Function Calling의 흐름은 4단계예요.

1단계: 함수 정의를 LLM에 전달

어떤 함수가 있는지, 각 함수가 어떤 인자를 받는지 JSON Schema 형태로 LLM에게 알려줍니다.

2단계: LLM이 함수 호출 여부 판단

사용자 질문을 보고 LLM이 직접 답할 수 있으면 그냥 답하고, 함수가 필요하면 어떤 함수를 어떤 인자로 호출할지 반환합니다.

3단계: 개발자 코드에서 함수 실행

LLM이 반환한 함수 이름과 인자를 보고 실제 함수를 실행합니다.

4단계: 결과를 LLM에 다시 전달

함수 실행 결과를 LLM에게 넘기면 LLM이 최종 응답을 생성합니다.


실제 구현

OpenAI API 기준

함수 정의

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

1차 LLM 호출

from openai import OpenAI
import json

client = OpenAI()

messages = [{"role": "user", "content": "서울 날씨 알려줘"}]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice="auto"   # auto: LLM이 알아서 판단 / required: 무조건 함수 호출
)

message = response.choices[0].message

함수 호출 여부 확인 및 실행

# 실제 함수 구현
def get_weather(city: str, unit: str = "celsius") -> dict:
    # 실제로는 날씨 API 호출
    return {
        "city": city,
        "temperature": 15,
        "condition": "맑음",
        "unit": unit
    }

# LLM이 함수 호출을 요청했는지 확인
if message.tool_calls:
    tool_call = message.tool_calls[0]
    function_name = tool_call.function.name
    function_args = json.loads(tool_call.function.arguments)

    # 함수 실행
    if function_name == "get_weather":
        result = get_weather(**function_args)

    # 결과를 messages에 추가
    messages.append(message)  # LLM의 tool_call 메시지
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(result, ensure_ascii=False)
    })

2차 LLM 호출 — 최종 응답 생성

final_response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages
)

print(final_response.choices[0].message.content)
# "서울의 현재 날씨는 맑음이며 기온은 15도입니다."

여러 함수 동시 등록

실제 에이전트에서는 함수를 여러 개 등록해요. LLM이 질문에 맞는 함수를 골라서 호출합니다.

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "특정 도시의 현재 날씨 조회",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string"}
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "웹에서 정보를 검색합니다",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "검색어"}
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "수학 계산을 수행합니다",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {"type": "string", "description": "계산할 수식"}
                },
                "required": ["expression"]
            }
        }
    }
]

함수 실행 부분은 딕셔너리로 매핑하면 깔끔해요.

function_map = {
    "get_weather": get_weather,
    "search_web": search_web,
    "calculate": calculate
}

if message.tool_calls:
    for tool_call in message.tool_calls:
        func = function_map.get(tool_call.function.name)
        args = json.loads(tool_call.function.arguments)
        result = func(**args)
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result, ensure_ascii=False)
        })

Parallel Function Calling

하나의 질문에 여러 함수를 동시에 호출할 수도 있어요.

"서울이랑 부산 날씨 둘 다 알려줘"
→ get_weather(city="서울")
→ get_weather(city="부산")
두 개 동시에 호출

LLM이 tool_calls 배열에 여러 개를 담아서 반환하기 때문에 위 코드처럼 for tool_call in message.tool_calls로 처리하면 자동으로 대응돼요.


LangChain에서 Function Calling

LangChain에서는 @tool 데코레이터로 더 간단하게 쓸 수 있어요.

from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

@tool
def get_weather(city: str) -> str:
    """특정 도시의 현재 날씨를 조회합니다."""
    return f"{city}의 현재 날씨는 맑음, 15도입니다."

@tool
def search_web(query: str) -> str:
    """웹에서 정보를 검색합니다."""
    return f"'{query}' 검색 결과: ..."

llm = ChatOpenAI(model="gpt-4o")
llm_with_tools = llm.bind_tools([get_weather, search_web])

response = llm_with_tools.invoke("서울 날씨 알려줘")

함수의 docstring이 LLM에게 전달되는 description이 되기 때문에 docstring을 명확하게 써야 LLM이 올바르게 함수를 선택해요.


tool_choice 옵션

함수 호출 여부를 제어하는 옵션이에요.

# auto: LLM이 알아서 판단 (기본값)
tool_choice="auto"

# required: 반드시 함수 호출 (어떤 함수든)
tool_choice="required"

# none: 함수 호출 금지
tool_choice="none"

# 특정 함수 강제 호출
tool_choice={"type": "function", "function": {"name": "get_weather"}}

Function Calling vs 프롬프트로 JSON 뽑기

Function Calling 이전에는 프롬프트에 "JSON으로 답해줘"라고 써서 함수 호출 여부를 판단했어요. 차이는 이렇습니다.

구분 프롬프트 JSON Function Calling

안정성 파싱 실패 가능 구조화된 출력 보장
함수 선택 LLM이 텍스트로 판단 스키마 기반 정확한 선택
인자 추출 부정확할 수 있음 타입까지 검증됨
복잡도 간단 약간 복잡

프로덕션 에이전트에서는 Function Calling을 쓰는 게 훨씬 안정적이에요.


마무리

Function Calling의 핵심은 세 가지입니다.

첫째, LLM은 함수를 실행하지 않고 "어떤 함수를 어떤 인자로 호출할지"만 반환합니다. 둘째, 실제 실행과 결과를 다시 LLM에 전달하는 건 개발자 코드의 몫이에요. 셋째, 함수 description을 명확하게 써야 LLM이 올바른 함수를 선택합니다.

AI 에이전트에서 외부 데이터나 시스템과 연동이 필요하다면 Function Calling이 가장 안정적인 방법이에요. 😄


 

반응형