AI, 클라우드, 문서, 자동화/AI_AGENT

XI. Agent의 핵심 부품을 여러 파일로 나누어 실제 프로젝트 구조 만들기

아톨 2026. 8. 24. 22:06
반응형

 

    #-------- 1. tools.py 파일와 main.py 파일: main.py에서 tools 사용하기

    # 파일1: tools.py

 

    def calculator(expr):

        return eval(expr)

 

    def word_count(text):

        return len(text.split())

 

    REGISTRY = {"calculator":calculator, "word_count":word_count}

 

    # 파일2: main.py

    import tools

    from tools import REGISTRY, word_count

 

    print(tools.calculator("8*5"))

    print(word_count("에이전트는 루프입니다"))

    print("available: ", list(REGISTRY))

    print(REGISTRY["calculator"]("200/25"))

 

    #-------- 2. __name__ __main__

    def build_agent():

        return {"name":"Scout", "tools":["search"]}

 

    print("__name__is: ", __name__)

 

    if __name__ == "__main__":

        agent = build_agent()

        print("직접 실행 , 시작: ", agent["name"])

    else:

        print("가져오기 완료, 아무것도 시작하지 않음")

 

    #-------- 3. 실제 프로젝트 폴더 구조

    # my-agent/

    #     venv/               가상 환경 (커밋되지 않음)- the virtual environment, never committed

    #     .env                비밀 (커밋되지 않음) - your secrets, never committed

    #     .gitignore          Git 건너뛸 항목을 지정 - tells git what to skip

    #     requirements.txt    고정된 종속성 - pinned dependencies

    #     main.py             진입점, 모든 것을 연결 - entry point, wires everthing up

    #     agent.py            에이전트 클래스 반복문 - the Agent class and the loop

    #     tools.py            도구 함수 레지스트리 - tool functions and the registry

    #     llm.py              모델 호출 응답 파싱 - the model call and response parsing

    #     agent_data/         런타임에 기록되는 기록 로그 - history and logs written at runtime

 

    #-------- 4. 환경변수에서 API Key 읽기

    import os

 

    api_key = os.environ.get("MODEL_API_KEY")

 

    if not api_key:

        print("MODEL_API_KEY is not set, refusing to start")

    else:

        print("key loaded, length: ", len(api_key))

        print("safe preview: ", api_key[:4] + " ... " + api_key[-2:])

 

    #-------- 5. .env python-dotenv 그리고 config Dictionary 만들기

    # pip install python-dotenv

 

    # .env  (a plain text file, no quotes, no spaces around=)

    # MODEL_API_KEY=sk-abcde12345

    # MODEL_NAME=some-model

    # MAX_STEPS=5

    import os

    from dotenv import load_dotenv

 

    load_dotenv()

 

    config ={

        "api_key":os.environ.get("MODEL_API_KEY"),

        "model":os.environ.get("MODEL_NAME", "default-model"),

        "max_steps":int(os.environ.get("MAX_STEPS", "5")),

    }

 

    print("model: ", config["model"])

    print("max_steps: ", config["max_steps"], type(config["max_steps"]))

    print("key present: ", bool(config["api_key"]))

 

    #-------- 6. .gitignore

    # gitignore 파일

    # .env

    # venv/

    # __pycache__/

    # *.pyc

    # agent_data/

    # .DS_Store

 

    #-------- 7. 이제 진짜 프로젝트 구조

    # 파일1. tool.py 파일

    def calculator(expr):

        allowed = set("0123456789+-*/().")

        if not set(expr) <= allowed:

            return "unsupported characters"

        return str(eval(expr))

 

    def greet(name):

        return f"안녕, {name}!"

 

    REGISTRY = {"calculator":calculator, "greet":greet}

 

    # 파일2. agent.py

    class Agent:

        def __init__(self, tools, max_steps=5):

            self.tools=tools

            self.max_steps=max_steps

            self.steps=0

            self.history=[]

 

        def run_tool(self, name, args):

            self.steps +=1

            if name not in self.tools:

                return f"unknown tool '{name}'"

            try:

                return str(self.tools[name](**args))

            except Exception as e:

                return f"tool failed: {e}"

 

    # 파일3: main.py

    import os

    from agent import Agent

    from tools import REGISTRY

 

    def load_config():

        return {                

            "api_key":os.environ.get("MODEL_API_KEY"),

            "model":os.environ.get("MODEL_NAME", "default-model"),

            "max_steps":int(os.environ.get("MAX_STEPS", "5")),    

        }

 

    def main():

        config = load_config()

        print(f"model={config['model']}  max_steps={config['max_steps']}")

        print("key configured: ", bool(config["api_key"]))

 

        bot = Agent(REGISTRY, max_steps=config["max_steps"])

        print(bot.run_tool("calculator", {"expr":"(25*4)+8"}))

        print(bot.run_tool("greet", {"name":"스티븐"}))

        print(f"steps used: {bot.steps}/{bot.max_steps}")

 

    if __name__ == "__main__":

        main()

 

