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

VI. Python의 반복문과 자료구조를 이용해 AI Agent의 실제 실행 흐름을 만드는 과정

아톨 2026. 8. 23. 13:41
반응형

 

    #-------- 1. history 순회 + range()

    history = [

        {"role":"user","content":"제품번호 5001 재고를 확인해라."},

        {"role":"assistant", "content":"지금 찾아보고 있어요."},

        {"role":"user", "content":" 제품이 어느 창고로부터 배송되는지 확인해라."},

    ]

 

    for message in history:

        print(f"{message['role']}: {message['content']}")

 

    print()

    for i in range(3):

        print(f"step {i}")

 

    #-------- 2. enumerate() + zip()

    tool_names = ["search", "calculator", "read_file"]

    durations = [1.4, 0.02, 0.31]  

 

    for position, name in enumerate(tool_names, start=1):

        print(f"{position}. {name}")

 

    print()

    for name, seconds in zip(tool_names, durations):

        print(f"{name:<12} took {seconds:>5.2f}s")

 

    #-------- 3. while + Agent 종료 조건

    step = 0

    max_steps = 4

    done = False

 

    while not done and step < max_steps:

        step += 1

        print(f"Step {step}: 모델을 호출하세요.")

        if step == 3:

            done = True

 

    if done:

        print(f"{step} 단계 이후에 자연스럽게 종료되었습니다.")

    else:

        print(f"종료되지 않고, {max_steps} 단계 제한에 도달하였습니다.")

 

    #-------- 4. continue + break

    results = [

        {"tool": "search", "ok":True, "data":"3 일치"},

        {"tool": "search", "ok":False, "data": "시간 초과"},

        {"tool": "finish", "ok":True, "data": "답변 준비 완료"},

        {"tool":"search", "ok":True, "data":"도달하지 못함"},

    ]

 

    for r in results:

        if not r["ok"]:

            print(f"실패한 {r['tool']}/ 건너뜁니다.")

            continue

        if r["tool"] == "finish":

            print(f"종료: {r['data']}")

            break

        print(f"{r['tool']}: {r['data']} 진행되었습니다.")

       

    #-------- 5. List Comprehension으로 history 분석

    history=[

        {"role":"user","content":"안녕"},

        {"role":"assistant","content":"안녕하세요"},

        {"role":"user", "content":"재고 확인해"},

        {"role":"tool","content":"12"},

    ]

 

    user_texts = [m["content"] for m in history if m["role"]=="user"]

    roles = [m["role"] for m in history]

    lengths = [len(m["content"]) for m in history]

 

    print(user_texts)

    print(roles)

    print(lengths)

    print(" 글자수: ", sum(lengths),"")

 

    #-------- 6. 가짜 모델(fake_model) + Tool + Agent Loop

    MAX_STEPS = 5

    SCRIPT = [

        {"tool":"search", "args":{"query":"unit price 검색하세요."}},

        {"tool":"calculator", "args":{"expr":"24 * 7 계산하세요."}},

        {"tool":"finish", "args":{"answer":"이제 답을 드려요. 합계는 168입니다.."}},

    ]

 

    def fake_model(history):

        turn = len([m for m in history if m["role"]=="assistant"])

        if turn < len(SCRIPT):

            return SCRIPT[turn]

        return {"tool":"finish", "args":{"answer":"아이디어가 없어요."}}

 

    def run_tool(name, args):

        if name=="search":

            return f"{args['query']} 대해 3개의 결과를 발견했어요."

        if name == "calculator":

            return f"{args['expr']} = 168"

        return "unknown tool"

 

    history = [{"role":"user", "content":"24개에 대한 총액이 얼마야? "}]

    step = 0

    answer = None

 

    while answer is None and step < MAX_STEPS:

        step += 1

        call = fake_model(history)

        history.append({"role":"assistant", "content":str(call)})

        print(f"Step {step}: 모델이 {call['tool']} 선택했습니다.")

 

        if call["tool"] == "finish":

            answer = call["args"]["answer"]

            break

 

        observation = run_tool(call["tool"], call["args"])

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

        print(f"    observed: {observation}")

 

    print("-"*50)

    print("answer: ", answer if answer else "step limit reached")

    print("message in history: ", len(history))

 

1. 첫 번째 코드

history 순회와 range()

 
history = [
    {"role":"user","content":"제품번호 5001의 재고를 확인해라."},
    {"role":"assistant", "content":"지금 찾아보고 있어요."},
    {"role":"user", "content":"그 제품이 어느 창고로부터 배송되는지 확인해라."},
]
 

