logo
홈블로그소개
3,994

Built with Next.js, Bun, Tailwind CSS and Shadcn/UI

·개인정보처리방침
AIPython

밑바닥부터 만드는 AI 에이전트 - OpenAI API 만으로 메모리, 도구 호출 구현하기

Toma
2026년 6월 8일
약 19분
목차
Environment Setup
⚡ UV
📦 Pyproject
📓 Jupyter
First AI Agent
🔑 Setup
💬 AI Response
🤖 First AI Agent
🧠 Adding Memory
🛠️ Adding Tools
📤 Tool Results
이전 포스트Claude Code로 GitHub PR 자동 리뷰 설정하기
다음 포스트OpenAI Agents SDK 기초 - Runner, Streaming, Session, Handoff, Tracing

목차

Environment Setup
⚡ UV
📦 Pyproject
📓 Jupyter
First AI Agent
🔑 Setup
💬 AI Response
🤖 First AI Agent
🧠 Adding Memory
🛠️ Adding Tools
📤 Tool Results

🤖 이 글은 별도 프레임워크 없이 순수 OpenAI API만으로 에이전트를 직접 구현해 보며 그 동작 원리를 이해한 기록입니다.


Environment Setup

⚡ UV

UV는 Python의 package / project manager로, JS 환경에서 npm과 같은 역할을 한다.

✅ UV 하나로 대체되는 도구들 — pip, pip-tools, pyenv, twine, virtualenv

설치는 아래 둘 중 하나를 사용한다.

bash
curl -LsSf https://astral.sh/uv/install.sh | sh
# 또는
brew install uv

📦 Pyproject

프로젝트 초기화는 uv init 프로젝트명 명령어로 한다.

pyproject.toml은 Node 환경의 package.json과 같은 역할을 하며, 이를 통해 한 줄 명령어로 필요한 의존성을 설치할 수 있다.

  • uv sync : pyproject.toml의 의존성을 확인하고 설치하는 명령어
  • uv add : uv를 사용해 패키지를 설치하는 명령어

그러면 자동으로 .venv 라는 폴더가 생길텐데 이는 가상환경을 의미하한다.

  • 시스템 전체에 패키지를 전역으로 설치하는 대신 이 폴더에만 패키지를 설치

패키지 설치 후에는 code editor에서 생성된 venv 경로에 맞는 파이썬 가상환경을 선택해준다.

python main.py 를 터미널에서 실행하면 에러가 발생할텐데

이는 python 명령어가 .venv가 아니라 전역을 바라보고 있기 때문이며, 터미널에서 가상환경을 활성화 하거나 바로 실행시킬 수 있는 방법으로 2가지가 있다.

  1. 터미널에 source .venv/bin/activate를 입력해 가상환경을 활성화한다. (비활성화 시엔 deactivate)
  2. 또는 가상환경을 활성화하지 않은 상태에서 uv run main.py로 실행한다.

📓 Jupyter

Jupyter extension을 설치하면 Jupyter notebook을 사용할 수 있다.

파일명.ipynb 형식으로 Jupyter 파일을 생성한다.

1+1 과 같은 코드 실행 시 커널이 없어서 에러가 발생할텐데 화면 우측 상단에서 커널을 선택하고 표시되는 모달의 안내에 따라 pip로 설치하지 않고 아래와 같이 uv 활용

uv add ipykernel --dev 명령어 실행 (Jupyter Notebook 커널 설치)


First AI Agent

🔑 Setup

OpenAI API key를 발급받은 뒤 아래 코드로 정상 동작을 확인한다.

python
from dotenv import load_dotenv
import os

load_dotenv()
print(os.getenv("OPENAI_API_KEY"))

💬 AI Response

OpenAI client를 만들 때 OpenAI 패키지를 사용하는 이유는, API URL을 직접 활용하는 것보다 개발자 경험이 좋기 때문이다.

python
import openai

client = openai.OpenAI()

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {
            "role": "user",
            "content": "Hello, how are you?"
        }
    ]
)

response

그러면 아래와 같은 ChatCompletion 객체를 응답으로 받는다.

