이미지 로딩 중...

OpenAI Functions & Tools 활용 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 16. · 5 Views

OpenAI Functions & Tools 활용 완벽 가이드

OpenAI의 Function Calling과 Tools API를 활용하여 AI가 실시간으로 외부 데이터를 가져오고 특정 작업을 수행하도록 만드는 방법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 예제와 함께 설명합니다.


목차

  1. Function Calling 기본 개념
  2. 함수 파라미터 추출 자동화
  3. 여러 함수 동시 관리하기
  4. 함수 실행 결과 AI에게 전달하기
  5. 에러 처리와 예외 상황 관리
  6. Streaming과 실시간 응답 구현
  7. Parallel Function Calling으로 성능 최적화
  8. 함수 결과 캐싱으로 비용 절감
  9. Tools API를 활용한 고급 기능 구현
  10. 실전 프로젝트 패턴과 아키텍처

1. Function Calling 기본 개념

시작하며

여러분이 ChatGPT에게 "오늘 서울 날씨 어때?"라고 물어봤는데, ChatGPT가 "죄송합니다, 실시간 정보는 제공할 수 없어요"라고 답한 경험 있으시죠? 이건 AI가 인터넷에 접속할 수 없기 때문입니다.

그런데 만약 AI가 스스로 날씨 API를 호출해서 최신 정보를 가져올 수 있다면 어떨까요? 마치 비서가 필요한 정보를 직접 찾아서 보고하는 것처럼요.

바로 이것을 가능하게 만드는 기술이 OpenAI의 Function Calling입니다. AI가 필요할 때 우리가 만든 함수를 호출해서 실시간 데이터를 가져오고, 그 정보를 바탕으로 답변할 수 있게 됩니다.

개요

간단히 말해서, Function Calling은 AI가 대화 중에 필요한 함수를 스스로 선택해서 실행하도록 요청하는 기능입니다. 실제 개발 현장에서 AI 챗봇을 만들 때 가장 큰 문제는 "AI가 학습 데이터에만 의존한다"는 점입니다.

예를 들어, 쇼핑몰 챗봇이 실시간 재고를 확인하거나, 고객 주문 상태를 조회하는 것은 불가능했습니다. 하지만 Function Calling을 사용하면 AI가 데이터베이스를 조회하거나 외부 API를 호출할 수 있게 됩니다.

기존에는 사용자가 특정 키워드를 입력하면 미리 정해진 함수를 실행하는 방식이었다면, 이제는 AI가 문맥을 이해하고 "아, 이 질문에는 날씨 함수를 호출해야겠구나"라고 스스로 판단합니다. 핵심 특징은 크게 세 가지입니다.

첫째, AI가 함수 선택을 자동으로 합니다. 둘째, 함수에 전달할 파라미터도 AI가 추출합니다.

셋째, 여러 함수 중에서 가장 적합한 것을 골라냅니다. 이러한 특징들이 있기에 개발자는 함수만 만들어두면, AI가 알아서 적절한 타이밍에 호출해줍니다.

코드 예제

from openai import OpenAI

client = OpenAI(api_key="your-api-key")

# 함수 정의 (실제로 날씨를 가져오는 함수)
def get_weather(location):
    # 실제로는 API 호출, 여기서는 예시
    return f"{location}의 날씨는 맑음, 기온 22도입니다."

# AI에게 함수 설명 제공
tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "특정 지역의 현재 날씨 정보를 가져옵니다",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string", "description": "도시 이름"}
            },
            "required": ["location"]
        }
    }
}]

# AI와 대화
response = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=[{"role": "user", "content": "서울 날씨 알려줘"}],
    tools=tools
)

설명

이것이 하는 일은 AI와 여러분의 코드를 연결하는 다리를 만드는 것입니다. AI는 텍스트만 이해하지만, Function Calling을 통해 실제 프로그램 기능을 사용할 수 있게 됩니다.

첫 번째 단계로, tools 배열에 함수 정보를 JSON 형식으로 설명합니다. 이건 마치 AI에게 "이런 도구가 있어, 이렇게 사용하면 돼"라고 설명서를 주는 것과 같습니다.

name은 함수 이름, description은 이 함수가 무엇을 하는지 AI가 이해할 수 있게 설명하는 부분입니다. parameters는 함수에 필요한 입력값을 정의합니다.

두 번째 단계에서, AI가 사용자의 질문 "서울 날씨 알려줘"를 분석합니다. AI는 "날씨"라는 키워드를 보고 get_weather 함수가 필요하다고 판단합니다.

그리고 "서울"이라는 지역명을 자동으로 추출해서 location 파라미터로 사용하겠다고 결정합니다. 이 모든 과정이 AI 내부에서 자동으로 일어납니다.

세 번째 단계로, response 객체에는 AI가 선택한 함수 이름과 파라미터가 담겨 옵니다. 우리는 이 정보를 받아서 실제로 get_weather("서울") 함수를 실행합니다.

그 결과를 다시 AI에게 전달하면, AI는 그 데이터를 바탕으로 "서울의 날씨는 맑고 기온은 22도예요"라고 자연스러운 문장으로 답변합니다. 여러분이 이 코드를 사용하면 단순 질의응답을 넘어서 실제 비즈니스 로직과 연결된 AI 애플리케이션을 만들 수 있습니다.

예를 들어 재고 확인, 주문 조회, 예약 시스템 등 실시간 데이터가 필요한 모든 곳에서 활용 가능합니다. 또한 사용자가 어떤 표현으로 물어보든 AI가 알아서 적절한 함수를 찾아주기 때문에, 키워드 매칭 같은 복잡한 로직이 필요 없습니다.

실전 팁

💡 함수 description은 최대한 구체적으로 작성하세요. "날씨 가져오기"보다 "특정 도시의 현재 날씨와 기온을 실시간으로 조회합니다"처럼 상세할수록 AI가 정확하게 판단합니다.

💡 처음에는 함수 하나로 시작해서 테스트하고, 점진적으로 늘려가세요. 한 번에 너무 많은 함수를 등록하면 AI가 잘못된 함수를 선택할 확률이 높아집니다.

💡 실제 함수 실행은 AI가 하지 않습니다. AI는 "이 함수를 이 파라미터로 호출하세요"라고 알려줄 뿐, 실제 실행은 여러분의 코드에서 해야 합니다. 이걸 놓치는 초보자가 많습니다.

💡 함수 실행 결과는 반드시 다시 AI에게 전달해야 최종 답변을 받을 수 있습니다. 함수 호출 → 결과 받기 → AI에게 전달 → 최종 답변, 이 흐름을 꼭 기억하세요.

💡 에러 처리를 꼭 추가하세요. 함수 실행이 실패할 수 있으니 try-except로 감싸고, 에러 발생 시 AI에게 "함수 실행 실패"라고 알려주면 AI가 대체 답변을 생성합니다.


2. 함수 파라미터 추출 자동화

시작하며

여러분이 사용자에게 "어느 도시 날씨를 알려드릴까요?"라고 물어보고, 답변을 기다려야 했던 불편함 경험해보셨죠? 사용자는 그냥 자연스럽게 "내일 부산 날씨 어때?"라고 한 번에 물어보고 싶어 합니다.

문제는 이 문장에서 "부산"이라는 도시명과 "내일"이라는 날짜를 추출하는 게 쉽지 않다는 점입니다. 정규표현식으로 하자니 모든 경우의 수를 다 커버하기 어렵고, 사용자가 "부산 내일 날씨", "내일 부산은?", "부산은 내일 어때?" 등 다양하게 표현할 수 있기 때문입니다.

OpenAI Function Calling은 이런 파라미터 추출을 AI가 자동으로 해줍니다. 개발자는 "이런 정보가 필요해"라고만 선언하면, AI가 알아서 문장을 분석해서 값을 뽑아냅니다.

개요

간단히 말해서, 파라미터 추출 자동화는 AI가 사용자의 자연어 질문에서 함수에 필요한 값들을 스스로 찾아내는 기능입니다. 실무에서 챗봇을 개발할 때 가장 시간이 많이 걸리는 부분이 바로 "의도 파악"과 "엔티티 추출"입니다.

예를 들어, "다음 주 화요일 오후 3시에 회의실 예약해줘"라는 문장에서 날짜, 시간, 예약 대상을 각각 추출하려면 복잡한 NLP 모델이 필요했습니다. 하지만 Function Calling을 사용하면 이 모든 과정이 자동화됩니다.

