Published on

OpenAI의 함수 호출(Function Calling) 소개 및 Python 예시

Authors
  • 테크버킷
    Name
    테크버킷
    Twitter

요약

OpenAI에서 함수 호출(Function Calling)기능을 발표했습니다. ChatGPT에서 플러그인을 사용하는 것과 비슷해 보이지만 차이가 있습니다.

이 기능의 핵심은 사용자가 정의한 함수를 GPT가 호출하도록 한다는 것인데, 사용하는 방식이 조금 생소하지만 python 예시를 통해 이해를 해보겠습니다.

사용하는 기술

  • OpenAI API
    • chat completion
    • function calling
  • python

Function Calling의 동작 원리

OpenAI의 Function Calling을 한마디로 하면, GPT에게 함수를 미리 알려주면 필요할 때 함수를 GPT가 부른다는 것입니다. 이로써 OpenAI API를 사용할 때, 사용자가 가지고 있는 함수를 활용할 수 있도록 해줍니다.

여기서 이해해야 할 포인트는 GPT가 함수를 실행하지는 않는다는 것입니다. GPT는 함수를 실행하지 않고 함수를 실행할 때 필요한 인자를 알려주는 것입니다. 실제 실행은 사용자가 직접 하고, 결과를 GPT에게 다시 알려줘야 합니다. 그러면 GPT는 결과를 활용한 응답을 다시 사용자에게 알려줍니다.

"GPT기 알아서 다 해주면 좋을텐데, 실행할 함수도 사용자가 알려주고 실행도 사용자가 직접하면 이게 무슨 의미인가요?" 라는 의문이 들 수도 있습니다. 하지만, 기능은 일반 사용자가 실행한다기보다는 OpenAI API를 활용한 어플리케이션을 만들 때에 주로 사용하는 기능으로 이해할 수 있습니다.

이해를 돕기위해 GPT의 작동 상황을 상황극으로 만들어 보았습니다.

  사용자:
    GPT야, `get_price`라는 함수를 알려줄게.
    이 함수는 물건 가격을 알려주는 함수야.
    이 함수가 필요하면 물건이름을 이야기 하도록해.
  GPT: 네 알겠습니다.
  사용자: GPT야. 사과 10개를 사려면 얼마야?
  GPT: 함수 `get_price`를 실행해주세요. 물건이름은 "사과"입니다.
  사용자: 함수 실행결과. "사과는 1000원"
  GPT: 사과 1개는 1000원이니까 10개 사면 10,000원입니다.

위의 상황극을 보면 GPT는 함수를 실행하지 않고, 함수가 필요할 때 필요한 인자를 알려주는 것을 볼 수 있습니다. 실제 실행은 사용자가 직접 하고, 결과를 GPT에게 다시 알려줘야 합니다. 그러면 GPT는 결과를 활용한 응답을 다시 사용자에게 알려줍니다.

Q.어떤 함수를 사용할 수 있나요?

어떤 함수를 사용하는지에는 제한이 없습니다. 숫자를 연산하는 함수, 문자열을 처리하는 함수, 파일을 읽고 쓰는 함수, 웹페이지를 읽어오거나 api를 사용하는 함수 등 어떤 함수든 사용할 수 있으며 프로그래밍 언어도 제한이 없습니다. 왜냐하면 실행은 사용자가 하고 결과만 알려주면 되기 때문입니다. 좀 더 극단적으로 말하자면 OpenAI에서는 함수가 어떤식으로 실행되었는지는 알 수 없습니다. 그저 함수를 실행한 결과를 알려주기만 하면 됩니다.

Python 코드 예시

function 기능을 사용한다면 최소 2번의 OpenAI api 요청을 해야 합니다. 왜 그런지는 예시를 통해 좀 더 알아보겠습니다. 이번 예시에서는 OpenAI의 gpt-3.5-turbo-0613 모델을 사용합니다.

1. 라이브러리 및 API 키 설정.

OpenAI 라이브러리와 JSON 모듈을 가져옵니다. 그리고 OpenAI API 키를 openai.api_key에 지정합니다. API 키는 platform.openai.com에서 발급받을 수 있습니다. (사용량에 따라 요금이 부과될 수 있습니다.)

import openai
import json

openai.api_key = 'YOUR_API_KEY'

2. 사용할 함수 정의

GPT에게 알려줄 함수를 정의합니다. 이번 예제에서는 입력된 위치의 현재 날씨 정보를 반환하는 get_current_weather 함수를 정의합니다.