python
ChatCompletion(id='chatcmpl-DoTMqGFDXOErae4jg0rMqbuLlFRbE', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content="Hello! I'm just a program, so I don't have feelings, but I'm here and ready to assist you. How can I help you today?", refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))], created=1780921660, model='gpt-4o-mini-2024-07-18', object='chat.completion', moderation=None, service_tier='default', system_fingerprint='fp_080e1eab01', usage=CompletionUsage(completion_tokens=29, prompt_tokens=13, total_tokens=42, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=0, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=0), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0)))

이 중 choices는 리스트 형태로, choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content="Hello! I'm just a program, so I don't have feelings, but I'm here and ready to assist you. How can I help you today?", refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))]

이는 질문에 가장 그럴듯한, 즉 확률이 가장 높은 답을 하나만 반환한다. 만약 다른 선택지도 보고 싶다면 create에 model, message 외에 n을 넣어 n=10처럼 선택지를 늘릴 수 있다.

python
for choice in response.choices:
    print(choice)
python
Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content="Hello! I'm here and ready to help you. How can I assist you today?", refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))
Choice(finish_reason='stop', index=1, logprobs=None, message=ChatCompletionMessage(content="Hello! I'm just a program, but I'm here and ready to help you. How can I assist you today?", refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))
Choice(finish_reason='stop', index=2, logprobs=None, message=ChatCompletionMessage(content="Hello! I'm just a computer program, so I don't have feelings, but I'm here and ready to help you. How can I assist you today?", refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))
Choice(finish_reason='stop', index=3, logprobs=None, message=ChatCompletionMessage(content="Hello! I'm just a computer program, so I don't have feelings, but I'm here and ready to help you. How can I assist you today?", refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))
Choice(finish_reason='stop', index=4, logprobs=None, message=ChatCompletionMessage(content="Hello! I'm here and ready to help you. How can I assist you today?", refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))

choice 리스트 내부에서 진짜 필요한 것은 message다. ChatCompletionMessage가 있고, 그 내부의 content가 실제 응답값이다.

🤖 First AI Agent

💡 에이전트란? 사용자를 대신해 어떤 행동을 해줄 수 있는 시스템. 단순히 LLM에게 질문만 하는 것이 아니라, 다양한 도구를 모델에게 쥐어줌으로써 에이전트가 스스로 그 도구를 실행하거나 활용할 수 있다.

예를 들어 LLM 모델에게 프롬프트로 “너는 3가지 도구를 가지고 있고, 아래와 같은 질문에 필요한 도구는 무엇인지 알려줘.”라고 하면

모델은 3가지 도구 중 적합한 도구를 알려준다.

그러면 그 도구를 호출해 결과값을 받고 이를 다시 LLM 모델에 전달해 최종 응답을 반환한다.

이것이 AI 에이전트가 작동하는 방식이다.

python
import openai

client = openai.OpenAI()

PROMPT = """
  너는 아래 3가지 함수를 가지고 있어.
  
  - get_popular() - 인기 항목를 조회.
  - get_details(id) - 상세 정보를 조회.
  - get_credits(id) - 주문 내역을 조회.
  
  이제 다음 3가지 질문에 맞는 함수를 선택해서 함수명과 인자(argument)를 반환하고 이외의 불필요한 대답은 하지마.

  1. 지금 인기있는 항목이 무엇인지 알려줘.
  2. ID 550에 해당하는 항목의 상세정보를 알려줘.
  3. ID 550에 해당하는 주문 내역을 알려줘.
"""

response = client.chat.completions.create(
    model="gpt-4o-mini",
    # n=5,
    messages=[
        {
            "role": "user",
            "content": PROMPT
        }
    ],
)

🧠 Adding Memory

시스템에 메모리를 활성화해야 한다.

메모리가 없는 상태에서 모델에게 “내 이름은 Toma이야.”라고 알린 뒤 다시 “내 이름이 뭐라고?”라고 물으면, 모델 입장에서는 매 요청이 새로운 요청이기 때문에 이름을 기억하지 못한다.

