아톨러브

X. Agent의 여러 요소를 하나의 class Agent로 묶어가는 과정. 본문

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

X. Agent의 여러 요소를 하나의 class Agent로 묶어가는 과정.

아톨 2026. 8. 24. 20:03
반응형

 

    #-------- 1. Dictionary Class 차이

    # 다음과 같은 경우 딕셔너리를 사용하세요:

    #     단순히 데이터일

    #     JSON 바로 전달할

    #     별도의 규칙을 적용할 필요가 없을

 

    # 다음과 같은 경우 클래스를 사용하세요:

    #     데이터와 동작이 함께 있어야

    #     여러 개의 독립적인 복사본이 필요할

    #     유효하지 않은 상태를 방지해야

 

    #-------- 2. 번째 Agent 클래스(Agent라는 새로운 자료형 만들기): __init__() self

    class Agent:

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

            self.name = name

            self.max_steps = max_steps

            self.history = []

            self.steps = 0  

 

        def add_message(self, role, content):

            self.history.append({"role":role, "content":content})

            return len(self.history)

 

        def budget_left(self):

            return self.max_steps - self.steps

 

    scout = Agent("Scout", max_steps=3)

    scout.add_message("user","송장의 총금액을 확인하세요.")

    scout.steps += 1

 

    print(scout.name, "", scout.budget_left(), "단계가 남았습니다.")

    print("history: ", scout.history)

 

    #-------- 3. 여러 개의 독립적인 Agent: 객체마다 독립적인 상태 유지

    class Agent:

        def __init__(self, name):

            self.name = name

            self.history = []

 

        def add_message(self, role, content):

            self.history.append({"role":role, "content":content})

 

    support = Agent("support")

    research = Agent("research")

 

    support.add_message("user", " 주문서는 어디에 있죠?")

    support.add_message("user", " 보고서를 요약해주세요.")

    research.add_message("assistant", "세개의 주요사항을 찾았습니다.")

 

    print(support.name, len(support.history))

    print(research.name, len(research.history))

 

    #-------- 4. 매우 중요한 Class 변수 vs Instance 변수 차이

    class Bad:

        history = []

 

        def add(self, item):

            self.history.append(item)

 

    class Good:

        def __init__(self):

            self.history = []

 

        def add(self, item):

            self.history.append(item)

 

    a, b = Bad(), Bad()

    a.add("one")

    b.add("two")

    print("Bad: ", a.history, b.history)

 

    c, d = Good(), Good()

    c.add("one")

    d.add("two")

    print("Good: ", c.history, d.history)

 

    #-------- 5. __repr__()

    class Agent:

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

            self.name=name

            self.max_steps=max_steps

            self.steps=0

            self.history=[]

 

        def __repr__(self):

            return f"Agent(name={self.name!r}, steps={self.steps}/{self.max_steps}, msgs={len(self.history)})"

 

    a = Agent("Scout")

    a.steps = 2

    a.history = [{"role":"user", "content":"안녕!"}]

    print(a)

    print([a, Agent("Backup", max_steps=2)])

 

    #-------- 6. dataclass

    from dataclasses import dataclass, field

 

    @dataclass

    class ToolResult:

        name:str

        ok:bool

        output:str=""

        meta:dict = field(default_factory=dict)

 

    r1 = ToolResult("search", True, "3 matches", {"ms":150})

    r2 = ToolResult("read_file", False)

 

    print(r1)

    print(r2)

    print(r1.output, "|", r1.meta["ms"], "ms")

    print("both empty by default: ", r2.meta, r2.output=="")

 

    #-------- 7. 진짜 Agent 가까운 Agent클래스: 작은 Agent 설계도

    class Agent:

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

            self.name=name

            self.tools=tools

            self.max_steps=max_steps

            self.steps=0    

            self.history=[]

 

        def __repr__(self):

            return f"Agent({self.name!r}, steps={self.steps}/{self.max_steps})"

 

        def add_message(self, role, content):

            self.history.append({"role":role, "content":content})

 

        def can_continue(self):

            return self.steps < self.max_steps

 

        def run_tool(self, name, args):

            self.steps +=1

 

            if name not in self.tools:

                result = f"unknown tool '{name}'"  

            else:

                try:

                    result = str(self.tools[name](**args))

                except Exception as e:

                    result = f"tool failed: {type(e).__name__}: {e}"

 

            self.add_message("tool", f"{name} -> {result}")

            return result

 

        def reset(self):

            self.steps=0

            self.history=[]

 

        def transcript(self):

            return "\n".join(f"{m['role']:>9} | {m['content']}" for m in self.history)

 

    def calculator(expr):

        return eval(expr)

 

    def greet(name):

        return f"안녕, {name}!"

 

    bot = Agent("Scout", tools={"calculator":calculator, "greet":greet}, max_steps=3)

    bot.add_message("user", "25 곱하기 4 구하세요, 그리고 스티븐에게 인사하세요.")

 

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

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

    bot.run_tool("emailer", {"to":"스티븐"})

 

    print(bot)

    print("can_continue: ", bot.can_continue())

    print("-"*50)

    print(bot.transcript())

 