이 history는 이전에 배운 Agent의 대화 기록입니다.

구조를 보면:

history
│
├── 0번째 메시지
│     role: user
│     content: 제품번호 5001의 재고를 확인해라.
│
├── 1번째 메시지
│     role: assistant
│     content: 지금 찾아보고 있어요.
│
└── 2번째 메시지
      role: user
      content: 그 제품이 어느 창고로부터 배송되는지 확인해라.
 

즉:

 
history[0]
 

은:

 
{"role":"user","content":"제품번호 5001의 재고를 확인해라."}
 

입니다.


for message in history

 
for message in history:
    print(f"{message['role']}: {message['content']}")
 

history 안에 있는 메시지를 하나씩 꺼냅니다.

첫 번째 반복:

 
message = {
    "role":"user",
    "content":"제품번호 5001의 재고를 확인해라."
}
 

따라서:

 
message["role"]
 

user
 

그리고:

 
message["content"]
 

제품번호 5001의 재고를 확인해라.
 

출력:

user: 제품번호 5001의 재고를 확인해라.
assistant: 지금 찾아보고 있어요.
user: 그 제품이 어느 창고로부터 배송되는지 확인해라.
 

range(3)

 
for i in range(3):
    print(f"step {i}")
 

range(3)은:

0, 1, 2
 

입니다.

따라서:

step 0
step 1
step 2
 

가 출력됩니다.

중요한 점

Python은 보통 숫자를 0부터 시작합니다.

 
range(3)
 

은:

0
1
2
 

이지:

1
2
3
 

이 아닙니다.


2. 두 번째 코드

enumerate()와 zip()

 
tool_names = ["search", "calculator", "read_file"]
durations = [1.4, 0.02, 0.31]
 

두 개의 리스트가 있습니다.

tool_names                durations

search          ←→        1.4
calculator      ←→        0.02
read_file       ←→        0.31
 

2-1. enumerate()

 
for position, name in enumerate(tool_names, start=1):
    print(f"{position}. {name}")
 

enumerate()는:

리스트의 값 + 번호

를 함께 줍니다.

결과:

1. search
2. calculator
3. read_file
 

내부적으로 생각하면

대략 이렇게 됩니다.

position = 1
name = search

position = 2
name = calculator

position = 3
name = read_file
 

start=1

 
enumerate(tool_names, start=1)
 

때문에 번호가:

1, 2, 3
 

으로 시작합니다.

만약:

 
enumerate(tool_names)
 

라고 하면:

0. search
1. calculator
2. read_file
 

가 됩니다.


2-2. zip()

 
for name, seconds in zip(tool_names, durations):
 

zip()은 두 개의 리스트를 묶어줍니다.

tool_names        durations

search            1.4
calculator        0.02
read_file         0.31
 

결과적으로:

("search", 1.4)
("calculator", 0.02)
("read_file", 0.31)
 

처럼 됩니다.


출력

 
print(f"{name:<12} took {seconds:>5.2f}s")
 

예:

search       took  1.40s
calculator   took  0.02s
read_file    took  0.31s
 

여기서:

 
{name:<12}
 

는 12칸 확보 후 왼쪽 정렬.

 
{seconds:>5.2f}
 

는:

  • > 오른쪽 정렬
  • 5 최소 5칸
  • .2f 소수점 둘째 자리까지

입니다.

예:

1.4
 

1.40
 

3. 세 번째 코드

while과 Agent 종료 조건

 
step = 0
max_steps = 4
done = False
 

현재 상태:

step = 0
최대 단계 = 4
완료 여부 = False
 

핵심 while 조건

 
while not done and step < max_steps:
 

이 부분이 아주 중요합니다.

Agent Loop에서도 거의 같은 구조를 사용합니다.

의미는:

아직 완료되지 않았고
그리고
최대 단계에 도달하지 않았다면
계속 반복해라
 

그림으로 보면:

done = False?
      │
      Yes
      ↓
step < max_steps?
      │
      Yes
      ↓
반복 계속
 

Step 증가

 
step += 1
 

처음:

step = 0
 

step = 1
 

모델 호출

 
print(f"Step {step}: 모델을 호출하세요.")
 

실제 Agent라면 나중에:

 
response = call_model(history)
 

같은 코드가 들어갈 자리입니다.

현재는 단순히:

모델을 호출한다고 가정
 

하는 예제입니다.


Step 3에서 완료

 
if step == 3:
    done = True
 

실행 흐름:

Step 1 → done False
Step 2 → done False
Step 3 → done True
 

다음 반복 시:

 
not done
 

은:

not True
 

False
 

따라서 while 종료.


결과

 
if done:
    print(f"{step} 단계 이후에 자연스럽게 종료되었습니다.")
else:
    print(...)
 

결과:

3 단계 이후에 자연스럽게 종료되었습니다.
 

Agent 관점에서 매우 중요

실제 Agent는:

Step 1
LLM 호출
↓
Tool 실행

Step 2
LLM 호출
↓
Tool 실행

Step 3
LLM 호출
↓
최종 답변
↓
done = True
↓
종료
 

또는:

Step 1
Step 2
Step 3
Step 4
↓
아직 답 없음
↓
MAX_STEPS 도달
↓
강제 종료
 

두 가지 종료 방법이 있습니다.

정상 종료

done = True
 

안전 종료

step >= max_steps
 

입니다.


4. 네 번째 코드

continue와 break

 
results = [
    {"tool": "search", "ok":True, "data":"3개 일치"},
    {"tool": "search", "ok":False, "data": "시간 초과"},
    {"tool": "finish", "ok":True, "data": "답변 준비 완료"},
    {"tool":"search", "ok":True, "data":"도달하지 못함"},
]
 

여러 Tool 실행 결과가 있습니다.


첫 번째 결과

 
{"tool": "search", "ok":True, "data":"3개 일치"}
 

정상입니다.

따라서:

search: 3개 일치 진행되었습니다.
 

4-1. continue

 
if not r["ok"]:
    print(f"실패한 {r['tool']}를/을 건너뜁니다.")
    continue
 

두 번째 결과:

 
{
    "tool":"search",
    "ok":False,
    "data":"시간 초과"
}
 

여기서:

 
not r["ok"]
 

 
not False
 

True
 

따라서:

 
continue
 

가 실행됩니다.

continue는:

현재 반복의 나머지 작업을 건너뛰고 다음 반복으로 가라.

입니다.


흐름

현재 search 실패
      ↓
"건너뜁니다" 출력
      ↓
continue
      ↓
다음 결과로 이동
 

4-2. break

 
if r["tool"] == "finish":
    print(f"종료: {r['data']}")
    break
 

세 번째 결과:

 
{
    "tool":"finish",
    "ok":True,
    "data":"답변 준비 완료"
}
 

따라서:

종료: 답변 준비 완료
 

출력 후:

 
break
 

실행.

break는:

반복문 자체를 완전히 종료해라.

입니다.

따라서 네 번째:

 
{"tool":"search", "ok":True, "data":"도달하지 못함"}
 

는 실행되지 않습니다.


continue와 break 차이

continue
│
└── 이번 반복만 건너뛰고
    다음 반복 계속


break
│
└── 반복문 전체 종료
 

Agent에서:

Tool 실패
↓
continue
↓
다음 작업


FINISH
↓
break
↓
Agent Loop 종료
 

라고 생각하면 됩니다.


5. 다섯 번째 코드

List Comprehension으로 history 분석

 
history=[
    {"role":"user","content":"안녕"},
    {"role":"assistant","content":"안녕하세요"},
    {"role":"user", "content":"재고 확인해"},
    {"role":"tool","content":"12개"},
]
 

사용자 메시지만 추출

 
user_texts = [
    m["content"]
    for m in history
    if m["role"]=="user"
]
 

결과:

 
[
    "안녕",
    "재고 확인해"
]
 

일반 for문으로 풀면

 
user_texts = []

for m in history:
    if m["role"] == "user":
        user_texts.append(m["content"])
 

입니다.

List Comprehension은 이것을 짧게 쓴 것입니다.


모든 role 추출

 
roles = [m["role"] for m in history]
 

결과:

 
[
    "user",
    "assistant",
    "user",
    "tool"
]
 

메시지 길이 계산

 
lengths = [len(m["content"]) for m in history]
 

예:

안녕       → 2
안녕하세요 → 5
재고 확인해 → 6
12개       → 3
 

대략 이런 식입니다.

그리고:

 
sum(lengths)
 

는 모두 더합니다.

2 + 5 + 6 + 3
 

16
 

실제 Agent에서 왜 중요할까?

LLM은 대화가 길어질수록 history가 계속 늘어납니다.

history

user
assistant
tool
assistant
tool
assistant
user
...
 

따라서 Agent는 종종:

 
len(history)
 

또는:

 
sum(len(m["content"]) for m in history)
 

같은 방식으로 대화량을 관리합니다.