그렇기에 메모리 시스템이 필요하다.

python
import openai

client = openai.OpenAI()
messages = []

def call_ai():
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
    )
    message = response.choices[0].message.content
    messages.append({
        "role": "assistant",
        "content": message
    })
    print(f"AI: {message}")

while True:
    message = input("Send a message to the LLM ...")
    if message == "quit" or message == "q":
        break
    else:
        messages.append({
            "role": "user",
            "content": message
        })
        print(f"User: {message}")
        call_ai()
python
User: my name is toma
AI: Nice to meet you, Toma! How can I assist you today?
User: what is my name?
AI: Your name is Toma. How can I help you today?

messages 리스트를 만들고 사용자 message를 받도록 한다. 사용자 message가 q 또는 quit이면 while loop를 종료하고, 그렇지 않다면 role을 user로 해서 content를 messages 리스트에 저장한 뒤 출력하고 call_ai() 함수를 호출한다.

call_ai 함수는 LLM 모델을 호출하는 함수로, messages 리스트를 받아 질문에 응답한다. 해당 응답을 messages 리스트에 추가하고 출력한다. 이러한 대화 기록을 통해 시스템이 메모리를 갖게 된다.

⚠️ 다만 messages에 대화 이력을 계속 쌓게 되면 컨텍스트가 무한히 커지므로, 대화를 압축하거나 가장 오래된 메시지를 삭제하는 전략이 필요하다.

🛠️ Adding Tools

애플리케이션이 모델에게 도구 구조(Schema)를 제공하면, AI 모델이 tool_calls 응답을 생성하고 애플리케이션이 실제 함수를 실행한다. 함수 실행 후 반환받은 결과를 다시 AI 모델에 전달하고, 그대로 최종 응답을 출력할지 또는 또 다른 도구를 호출할지를 AI 모델(LLM)이 결정하는 워크플로우다.

python
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "A function to get the weather of a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "The name of the city to get the weather of"
                    }
                },
                "required": ["city"]
            }
        }
    }
]

위와 같이 내가 가진 함수들을 모델에게 설명한다. 타입은 무엇인지, 이름은 무엇인지, 어떤 역할을 하는지(description), 파라미터로는 어떤 타입이 필요한지, 객체라면 어떤 프로퍼티를 가지며 그 프로퍼티에 대한 설명과 필수 파라미터는 무엇인지 등을 명시한다.

이를 가지고 모델을 호출할 때, tools를 같이 넘기면 요청을 받은 OpenAI 서버가 모델한테 이 함수를 호출하고 싶을 땐 어떻게 답변해야하는지 알려줌.

그러면 message에 tool_calls라는 응답이 추가되어 넘어오는데, 이 함수를 애플리케이션(python 서버 등)에서 실행해야 한다.

그리고 AI 모델에게 다시 “내게 이 ID로 이 함수를 실행하라고 했지? 내가 함수를 실행했고 응답은 30이야. 그래서 나는 이 call ID와 함께 이 함수의 응답을 너에게 보낼거야.”라는 의미의 코드를 작성해야 한다.

  1. AI 모델에게 tool schema 제공
  2. AI 모델이 도구를 호출하고 싶을 때 발생하는 응답을 처리한다. (message.content에 “이 함수를 호출해줘”라는 식으로 답변하지 않는다.)
  3. 대신 message.tool_calls로 온다.
  4. 우리가 할 일은 그것을 메모리에 적재하고 함수를 실행한 뒤, 그 실행 결과도 메모리에 적재하는 것이다.
  5. 그래야 AI 모델이 함수를 실행하라고 한 것, 우리가 실행한 것, 그리고 실행 결과를 볼 수 있다.

함수 정의

python
def get_weather(city):
    return f"The weather in {city} is sunny"

FUNCTIONS_MAP = {
    "get_weather": get_weather
}

AI 모델이 요청한 함수 실행과 그 실행 결과를 함께 기록한다.

python
from openai.types.chat import ChatCompletionMessage