기존에는 각 파라미터마다 별도의 추출 로직을 만들고 예외 처리를 해야 했다면, 이제는 함수 스키마만 정의하면 AI가 알아서 처리합니다. 핵심 특징은 첫째, 다양한 데이터 타입을 지원합니다(문자열, 숫자, 불린, 배열, 객체).

둘째, 필수 파라미터와 선택 파라미터를 구분할 수 있습니다. 셋째, enum으로 허용 가능한 값을 제한할 수 있습니다.

이러한 특징 덕분에 정확하고 안전한 데이터 추출이 가능합니다.

코드 예제

# 여러 파라미터를 가진 복잡한 함수 정의
tools = [{
    "type": "function",
    "function": {
        "name": "book_meeting_room",
        "description": "회의실을 예약합니다",
        "parameters": {
            "type": "object",
            "properties": {
                "date": {
                    "type": "string",
                    "description": "예약 날짜 (YYYY-MM-DD 형식)"
                },
                "time": {
                    "type": "string",
                    "description": "예약 시간 (HH:MM 형식)"
                },
                "room": {
                    "type": "string",
                    "enum": ["회의실A", "회의실B", "회의실C"],
                    "description": "예약할 회의실"
                },
                "attendees": {
                    "type": "integer",
                    "description": "참석 인원"
                }
            },
            "required": ["date", "time", "room"]
        }
    }
}]

# 사용자 질문: "다음 주 화요일 오후 2시에 회의실A 예약해줘, 5명이 참석해"
# AI가 자동으로 모든 파라미터를 추출합니다

설명

이것이 하는 일은 사용자의 복잡한 문장을 분석해서 구조화된 데이터로 변환하는 것입니다. 마치 비서가 여러분의 말을 듣고 중요한 정보만 메모하는 것과 비슷합니다.

첫 번째로, properties 객체에서 각 파라미터의 타입과 설명을 정의합니다. date는 문자열 타입이고 날짜 형식을 설명에 명시했습니다.

time도 마찬가지로 시간 형식을 지정합니다. AI는 이 설명을 보고 "다음 주 화요일"을 "2024-01-16" 형식으로 변환해야 한다는 걸 이해합니다.

두 번째로, enum 속성을 주목하세요. room 파라미터는 세 가지 값만 허용됩니다.

사용자가 "큰 회의실"이라고 애매하게 말하면 AI가 "회의실A, B, C 중 어느 곳인가요?"라고 되묻거나, 문맥상 가장 적절한 것을 선택합니다. 이렇게 하면 잘못된 값이 함수에 전달되는 것을 방지할 수 있습니다.

세 번째로, required 배열을 보세요. date, time, room은 필수지만 attendees는 선택사항입니다.

만약 사용자가 인원을 말하지 않으면, AI는 이 파라미터를 생략하고 함수 호출을 요청합니다. 하지만 필수 파라미터가 빠지면 AI가 자동으로 "몇 시에 예약하시겠어요?"처럼 되묻습니다.

최종적으로, AI는 "다음 주 화요일 오후 2시에 회의실A 예약해줘, 5명이 참석해"라는 문장을 {"date": "2024-01-16", "time": "14:00", "room": "회의실A", "attendees": 5}라는 깔끔한 JSON으로 변환해줍니다. 여러분이 이 기능을 활용하면 복잡한 폼 입력을 자연어 대화로 대체할 수 있습니다.

사용자는 여러 단계의 입력 폼을 거치지 않고 한 문장으로 모든 정보를 전달할 수 있어 UX가 크게 개선됩니다. 또한 파라미터 검증 로직을 일일이 작성할 필요 없이, 스키마 정의만으로 데이터 품질을 보장받을 수 있습니다.

실전 팁

💡 파라미터 description을 상세히 작성하면 AI의 추출 정확도가 높아집니다. "날짜"보다 "예약할 날짜를 YYYY-MM-DD 형식으로, 예: 2024-01-15"처럼 예시까지 넣으면 더 좋습니다.

💡 enum을 적극 활용하세요. 정해진 선택지가 있는 경우 enum으로 제한하면 잘못된 입력을 원천 차단할 수 있습니다.

💡 필수 파라미터는 최소한으로 유지하세요. 너무 많으면 사용자가 여러 번 질문받게 되어 불편합니다. 선택 파라미터는 기본값을 함수 내부에서 처리하는 게 좋습니다.

💡 날짜/시간 파라미터는 설명에 타임존 정보도 명시하세요. "한국 시간(KST) 기준"처럼 추가하면 AI가 시간대 변환을 올바르게 처리합니다.


3. 여러 함수 동시 관리하기

시작하며

여러분의 AI 챗봇이 점점 똑똑해져서 날씨도 알려주고, 예약도 하고, 검색도 하게 되었습니다. 그런데 문제가 생깁니다.

사용자가 "날씨 좋으면 내일 회의실 예약해줘"라고 복합적인 요청을 할 때, 어떤 함수를 먼저 호출해야 할까요? 이런 상황에서 함수 간 우선순위를 정하고, 의존 관계를 파악하고, 조건부 실행을 처리하는 건 정말 복잡합니다.

코드가 스파게티처럼 엉킬 수 있죠. OpenAI Function Calling은 여러 함수를 깔끔하게 관리할 수 있는 구조를 제공합니다.

AI가 알아서 어떤 함수가 필요한지 판단하고, 때로는 여러 함수를 순차적으로 호출하기도 합니다.

개요

간단히 말해서, 여러 함수 관리는 다양한 기능을 가진 함수들을 하나의 tools 배열에 등록하고, AI가 상황에 맞게 선택하도록 하는 패턴입니다. 실무에서는 하나의 AI 애플리케이션에 수십 개의 함수가 필요할 수 있습니다.

고객 관리 시스템이라면 고객 조회, 주문 생성, 배송 추적, 환불 처리 등 다양한 기능이 있겠죠. 이 모든 함수를 효율적으로 관리하고, AI가 올바른 함수를 선택하도록 하는 게 핵심입니다.

기존에는 if-else 분기문으로 사용자 의도를 파악하고 해당 함수를 호출했다면, 이제는 모든 함수를 등록만 해두면 AI가 알아서 판단합니다. 핵심 특징은 첫째, 함수 개수에 제한이 거의 없습니다(실전에서는 20-30개까지 테스트 완료).

둘째, AI가 함수의 description을 비교해서 가장 적합한 것을 선택합니다. 셋째, 한 번의 대화에서 여러 함수를 순차적으로 호출할 수도 있습니다.

이러한 특징으로 복잡한 비즈니스 로직도 깔끔하게 구현됩니다.

코드 예제

# 다양한 기능의 함수들을 한 번에 등록
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "특정 도시의 날씨 조회",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {"type": "string"}
                },
                "required": ["location"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_product",
            "description": "제품 검색 및 재고 확인",
            "parameters": {
                "type": "object",
                "properties": {
                    "keyword": {"type": "string"},
                    "category": {"type": "string"}
                },
                "required": ["keyword"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "create_order",
            "description": "주문 생성",
            "parameters": {
                "type": "object",
                "properties": {
                    "product_id": {"type": "string"},
                    "quantity": {"type": "integer"}
                },
                "required": ["product_id", "quantity"]
            }
        }
    }
]

# AI가 대화 맥락에 따라 적절한 함수 자동 선택
response = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=[{"role": "user", "content": "노트북 검색해줘"}],
    tools=tools
)

설명

이것이 하는 일은 여러분의 모든 기능을 AI에게 소개하고, AI가 상황에 맞는 도구를 선택하도록 하는 것입니다. 마치 도구 상자에 여러 도구를 넣어두고, 필요할 때마다 적절한 것을 꺼내 쓰는 것과 같습니다.

첫 번째 단계로, tools 배열에 모든 함수를 등록합니다. 각 함수는 독립적인 JSON 객체로 정의되고, name과 description이 명확히 구분되어야 합니다.

AI는 이 description들을 비교 분석해서 가장 관련성 높은 함수를 찾습니다. 예를 들어 "노트북 검색해줘"라는 질문에는 "제품 검색"이라는 키워드가 있는 search_product가 선택될 확률이 높습니다.

두 번째로, AI의 선택 과정을 이해해봅시다. AI는 단순히 키워드 매칭만 하는 게 아닙니다.

전체 문맥을 파악합니다. 만약 사용자가 "어제 주문한 노트북 배송 어디까지 왔어?"라고 물으면, "노트북"이라는 단어가 있어도 search_product가 아니라 track_delivery 같은 함수를 선택합니다.

