2025.8.13: 제미나이 API로 함수 호출하기

jiyongg·2025년 8월 13일

TIL: Today I Learned

목록 보기
21/30

오늘은 제미나이 API로 함수를 호출하는 방법에 대해 실습해 보았다. 이번 해커톤 프로젝트로 개발하는 서비스에는 AI를 이용한 기능이 필요하다. 어떻게 하면 AI를 이용한 기능을 구현할 수 있을지 알아보다가, 제미나이 API에 함수 호출 기능이 있다는 것을 알게 되었다. 이것을 잘 이용하면 원하는 기능을 구현할 수 있지 않을까라는 생각이 들어 공식 문서의 설명을 참고해 실습을 진행해 보았다.

기본 흐름

(출처: Gemini API를 사용한 함수 호출 | Google AI for Developers)

  • 앱에서 제미나이에 프롬프트와 함수 정의를 전송한다.
  • 제미나이는 함수를 호출해야 하는지 판단한다.
    • 함수를 호출해야 한다면 functionCall 객체를 반환한다.
    • 함수 호출이 필요 없다면 텍스트를 생성한다.
  • 만약 functionCall 객체가 반환되었다면 이 객체의 내용을 바탕으로 함수를 호출한다.
  • 함수를 호출한 결과를 기존 응답에 덧붙이고, 이를 제미나이에 전송해서 최종 응답을 생성한다.

제미나이는 직접 함수를 호출할 수 없다. 함수 호출의 필요성을 판단하는 것은 제미나이이지만, 함수를 호출하는 것은 앱의 책임이라는 것이다. 그런데, 파이썬의 경우 함수를 자동으로 호출할 수 있는데, 이는 후술할 것이다.

📢 수동 호출

이번에 개발중인 서비스에 AI를 이용한 분석, 추천 기능을 넣고자 한다. 그래서 데이터 분석의 예시로 제미나이의 함수 호출 기능을 이용해 A의 데이터(리스트 타입)에서 최댓값을 구하는 예제를 만들어 보고자 한다.

기본 예제

공식 문서에서 큰 틀을 가져오고, 내용은 위에서 제시한 상황에 맞게 수정하였다.

1. 파이썬 함수 선언

from random import randint


def get_data_of_a() -> list[int]:
    '''
    A의 데이터를 조회하는 것을 가장한 함수.

    실제 데이터는 1~100000에서 중복 포함하여 무작위로 50개가 선택된 리스트이다.
    '''
    return [randint(1, 100000) for _ in range(50)]

2. 함수 정의 작성

get_data_of_a_declaration = {
    'name': 'get_data_of_a',
    'description': 'A의 데이터를 조회한다.',
    'parameters': {},
}

공식 문서를 참고해서 함수의 정의를 작성한다.

3. 함수 정의를 바탕으로 도구 제공

from google import genai
from google.genai import types


tools = types.Tool(function_declarations=[get_data_of_a_declaration])
content_config = types.GenerateContentConfig(tools=[tools])

4. 모델 호출 및 응답 생성

from decouple import config


# API 클라이언트 호출
client = genai.Client(api_key=config('GEMINI_API_KEY'))

# 프롬프트 구성
PROMPT = '''
A의 데이터를 조회해서, 해당 데이터에서 가장 큰 값을 찾아서 알려줘.

<예시>
A의 데이터가 [1, 5, 6, 2]라면 가장 큰 값은 6
A의 데이터가 [67, 24, 40, 30, 89, 1000, 424, 500000]이라면 가장 큰 값은 500000
'''

contents = [
    types.Content(
        role='user', parts=[types.Part(text=PROMPT)]
    )
]

# 모델 호출
response = client.models.generate_content(
    model='gemini-2.5-flash',
    contents=contents,
    config=content_config,
)
  • 제미나이의 API 키를 .env라는 파일에 분리해서 보관했다.

5. 생성된 응답 확인

print(response.candidates[0].content.parts[0].function_call)

제미나이가 함수 호출이 필요하다고 판단했다면 아래와 같이 출력된다. 만약, 그렇지 않다면 None이 출력될 것이다.

id=None args={} name='get_data_of_a'

6. 필요한 함수 호출

tool_call = response.candidates[0].content.parts[0].function_call

if tool_call.name == 'get_data_of_a':
    result = get_data_of_a(**tool_call.args)
    print(result)

함수를 호출하고, 생성된 A 데이터를 확인해보자.