1. Dictionary를 쓸 때 vs Class를 쓸 때

주석부터 보겠습니다.

 
# 다음과 같은 경우 딕셔너리를 사용하세요:
    #     단순히 데이터일 때
    #     JSON에 바로 전달할 때
    #     별도의 규칙을 적용할 필요가 없을 때
 

해석하면:

Dictionary는 데이터 자체만 중요할 때 사용한다.

예를 들어:

 
message = {
    "role": "user",
    "content": "안녕하세요"
}
 

이 데이터는 특별한 행동이 필요 없습니다.

role
content
 

라는 데이터를 저장하는 것이 목적입니다.

또 JSON으로 바로 변환하기 좋습니다.

 
json.dumps(message)
 

반면:

 
# 다음과 같은 경우 클래스를 사용하세요:
    #     데이터와 동작이 함께 있어야 할 때
    #     여러 개의 독립적인 복사본이 필요할 때
    #     유효하지 않은 상태를 방지해야 할 때
 

Class는:

데이터와 행동(기능)이 함께 있을 때 사용한다.

예를 들어 Agent는 단순한 데이터가 아닙니다.

Agent에게는:

이름
최대 단계
현재 단계
대화 기록
 

이라는 데이터가 있고,

또:

메시지 추가
툴 실행
계속 실행 가능한지 확인
초기화
대화 기록 출력
 

같은 행동이 있습니다.

그래서 Agent는 Dictionary보다는 Class가 잘 어울립니다.


2. 첫 번째 Agent 클래스

 
class Agent:
 

이제 Agent라는 새로운 자료형을 만드는 것입니다.

Python에는 기본적으로:

int
str
list
dict
 

같은 자료형이 있습니다.

우리가 직접:

Agent
 

라는 새로운 자료형을 만드는 것입니다.


__init__()

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

객체가 만들어질 때 자동으로 실행되는 초기화 함수입니다.

예를 들어:

 
scout = Agent("Scout", max_steps=3)
 

이 실행되면 내부적으로 대략:

 
Agent.__init__(scout, "Scout", 3)
 

처럼 동작합니다.

여기서 self는:

지금 만들어지고 있는 자기 자신(객체)

입니다.


객체 속성 저장

 
self.name = name
 

예:

scout.name
 

Scout
 

 
self.max_steps = max_steps
 

scout.max_steps
 

3
 

 
self.history = []
 

각 Agent마다 자신의 대화 기록을 가집니다.

Scout
 └── history

Research Agent
 └── history

Support Agent
 └── history
 

각각 독립적입니다.


 
self.steps = 0
 

현재 실행 단계를 기록합니다.


add_message()

 
def add_message(self, role, content):
    self.history.append({
        "role":role,
        "content":content
    })
    return len(self.history)
 

예:

 
scout.add_message(
    "user",
    "송장의 총금액을 확인하세요."
)
 

결과:

 
scout.history
 

 
[
    {
        "role": "user",
        "content": "송장의 총금액을 확인하세요."
    }
]
 

왜 self.history인가?

Agent마다 다른 history를 가져야 하기 때문입니다.

scout.history
support.history
research.history
 

각각 독립적으로 존재합니다.


budget_left()

 
def budget_left(self):
    return self.max_steps - self.steps
 

Agent가 앞으로 몇 번 더 실행할 수 있는지 계산합니다.

예:

max_steps = 3
steps = 1
 

그러면:

3 - 1 = 2
 

즉:

 
scout.budget_left()
 

2
 

실행 과정

 
scout = Agent("Scout", max_steps=3)
 

현재 상태:

Scout Agent

name       = Scout
max_steps  = 3
steps      = 0
history    = []
 

 
scout.add_message(...)
 

history
[
  user 메시지
]
 

 
scout.steps += 1
 

steps = 1
 

따라서:

최대 3단계
현재 1단계 사용
남은 2단계
 

입니다.


3. 여러 개의 독립적인 Agent

 
support = Agent("support")
research = Agent("research")
 

두 개의 객체를 만듭니다.

Agent
│
├── support
│
└── research
 

각각:

 
support.history
 

 
research.history
 

를 따로 가지고 있습니다.

 
support.add_message(...)
 

를 해도:

 
research.history
 

에는 영향을 주지 않습니다.

이것이 객체지향의 중요한 개념입니다.

하나의 Class를 이용해 여러 개의 독립적인 객체를 만들 수 있다.


4. 매우 중요한 Class 변수 vs Instance 변수

이 부분은 꼭 이해하시면 좋습니다.

Bad

 
class Bad:
    history = []
 

여기서 history는 Class에 소속되어 있습니다.

즉:

Bad 클래스

history = []
 

하나를 모든 객체가 공유합니다.


 
a, b = Bad(), Bad()
 

두 객체를 만들었습니다.

        Bad
         │
    history []
       /   \
      /     \
     a       b
 

둘 다 같은 history를 봅니다.


 
a.add("one")
 

history = ["one"]
 

그 다음:

 
b.add("two")
 

history = ["one", "two"]
 

그래서:

 
print(a.history)
 

["one", "two"]
 

그리고:

 
print(b.history)
 

도:

["one", "two"]
 

입니다.

왜냐하면 공유하기 때문입니다.


Good

 
class Good:
    def __init__(self):
        self.history = []
 

이번에는 객체를 만들 때마다 새로운 List를 만듭니다.

c
└── history []

d
└── history []
 

 
c.add("one")
d.add("two")
 

결과:

c.history = ["one"]

d.history = ["two"]
 

입니다.


Agent에서는 반드시 Good 방식

Agent는 각각 독립적인 대화를 해야 합니다.

Agent A
 └── 자신의 history

Agent B
 └── 자신의 history
 

따라서:

 
self.history = []
 

를 __init__() 안에 넣는 것입니다.

이 부분은 실제 Agent 개발에서도 상당히 중요합니다.


5. __repr__()

 
def __repr__(self):
    return f"Agent(name={self.name!r}, steps={self.steps}/{self.max_steps}, msgs={len(self.history)})"
 

__repr__()은:

객체를 출력할 때 어떻게 보여줄 것인가

를 정의하는 특수 메서드입니다.


보통:

 
print(a)
 

를 하면 객체는 기본적으로 이상한 형태로 나옵니다.

예:

<__main__.Agent object at 0x000001A3...>
 

하지만 __repr__()을 만들면:

Agent(name='Scout', steps=2/5, msgs=1)
 

처럼 사람이 이해하기 쉽게 나옵니다.


!r

 
self.name!r
 

은 Python의 repr() 표현을 사용합니다.

예:

 
name = "Scout"
 
 
print(f"{name}")
 

Scout
 

하지만:

 
print(f"{name!r}")
 

'Scout'
 

문자열이라는 것이 명확하게 보입니다.


List에서도 잘 작동

 
print([a, Agent("Backup", max_steps=2)])
 

각 객체를 출력할 때도:

 
__repr__()
 

가 사용됩니다.

예:

[
 Agent(name='Scout', steps=2/5, msgs=1),
 Agent(name='Backup', steps=0/2, msgs=0)
]
 

디버깅할 때 매우 좋습니다.


6. dataclass

 
from dataclasses import dataclass, field
 

dataclass는:

데이터를 저장하는 클래스를 쉽게 만들기 위한 도구

입니다.


일반 클래스로 쓰면:

 
class ToolResult:
    def __init__(self, name, ok, output="", meta=None):
        self.name = name
        self.ok = ok
        self.output = output
        self.meta = meta if meta else {}
 