이게 AI의 강점입니다. 세 번째로, 함수가 실행되고 결과가 돌아오는 과정입니다.

response.choices[0].message.tool_calls를 확인하면 AI가 선택한 함수 이름과 파라미터를 볼 수 있습니다. 여러분은 이 정보를 바탕으로 실제 함수를 실행하고, 결과를 다시 AI에게 전달합니다.

AI는 그 결과를 자연스러운 문장으로 변환해서 사용자에게 답변합니다. 여러분이 이 패턴을 사용하면 기능이 추가될 때마다 단순히 tools 배열에 새 함수만 추가하면 됩니다.

기존 코드를 수정할 필요가 없어 유지보수가 매우 쉽습니다. 또한 함수 간 결합도가 낮아져서 각 함수를 독립적으로 테스트하고 개선할 수 있습니다.

실전에서는 함수별로 별도 파일로 관리하고, 자동으로 tools 배열을 구성하는 패턴도 많이 사용됩니다.

실전 팁

💡 함수 이름은 명확하고 직관적으로 지으세요. func1, func2보다 get_weather, create_order처럼 기능이 명확히 드러나는 이름이 AI의 선택 정확도를 높입니다.

💡 비슷한 기능의 함수들은 description을 더 구체적으로 구분하세요. 예를 들어 search_productsearch_order가 있다면, 하나는 "제품 카탈로그에서 검색", 다른 하나는 "과거 주문 내역에서 검색"이라고 명시하세요.

💡 함수가 10개를 넘어가면 카테고리별로 그룹화하는 걸 고려하세요. 예를 들어 "주문 관련", "배송 관련", "고객 정보 관련"처럼 나누면 관리가 쉽습니다.

💡 실전에서는 tool_choice 파라미터를 사용할 수 있습니다. tool_choice="auto"(기본값)는 AI가 선택, tool_choice="none"은 함수 사용 안 함, tool_choice={"type": "function", "function": {"name": "특정함수"}}는 특정 함수 강제 사용입니다.

💡 함수 실행 시간이 긴 경우, 사용자에게 "잠시만요, 확인 중입니다..."같은 중간 메시지를 보여주세요. AI가 함수 호출을 요청한 시점과 결과를 받는 시점 사이에 UX 개선이 필요합니다.


4. 함수 실행 결과 AI에게 전달하기

시작하며

여러분이 Function Calling을 처음 사용할 때 가장 많이 하는 실수가 있습니다. AI가 함수를 호출하라고 알려주면, 함수를 실행하고 결과를 받은 뒤...

그걸로 끝이라고 생각하는 거죠. "어?

AI가 왜 답변을 안 하지?"라고 당황하게 됩니다. 그도 그럴 것이, AI는 함수 결과를 모르는 상태니까요.

AI는 단지 "이 함수를 실행하세요"라고만 했지, 실제로 실행하거나 결과를 본 건 아닙니다. 해결책은 간단합니다.

함수 실행 결과를 AI에게 다시 전달해야 합니다. 그래야 AI가 그 데이터를 바탕으로 최종 답변을 생성합니다.

이 과정을 "function calling flow"라고 부릅니다.

개요

간단히 말해서, 함수 결과 전달은 AI가 요청한 함수를 실행한 뒤, 그 결과를 메시지 형태로 AI에게 다시 보내는 프로세스입니다. 실무에서 이 부분을 제대로 이해하지 못하면 AI가 제대로 작동하지 않습니다.

AI와 여러분의 코드는 서로 다른 세계에 있습니다. AI는 함수 실행 권한이 없고, 여러분은 AI의 언어 능력이 없습니다.

이 둘을 연결하는 게 바로 메시지 전달 메커니즘입니다. 기존에는 함수 실행 후 결과를 직접 포맷팅해서 사용자에게 보여줬다면, 이제는 AI가 중간에서 결과를 해석하고 자연스러운 문장으로 만들어줍니다.

핵심 특징은 첫째, 결과는 tool 역할의 메시지로 전달됩니다. 둘째, 각 함수 호출마다 고유한 tool_call_id가 있어서 매칭됩니다.

셋째, 결과를 전달한 후 다시 AI를 호출해야 최종 답변을 받습니다. 이 플로우를 이해하면 AI와 코드의 완벽한 협업이 가능합니다.

코드 예제

# 1단계: AI에게 질문
response = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=[{"role": "user", "content": "서울 날씨 알려줘"}],
    tools=tools
)

# 2단계: AI의 함수 호출 요청 확인
tool_call = response.choices[0].message.tool_calls[0]
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)

# 3단계: 실제 함수 실행
result = get_weather(function_args["location"])  # "서울의 날씨는 맑음, 22도"

# 4단계: 결과를 AI에게 전달 (중요!)
messages = [
    {"role": "user", "content": "서울 날씨 알려줘"},
    response.choices[0].message,  # AI의 함수 호출 요청
    {
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": result
    }
]

# 5단계: AI에게 다시 요청해서 최종 답변 받기
final_response = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=messages
)

print(final_response.choices[0].message.content)
# 출력: "서울은 현재 맑은 날씨이고 기온은 22도입니다."

설명

이것이 하는 일은 AI와 여러분의 코드 사이에 완전한 대화 루프를 만드는 것입니다. 마치 팀으로 일하는 것처럼, AI가 요청하면 여러분이 실행하고, 그 결과를 다시 AI에게 보고하는 과정입니다.

첫 번째 단계에서, 초기 AI 호출 시 response.choices[0].message.tool_calls를 확인합니다. 이게 None이 아니면 AI가 함수 호출을 요청한 것입니다.

tool_call.function.name에서 함수 이름을, tool_call.function.arguments에서 파라미터를 가져옵니다. 파라미터는 JSON 문자열로 오기 때문에 json.loads()로 파싱해야 합니다.

두 번째로, 실제 함수를 실행합니다. 이 부분은 여러분의 비즈니스 로직입니다.

데이터베이스 조회든, API 호출이든, 파일 읽기든 뭐든 할 수 있습니다. 중요한 건 결과를 문자열로 만드는 것입니다.

JSON 객체를 반환한다면 json.dumps()로 문자열화하세요. 세 번째로, 결과를 messages 배열에 추가합니다.

여기서 핵심은 대화 히스토리를 유지하는 것입니다. 원래 사용자 질문, AI의 함수 호출 요청, 함수 실행 결과를 모두 포함해야 합니다.

role: "tool"로 설정하고, tool_call_id를 정확히 매칭시켜야 AI가 어떤 함수의 결과인지 알 수 있습니다. 네 번째 단계는 이 완성된 대화 히스토리를 가지고 AI를 다시 호출하는 것입니다.

이번에는 tools 파라미터를 생략해도 됩니다(이미 함수 호출이 끝났으니까요). AI는 함수 결과를 보고 "아, 서울은 맑고 22도구나"라고 이해한 뒤, 사용자 친화적인 문장으로 변환해서 답변합니다.

여러분이 이 플로우를 마스터하면 AI가 여러 함수를 연속으로 호출하는 복잡한 시나리오도 처리할 수 있습니다. 예를 들어 "날씨 좋으면 레스토랑 예약해줘"는 날씨 조회 → 결과 확인 → 예약 함수 호출의 2단계 프로세스인데, 이 패턴을 반복하면 됩니다.

또한 에러가 발생했을 때도 에러 메시지를 content로 전달하면 AI가 "죄송합니다, 날씨 정보를 가져오는데 실패했습니다"처럼 적절히 대응합니다.

실전 팁

💡 tool_call_id를 절대 빠뜨리지 마세요. 이게 없으면 AI가 어떤 함수의 결과인지 모르고 에러가 발생합니다.

💡 함수 결과는 가능한 한 구조화된 형태로 전달하세요. 단순 문자열보다 JSON 형태가 AI가 이해하기 쉽습니다. 예: {"weather": "맑음", "temperature": 22, "humidity": 60}

💡 대화 히스토리를 계속 누적하면 토큰 사용량이 증가합니다. 실전에서는 최근 N개 메시지만 유지하거나, 요약 기능을 사용하세요.

💡 함수 실행이 오래 걸리면(5초 이상) 비동기 처리를 고려하세요. 사용자에게 "확인 중입니다" 메시지를 보여주고, 백그라운드에서 함수를 실행한 뒤 결과가 나오면 알림을 보내는 패턴도 좋습니다.

💡 디버깅 시에는 각 단계의 messages 배열을 출력해보세요. 대화 히스토리가 제대로 쌓이는지 확인하면 문제를 빠르게 찾을 수 있습니다.