[51235, 99839, 58163, 62612, 89199, 40079, 75556, 77427, 16015, 84348, 75233, 28284, 49923, 23227, 82331, 76568, 57729, 8845, 79453, 21151, 25539, 13586, 10453, 31316, 8343, 62451, 72098, 58541, 75980, 331, 51398, 56534, 37537, 14877, 6255, 1174, 59307, 41466, 49536, 34552, 88420, 50865, 13021, 10913, 13088, 14434, 74260, 56139, 90880, 1847]

1~100000 사이의 정수가 중복을 포함하여 무작위로 50개 선택되므로 실제 출력은 매번 달라질 것이다.

7. 함수 호출 결과를 바탕으로 최종 응답 생성

# 함수 응답 파트 생성
function_response_part = types.Part.from_function_response(
    name=tool_call.name,
    response={'result': result}
)

# 기존 프롬프트에 이전 응답과 함수 호출 결과 덧붙이기
contents.append(response.candidates[0].content)
contents.append(types.Content(role='user', parts=[function_response_part]))

# 최종 응답 생성
final_response = client.models.generate_content(
    model='gemini-2.5-flash',
    config=content_config,
    contents=contents,
)

print(final_response.text)

지금은 응답의 구조를 정해주지 않았기 때문에 "A의 데이터는 ~이며, 최댓값은 ~입니다"와 같이 출력될 것이다.

unittest를 이용한 테스트

unittest.TestCaseassertEqual 메소드를 이용해서 제미나이의 답이 실제 정답과 일치하는지 테스트해보고자 한다.

setUp 메소드로 테스트 Fixture 세팅

def setUp(self):
    PROMPT = '''
    A의 데이터를 조회해서, 해당 데이터에서 가장 큰 값을 찾아서 다음의 JSON 형식에 맞게 출력해줘.

    <JSON> 
    result = { "max_value": integer }
    return result
    '''

    self.client = genai.Client(api_key=config('GEMINI_API_KEY'))
    self.tools = types.Tool(function_declarations=[get_data_of_a_declaration])
    self.config = types.GenerateContentConfig(tools=[self.tools], temperature=0)
    self.contents = [
        types.Content(
            role='user', parts=[types.Part(text=PROMPT)]
        ),
    ]
  • 테스트 Fixture는 테스트를 실행하기 전에 필요한 준비 과정을 의미한다. 테스트용 데이터베이스 생성, 디렉토리, 서버 실행 등이 여기에 속한다.
  • setUp 메소드는 테스트 메소드를 호출하기 직전에 호출된다. 따라서 각각의 테스트 메소드를 실행하기 전에 필요한 작업을 setUp에 작성하면 효율적으로 테스트 코드를 작성할 수 있다.
  • 아까의 프롬프트와는 다르게 JSON 형식에 맞게 출력할 것을 요구하고 있다. JSON 문자열을 이용해 최댓값만을 얻기 위해서 구조화된 출력 프롬프트를 사용했다.
  • config에서 temperature를 0으로 설정한 것을 볼 수 있을 것이다. LLM에는 온도 설정이 존재하는데, 이 온도가 낮을수록 응답의 무작위성이 낮아진다. 후술하겠지만, 지금의 프롬프트는 테스트 결과가 불안정하다. 그나마 조금이나마 덜 불안정하게 하기 위해서 온도를 아예 0으로 세팅했다.

응답 생성 메소드

def generate_response(self, client: genai.Client=None, config: types.GenerateContentConfig=None, contents: list[types.Content]=None):
    client: genai.Client = (getattr(self, 'client'), client)[bool(client)]
    config: types.GenerateContentConfig = (getattr(self, 'config'), config)[bool(config)]
    contents: list[types.Content] = (getattr(self, 'contents'), contents)[bool(contents)]
    response = client.models.generate_content(
        model='gemini-2.5-flash',
        contents=contents,
        config=config,
    )
    return response

클래스 내이므로 응답을 생성하는 메소드를 분리했다.

  • 각 매개변수의 기본값을 None으로 두고, None이라면 인스턴스 속성의 값을 활용하게끔 만들었다. 그래서 뒤의 테스트 메소드에서 이 메소드를 그냥 self.generate_response()로 호출한다.

컨텐츠를 덧붙이는 메소드

def append_content(self, content):
    content_list = self.contents
    content_list.append(content)

컨텐츠를 덧붙이는 것도 메소드를 만들어서 분리했다.

테스트 메소드