실제 LLM에서는 이것보다 정확한 token 계산을 해야 합니다.


6. 마지막 코드

⭐ 지금까지 배운 내용을 합친 Mini Agent

이 부분이 가장 중요합니다.


6-1. 최대 Step

 
MAX_STEPS = 5
 

Agent가 최대 5번까지만 생각하고 행동하도록 제한합니다.

무한 반복 방지
 

입니다.


6-2. Agent의 미리 작성된 행동 계획

 
SCRIPT = [
    {"tool":"search", "args":{"query":"unit price를 검색하세요."}}, 
    {"tool":"calculator", "args":{"expr":"24 * 7을 계산하세요."}}, 
    {"tool":"finish", "args":{"answer":"이제 답을 드려요. 합계는 168입니다.."}}, 
]
 

이것은 실제 AI가 아닙니다.

Agent가 어떤 행동을 할지 미리 정해놓았습니다.

순서:

1단계
search

↓

2단계
calculator

↓

3단계
finish
 

입니다.


6-3. fake_model()

 
def fake_model(history):
 

이 함수는 실제 OpenAI API 대신 사용하는 가짜 AI 모델입니다.


Assistant 메시지 개수 계산

 
turn = len([
    m for m in history
    if m["role"]=="assistant"
])
 

현재 history에서 assistant 메시지가 몇 개인지 셉니다.

예를 들어 처음에는:

 
history = [
    {
        "role":"user",
        "content":"24개에 대한 총액이 얼마야?"
    }
]
 

assistant 메시지가 없습니다.

따라서:

turn = 0
 

첫 번째 행동 반환

 
if turn < len(SCRIPT):
    return SCRIPT[turn]
 

현재:

turn = 0
 

따라서:

 
SCRIPT[0]
 

반환.

즉:

 
{
    "tool":"search",
    "args":{
        "query":"unit price를 검색하세요."
    }
}
 

두 번째 호출

첫 번째 호출 후:

 
history.append({
    "role":"assistant",
    "content":str(call)
})
 

assistant 메시지가 하나 생깁니다.

따라서 다음 호출에서는:

turn = 1
 

그리고:

 
SCRIPT[1]
 

을 반환합니다.

calculator
 

입니다.


세 번째 호출

assistant 메시지가 2개.

turn = 2
 

따라서:

 
SCRIPT[2]
 

finish
 

를 반환합니다.


7. run_tool()

 
def run_tool(name, args):
 

Agent가 선택한 Tool을 실제로 실행합니다.


search

 
if name=="search":
    return f"{args['query']}에 대해 3개의 결과를 발견했어요."
 

예:

 
args = {
    "query":"unit price를 검색하세요."
}
 

결과:

unit price를 검색하세요.에 대해 3개의 결과를 발견했어요.
 

calculator

 
if name == "calculator":
    return f"{args['expr']} = 168"
 

예:

 
{
    "expr":"24 * 7을 계산하세요."
}
 

결과:

24 * 7을 계산하세요. = 168
 

8. Agent 초기 상태

 
history = [
    {
        "role":"user",
        "content":"24개에 대한 총액이 얼마야?"
    }
]

step = 0
answer = None
 

현재 상태:

history
│
└── user
    "24개에 대한 총액이 얼마야?"

step = 0

answer = None
 

9. ⭐ Agent Loop

 
while answer is None and step < MAX_STEPS:
 

이 코드가 Mini Agent의 심장입니다.

의미:

아직 최종 답변이 없고
그리고
최대 Step에 도달하지 않았다면
계속 Agent를 실행한다.
 

10. Step 1

 
step += 1
 

step = 1
 

모델 호출

 
call = fake_model(history)
 

현재 assistant 메시지 수:

0개
 

따라서:

 
SCRIPT[0]
 

반환:

 
{
    "tool":"search",
    "args":{
        "query":"unit price를 검색하세요."
    }
}
 

모델의 결정 저장

 
history.append({
    "role":"assistant",
    "content":str(call)
})
 

history는 이제:

user
  ↓
24개에 대한 총액이 얼마야?

assistant
  ↓
search Tool을 사용하겠다
 

출력

Step 1: 모델이 search을 선택했습니다.
 

11. finish인지 확인

 
if call["tool"] == "finish":
 

현재:

search
 

이므로 False.


12. Tool 실행

 
observation = run_tool(
    call["tool"],
    call["args"]
)
 

즉:

 
run_tool(
    "search",
    {"query":"unit price를 검색하세요."}
)
 