5. 에러 처리와 예외 상황 관리

시작하며

여러분이 만든 AI 챗봇이 실제 서비스에 올라가면 예상치 못한 상황들이 발생합니다. 함수 실행이 실패하거나, 네트워크가 끊기거나, AI가 잘못된 함수를 선택하거나, 파라미터가 부족한 경우 등등...

초보 개발자들은 "정상 케이스"만 코딩하고 배포했다가 실제 환경에서 에러 폭탄을 맞습니다. "서버 오류"라는 메시지만 보이고 챗봇이 먹통이 되는 거죠.

프로 개발자와 초보의 차이는 바로 에러 처리입니다. Function Calling에서도 마찬가지입니다.

모든 단계에서 발생할 수 있는 예외를 처리하고, 사용자에게 친절한 메시지를 보여줘야 합니다.

개요

간단히 말해서, 에러 처리는 Function Calling 과정에서 발생할 수 있는 모든 예외 상황을 감지하고, 적절히 대응해서 사용자 경험을 유지하는 기법입니다. 실무에서 AI 애플리케이션의 안정성은 에러 처리 수준에 달려 있습니다.

외부 API는 언제든 타임아웃될 수 있고, 데이터베이스 연결이 끊길 수 있으며, AI가 예상치 못한 함수를 호출할 수도 있습니다. 이 모든 경우에 대비해야 합니다.

기존에는 에러가 나면 프로그램이 죽거나 사용자에게 기술적인 에러 메시지가 노출됐다면, 이제는 AI를 활용해 에러를 자연스럽게 설명하고 대안을 제시할 수 있습니다. 핵심 특징은 첫째, try-except로 함수 실행을 보호합니다.

둘째, 에러 발생 시에도 대화 플로우를 유지합니다. 셋째, 에러 메시지를 AI에게 전달하면 AI가 사용자 친화적으로 변환합니다.

이러한 방어적 프로그래밍으로 안정적인 서비스를 만들 수 있습니다.

코드 예제

import json
from openai import OpenAI

def safe_function_call(function_name, function_args):
    """함수 실행을 안전하게 처리"""
    try:
        if function_name == "get_weather":
            result = get_weather(function_args.get("location"))
            return {"success": True, "data": result}
        elif function_name == "search_product":
            result = search_product(function_args.get("keyword"))
            return {"success": True, "data": result}
        else:
            return {"success": False, "error": f"알 수 없는 함수: {function_name}"}
    except KeyError as e:
        return {"success": False, "error": f"필수 파라미터 누락: {e}"}
    except TimeoutError:
        return {"success": False, "error": "요청 시간 초과, 다시 시도해주세요"}
    except Exception as e:
        return {"success": False, "error": f"함수 실행 중 오류: {str(e)}"}

# 실제 사용
response = client.chat.completions.create(...)
if response.choices[0].message.tool_calls:
    tool_call = response.choices[0].message.tool_calls[0]
    function_name = tool_call.function.name
    function_args = json.loads(tool_call.function.arguments)

    # 안전하게 함수 실행
    result = safe_function_call(function_name, function_args)

    if result["success"]:
        content = result["data"]
    else:
        content = f"오류 발생: {result['error']}"

    # 결과(성공이든 실패든)를 AI에게 전달
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": content
    })

    # AI가 에러를 자연스럽게 설명
    final_response = client.chat.completions.create(
        model="gpt-4-turbo",
        messages=messages
    )

설명

이것이 하는 일은 예상 가능한 모든 실패 지점에 안전장치를 설치하는 것입니다. 마치 건물에 비상구와 소화기를 배치하는 것처럼, 문제가 생겨도 시스템이 우아하게 대응하도록 만듭니다.

첫 번째로, safe_function_call 함수는 모든 함수 실행을 감싸는 래퍼입니다. 여기서 함수 이름에 따라 적절한 함수를 호출하고, 결과를 일관된 형태로 반환합니다.

{"success": True/False} 형태로 통일하면 나중에 결과를 처리하기 쉽습니다. 성공 시에는 data 필드에 결과를, 실패 시에는 error 필드에 에러 메시지를 담습니다.

두 번째로, 다양한 예외 타입을 구분해서 처리합니다. KeyError는 필수 파라미터가 빠진 경우, TimeoutError는 네트워크 지연, 일반 Exception은 예상치 못한 모든 에러를 잡습니다.

각 에러마다 명확한 메시지를 제공하면 디버깅할 때도 유용하고, AI가 사용자에게 정확한 상황을 설명할 수 있습니다. 세 번째로, 에러가 발생해도 대화 플로우를 중단하지 않습니다.

content에 에러 메시지를 담아서 AI에게 전달합니다. AI는 "오류 발생: 요청 시간 초과"라는 기술적 메시지를 받고, "죄송합니다, 날씨 정보를 가져오는 데 시간이 너무 오래 걸렸어요.

잠시 후 다시 시도해주시겠어요?"처럼 자연스럽게 변환해서 사용자에게 전달합니다. 네 번째로, 함수 이름 검증도 중요합니다.

AI가 존재하지 않는 함수를 호출하려고 할 수도 있습니다(드물지만 가능). else 블록에서 이를 잡아내고 명확한 에러 메시지를 반환합니다.

이런 경우는 주로 함수 이름을 변경했는데 tools 정의를 업데이트 안 했을 때 발생합니다. 여러분이 이런 에러 처리를 구현하면 서비스의 신뢰성이 크게 올라갑니다.

사용자는 에러가 나도 "무슨 문제인지, 어떻게 해결할 수 있는지" 알 수 있어서 만족도가 높아집니다. 또한 로그를 잘 남기면(여기서는 생략했지만) 어떤 함수에서 에러가 자주 나는지 분석해서 시스템을 개선할 수 있습니다.

실전 팁

💡 실전에서는 에러 로깅을 꼭 추가하세요. logging.error(f"함수 실행 실패: {function_name}, {str(e)}")처럼 기록하면 나중에 문제 분석이 쉽습니다.

💡 재시도 로직을 고려하세요. 네트워크 에러 같은 일시적 문제는 3번 정도 재시도하면 성공할 수 있습니다. tenacity 라이브러리를 사용하면 쉽게 구현 가능합니다.

💡 에러 메시지에 타임스탬프를 포함하면 고객 지원 시 유용합니다. "2024-01-15 14:30:22에 오류 발생"처럼 기록하면 로그를 찾기 쉽습니다.

💡 사용자에게 에러 ID를 제공하는 것도 좋은 패턴입니다. "오류 #A3F2B1이 발생했습니다. 고객센터에 문의 시 이 번호를 알려주세요"처럼 하면 디버깅이 편합니다.

💡 Critical한 에러(결제 실패 등)는 알림을 보내세요. Slack이나 이메일로 즉시 알림을 받으면 빠르게 대응할 수 있습니다.


6. Streaming과 실시간 응답 구현

시작하며

여러분이 ChatGPT를 사용할 때 답변이 한 글자씩 타이핑되듯 나타나는 걸 보셨을 겁니다. 이게 바로 스트리밍입니다.

반대로 답변 전체가 완성될 때까지 기다렸다가 한 번에 나타나면 어떨까요? 답답하고 느리게 느껴지겠죠.

Function Calling에서도 마찬가지입니다. 함수 실행 결과를 기다리는 동안 사용자는 "뭐 하고 있는 거지?"라고 불안해합니다.

특히 함수 실행이 5초, 10초 걸리는 경우에는 심각한 UX 문제가 됩니다. 해결책은 스트리밍을 활용하는 것입니다.

AI의 응답을 실시간으로 받아서 보여주고, 함수 호출 시에는 "날씨 정보를 확인하고 있습니다..."같은 중간 상태를 표시하면 사용자 경험이 크게 개선됩니다.

개요

간단히 말해서, 스트리밍은 AI의 응답을 한 번에 받지 않고 생성되는 대로 조금씩 받아서 실시간으로 표시하는 기술입니다. 실무에서 반응성은 UX의 핵심입니다.

연구에 따르면 사용자는 1초 이상 기다리면 불안을 느끼고, 3초 이상이면 이탈을 고려한다고 합니다. Function Calling은 필연적으로 지연이 발생하는데(AI 처리 + 함수 실행 + AI 재처리), 스트리밍 없이는 답답한 경험이 됩니다.