def test_gemini_response_is_true(self):
    # 1차 모델 호출
    response = self.generate_response()
    tool_call = response.candidates[0].content.parts[0].function_call

    if tool_call.name == 'get_data_of_a':
        result = get_data_of_a(**tool_call.args)

    function_response_part = types.Part.from_function_response(
        name=tool_call.name,
        response={'result': result}
    )

    # 이전 응답과 함수 호출 결과 덧붙이기
    self.append_content(response.candidates[0].content)
    self.append_content(types.Content(role='user', parts=[function_response_part]))

    # 최종 응답 생성
    final_response = self.generate_response()
    final_response_json = final_response.text.replace('json', '').strip('`')

    max_value = loads(final_response_json).get('max_value')
    self.assertEqual(max_value, max(result))
  • 최종 응답(final_response)의 텍스트를 출력해보면, JSON 문자열이 ```json ```으로 둘러싸인 것을 볼 수 있을 것이다. 그래서 이것을 replacestrip으로 제거한다.
  • json 모듈의 loads 함수를 이용해 JSON 문자열을 딕셔너리로 바꾸고, 딕셔너리에서 max_value의 값을 꺼내서 실제 답(max(result))과 비교한다.

테스트 실행

if __name__ == '__main__:
    unittest.main()

아마 실패할 수도 있는데, 인내심을 가지고 실행시켜보면 성공하는 것을 볼 수 있다. 다시 말해, 지금의 프롬프트는 불안정한 결과를 생성한다. 조금 더 안정적인 프롬프트를 만들기 위해 고민할 필요가 있겠다.

🤖 자동 호출

제미나이는 기본적으로 함수를 자동으로 호출할 수 없다. 하지만, 파이썬에서는 함수 정의 대신 호출 가능한 함수를 직접 도구로 제공할 수 있다! 이 경우에는 함수의 독스트링과 타입을 통해 함수의 정보를 얻게 된다.

이번에도 unittest를 이용한 테스트를 진행하는데, 아까와 달라진 부분만 설명하겠다.

전역 변수 a와 get_data_of_a 함수의 독스트링

a = []

# 파이썬 함수 선언 및 함수 정보
def get_data_of_a() -> list[int]:
    '''
    A의 데이터를 조회한다.

    Args:
        없음
    Returns:
        리스트로 표현된 A의 데이터
    '''
    global a
    a = [randint(1, 100000) for _ in range(50)]

    return a
  • 전역 변수로 사용할 a를 선언했다. 수동 호출에서는 get_data_of_a의 호출 결과를 직접 처리했지만, 자동 호출에서는 SDK가 자동으로 호출 결과를 처리하기 때문에 호출 결과를 가져오는 대신 전역 변수 a를 이용하여 get_data_of_a의 결과를 저장한다.
  • 따로 함수 정의가 없고 함수의 독스트링과 타입 힌트에서 함수의 정보를 얻기 때문에, 간단히 작성한 독스트링을 구글 스타일에 맞게 수정했다.

함수 정의 대신 함수 객체를 도구로 제공

def setUp(self):
    PROMPT = '''
    A의 데이터를 조회해서, 해당 데이터에서 가장 큰 값을 찾아서 다음의 JSON 형식에 맞게 출력해줘.

    <JSON> 
    result = { "max_value": integer }
    return result
    '''

    self.client = genai.Client(api_key=config('GEMINI_API_KEY'))
    # self.tools = types.Tool(function_declarations=[get_data_of_a])
    self.config = types.GenerateContentConfig(tools=[get_data_of_a], temperature=0)
    self.contents = [
        types.Content(
            role='user', parts=[types.Part(text=PROMPT)]
        ),
    ]
  • config의 toolstypes.Tool 대신 함수 객체를 직접 도구로 제공한다.

테스트 메소드

def test_gemini_response_is_true(self):
    # 모델 호출
    response = self.generate_response()
    response_json = response.text.replace('json', '').strip('`')
    max_value = loads(response_json).get('max_value')
    self.assertEqual(max_value, max(a))
  • 테스트 메소드의 코드가 더 짧아졌다. 아까는 모델을 두 번 호출했는데, 지금은 한 번만의 호출로 최종 응답을 얻을 수 있게 되었다.
  • max_valuemax(result)를 비교하는 것이 아니라, max_value와 아까 선언한 전역 변수 a에 대한 max(a)를 비교하는 것으로 변경되었다.

🔚 결론

이렇게 해서 제미나이로 함수를 호출하는 실습과 자동 함수 호출 실습을 진행해 보았다. 하지만, 위에서 언급했듯, 이 프롬프트를 이용한 테스트는 매우 불안정했다. 실제로 프로젝트에 사용하기 위해서는 조금 더 많은 실습과 디버깅 과정을 통해 안정적인 결과를 만들어 내는 프롬프트를 찾아낼 필요성이 있을 것 같다.

profile
그냥 쓰고 싶은 것 쓰는 개발(?) 블로그

0개의 댓글