실제로는 백엔드 API나 외부 API를 호출하도록 작성할 수도 있지만, 일단은 mock data를 반환하도록 작성합니다. (OpenAI에서는 이 함수가 어떻게 실행되었는지는 알 수 없기 때문에 가짜 함수를 사용해도 작동을 합니다.)

def get_current_weather(location, unit="섭씨"):
    weather_info = {
        "location": location,
        "temperature": "24",
        "unit": unit,
        "forecast": ["sunny", "windy"],
    }
    return json.dumps(weather_info)

3. OPENAI API를 사용하기 위한 데이터를 정의합니다.

messagesfunctions 를 정의합니다.

3-1. messages

message에는 사용자의 질문을 포함한 대화 기록을 작성합니다. 이 부분의 자세한 설명은 이전에 작성한 ChatGPT API 가이드를 참고하시기 바랍니다. messages는 대화 내용을 저장하는 변수로, 초기에는 사용자의 질문을 담고 있습니다.

messages = [{"role": "user", "content": "오늘 서울 날씨 어때??"}]

3-2. functions

functions는 사용 가능한 함수의 목록을 정의하는 부분입니다. 현재는 get_current_weather 함수 하나만 정의되어 있습니다.

functions를 정의하는 방법은 아래와 같이 함수의 이름(name)과 설명(description), parameter를 포함하여 작성해야 합니다. 필수적으로 필요한 파라미터는 required에 명시합니다. description은 사람이 이해할 수 있는 문장으로 작성하고, properties required JSON 형식으로 작성합니다.

아래 예시에서는 get_current_weather 함수에 locationunit이라는 두 개의 인자가 필요하다고 정의하고 있습니다. location은 필수적으로 필요한 인자이므로 required에 명시하였습니다. 또한 unit섭씨화씨 두 가지 중 하나의 값을 가질 수 있도록 정의되어있다는 것을 GPT에게 알려주고 있습니다.

functions = [
    {
        "name": "get_current_weather",
        "description": "주어진 지역의 현재 날씨를 알려줍니다.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "지역, e.g. 서울, 부산, 제주도",
                },
                "unit": {"type": "string", "enum": ["섭씨", "화씨"]},
            },
            "required": ["location"],
        },
    }
]

4. OpenAI의 GPT 모델에 대화와 함수 정보를 전달하고 응답을 받습니다.

OpenAI의 Chat Completion 인터페이스를 사용하여 OPENAI API에 대화와 함수 정보를 전달하고 응답을 확인합니다.

# OpenAI API에 대화와 함수 정보를 전달하고 응답을 확인합니다.
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=messages,
    functions=functions,
    function_call="auto",
)

# 우리가 관심있는 부분은 choices의 message이지만, 지금은 response전체를 출력해서 확인하도록 합니다.

# response_message = response["choices"][0]["message"]
# print(response["choices"][0]["message"])

# response 출력
print("첫번재 응답")
print(response)

아래는 첫번째로 받은 응답입니다. choices의 message를 보면 function_call이라는 항목이 있는데 이부분이 바로 GPT가 함수를 실행해 달라고 하는 부분입니다.

함수의 이름과 함수에 입력할 값을 name과 arguments로 알려주는 것을 볼 수 있습니다. 이 응답에서는 "get_current_weather"라는 함수에 {"location": "서울"} 이라는 인자를 가지고 실행해 달라고 합니다.

첫번재
{
  "id": "chatcmpl-XXXXXXXXXXXXXXX",
  "object": "chat.completion",
  "created": 1687397986,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "function_call": {
          "name": "get_current_weather",
          "arguments": "{\n  \"location\": \"서울\",\n  \"unit\": \"섭씨\"\n}"
        }
      },
      "finish_reason": "function_call"
    }
  ],
  "usage": {
    "prompt_tokens": 82,
    "completion_tokens": 18,
    "total_tokens": 100
  }
}

5. GPT가 요청한 함수를 실행합니다.

GPT가 요청한대로, get_current_weather를 실행합니다. get_current_weather({location: "서울"})를 실행하면 아래와 같은 결과를 얻을 수 있을 것입니다.

함수
{
  "location": "서울",
  "temperature": "24"
  "unit": "섭씨",
  "forecast": [
    "sunny",
    "windy"
  ]
}

6. 두번째 요청: GPT 모델에 함수를 실행한 결과를 전달합니다.

OPENAI API에 두번째 요청을 보냅니다. 이번에는 함수를 실행해달라고 한대로 실행한 결과를 GPT에게 알려주어야 합니다.

이전의 응답에 함수 응답 정보를 합칩니다. messages에 이전의 대화 정보가 있으니 이 곳에 GPT의 응답과 함수를 실행한 결과를 추가합니다.