기존에는 완성된 답변을 한 번에 받아서 보여줬다면, 이제는 생성되는 순간부터 사용자에게 보여줄 수 있습니다. 핵심 특징은 첫째, stream=True 옵션으로 활성화합니다.

둘째, 응답이 chunk 단위로 전달됩니다. 셋째, 함수 호출 요청도 스트리밍으로 감지할 수 있습니다.

이를 통해 부드럽고 빠르게 느껴지는 UX를 만들 수 있습니다.

코드 예제

from openai import OpenAI

client = OpenAI(api_key="your-api-key")

# 스트리밍 활성화
stream = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=[{"role": "user", "content": "서울 날씨 알려줘"}],
    tools=tools,
    stream=True  # 핵심!
)

# 실시간으로 응답 처리
tool_calls = []
for chunk in stream:
    delta = chunk.choices[0].delta

    # 일반 텍스트 응답
    if delta.content:
        print(delta.content, end="", flush=True)

    # 함수 호출 요청
    if delta.tool_calls:
        # 함수 호출 정보를 누적
        tool_calls.extend(delta.tool_calls)

# 함수 호출이 있으면 실행
if tool_calls:
    print("\n[날씨 정보를 확인하고 있습니다...]")

    # 함수 실행 및 결과 전달
    function_name = tool_calls[0].function.name
    function_args = json.loads(tool_calls[0].function.arguments)
    result = get_weather(function_args["location"])

    # 결과를 다시 스트리밍으로 받기
    final_stream = client.chat.completions.create(
        model="gpt-4-turbo",
        messages=[...],  # 대화 히스토리 포함
        stream=True
    )

    for chunk in final_stream:
        if chunk.choices[0].delta.content:
            print(chunk.choices[0].delta.content, end="", flush=True)

설명

이것이 하는 일은 AI의 "생각 과정"을 사용자에게 실시간으로 보여주는 것입니다. 마치 사람이 말하면서 생각을 정리하는 것처럼, AI도 답변을 생성하면서 보여줍니다.

첫 번째로, stream=True를 설정하면 응답 형태가 완전히 달라집니다. 일반 모드에서는 하나의 완성된 객체가 오지만, 스트리밍 모드에서는 chunk들의 연속입니다.

각 chunk는 한 단어, 때로는 한 글자 단위로 올 수도 있습니다. for chunk in stream 루프를 돌면서 실시간으로 처리합니다.

두 번째로, delta 객체를 주목하세요. 일반 모드에서는 message.content였지만, 스트리밍에서는 delta.content입니다.

delta는 "변화량"을 의미합니다. 이전 chunk와 비교해서 새로 추가된 부분만 담겨 있습니다.

print(delta.content, end="", flush=True)로 출력하면 타이핑 효과가 나타납니다. end=""는 줄바꿈 안 함, flush=True는 버퍼 즉시 출력을 의미합니다.

세 번째로, 함수 호출 감지입니다. delta.tool_calls가 있으면 AI가 함수를 호출하려는 것입니다.

스트리밍에서는 함수 정보도 여러 chunk에 걸쳐 올 수 있어서 tool_calls 배열에 누적합니다. 모든 chunk를 다 받은 후에 함수를 실행하고, "날씨 정보를 확인하고 있습니다..."같은 메시지를 사용자에게 보여줍니다.

네 번째로, 함수 결과를 AI에게 전달한 후 최종 답변도 스트리밍으로 받습니다. 이렇게 하면 전체 과정이 부드럽게 느껴집니다.

사용자는 "질문 → 함수 실행 중 메시지 → 답변이 타이핑되듯 나타남"을 경험하게 됩니다. 여러분이 스트리밍을 구현하면 체감 속도가 2배 이상 빨라집니다(실제 처리 시간은 같지만).

사용자는 뭔가 일어나고 있다는 걸 계속 보기 때문에 기다림이 지루하지 않습니다. 특히 긴 답변일수록 효과가 큽니다.

또한 프론트엔드에서 SSE(Server-Sent Events)나 WebSocket과 연동하면 더욱 멋진 실시간 채팅 경험을 만들 수 있습니다.

실전 팁

💡 프론트엔드에서는 EventSource(SSE)나 fetchReadableStream을 사용해서 스트리밍을 구현하세요. 백엔드에서 chunk를 보내면 실시간으로 화면에 표시됩니다.

💡 함수 실행이 긴 경우(10초 이상), 진행률 표시를 추가하세요. "재고 확인 중... 50% 완료"처럼 보여주면 사용자 안심에 도움됩니다.

💡 스트리밍 중 에러가 나면 어떻게 처리할지 고민하세요. 이미 일부가 출력된 상태에서 에러가 나면 "[오류 발생: ...]"을 이어서 출력하는 게 좋습니다.

💡 모바일에서는 스트리밍이 배터리를 더 소모할 수 있습니다. 옵션으로 스트리밍 on/off를 제공하는 것도 좋은 UX입니다.

💡 디버깅 시에는 스트리밍을 끄고 테스트하세요. 전체 응답을 한 번에 보는 게 문제 파악에 유리합니다.


7. Parallel Function Calling으로 성능 최적화

시작하며

여러분의 AI 챗봇이 "서울과 부산 날씨 비교해줘"라는 질문을 받았습니다. 가장 단순한 방법은 서울 날씨 조회 → 부산 날씨 조회를 순차적으로 하는 거겠죠.

각각 2초 걸린다면 총 4초입니다. 그런데 생각해보세요.

두 함수는 서로 관련이 없습니다. 동시에 실행해도 문제없죠.

그럼 2초로 줄일 수 있습니다! 이게 바로 병렬 처리의 힘입니다.

OpenAI는 최신 모델에서 Parallel Function Calling을 지원합니다. AI가 여러 함수를 동시에 호출해야 한다고 판단하면, 한 번에 여러 개의 함수 호출 요청을 보냅니다.

여러분은 이걸 병렬로 실행해서 시간을 크게 단축할 수 있습니다.

개요

간단히 말해서, Parallel Function Calling은 AI가 여러 개의 독립적인 함수를 동시에 호출하도록 요청하고, 개발자가 이를 병렬로 실행해서 성능을 최적화하는 기법입니다. 실무에서 성능은 곧 돈입니다.

API 호출이 많은 서비스는 응답 시간이 1초 줄어들면 서버 비용이 수백만 원 절감될 수 있습니다. 또한 사용자 만족도도 크게 올라갑니다.

"빠른 앱"이라는 인식이 생기면 재방문율이 높아집니다. 기존에는 함수를 하나씩 순차 실행했다면, 이제는 독립적인 함수들을 한 번에 실행할 수 있습니다.

핵심 특징은 첫째, AI가 tool_calls 배열에 여러 함수를 담아 보냅니다. 둘째, Python의 asyncioThreadPoolExecutor로 병렬 실행합니다.

셋째, 모든 결과를 모아서 한 번에 AI에게 전달합니다. 이를 통해 실행 시간을 획기적으로 줄일 수 있습니다.

코드 예제

import asyncio
from concurrent.futures import ThreadPoolExecutor

# 비동기 함수 정의
async def get_weather_async(location):
    # 실제로는 비동기 API 호출
    await asyncio.sleep(2)  # 2초 걸리는 작업 시뮬레이션
    return f"{location}: 맑음, 22도"

# AI가 여러 함수 호출 요청
response = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=[{"role": "user", "content": "서울, 부산, 대구 날씨 알려줘"}],
    tools=tools
)

# 여러 개의 tool_calls 확인
tool_calls = response.choices[0].message.tool_calls

# 병렬 실행
async def execute_parallel():
    tasks = []
    for tool_call in tool_calls:
        function_args = json.loads(tool_call.function.arguments)
        location = function_args["location"]
        tasks.append(get_weather_async(location))

    # 모든 작업을 동시에 실행
    results = await asyncio.gather(*tasks)
    return results

# 실행 (순차면 6초, 병렬이면 2초!)
results = asyncio.run(execute_parallel())

# 각 tool_call에 대응하는 결과를 메시지로 추가
for i, tool_call in enumerate(tool_calls):
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": results[i]
    })

# 최종 답변 받기
final_response = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=messages
)

설명

이것이 하는 일은 독립적인 작업들을 동시에 처리해서 전체 시간을 단축하는 것입니다. 마치 여러 명의 직원이 각자 다른 업무를 동시에 처리하는 것과 같습니다.

첫 번째로, AI의 응답에서 tool_calls 배열을 확인합니다. 이 배열에 여러 개의 요소가 있다면 AI가 병렬 실행을 의도한 것입니다.