def process_ai_response(message: ChatCompletionMessage):
    if message.tool_calls:
        messages.append({
            "role": "assistant",
            "content": message.content or "",
            "tool_calls": [
                {
                    "id": tool_call.id,
                    "type": "function",
                    "function": {
                        "name": tool_call.function.name,
                        "arguments": tool_call.function.arguments
                    }
                } for tool_call in message.tool_calls
            ]
        })

        for tool_call in message.tool_calls:
            function_name = tool_call.function.name
            function_args = tool_call.function.arguments
            
            print(f"Calling Function: {function_name} with args: {function_args}")
            
            try:
                arguments = json.loads(function_args)
            except json.JSONDecodeError:
                arguments = {}
            
            function_to_run = FUNCTIONS_MAP.get(function_name)

            result = function_to_run(**arguments)
            # function_args는 '{"city": "Seoul"}' 과 같이 생긴 문자열(모델이 주는건 텍스트이기 때문.)
            # 이걸 Python 딕셔너리로 변환하기 위해서 json.loads() 함수를 사용함.
            # get_weather 함수에 전달할 인자는 딕셔너리가 아니라 도시 이름 하나만 받으면 되기 때문에 ** 을 사용.
            # ** 과 같은 표기법은 해당 딕셔너리를 키워드 인자로 바꿔줌 city="Seoul" 와 같은 형태로.

            print(f"Ran function {function_name} with result: {result}")

            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "name": function_name,
                "content": result
            })
        
        call_ai()
    else:
        messages.append({
            "role": "assistant",
            "content": message.content
        })
        print(f"AI: {message.content}")

def call_ai():
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        tools=TOOLS,
    )
    process_ai_response(response.choices[0].message)

🔁 전체 흐름 정리 — 유저가 메시지를 보내면 그 메시지를 메모리에 추가하고 call_ai()를 호출한다. call_ai()는 메시지 메모리와 tools 구조를 함께 전달해 AI를 호출한다. 그 뒤 process_ai_response를 호출해 AI의 메시지를 받고, tool_call이 하나라도 있으면 AI가 함수 실행을 요청한 것을 메시지 메모리에 추가한다.

그 다음 AI 모델이 실행하라고 알려준 각 tool_call마다 함수 이름과 arguments를 추출한다.

그 argument를 문자열에서 딕셔너리로 변경한 후 AI 모델이 요청한 함수 이름으로 function_map에서 함수를 가져온다.

그 함수에 변환한 키워드 인자를 넘겨 실행한다.

메모리에 call ID, 함수 이름, 실행 결과를 추가한다.

마지막으로 메모리에 실행 요청과 결과를 추가했으니, 다시 AI 모델을 호출해 유저가 최종 응답을 받을 수 있도록 한다.

📤 Tool Results

python
[{'role': 'user', 'content': 'my name is toma'},
 {'role': 'user', 'content': 'my name is toma'},
 {'role': 'assistant',
  'content': 'Nice to meet you, Toma! How can I assist you today?'},
 {'role': 'user', 'content': 'what is my name?'},
 {'role': 'assistant', 'content': 'Your name is Toma.'},
 {'role': 'user', 'content': 'what is the weather in spain'},
 {'role': 'assistant',
  'content': '',
  'tool_calls': [{'id': 'call_D7OM0nlQJPpkNZ5tcP0dZU1c',
    'type': 'function',
    'function': {'name': 'get_weather', 'arguments': '{"city":"Spain"}'}}]},
 {'role': 'tool',
  'tool_call_id': 'call_D7OM0nlQJPpkNZ5tcP0dZU1c',
  'name': 'get_weather',
  'content': 'The weather in Spain is sunny'},
 {'role': 'assistant', 'content': 'The weather in Spain is currently sunny.'}]

✅ 전체 사이클 요약 — 여러 개의 함수를 정의하고 그 동작을 설명하면, AI 모델은 텍스트 또는 tool_calls로 응답한다. tool_calls가 존재하면 그 함수를 실제로 실행해 AI 모델에 다시 전달하고, AI 모델은 실행 결과를 보고 또 다른 도구를 호출할지, 최종 응답을 반환할지를 결정한다.