그 다음, ChatCompletion 인터페이스를 사용하여 새로운 messages를 포함하여 GPT 모델에 전달합니다. 새로운 응답을합니다.


## 함수 이름과 실행 결과
function_name="get_current_weather"
function_response = {
  "location": "서울",
  "temperature": "24"
  "unit": "섭씨",
  "forecast": [
    "sunny",
    "windy"
  ]
}

messages.append(response_message) # messages에 GPT의 첫번째 응답을 추가합니다.
messages.append( # messages에 함수의 실행결과를 추가합니다.
    {
        "role": "function",
        "name": function_name, # 함수 이름
        "content": function_response, # 함수 실행 결과
    }
)

# 두번째 OPENAI API 요청
second_response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=messages,
)

# 응답을 출력
print(second_response)

응답을 확인하면 아래와 같습니다.

second_response
{
  "id": "chatcmpl-XXXXXXXXXXXXXXX",
  "object": "chat.completion",
  "created": 1687399211,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "지금 서울의 날씨는 섭씨 24도이며, 맑은 날씨이고 바람이 조금 있습니다."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 263,
    "completion_tokens": 45,
    "total_tokens": 308
  }
}

7. GPT 모델의 응답에서 함수 호출 여부를 확인

지금은 함수를 호촐하는 질문을 의도적으로 했지만 함수를 실행할 필요가 없을 수도 있습니다. 함수 호출이 필요한지 확인하기 위해서는 GPT 모델의 응답에 function_call이 존재하는지 확인합니다. 존재한다면, 함수 호출이 요청된 것이므로 해당 함수를 실행합니다.

이 예제에서는 function_call이 존재하므로 get_current_weather 함수가 호출되었죠.

함수를 사용하는 경우라면, 함수 호출에 필요한 인수와 결과를 argumentsfunction_response에 저장하도록 변수를 정의하였습니다.

    if response_message.get("function_call"):
    available_functions = {
        "get_current_weather": get_current_weather,
    }
    function_name = response_message["function_call"]["name"]
    function_to_call = available_functions[function_name]
    function_args = json.loads(response_message["function_call"]["arguments"])
    function_response = function_to_call(
        location=function_args.get("location"),
        unit=function_args.get("unit"),
    )

[팁] 한글 표시

"\uc9c0\uae08 \uc11c\uc6b8\uc758 ..." 처럼 한글이 나오는 경우가 있는데, 다음과 같이 한글을 올바르게 출력할 수 있습니다.

json.dumps() 함수를 사용하여 JSON 데이터를 생성할 때, ensure_ascii=False 옵션을 추가해주면 됩니다.

weather_info = {
    "location": "\uc11c\uc6b8\",
}

# 딕셔너리를 JSON 문자열로 변환
json_data = json.dumps(weather_info, ensure_ascii=False)

print(json_data)

위와 같이 설정하면 한글이 올바르게 출력될 것입니다.

출력
    {"location": "서울"}

전체 코드

마지막으로 전체 코드를 아래에 첨부합니다.


import openai
import json

def get_current_weather(location, unit="fahrenheit"):
    weather_info = {
        "location": location,
        "temperature": "24",
        "unit": unit,
        "forecast": ["sunny", "windy"],
    }
    return json.dumps(weather_info)

messages = [{"role": "user", "content": "지금 서울날씨를 섭씨로 알려줘."}]
functions = [
    {
        "name": "get_current_weather",
        "description": "특정 지역의 날씨를 알려줍니다.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "지역이름 eg. 서울, 부산, 제주도",
                },
                "unit": {"type": "string", "enum": ["섭씨", "화씨"]},
            },
            "required": ["location"],
        },
    }
]
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=messages,
    functions=functions,
    function_call="auto",
    )
response_message = response["choices"][0]["message"]

print(response_message)

if response_message.get("function_call"):
    # Note: the JSON response may not always be valid; be sure to handle errors
    available_functions = {
        "get_current_weather": get_current_weather,
    }
    function_name = response_message["function_call"]["name"]
    fuction_to_call = available_functions[function_name]
    function_args = json.loads(response_message["function_call"]["arguments"])
    function_response = fuction_to_call(
        location=function_args.get("location"),
        unit=function_args.get("unit"),
    )

    messages.append(response_message)
    messages.append(
        {
            "role": "function",
            "name": function_name,
            "content": function_response,
        }
    )
    second_response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=messages,
    )  # get a new response from GPT where it can see the function response


    json_data = json.dumps(second_response, ensure_ascii=False)

    print(second_response)

참고 자료