예를 들어 "서울, 부산, 대구 날씨"라고 물으면 세 개의 get_weather 호출이 배열에 담겨 옵니다. 각각 다른 tool_call_id를 가지고 있어서 나중에 결과를 매칭할 수 있습니다.

두 번째로, asyncio를 사용한 병렬 실행 패턴입니다. tasks 배열에 모든 비동기 함수 호출을 담고, asyncio.gather(*tasks)로 동시 실행합니다.

gather는 모든 작업이 완료될 때까지 기다렸다가 결과를 리스트로 반환합니다. 순서는 tasks에 추가한 순서와 동일하게 유지됩니다.

3개 작업이 각각 2초 걸린다면, 순차는 6초지만 병렬은 2초입니다! 세 번째로, 결과를 메시지에 추가하는 부분입니다.

for i, tool_call in enumerate(tool_calls) 루프에서 각 함수 호출에 대응하는 결과를 찾습니다. tool_call_id를 정확히 매칭시켜야 AI가 "첫 번째 결과는 서울, 두 번째는 부산"이라고 이해합니다.

순서가 섞여도 ID로 매칭되니 안전합니다. 네 번째로, 동기 함수만 있다면 ThreadPoolExecutor를 사용할 수도 있습니다.

with ThreadPoolExecutor() as executor: futures = [executor.submit(func, arg) for ...] 패턴입니다. 하지만 asyncio가 더 효율적이고 현대적인 방식입니다.

여러분이 병렬 처리를 도입하면 성능이 극적으로 개선됩니다. 특히 외부 API 호출이 많은 경우(날씨, 주식, 뉴스 등) 효과가 큽니다.

또한 데이터베이스 쿼리 여러 개를 동시에 실행하는 경우도 병렬 처리로 속도를 높일 수 있습니다. 단, CPU 집약적 작업(복잡한 계산)은 GIL 때문에 병렬 효과가 적으니 주의하세요.

실전 팁

💡 모든 함수를 비동기로 만들 필요는 없습니다. 네트워크 I/O가 있는 함수만 async로 만들어도 충분한 효과를 볼 수 있습니다.

💡 너무 많은 작업을 동시에 실행하면 오히려 느려질 수 있습니다. API 서버가 동시 요청을 제한하거나, 네트워크 대역폭이 부족할 수 있으니 10-20개 정도로 제한하세요.

💡 에러 처리도 병렬로 해야 합니다. asyncio.gather(*tasks, return_exceptions=True)를 사용하면 일부 작업이 실패해도 나머지는 계속 실행됩니다.

💡 실행 시간을 측정해서 로그를 남기세요. import time; start = time.time(); ... ; print(f"실행 시간: {time.time()-start}초")로 병렬 처리 효과를 확인할 수 있습니다.

💡 비동기 프로그래밍이 처음이라면 asyncio 공식 문서를 꼭 읽어보세요. Event Loop, Task, Coroutine 개념을 이해하면 고급 최적화가 가능합니다.


8. 함수 결과 캐싱으로 비용 절감

시작하며

여러분의 AI 챗봇에 "서울 날씨"를 물어보는 사용자가 많습니다. 그런데 매번 날씨 API를 호출하면 어떻게 될까요?

API 비용이 폭탄처럼 나옵니다. 게다가 1분 전에 조회한 날씨와 지금 조회한 날씨가 똑같을 텐데 말이죠.

더 심각한 건 AI API 비용입니다. OpenAI는 토큰 단위로 과금하는데, 같은 질문-답변을 반복하면 돈이 계속 나갑니다.

특히 Function Calling은 일반 대화보다 토큰을 더 많이 소모합니다(함수 정의, 파라미터, 결과 등). 해결책은 캐싱입니다.

한 번 조회한 결과를 일정 시간 동안 저장해두고, 같은 요청이 오면 저장된 결과를 재사용합니다. 비용도 절감하고 응답 속도도 빨라지는 일석이조입니다.

개요

간단히 말해서, 함수 결과 캐싱은 자주 호출되는 함수의 결과를 메모리나 Redis에 저장해두고, 같은 요청이 오면 함수를 다시 실행하지 않고 저장된 값을 반환하는 기법입니다. 실무에서 캐싱은 필수입니다.

트래픽이 많은 서비스일수록 캐싱 전략이 중요합니다. 날씨 정보는 10분간 유효하다고 가정하면, 같은 10분 안에 100명이 물어봐도 API는 1번만 호출하면 됩니다.

비용이 1/100로 줄어드는 거죠. 기존에는 매번 함수를 실행해서 최신 데이터를 가져왔다면, 이제는 "충분히 최신"인 캐시 데이터를 활용합니다.

핵심 특징은 첫째, TTL(Time To Live)로 캐시 유효 시간을 설정합니다. 둘째, 함수 이름과 파라미터를 조합해서 캐시 키를 만듭니다.

셋째, 캐시 미스(없음)일 때만 실제 함수를 실행합니다. 이를 통해 비용과 속도 모두 최적화할 수 있습니다.

코드 예제

import time
from functools import lru_cache
import hashlib
import json

# 간단한 인메모리 캐시
cache_store = {}

def cached_function_call(function_name, function_args, ttl=600):
    """함수 결과를 TTL 동안 캐싱"""
    # 캐시 키 생성 (함수명 + 파라미터)
    cache_key = f"{function_name}:{json.dumps(function_args, sort_keys=True)}"

    # 캐시 확인
    if cache_key in cache_store:
        cached_data, cached_time = cache_store[cache_key]
        # TTL 체크
        if time.time() - cached_time < ttl:
            print(f"[캐시 히트] {cache_key}")
            return cached_data

    # 캐시 미스 - 실제 함수 실행
    print(f"[캐시 미스] {cache_key} - 함수 실행")
    if function_name == "get_weather":
        result = get_weather(function_args["location"])
    elif function_name == "search_product":
        result = search_product(function_args["keyword"])
    else:
        result = None

    # 결과를 캐시에 저장
    cache_store[cache_key] = (result, time.time())
    return result

# 사용 예시
# 첫 번째 호출 - API 호출
result1 = cached_function_call("get_weather", {"location": "서울"}, ttl=300)

# 5분 내 같은 호출 - 캐시 사용 (빠르고 비용 0)
result2 = cached_function_call("get_weather", {"location": "서울"}, ttl=300)

# 다른 파라미터 - 새로 호출
result3 = cached_function_call("get_weather", {"location": "부산"}, ttl=300)

# Redis를 사용한 분산 캐싱 (프로덕션 환경)
# import redis
# redis_client = redis.Redis(host='localhost', port=6379, db=0)
# redis_client.setex(cache_key, ttl, json.dumps(result))
# cached = redis_client.get(cache_key)

설명

이것이 하는 일은 "똑같은 일을 반복하지 않기"입니다. 마치 도서관에서 한 번 찾은 책의 위치를 메모해두는 것처럼, 이미 계산한 결과를 저장해두고 재사용합니다.

첫 번째로, 캐시 키 생성 로직을 봅시다. function_namefunction_args를 조합해서 고유한 키를 만듭니다.

json.dumps(..., sort_keys=True)를 사용하는 이유는 {"a": 1, "b": 2}{"b": 2, "a": 1}이 같은 내용이지만 순서가 다를 수 있기 때문입니다. sort_keys=True로 순서를 통일하면 같은 키가 생성됩니다.

두 번째로, 캐시 확인 로직입니다. cache_keycache_store에 있는지 확인하고, 있다면 저장 시간을 체크합니다.

time.time() - cached_time < ttl이 True면 아직 유효한 캐시입니다. 이 경우 저장된 결과를 즉시 반환하고 함수는 실행하지 않습니다.

사용자는 빠른 응답을 받고, 여러분은 비용을 절약합니다. 세 번째로, 캐시 미스일 때의 처리입니다.

캐시에 없거나 TTL이 만료됐으면 실제 함수를 실행합니다. 결과를 받은 후 cache_store[cache_key] = (result, time.time())으로 저장합니다.

결과와 함께 현재 시간을 저장해야 나중에 TTL 체크가 가능합니다. 네 번째로, 프로덕션 환경에서는 Redis 같은 분산 캐시를 사용합니다.

인메모리 캐시(cache_store 딕셔너리)는 서버가 재시작되면 사라지고, 여러 서버가 있으면 각자 독립적인 캐시를 가져서 효율이 떨어집니다. Redis는 모든 서버가 공유하는 캐시 저장소 역할을 합니다.