결과:

unit price를 검색하세요.에 대해 3개의 결과를 발견했어요.
 

13. Tool 결과를 history에 저장

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

현재 history:

USER
"24개에 대한 총액이 얼마야?"

↓

ASSISTANT
"search를 사용하겠다"

↓

TOOL
"3개의 결과를 발견했어요."
 

이제 다음 Step에서 모델은 이 Tool 결과를 보고 판단할 수 있습니다.

실제 Agent의 핵심입니다.


14. Step 2

이번에는 assistant 메시지가 하나 있습니다.

따라서:

 
turn = 1
 

 
SCRIPT[1]
 

 
{
    "tool":"calculator",
    "args":{
        "expr":"24 * 7을 계산하세요."
    }
}
 

계산기 실행:

24 * 7 = 168
 

그리고 Tool 결과를 history에 저장합니다.


15. Step 3

이번에는 assistant 메시지가 두 개입니다.

turn = 2
 

따라서:

 
SCRIPT[2]
 

 
{
    "tool":"finish",
    "args":{
        "answer":"이제 답을 드려요. 합계는 168입니다.."
    }
}
 

16. FINISH 처리

 
if call["tool"] == "finish":
    answer = call["args"]["answer"]
    break
 

여기가 Agent의 종료 지점입니다.

finish 발견
      ↓
answer에 최종 답 저장
      ↓
break
      ↓
while 종료
 

17. 전체 Agent 실행 흐름

이 코드를 그림으로 보면 아주 명확합니다.

┌────────────────────────────┐
│ 사용자 질문                 │
│ "24개에 대한 총액이 얼마야?" │
└──────────────┬─────────────┘
               ↓
       ┌──────────────┐
       │ Agent Loop   │
       └──────┬───────┘
              ↓
        Step 1
              ↓
       fake_model()
              ↓
         search 선택
              ↓
        run_tool()
              ↓
         검색 결과
              ↓
        history 저장
              ↓
        Step 2
              ↓
       fake_model()
              ↓
       calculator 선택
              ↓
        run_tool()
              ↓
          168
              ↓
        history 저장
              ↓
        Step 3
              ↓
       fake_model()
              ↓
         finish
              ↓
       answer 저장
              ↓
            종료
 

18. 마지막 출력

 
print("-"*50)
 

구분선 출력:

--------------------------------------------------
 

 
print("answer: ", answer if answer else "step limit reached")
 

answer가 있으면:

answer: 이제 답을 드려요. 합계는 168입니다..
 

없으면:

answer: step limit reached
 

를 출력합니다.

이 역시 삼항 연산자입니다.

풀어쓰면:

 
if answer:
    print("answer:", answer)
else:
    print("answer: step limit reached")
 

입니다.


history 길이

 
print("message in history: ", len(history))
 

최종 history를 세어보면:

1. user

2. assistant → search 선택
3. tool → search 결과

4. assistant → calculator 선택
5. tool → calculator 결과

6. assistant → finish 선택
 

따라서:

message in history: 6
 

이 됩니다.


🌟 이번 코드에서 가장 중요한 핵심

마지막 코드는 지금까지 공부하신 내용을 합친 Mini Agent의 원형입니다.

사용자 질문
      ↓
history 생성
      ↓
while loop 시작
      ↓
모델 호출
      ↓
모델이 Tool 선택
      ↓
assistant의 결정 저장
      ↓
finish인가?
 ┌──────┴──────┐
 Yes           No
 ↓             ↓
answer 저장     Tool 실행
 ↓             ↓
break          Tool 결과(observation)
 ↓             ↓
종료           history에 저장
                    ↓
                다시 모델 호출
 

이 흐름이 바로 앞으로 실제 OpenAI API를 연결해서 만들게 될 Agent의 기본 구조입니다.

특히 지금까지 배우신 코드와 연결하면 다음 역할로 이해하시면 좋습니다.

구성요소역할
history Agent의 기억
fake_model() AI 모델의 판단
SCRIPT 가짜 AI의 미리 정해진 행동
run_tool() 실제 Tool 실행
observation Tool 실행 결과
while Agent의 반복 사고
MAX_STEPS 무한 반복 방지
finish 작업 완료 신호
answer 최종 답변

한 문장으로 정리하면

이번 코드는 AI Agent가 "생각(모델 호출) → 행동(Tool 선택) → 관찰(Tool 결과) → 다시 생각 → 최종 답변"하는 전체 Agent Loop를 Python으로 직접 구현한 코드입니다.

 

반응형