지금까지는 하나의 .py 파일 안에서:

Agent
Tools
Config
환경변수
실행 코드
 

를 모두 작성했습니다.

이번 코드부터는 이렇게 나눕니다.

my-agent/
│
├── main.py       ← 프로그램 시작점
├── agent.py      ← Agent 클래스
├── tools.py      ← Tool 함수들
├── llm.py        ← AI 모델 호출
├── .env          ← API Key
└── requirements.txt ← 필요한 라이브러리
 

이것은 실제 AI Agent 프로젝트의 기본적인 구조입니다.


전체 구조를 먼저 그림으로 보면

                    ┌─────────────┐
                    │   main.py   │
                    │ 프로그램 시작 │
                    └──────┬──────┘
                           │
          ┌────────────────┼────────────────┐
          │                │                │
          ▼                ▼                ▼
     agent.py          tools.py          llm.py
       Agent             Tools           AI Model
          │                │                │
          └────────────────┼────────────────┘
                           │
                           ▼
                        .env
                    API KEY / 설정
 

이제 하나씩 보겠습니다.


1. tools.py 파일

 
def calculator(expr):
    return eval(expr)

def word_count(text):
    return len(text.split())

REGISTRY = {
    "calculator": calculator,
    "word_count": word_count
}
 

이 파일의 역할은:

Agent가 사용할 도구들을 정의하고 관리하는 것

입니다.


calculator

 
def calculator(expr):
    return eval(expr)
 

예:

 
calculator("8*5")
 

40
 

word_count

 
def word_count(text):
    return len(text.split())
 

예:

 
word_count("에이전트는 루프입니다")
 

split() 결과:

 
["에이전트는", "루프입니다"]
 

길이:

2
 

REGISTRY

 
REGISTRY = {
    "calculator": calculator,
    "word_count": word_count
}
 

이것은 우리가 이전에 공부했던 Tool Registry입니다.

구조는:

Tool 이름                실제 함수
─────────              ─────────
calculator       →     calculator()
word_count       →     word_count()
 

즉:

 
REGISTRY["calculator"]
 

 
calculator
 

그리고:

 
REGISTRY["calculator"]("8*5")
 

40
 

입니다.


2. main.py에서 tools 사용하기

 
import tools
 

의 의미:

tools.py 파일 전체를 가져온다.

이제:

 
tools.calculator("8*5")
 

처럼 사용할 수 있습니다.

구조:

tools
 │
 ├── calculator
 ├── word_count
 └── REGISTRY
 

따라서:

 
tools.calculator("8*5")
 

40
 


3. from tools import ...

 
from tools import REGISTRY, word_count
 

이번에는 필요한 것만 가져옵니다.

그래서:

 
word_count("에이전트는 루프입니다")
 

처럼 사용할 수 있습니다.

tools.word_count()라고 안 써도 됩니다.


두 import 방식 비교

방법 1

 
import tools
 

사용:

 
tools.calculator("8*5")
 

장점:

어느 파일의 함수인지 명확함
 

방법 2

 
from tools import calculator
 

사용:

 
calculator("8*5")
 

장점:

짧고 편함
 

실제 프로젝트에서는 개인적으로:

 
import tools
 

또는:

 
from tools import REGISTRY
 

처럼 사용하는 것이 구조 파악에 편합니다.


4. list(REGISTRY)

 
print("available: ", list(REGISTRY))
 

Dictionary에서:

 
list(REGISTRY)
 

는 기본적으로 Key 목록을 가져옵니다.

즉:

 
{
    "calculator": calculator,
    "word_count": word_count
}
 

 
["calculator", "word_count"]
 

출력:

available: ['calculator', 'word_count']
 

5. Registry에서 Tool 실행

 
REGISTRY["calculator"]("200/25")
 

이 코드는 처음 보면 약간 신기합니다.

단계별로 보면:

 
REGISTRY["calculator"]
 

 
calculator
 

즉 함수 자체를 가져옵니다.

그리고:

 
("200/25")
 

를 붙이면:

 
calculator("200/25")
 

가 됩니다.

8.0
 

6. __name__과 __main__