redis_client.setex(key, ttl, value)로 저장하면 TTL이 자동으로 관리됩니다. 여러분이 캐싱을 도입하면 비용이 50-90% 절감될 수 있습니다(트래픽 패턴에 따라 다름).

특히 인기 있는 쿼리(서울 날씨, 베스트셀러 제품 등)는 캐시 히트율이 높아서 효과가 큽니다. 또한 응답 속도가 API 호출 시간(2-3초)에서 캐시 조회 시간(0.01초)으로 줄어들어 UX도 크게 개선됩니다.

실전 팁

💡 TTL은 데이터 특성에 맞게 설정하세요. 날씨는 10-30분, 재고는 1-5분, 사용자 프로필은 1시간 정도가 적당합니다. 너무 길면 오래된 데이터, 너무 짧으면 캐싱 효과가 없습니다.

💡 캐시 무효화(Invalidation) 전략도 중요합니다. 데이터가 변경되면 관련 캐시를 즉시 삭제해야 합니다. 예: 제품 가격 변경 → 해당 제품 캐시 삭제.

💡 캐시 메모리 사용량을 모니터링하세요. 캐시가 무한정 쌓이면 메모리 부족 문제가 생깁니다. LRU(Least Recently Used) 정책으로 오래된 캐시를 자동 삭제하세요.

💡 민감한 데이터(개인정보, 결제 정보)는 캐싱하지 마세요. 보안 위험이 있고, 사용자마다 다른 데이터라 캐시 효율도 낮습니다.

💡 캐시 히트율을 로그로 기록하세요. "전체 요청 중 몇 %가 캐시로 처리됐는지" 알면 캐싱 전략을 개선할 수 있습니다.


9. Tools API를 활용한 고급 기능 구현

시작하며

여러분이 지금까지 본 Function Calling은 OpenAI의 기본 기능입니다. 하지만 실제 프로덕션 환경에서는 더 복잡한 요구사항이 있습니다.

"특정 상황에서만 함수 사용", "함수 실행 권한 체크", "함수 실행 이력 추적" 등등... OpenAI의 Tools API는 Function Calling을 더 강력하게 만들어주는 고급 기능들을 제공합니다.

단순히 함수를 호출하는 걸 넘어서, 복잡한 워크플로우를 구성하고 제어할 수 있습니다. 예를 들어, 고객 등급에 따라 사용 가능한 함수를 다르게 하거나, 함수 실행 전에 승인을 받거나, 여러 함수를 체인처럼 연결하는 등의 고급 패턴을 구현할 수 있습니다.

개요

간단히 말해서, Tools API는 Function Calling의 확장 버전으로, 함수 실행을 더 세밀하게 제어하고 복잡한 비즈니스 로직을 구현할 수 있게 해주는 API입니다. 실무에서는 단순 함수 호출로는 부족한 경우가 많습니다.

예를 들어 금융 앱에서 "100만원 이체해줘"라는 요청을 받으면, 단순히 이체 함수를 호출하면 안 됩니다. 잔액 확인, 본인 인증, 이체 한도 체크, 승인 프로세스 등 여러 단계가 필요합니다.

기존 Function Calling은 "함수 실행"에만 집중했다면, Tools API는 "워크플로우 관리"까지 가능합니다. 핵심 특징은 첫째, tool_choice 파라미터로 함수 사용을 강제하거나 금지할 수 있습니다.

둘째, 함수 실행 전후에 커스텀 로직을 삽입할 수 있습니다. 셋째, 여러 도구(함수, 코드 실행, 검색 등)를 조합할 수 있습니다.

이를 통해 엔터프라이즈급 AI 애플리케이션을 만들 수 있습니다.

코드 예제

from openai import OpenAI

client = OpenAI(api_key="your-api-key")

# 고급 도구 정의 (권한 레벨 포함)
tools = [
    {
        "type": "function",
        "function": {
            "name": "view_balance",
            "description": "계좌 잔액 조회 (모든 사용자 가능)",
            "parameters": {"type": "object", "properties": {}}
        }
    },
    {
        "type": "function",
        "function": {
            "name": "transfer_money",
            "description": "송금 실행 (VIP 사용자만 가능)",
            "parameters": {
                "type": "object",
                "properties": {
                    "amount": {"type": "integer"},
                    "recipient": {"type": "string"}
                },
                "required": ["amount", "recipient"]
            }
        }
    }
]

# 사용자 권한에 따른 도구 필터링
def get_allowed_tools(user_level):
    if user_level == "VIP":
        return tools  # 모든 도구 사용 가능
    else:
        # 일반 사용자는 조회만 가능
        return [t for t in tools if "view" in t["function"]["name"]]

# 함수 실행 전 검증
def validate_and_execute(function_name, function_args, user):
    # 권한 체크
    if function_name == "transfer_money" and user["level"] != "VIP":
        return {"error": "VIP 사용자만 송금할 수 있습니다"}

    # 금액 한도 체크
    if function_name == "transfer_money":
        if function_args["amount"] > 1000000:
            return {"error": "1회 송금 한도는 100만원입니다"}

    # 실행 이력 기록
    log_function_call(user["id"], function_name, function_args)

    # 실제 함수 실행
    if function_name == "view_balance":
        return view_balance(user["account"])
    elif function_name == "transfer_money":
        return transfer_money(user["account"], **function_args)

# tool_choice로 함수 사용 제어
response = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=[{"role": "user", "content": "잔액 알려줘"}],
    tools=get_allowed_tools(user_level="BASIC"),
    tool_choice="auto"  # auto(자동), none(사용안함), required(필수)
)

설명

이것이 하는 일은 단순한 함수 호출을 비즈니스 프로세스로 확장하는 것입니다. 마치 회사에서 결재 시스템을 거치는 것처럼, 함수 실행에도 규칙과 절차를 적용합니다.

첫 번째로, 권한 기반 도구 필터링을 봅시다. get_allowed_tools 함수는 사용자 레벨에 따라 다른 도구 목록을 반환합니다.

VIP는 모든 함수를, 일반 사용자는 조회 함수만 사용할 수 있습니다. AI는 제공된 도구만 선택할 수 있으므로, 애초에 일반 사용자에게는 송금 함수가 보이지 않습니다.

이게 가장 안전한 방법입니다. 두 번째로, validate_and_execute에서 다층 검증을 수행합니다.

첫 번째 검증은 권한 체크입니다. 만약 일반 사용자가 어떻게든 송금 함수를 호출하려 하면(실수 또는 해킹 시도) 즉시 차단됩니다.

두 번째 검증은 비즈니스 규칙입니다. 100만원 이상 송금은 금지됩니다.

이런 규칙들은 함수 외부에서 관리하는 게 좋습니다(함수 내부에 넣으면 나중에 수정이 어려움). 세 번째로, 실행 이력 기록(log_function_call)을 주목하세요.

누가, 언제, 어떤 함수를, 어떤 파라미터로 실행했는지 로그를 남깁니다. 이건 보안, 감사, 디버깅 모두에 중요합니다.

특히 금융이나 의료 같은 규제 산업에서는 필수입니다. 나중에 "왜 이 송금이 실행됐는지" 추적할 수 있어야 합니다.

네 번째로, tool_choice 파라미터의 활용입니다. "auto"는 AI가 판단, "none"은 함수를 절대 사용하지 않음(일반 대화만), "required"는 반드시 함수를 사용해야 함(대화만으로 답변 금지), {"type": "function", "function": {"name": "특정함수"}}는 특정 함수 강제 사용입니다.

상황에 따라 적절히 선택하면 AI 동작을 정밀하게 제어할 수 있습니다. 여러분이 이런 고급 패턴을 사용하면 프로덕션급 AI 서비스를 만들 수 있습니다.

단순히 "작동하는" 수준을 넘어서 "안전하고 신뢰할 수 있는" 시스템을 구축할 수 있습니다. 특히 돈이나 개인정보를 다루는 서비스에서는 이런 검증 로직이 필수입니다.

한 번 설정해두면 모든 함수 호출에 자동으로 적용되므로 유지보수도 쉽습니다.

실전 팁

💡 권한 체크는 "화이트리스트" 방식으로 하세요. "이건 금지"보다 "이것만 허용"이 더 안전합니다. 새 함수가 추가되면 기본적으로 차단되고, 명시적으로 허용해야 사용 가능하게 만드세요.

💡 민감한 작업(송금, 삭제 등)은 2단계 인증을 추가하세요. AI가 함수를 호출하려 하면 사용자에게 "정말 100만원을 송금하시겠습니까? [확인]/[취소]" 같은 확인 메시지를 보여주세요.