이렇게 써야 합니다.

하지만 dataclass:

 
@dataclass
class ToolResult:
    name: str
    ok: bool
    output: str = ""
    meta: dict = field(default_factory=dict)
 

훨씬 간단합니다.


ToolResult 만들기

 
r1 = ToolResult(
    "search",
    True,
    "3 matches",
    {"ms":150}
)
 

결과:

name   = search
ok     = True
output = 3 matches
meta   = {"ms":150}
 

두 번째:

 
r2 = ToolResult("read_file", False)
 

기본값이 적용됩니다.

name   = read_file
ok     = False
output = ""
meta   = {}
 

왜 default_factory=dict를 사용할까?

매우 중요합니다.

잘못 쓰면:

 
@dataclass
class ToolResult:
    meta: dict = {}
 

이런 식으로 쓰고 싶을 수 있습니다.

하지만 mutable 객체인 Dictionary/List는 공유 문제가 발생할 수 있습니다.

그래서:

 
field(default_factory=dict)
 

를 사용합니다.

의미:

객체를 만들 때마다 새로운 빈 Dictionary를 만들어라.

즉:

r1.meta → {}

r2.meta → {}
 

각각 독립적입니다.


7. 마지막: 진짜 Agent에 가까운 클래스

이제 지금까지 배운 것을 모두 합칩니다.

 
class Agent:
 

Agent는 다음을 가지고 있습니다.

Agent
│
├── name
├── tools
├── max_steps
├── steps
├── history
│
├── add_message()
├── can_continue()
├── run_tool()
├── reset()
└── transcript()
 

이제 상당히 Agent답습니다.


초기화

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

Agent를 만들려면:

이름
사용 가능한 도구
최대 실행 단계
 

가 필요합니다.


 
self.tools = tools
 

예:

 
{
    "calculator": calculator,
    "greet": greet
}
 

즉:

도구 이름 → 실제 함수
 

연결입니다.


can_continue()

 
def can_continue(self):
    return self.steps < self.max_steps
 

예:

steps = 2
max_steps = 3
 

True
 

하지만:

steps = 3
max_steps = 3
 

False
 

Agent Loop에서:

 
while agent.can_continue():
 

같은 방식으로 사용할 수 있습니다.


가장 중요한 run_tool()

 
def run_tool(self, name, args):
 

Agent가 Tool을 실행하는 핵심 메서드입니다.


Step 증가

 
self.steps += 1
 

Tool을 한 번 실행할 때마다:

steps + 1
 

Tool 존재 확인

 
if name not in self.tools:
 

예:

 
name = "emailer"
 

하지만:

 
self.tools = {
    "calculator": calculator,
    "greet": greet
}
 

에는 없습니다.

그러면:

 
result = f"unknown tool '{name}'"
 

unknown tool 'emailer'
 

Tool 실행

 
result = str(self.tools[name](**args))
 

이 부분은 매우 중요합니다.

예:

 
name = "calculator"

args = {
    "expr": "25*4"
}
 

그러면:

 
self.tools[name]
 

 
calculator
 

그리고:

 
(**args)
 

 
calculator(expr="25*4")
 

결과:

100
 

예외 처리

 
except Exception as e:
 

Tool 실행 중 문제가 발생하면 Agent 전체가 죽지 않게 합니다.

예:

 
calculator(expr="잘못된 계산")
 

오류 발생.

그러면:

 
result = f"tool failed: {type(e).__name__}: {e}"
 

예:

tool failed: SyntaxError: invalid syntax
 

Tool 결과를 History에 저장

 
self.add_message(
    "tool",
    f"{name} -> {result}"
)
 

예:

tool | calculator -> 100
 

이제 Agent History는:

user
 ↓
assistant/tool 선택
 ↓
tool 결과
 ↓
다음 판단
 

구조로 발전할 수 있습니다.


reset()

 
def reset(self):
    self.steps=0
    self.history=[]
 

Agent 상태를 초기화합니다.

기존 대화 삭제
단계 초기화
 

transcript()

 
def transcript(self):
    return "\n".join(
        f"{m['role']:>9} | {m['content']}"
        for m in self.history
    )
 

History를 사람이 보기 좋은 대화 기록으로 바꿉니다.