다음 코드입니다.

 
def build_agent():
    return {
        "name": "Scout",
        "tools": ["search"]
    }

print("__name__ is: ", __name__)
 

Python 파일에는 자동으로:

 
__name__
 

이라는 특별한 변수가 있습니다.


직접 실행하면

예:

 
python main.py
 

이 경우:

 
__name__
 

은:

__main__
 

입니다.


다른 파일에서 import하면

예:

 
import agent
 

이면 agent.py 안에서는:

 
__name__
 

이:

agent
 

가 됩니다.


그래서:

 
if __name__ == "__main__":
 

의 의미는:

이 파일을 직접 실행했을 때만 아래 코드를 실행해라.

입니다.


직접 실행

 
if __name__ == "__main__":
    agent = build_agent()
    print("직접 실행 중")
 

실행됩니다.


import

 
import some_file
 

if __name__ == "__main__":
 

조건이 False입니다.

따라서:

Agent를 자동으로 시작하지 않습니다.
 

이것이 매우 중요합니다.


쉽게 비유하면

tools.py는:

공구함

입니다.

안에는:

망치
드라이버
렌치
 

가 있습니다.

하지만 tools.py를 import했다고 해서 망치질을 자동으로 시작하면 이상합니다.

그래서:

도구 정의
 

와:

실제 프로그램 실행
 

을 분리하는 것입니다.


7. 실제 프로젝트 폴더 구조

주석에 있는 구조:

my-agent/
│
├── venv/
├── .env
├── .gitignore
├── requirements.txt
├── main.py
├── agent.py
├── tools.py
├── llm.py
└── agent_data/
 

하나씩 보겠습니다.


venv/

가상환경
 

프로젝트별 Python 환경입니다.

예:

Project A
└── requests 2.31

Project B
└── requests 2.32
 

처럼 서로 독립적으로 관리할 수 있습니다.

Git에는 올리지 않습니다.


.env

비밀 설정
 

예:

MODEL_API_KEY=sk-xxxx
MODEL_NAME=gpt-model
MAX_STEPS=5
 

API Key를 코드에 직접 쓰지 않는 이유는:

main.py
    ↓
GitHub
    ↓
API Key 노출 ❌
 

위험하기 때문입니다.


.gitignore

Git에게:

이 파일들은 추적하지 마세요.

라고 알려줍니다.

예:

.env
venv/
agent_data/
 

requirements.txt

프로젝트에 필요한 라이브러리 목록입니다.

예:

openai==...
python-dotenv==...
requests==...
 

다른 사람이:

 
pip install -r requirements.txt
 

하면 필요한 라이브러리를 설치할 수 있습니다.


8. 환경변수에서 API Key 읽기

 
import os

api_key = os.environ.get("MODEL_API_KEY")
 

운영체제 환경변수에서:

MODEL_API_KEY
 

를 찾습니다.


없으면

 
if not api_key:
    print("MODEL_API_KEY is not set, refusing to start")
 

Agent를 실행하지 않습니다.

이것은 좋은 습관입니다.

왜냐하면 API Key 없이 실행하면:

API 호출 실패
 

가 발생하기 때문입니다.


있으면

 
else:
    print("key loaded")
 

정상적으로 Key를 읽었다는 의미입니다.


9. API Key Preview

 
api_key[:4]
 

앞 4글자:

sk-a
 

 
api_key[-2:]
 

뒤 2글자입니다.

예:

sk-abcde12345
 

sk-a ... 45
 

전체 Key를 출력하지 않고:

정상적으로 Key가 로드되었는지
 

확인하는 방법입니다.

실제 운영 환경에서는 Key 길이나 일부 문자열조차 로그에 남기지 않는 것이 더 안전한 경우도 많습니다.


10. .env와 python-dotenv

 
from dotenv import load_dotenv

load_dotenv()
 

이 함수의 역할:

.env 파일을 읽어서 환경변수로 등록한다.

예:

.env

MODEL_API_KEY=sk-abcde12345
MODEL_NAME=some-model
MAX_STEPS=5
 

실행:

 
load_dotenv()
 

이후:

 
os.environ.get("MODEL_API_KEY")
 

로 읽을 수 있습니다.


⚠️ 코드에 작은 오타가 있습니다

주석에서는:

MODLE_NAME = some-model
 

라고 되어 있습니다.

하지만 코드에서는:

 
os.environ.get("MODEL_NAME", "default-model")
 

를 찾습니다.

즉:

MODLE_NAME ❌
MODEL_NAME ⭕
 

이어야 합니다.

올바른 .env:

MODEL_API_KEY=sk-abcde12345
MODEL_NAME=some-model
MAX_STEPS=5
 

그리고 = 앞뒤에 공백을 두지 않는 편이 좋습니다.


11. config Dictionary 만들기

 
config = {
    "api_key": os.environ.get("MODEL_API_KEY"),
    "model": os.environ.get("MODEL_NAME", "default-model"),
    "max_steps": int(os.environ.get("MAX_STEPS", "5")),
}
 

여기서 중요한 것은:

환경변수 → 문자열
 

이라는 것입니다.

예:

MAX_STEPS=5
 

이더라도:

 
os.environ.get("MAX_STEPS")
 

결과는:

"5"
 

문자열입니다.

그래서:

 
int(...)
 

로 변환합니다.

5
 

정수입니다.


기본값

 
os.environ.get(
    "MODEL_NAME",
    "default-model"
)
 

의 의미:

MODEL_NAME이 있으면 그것을 사용하고, 없으면 default-model을 사용하라.

입니다.


12. .gitignore

.env
venv/
__pycache__/
*.pyc
agent_data/
.DS_Store
 

하나씩 보면:

항목의미
.env API Key 등 비밀
venv/ 가상환경
__pycache__/ Python 캐시
*.pyc Python 컴파일 캐시
agent_data/ 실행 중 생성된 데이터
.DS_Store macOS 시스템 파일

특히:

.env
 

는 정말 중요합니다.

API Key가 GitHub에 올라가면 매우 위험합니다.


13. 이제 진짜 프로젝트 구조

마지막 부분은 앞의 모든 내용을 합칩니다.


tools.py

 
def calculator(expr):
 

계산기입니다.


 
allowed = set("0123456789+-*/().")
 

허용되는 문자만 지정합니다.

예:

0123456789
+
-
*
/
(
)
.
 

 
if not set(expr) <= allowed:
 

의미:

expr 안의 모든 문자가 allowed 안에 들어있는가?

예:

 
expr = "25*4"
 

모든 문자가 허용됨.

True
 

하지만:

 
expr = "__import__('os')"
 

허용되지 않은 문자가 포함됩니다.

unsupported characters
 

그래서 이전의 단순한:

 
eval(expr)
 

보다 조금 안전합니다.

다만 실제 서비스용 계산기는 eval() 자체를 피하는 것이 더 좋습니다. 나중에 Agent를 실제 OpenAI 모델과 연결할 때는 안전한 계산기 함수를 별도로 만드는 것을 추천합니다.


greet Tool

 
def greet(name):
    return f"안녕, {name}!"
 

간단한 Tool입니다.


REGISTRY

 
REGISTRY = {
    "calculator": calculator,
    "greet": greet
}
 

Agent에게 줄 Tool 목록입니다.

Agent
  │
  └── REGISTRY
        │
        ├── calculator()
        └── greet()
 

agent.py

 
class Agent:
 

이 파일은 Agent 자체를 정의합니다.


 
def __init__(self, tools, max_steps=5):
 

Agent 생성 시:

사용 가능한 Tool
최대 단계
 

를 받습니다.


run_tool

 
def run_tool(self, name, args):
 

Agent가 Tool을 실행합니다.


Step 증가

 
self.steps += 1
 

Tool 호출 횟수를 기록합니다.


Tool 존재 확인

 
if name not in self.tools:
 

예:

 
name = "emailer"
 

그런데 Registry에는:

calculator
greet
 

만 있다면:

unknown tool 'emailer'
 

을 반환합니다.

여기에는 작은 오타가 있습니다.

 
return f"unknown too '{name}'"
 

는:

 
return f"unknown tool '{name}'"
 

가 자연스럽습니다.


Tool 실행

 
self.tools[name](**args)
 

예:

 
name = "greet"

args = {
    "name": "스티븐"
}
 

 
greet(name="스티븐")
 

안녕, 스티븐!
 

main.py

이 파일은 전체 프로그램을 조립하는 곳입니다.

main.py
│
├── 설정 읽기
│
├── Agent 만들기
│
├── Tools 연결
│
└── Agent 실행
 

load_config()

 
def load_config():
 

설정을 읽는 일을 하나의 함수로 분리했습니다.

이것이 좋은 습관입니다.

이전에는:

 
api_key = ...
model = ...
max_steps = ...
 

가 프로그램 여기저기에 흩어질 수 있었습니다.

하지만 이제:

load_config()
 

만 보면:

설정은 여기서 관리하는구나.

하고 알 수 있습니다.


main()

 
def main():
 