💡 함수 실행 이력은 별도 데이터베이스 테이블로 관리하고, 주기적으로 분석하세요. "어떤 함수가 자주 실패하는지", "어떤 사용자가 많이 사용하는지" 파악하면 서비스 개선 인사이트를 얻을 수 있습니다.

💡 Rate Limiting을 구현하세요. 한 사용자가 1분에 100번 함수를 호출하면 뭔가 잘못된 것입니다. Redis로 간단히 구현할 수 있습니다.

💡 tool_choice="required"는 조심해서 사용하세요. AI가 적절한 함수를 못 찾으면 에러가 나므로, 함수 목록이 명확한 상황에서만 사용하세요.


10. 실전 프로젝트 패턴과 아키텍처

시작하며

여러분이 지금까지 배운 Function Calling 기법들을 실제 프로젝트에 어떻게 적용할까요? 코드 몇 줄로 데모는 만들 수 있지만, 실제 서비스는 완전히 다른 레벨입니다.

유지보수 가능한 코드 구조, 확장성, 테스트 용이성, 모니터링, 에러 추적... 이런 것들을 고려하지 않으면 나중에 코드가 스파게티가 되어 손댈 수 없게 됩니다.

이 섹션에서는 실전에서 검증된 아키텍처 패턴을 소개합니다. 함수를 모듈로 분리하고, 설정을 외부화하고, 로깅과 모니터링을 추가하는 등 프로다운 구조를 만드는 방법을 배웁니다.

개요

간단히 말해서, 실전 프로젝트 패턴은 Function Calling 코드를 유지보수 가능하고 확장 가능한 구조로 만들기 위한 아키텍처 설계 원칙과 코드 구성 방법입니다. 실무에서는 혼자 개발하더라도 6개월 후의 자신은 "다른 사람"입니다.

코드를 읽고 이해하기 쉽게 만들지 않으면, 나중에 기능 추가하거나 버그 수정할 때 엄청난 시간이 낭비됩니다. 팀으로 개발한다면 더욱 중요합니다.

기존에는 모든 코드를 한 파일에 넣었다면, 이제는 역할별로 분리하고 각 부분이 독립적으로 테스트 가능하게 만듭니다. 핵심 특징은 첫째, 함수를 별도 모듈로 분리합니다.

둘째, 도구 정의를 JSON 파일로 관리합니다. 셋째, 환경 변수로 설정을 분리합니다.

이를 통해 프로페셔널한 코드베이스를 만들 수 있습니다.

코드 예제

# 프로젝트 구조
# project/
# ├── main.py              # 메인 애플리케이션
# ├── functions/           # 함수 모듈
# │   ├── weather.py
# │   ├── search.py
# │   └── order.py
# ├── tools/               # 도구 정의
# │   └── tools.json
# ├── config.py            # 설정
# └── utils/               # 유틸리티
#     ├── logger.py
#     └── cache.py

# functions/weather.py
import requests

def get_weather(location: str) -> dict:
    """날씨 정보를 가져옵니다"""
    try:
        # 실제 API 호출
        response = requests.get(f"https://api.weather.com/{location}")
        return {"success": True, "data": response.json()}
    except Exception as e:
        return {"success": False, "error": str(e)}

# tools/tools.json
# {
#   "weather": {
#     "name": "get_weather",
#     "description": "특정 지역의 날씨 정보를 조회합니다",
#     "parameters": { ... }
#   },
#   "search": { ... }
# }

# main.py
import json
import os
from openai import OpenAI
from functions import weather, search, order
from utils.logger import log_function_call
from utils.cache import cached_function_call

# 도구 정의 자동 로드
def load_tools():
    with open("tools/tools.json", "r") as f:
        tools_config = json.load(f)
    return [{"type": "function", "function": v}
            for v in tools_config.values()]

# 함수 라우터 (함수명 -> 실제 함수 매핑)
FUNCTION_MAP = {
    "get_weather": weather.get_weather,
    "search_product": search.search_product,
    "create_order": order.create_order
}

def execute_function(name, args):
    """함수 실행 + 로깅 + 캐싱"""
    log_function_call(name, args)

    # 캐싱 적용 (설정에서 TTL 가져옴)
    result = cached_function_call(name, args, ttl=os.getenv("CACHE_TTL", 300))

    return result

# 메인 로직
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
tools = load_tools()

response = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=[{"role": "user", "content": "서울 날씨"}],
    tools=tools
)

설명

이것이 하는 일은 코드를 "프로덕션급"으로 만드는 것입니다. 마치 프로토타입 제품을 실제 양산 제품으로 발전시키는 과정과 같습니다.

첫 번째로, 디렉토리 구조를 봅시다. functions/ 폴더에 각 기능별로 파일을 분리했습니다.

weather.py, search.py, order.py처럼 도메인별로 나누면 나중에 어디를 수정해야 할지 쉽게 찾을 수 있습니다. 각 파일은 독립적으로 테스트 가능하고, 다른 프로젝트에서 재사용도 가능합니다.

팀원이 동시에 다른 함수를 개발할 때도 충돌이 없습니다. 두 번째로, tools/tools.json에 도구 정의를 외부화했습니다.

코드에 하드코딩하지 않고 JSON 파일로 관리하면 장점이 많습니다. 첫째, 비개발자(기획자, PM)도 수정 가능합니다.

둘째, 환경별로 다른 도구를 사용할 수 있습니다(개발/스테이징/프로덕션). 셋째, Git에서 변경 이력 추적이 쉽습니다.

load_tools() 함수가 자동으로 JSON을 파싱해서 OpenAI 형식으로 변환합니다. 세 번째로, FUNCTION_MAP 딕셔너리는 함수 이름(문자열)과 실제 함수 객체를 매핑합니다.

AI가 "get_weather"를 호출하라고 하면, FUNCTION_MAP["get_weather"]로 실제 함수를 찾아서 실행합니다. 새 함수를 추가할 때도 이 딕셔너리에 한 줄만 추가하면 되므로 확장이 쉽습니다.

네 번째로, execute_function은 모든 부가 기능을 담당하는 래퍼입니다. 로깅, 캐싱, 에러 처리 등을 여기서 통합 관리합니다.

각 함수를 직접 호출하지 않고 이 래퍼를 통하면, 나중에 "모든 함수 호출에 인증 체크를 추가하고 싶다"는 요구사항이 와도 한 곳만 수정하면 됩니다. 다섯 번째로, 환경 변수(os.getenv)로 설정을 분리했습니다.

API 키, 캐시 TTL 등을 코드에 직접 넣지 않고 .env 파일이나 환경 변수로 관리하면 보안도 좋고 환경별 설정 변경도 쉽습니다. 개발 환경에서는 캐시 TTL을 10초, 프로덕션에서는 600초로 설정하는 식입니다.

여러분이 이런 구조로 프로젝트를 시작하면 처음엔 복잡해 보여도, 기능이 추가되고 팀이 커질수록 가치를 실감합니다. 버그가 나면 어디를 봐야 할지 명확하고, 새 팀원이 와도 코드 구조를 쉽게 이해할 수 있습니다.

또한 단위 테스트 작성이 쉬워져서 코드 품질이 자연스럽게 올라갑니다.

실전 팁

💡 함수마다 Type Hint를 추가하세요. def get_weather(location: str) -> dict: 처럼 하면 IDE가 자동완성을 도와주고 버그를 사전에 잡아줍니다.

💡 각 함수에 Docstring을 작성하세요. """날씨 정보를 가져옵니다. Args: location (str): 도시명. Returns: dict: 날씨 정보"""처럼 하면 나중에 문서 자동 생성도 가능합니다.

💡 pytest로 단위 테스트를 작성하세요. tests/test_weather.py 파일을 만들고 각 함수를 테스트하면 리팩토링할 때 안심하고 수정할 수 있습니다.

💡 CI/CD 파이프라인을 구축하세요. GitHub Actions나 GitLab CI로 코드 푸시할 때마다 자동으로 테스트 실행, 린팅, 배포까지 자동화할 수 있습니다.

💡 로깅은 레벨(DEBUG, INFO, WARNING, ERROR)을 구분해서 사용하세요. 개발 환경에서는 DEBUG 레벨로 모든 걸 보고, 프로덕션에서는 WARNING 이상만 보면 로그가 깔끔합니다.


#OpenAI#FunctionCalling#Tools#API#ChatGPT#AI

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.