예:

     user | 25 곱하기 4를 구하세요
     tool | calculator -> 100
     tool | greet -> 안녕, 스티븐!
     tool | emailer -> unknown tool 'emailer'
 

:>9

 
{m['role']:>9}
 

오른쪽 정렬 9칸입니다.

예:

     user
assistant
     tool
 

세로가 깔끔하게 맞습니다.


실제 Tool 함수

Calculator

 
def calculator(expr):
    return eval(expr)
 

예:

 
calculator("25*4")
 

100
 

⚠️ 참고로 실제 서비스에서는 사용자나 모델이 만든 문자열을 eval()로 직접 실행하는 것은 위험할 수 있습니다. 지금은 학습용으로 이해하시면 됩니다.


greet

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

예:

 
greet("스티븐")
 

안녕, 스티븐!
 

Agent 생성

 
bot = Agent(
    "Scout",
    tools={
        "calculator":calculator,
        "greet":greet
    },
    max_steps=3
)
 

현재 상태:

Agent Scout

steps = 0
max_steps = 3

tools
├── calculator
└── greet

history = []
 

사용자 메시지

 
bot.add_message(
    "user",
    "25 곱하기 4를 구하세요, 그리고 스티븐에게 인사하세요."
)
 

History:

user
│
└── 25 곱하기 4를 구하세요...
 

첫 번째 Tool

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

내부 과정:

steps
0 → 1

calculator 존재?
↓ YES

calculator(expr="25*4")
↓
100

history에 저장
 

두 번째 Tool

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

과정:

steps
1 → 2

greet(name="스티븐")
↓
안녕, 스티븐!

history 저장
 

세 번째 Tool

 
bot.run_tool(
    "emailer",
    {"to":"스티븐"}
)
 

하지만:

TOOLS

calculator ⭕
greet ⭕

emailer ❌
 

따라서:

unknown tool 'emailer'
 

을 결과로 History에 기록합니다.


최종 Step

3개의 Tool을 실행했으므로:

steps = 3
max_steps = 3
 

따라서:

 
bot.can_continue()
 

 
3 < 3
 

False
 

전체 흐름

이번 마지막 Agent는 이렇게 동작합니다.

                 ┌─────────────┐
                 │    Agent    │
                 └──────┬──────┘
                        │
        ┌───────────────┼────────────────┐
        │               │                │
        ▼               ▼                ▼
     history          tools            steps
        │               │                │
        ▼               ▼                ▼
   대화 기록       calculator        실행 제한
                  greet
        │
        ▼
   transcript()
 

그리고 Tool 실행 시:

run_tool()
    │
    ▼
steps += 1
    │
    ▼
Tool 존재?
    │
 ┌──┴─────┐
 │        │
YES       NO
 │        │
 ▼        ▼
실행     unknown tool
 │        │
 └───┬────┘
     ▼
history 기록
     ▼
결과 반환
 

⭐ 이번 코드에서 가장 중요한 깨달음

지금까지는 우리가 이런 것들을 따로 만들었습니다.

history
run_tool()
steps
max_steps
tools
transcript()
reset()
 

하지만 이제:

 
class Agent:
 

안에 모두 넣었습니다.

즉:

이전 방식

history 따로
tools 따로
steps 따로
함수 따로
 

에서:

Agent
│
├── history
├── tools
├── steps
├── run_tool()
├── reset()
└── transcript()
 

로 바뀐 것입니다.

이것이 바로 객체지향 프로그래밍(OOP)의 가장 중요한 장점입니다.

관련된 데이터와 행동을 하나의 객체로 묶는다.

그리고 지금 여러분이 공부하는 흐름을 보면, 다음 단계는 아주 자연스럽습니다.

Python 기본 문법
      ↓
함수
      ↓
Dictionary / JSON
      ↓
History
      ↓
Tool Dispatcher
      ↓
Agent Loop
      ↓
Error / Retry
      ↓
File / Logging
      ↓
Class Agent   ← 지금 여기
      ↓
실제 OpenAI API
      ↓
Model → Tool 선택
      ↓
Tool Result → Model
      ↓
Final Answer
 

즉, 지금부터는 단순한 Python 문법 공부에서 실제 AI Agent 구조를 조립하는 단계로 넘어왔다고 보시면 됩니다.

반응형