실제 프로그램의 중심입니다.


설정 로드

 
config = load_config()
 

 
{
    "api_key": "...",
    "model": "...",
    "max_steps": 5
}
 

Agent 생성

 
bot = Agent(
    REGISTRY,
    max_steps=config["max_steps"]
)
 

이 부분이 중요합니다.

REGISTRY
    │
    ├── calculator
    └── greet
          │
          ▼
        Agent
 

Agent는 Tool을 직접 모르고 있습니다.

단지:

 
tools
 

라는 Dictionary를 받습니다.

이것이 좋은 설계입니다.


첫 번째 Tool 실행

 
bot.run_tool(
    "calculator",
    {"expr": "(25*4)+8"}
)
 

흐름:

main.py
    │
    ▼
Agent.run_tool()
    │
    ▼
REGISTRY["calculator"]
    │
    ▼
calculator(expr="(25*4)+8")
    │
    ▼
108
 

두 번째 Tool

 
bot.run_tool(
    "greet",
    {"name": "스티븐"}
)
 

안녕, 스티븐!
 

Step 출력

 
print(f"steps used: {bot.steps}/{bot.max_steps}")
 

결과:

steps used: 2/5
 

입니다.


🔥 가장 중요한 전체 흐름

이 프로젝트 구조를 이해하면:

                  main.py
                     │
                     │ 시작
                     ▼
               load_config()
                     │
                     ▼
                .env / 환경변수
                     │
                     ▼
                  config
                     │
                     ▼
                 Agent 생성
                     │
          ┌──────────┴──────────┐
          │                     │
          ▼                     ▼
       REGISTRY              max_steps
          │
     ┌────┴─────┐
     │          │
     ▼          ▼
calculator     greet
 

실행할 때:

사용자 요청
    │
    ▼
Agent
    │
    ▼
어떤 Tool?
    │
    ▼
REGISTRY[name]
    │
    ▼
실제 함수 실행
    │
    ▼
결과 반환
 

⭐ 그런데 현재 코드에는 한 가지 중요한 부분이 빠져 있습니다

마지막 main.py 코드에는 현재:

 
import os
 

만 있습니다.

하지만 실제로는 Agent와 REGISTRY를 가져와야 합니다.

따라서 실제 main.py는 보통 이렇게 됩니다.

 
import os

from agent import Agent
from tools import REGISTRY


def load_config():
    return {
        "api_key": os.environ.get("MODEL_API_KEY"),
        "model": os.environ.get("MODEL_NAME", "default-model"),
        "max_steps": int(os.environ.get("MAX_STEPS", "5")),
    }


def main():
    config = load_config()

    print(
        f"model={config['model']}  "
        f"max_steps={config['max_steps']}"
    )

    print("key configured:", bool(config["api_key"]))

    bot = Agent(
        REGISTRY,
        max_steps=config["max_steps"]
    )

    print(
        bot.run_tool(
            "calculator",
            {"expr": "(25*4)+8"}
        )
    )

    print(
        bot.run_tool(
            "greet",
            {"name": "스티븐"}
        )
    )

    print(
        f"steps used: {bot.steps}/{bot.max_steps}"
    )


if __name__ == "__main__":
    main()
 

🎯 이번 코드의 핵심은 이것입니다

지금까지 공부한 Agent를 실제 프로젝트로 바꾸는 과정입니다.

[이전]

하나의 파일

main.py
├── Agent
├── Tools
├── Config
└── 실행
 

[지금]

프로젝트
│
├── main.py
│     └── 조립 및 시작
│
├── agent.py
│     └── Agent의 행동
│
├── tools.py
│     └── Tool 정의
│
├── llm.py
│     └── OpenAI 모델 호출
│
├── .env
│     └── API Key
│
└── agent_data/
      └── History / Log
 

이제 다음 단계에서 여기에:

OpenAI API
 

가 들어오면 진짜 Agent Loop가 됩니다.

사용자
   ↓
main.py
   ↓
Agent
   ↓
OpenAI Model
   ↓
"calculator를 사용하세요"
   ↓
Agent
   ↓
Tool 실행
   ↓
결과
   ↓
OpenAI Model
   ↓
최종 답변
 

지금 배우는 순서가 아주 좋습니다. 앞에서 배운 함수 → Dictionary → JSON → Tool Registry → History → Loop → Error 처리 → Class → 파일 분리가 바로 실제 AI Agent를 만들기 위한 부품들이었고, 이번 코드에서 그 부품들을 처음으로 프로젝트 구조로 조립하기 시작한 것입니다.

 

반응형