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

III. 대화 기록(history)을 어떻게 Python으로 관리하는가

아톨 2026. 8. 22. 19:22
반응형

   

    history = [

        {"role":"system", "content":"너는 도움을 주는 에이전트야."},

        {"role":"user", "content":" 주문에 대한 배송이 늦네."},

        {"role":"assistant", "content":"내가 주문을 확인해 볼께."},

    ]

 

    print(history[1]["content"])

    print("Turns: ", len(history))

 

    for msg in history:

        print(f"[{msg['role']:^9}] {msg['content']}")

 

    #--------

    response = {

        "id":"steven_20",

        "usage":{"input_token":1200, "output_token":605},

        "content":[{"type":"text","text":"주문배송이 월요일에 시작되었습니다."},]

    }

 

    print(response["usage"]["output_token"])

    print(response["content"][0]["text"])

 

    for key, value in response["usage"].items():

        print(f"{key} = {value}")

 

    #--------

    history = []

 

    def add(role, content):

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

        # print(history)

 

    def trim(max_turns):

        system = [m for m in history if m["role"]=="system"]

        rest = [m for m in history if m["role"]!="system"]  

        # print(system)

        # print(rest)

        return system + rest[-max_turns:]  

 

    def show(messages):

        for m in messages:

            role = m["role"].upper()

            print(f"{role:<9} | {m['content']}")

 

    add("system","너는 예약 에이전트야.")

    add("user", "뉴욕까지 가는 항공기를 찾아줘.")

    add("assistant", "지금 비행편을 확인합니다.")

    add("user", ", 저녁에 출발하는 비행기만 부탁해.")

    add("assistant", " 개의 항공편을 찾았습니다.")

 

    print(f"Stored messages: {len(history)}")

    print("-"*50)

    show(trim(max_turns=2))

 

1. 첫 번째 코드: history

 
history = [

    {"role":"system", "content":"너는 도움을 주는 에이전트야."},

    {"role":"user", "content":"내 주문에 대한 배송이 늦네."},

    {"role":"assistant", "content":"내가 그 주문을 확인해 볼께."},

]
 

가장 먼저 이것부터 이해해야 합니다.

history는 list(리스트)입니다.

그런데 리스트 안에 dictionary(딕셔너리)가 들어 있습니다.

구조를 그림으로 보면:

history
  │
  ├── [0] → dictionary
  │          role    = system
  │          content = 너는 도움을 주는 에이전트야.
  │
  ├── [1] → dictionary
  │          role    = user
  │          content = 내 주문에 대한 배송이 늦네.
  │
  └── [2] → dictionary
             role    = assistant
             content = 내가 그 주문을 확인해 볼께.
 

즉:

 
history[0]
history[1]
history[2]
 

각각 하나의 메시지를 의미합니다.


2. 왜 role과 content로 나누는가?

각 메시지를:

 
{
    "role": "...",
    "content": "..."
}
 

형태로 저장하고 있습니다.

role은 누가 말했는지를 의미합니다.

대표적으로:

system
user
assistant
 

세 가지가 있습니다.

system

AI에게 역할이나 규칙을 알려줍니다.

너는 도움을 주는 에이전트야.
 

user

사용자의 말입니다.

내 주문에 대한 배송이 늦네.
 

assistant

AI가 이전에 했던 말입니다.

내가 그 주문을 확인해 볼께.
 

따라서 LLM 입장에서는:

SYSTEM:
너는 도움을 주는 에이전트야.

USER:
내 주문에 대한 배송이 늦네.

ASSISTANT:
내가 그 주문을 확인해 볼께.
 

라는 대화 기록이 되는 것입니다.


3. history[1]["content"]

 
print(history[1]["content"])
 

이 부분은 Python의 list + dictionary 접근을 동시에 사용합니다.

먼저:

 
history[1]
 

은 리스트의 두 번째 항목입니다.

Python은 0부터 시작하므로:

history[0] → 첫 번째
history[1] → 두 번째
history[2] → 세 번째
 

입니다.

따라서:

 
history[1]
 

은:

 
{
    "role":"user",
    "content":"내 주문에 대한 배송이 늦네."
}
 

입니다.

여기서:

 
["content"]
 

를 붙이면:

 
history[1]["content"]
 

내 주문에 대한 배송이 늦네.
 

가 됩니다.


4. 이것은 굉장히 중요한 Python 패턴

앞으로 Agent 코드를 읽을 때 이런 형태를 정말 많이 보게 됩니다.

 
messages[i]["role"]
messages[i]["content"]
response["usage"]
response["content"]
 

이것은 모두:

딕셔너리에서 특정 key의 값을 꺼내는 것

입니다.

예를 들어:

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

라면:

 
msg["role"]
 

→ "user"

 
msg["content"]
 

→ "안녕하세요"

입니다.


5. len(history)

 
print("Turns: ", len(history))
 

len()은 리스트에 몇 개의 항목이 있는지 알려줍니다.

현재:

system
user
assistant
 

3개의 메시지가 있으므로:

Turns: 3
 

이 출력됩니다.

다만 여기서 Turns라는 표현은 조금 주의해야 합니다.

현재 history에는 메시지가 3개입니다.

하지만 일반적으로 대화의 "turn"을:

user → assistant
 

한 쌍으로 보는 경우도 있습니다.

따라서 이 코드에서 len(history)가 정확히 의미하는 것은:

저장된 message의 개수

라고 이해하는 것이 더 정확합니다.


6. for msg in history

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

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

첫 번째 반복:

 
msg = {
    "role":"system",
    "content":"너는 도움을 주는 에이전트야."
}
 

두 번째:

 
msg = {
    "role":"user",
    "content":"내 주문에 대한 배송이 늦네."
}
 

세 번째:

 
msg = {
    "role":"assistant",
    "content":"내가 그 주문을 확인해 볼께."
}
 

입니다.


7. ^9가 무엇인가?

 
f"[{msg['role']:^9}]"
 

^는 가운데 정렬(center alignment)입니다.

9는 9칸을 확보한다는 뜻입니다.

즉:

 
f"{role:^9}"
 

은:

role을 9칸짜리 공간의 가운데에 배치하라.

는 뜻입니다.

예를 들어:

system
 

이 가운데 정렬되어 출력됩니다.

그래서 결과가 대략:

[ system  ] 너는 도움을 주는 에이전트야.
[  user   ] 내 주문에 대한 배송이 늦네.
[assistant ] 내가 그 주문을 확인해 볼께.
 

처럼 보기 좋게 정렬됩니다.


8. 두 번째 부분: response

이제 조금 다른 데이터 구조가 나옵니다.

 
response = {

    "id":"steven_20",

    "usage":{
        "input_token":1200,
        "output_token":605
    },

    "content":[
        {
            "type":"text",
            "text":"주문배송이 월요일에 시작되었습니다."
        },
    ]
}
 

이것도 핵심은 dictionary 안에 dictionary와 list가 들어 있는 복합 구조입니다.

그림으로 보면:

response
│
├── id
│    └── "steven_20"
│
├── usage
│    ├── input_token  → 1200
│    └── output_token → 605
│
└── content
     │
     └── [0]
          ├── type → "text"
          └── text → "주문배송이 월요일에 시작되었습니다."
 

이런 구조입니다.


9. response["usage"]["output_token"]

 
print(response["usage"]["output_token"])
 

처음 보면 복잡하지만 차례대로 읽으면 쉽습니다.

먼저:

 
response["usage"]
 

 
{
    "input_token":1200,
    "output_token":605
}
 

그 다음:

 
["output_token"]
 

605
 

따라서:

 
response["usage"]["output_token"]
 

→ 605

입니다.


10. content[0]["text"]

 
print(response["content"][0]["text"])
 

이것은 조금 더 복잡합니다.

먼저:

 
response["content"]
 

→ 리스트입니다.

 
[
    {
        "type":"text",
        "text":"주문배송이 월요일에 시작되었습니다."
    }
]
 

그 다음:

 
[0]
 

첫 번째 dictionary를 꺼냅니다.

 
response["content"][0]
 

 
{
    "type":"text",
    "text":"주문배송이 월요일에 시작되었습니다."
}
 

그리고:

 
["text"]
 

를 꺼냅니다.

결과:

주문배송이 월요일에 시작되었습니다.
 

11. 이것을 읽는 습관

이런 복잡한 코드를 만나면 한꺼번에 읽지 마세요.

다음처럼 왼쪽부터 단계적으로 읽으면 됩니다.

 
response["content"][0]["text"]
 

response
 ↓
content
 ↓
첫 번째 [0]
 ↓
text
 

즉:

response 안에서 content를 찾고 → 그중 첫 번째 것을 꺼내고 → 그 안의 text를 꺼낸다.

입니다.


12. .items()

다음입니다.

 
for key, value in response["usage"].items():
    print(f"{key} = {value}")
 

먼저:

 
response["usage"]
 

는:

 
{
    "input_token":1200,
    "output_token":605
}
 

입니다.

여기에:

 
.items()
 

를 사용하면 key와 value를 함께 꺼낼 수 있습니다.

첫 번째:

key = "input_token"
value = 1200
 

두 번째:

key = "output_token"
value = 605
 

따라서 결과는:

input_token = 1200
output_token = 605
 

입니다.


13. 이제 Agent의 핵심: history 관리

여기서부터 정말 중요한 부분입니다.

 
history = []
 

빈 리스트를 만들었습니다.

즉:

현재 대화 기록 없음
 

입니다.

이제 여기에 메시지를 하나씩 추가할 것입니다.


14. add() 함수

 
def add(role, content):

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

이 함수는 아주 간단합니다.

history에 메시지 하나를 추가하는 함수

입니다.

예를 들어:

 
add("user", "안녕하세요")
 

를 실행하면:

 
history.append({
    "role":"user",
    "content":"안녕하세요"
})
 

가 됩니다.

결과:

 
history = [
    {
        "role":"user",
        "content":"안녕하세요"
    }
]
 

15. .append()

 
history.append(...)
 

append()는 리스트의 맨 뒤에 새로운 항목을 추가합니다.

예:

 
numbers = [1, 2, 3]

numbers.append(4)
 

결과:

 
[1, 2, 3, 4]
 

입니다.

Agent에서는:

 
history.append(message)
 

를 통해 새로운 대화 내용을 계속 저장하는 것입니다.


16. 실제 대화 기록 만들기

다음 코드:

 
add("system","너는 예약 에이전트야.")

add("user", "뉴욕까지 가는 항공기를 찾아줘.")

add("assistant", "지금 비행편을 확인합니다.")

add("user", "참, 저녁에 출발하는 비행기만 부탁해.")

add("assistant", "세 개의 항공편을 찾았습니다.")
 

실행이 끝나면 history는 사실상 이렇게 됩니다.

 
history = [
    {"role":"system", "content":"너는 예약 에이전트야."},

    {"role":"user", "content":"뉴욕까지 가는 항공기를 찾아줘."},

    {"role":"assistant", "content":"지금 비행편을 확인합니다."},

    {"role":"user", "content":"참, 저녁에 출발하는 비행기만 부탁해."},

    {"role":"assistant", "content":"세 개의 항공편을 찾았습니다."},
]
 

Agent의 대화 기억입니다.


17. 왜 history가 필요한가?

이것이 Agent에서 매우 중요합니다.

사용자가 처음:

뉴욕까지 가는 항공기를 찾아줘.
 

라고 했습니다.

그런데 다음에:

참, 저녁에 출발하는 비행기만 부탁해.
 

라고 했습니다.

두 번째 말만 보면:

"무엇의 저녁 출발?"

이라고 생각할 수도 있습니다.

하지만 Agent가 history를 가지고 있으면:

USER:
뉴욕까지 가는 항공기를 찾아줘.

USER:
참, 저녁에 출발하는 비행기만 부탁해.
 

를 보고:

"아, 뉴욕행 항공편 중에서 저녁 출발편을 말하는 것이구나."

라고 이해할 수 있습니다.

history가 문맥(context)을 유지해주는 역할을 합니다.


18. trim() 함수

이제 아주 중요한 함수가 나옵니다.

 
def trim(max_turns):

    system = [m for m in history if m["role"]=="system"]

    rest = [m for m in history if m["role"]!="system"]

    return system + rest[-max_turns:]
 

이 함수의 목적은:

대화 기록이 너무 길어지면 필요한 만큼만 잘라서 사용한다.

입니다.

왜 필요할까요?

LLM에는 context window가 있기 때문입니다.

그리고 대화가 길어질수록:

대화 내용 증가
    ↓
input token 증가
    ↓
비용 증가
    ↓
context window 부담
 

이 발생합니다.

그래서 오래된 대화 일부를 잘라내는 것입니다.


19. system을 따로 보관

 
system = [m for m in history if m["role"]=="system"]
 

이것은 list comprehension이라는 문법입니다.

쉽게 풀어쓰면:

 
system = []

for m in history:
    if m["role"] == "system":
        system.append(m)
 

와 거의 같은 의미입니다.

즉:

history에서 role이 system인 메시지만 골라서 system에 넣어라.

입니다.

현재 history에는:

system
user
assistant
user
assistant
 

가 있으므로:

system
 

하나만 선택됩니다.


20. rest

 
rest = [m for m in history if m["role"]!="system"]
 

이번에는 반대입니다.

system이 아닌 메시지만 골라라.
 

따라서:

user
assistant
user
assistant
 

가 rest에 들어갑니다.

결국:

history
│
├── system
│
├── user
├── assistant
├── user
└── assistant
 

system
 

user
assistant
user
assistant
 

로 분리한 것입니다.


21. 왜 system을 따로 빼는가?

이 부분이 Agent 설계에서 상당히 중요합니다.

System Prompt는 Agent의 기본 역할과 규칙입니다.

예:

너는 예약 에이전트야.
 

이런 것은 대화가 길어졌다고 해서 함부로 버리면 안 됩니다.

그래서:

 
system = [...]
 

으로 따로 보관하고,

나머지 오래된 대화는 잘라내는 것입니다.

구조적으로:

System Prompt
     ↓
항상 유지

최근 대화
     ↓
필요한 만큼 유지
 

입니다.


22. rest[-max_turns:]

이 부분도 반드시 이해해야 합니다.

 
rest[-max_turns:]
 

이것은 리스트의 뒤에서부터 max_turns개를 가져오는 것입니다.

예를 들어:

 
rest = ["A", "B", "C", "D"]
 

이고:

 
max_turns = 2
 

이면:

 
rest[-2:]
 

 
["C", "D"]
 

입니다.

즉:

가장 최근의 2개 메시지를 가져온다.

입니다.


23. return system + rest[-max_turns:]

결국:

 
return system + rest[-max_turns:]
 

System 메시지는 유지하고, 나머지 메시지는 최근 것만 가져와서 합친다.

는 뜻입니다.

현재 history가:

1. system
2. user
3. assistant
4. user
5. assistant
 

이고:

 
max_turns = 2
 

이면:

system
+
user(최근)
assistant(최근)
 

가 됩니다.

즉:

system
user: 참, 저녁에 출발하는 비행기만 부탁해.
assistant: 세 개의 항공편을 찾았습니다.
 

정도가 남습니다.


24. 그런데 max_turns라는 이름은 조금 애매하다

여기서 하나 짚고 넘어가야 합니다.

코드는:

 
rest[-max_turns:]
 

이므로 실제로는 메시지 개수를 자르고 있습니다.

따라서:

 
max_turns=2
 

라고 해도 엄밀하게는:

최근 2개의 메시지

입니다.

일반적으로 turn을:

user → assistant
 

한 쌍으로 생각한다면 max_turns=2는 4개의 메시지를 의미할 수도 있습니다.

따라서 교육용 코드에서는 괜찮지만, 실전에서는 이름을:

 
max_messages
 

처럼 짓는 것이 더 정확합니다.


25. show() 함수

 
def show(messages):

    for m in messages:
        role = m["role"].upper()
        print(f"{role:<9} | {m['content']}")
 

이 함수는:

대화 history를 사람이 보기 좋게 출력하는 함수

입니다.


26. .upper()

 
role = m["role"].upper()
 

예를 들어:

 
"user".upper()
 

USER
 
 
"assistant".upper()
 

ASSISTANT
 

입니다.

그래서 화면에:

USER
ASSISTANT
 

처럼 대문자로 표시됩니다.


27. <9는 왼쪽 정렬

여기:

 
f"{role:<9}"
 

에서:

<
 

왼쪽 정렬입니다.

그리고:

9
 

는 9칸을 확보합니다.

따라서:

USER     
 

처럼 9칸에 맞춰 왼쪽에 붙습니다.

그래서:

 
print(f"{role:<9} | {m['content']}")
 

결과가:

SYSTEM    | 너는 예약 에이전트야.
USER      | 뉴욕까지 가는 항공기를 찾아줘.
ASSISTANT  | 지금 비행편을 확인합니다.
 

처럼 정렬됩니다.


28. 마지막 부분

 
print(f"Stored messages: {len(history)}")
 

현재 history에 몇 개의 메시지가 들어 있는지 출력합니다.

5개이므로:

Stored messages: 5
 

입니다.


그리고:

 
print("-"*50)
 

구분선을 출력합니다.

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

29. show(trim(max_turns=2))

마지막으로:

 
show(trim(max_turns=2))
 

가 실행됩니다.

이것을 안쪽부터 읽으면 쉽습니다.

 
trim(max_turns=2)
 

먼저 history를 정리합니다.

현재:

SYSTEM
USER
ASSISTANT
USER
ASSISTANT
 

에서:

SYSTEM
 

은 유지하고,

나머지 중 최근 2개:

USER
ASSISTANT
 

만 가져옵니다.


그 결과를 show()에 전달합니다.

 
show(...)
 

그러면 예쁘게 출력합니다.

결과는 대략:

Stored messages: 5
--------------------------------------------------
SYSTEM    | 너는 예약 에이전트야.
USER      | 참, 저녁에 출발하는 비행기만 부탁해.
ASSISTANT  | 세 개의 항공편을 찾았습니다.
 

가 됩니다.


30. 이번 코드의 핵심 구조

이번 코드는 크게 세 단계입니다.

① 메시지를 저장한다
        ↓
     history
        ↓
② 필요한 메시지만 잘라낸다
        ↓
      trim()
        ↓
③ 보기 좋게 출력한다
        ↓
      show()
 

조금 더 Agent답게 표현하면:

                ┌──────────────┐
사용자 질문 ──→ │    history   │
                └──────┬───────┘
                       ↓
                 대화 내용 축적
                       ↓
                 너무 길어짐?
                       ↓
                    trim()
                       ↓
             ┌─────────┴─────────┐
             │                   │
         system 유지         최근 대화만
             │                   │
             └─────────┬─────────┘
                       ↓
                    LLM 호출
                       ↓
                    응답 생성
                       ↓
                  history 추가
 

이게 Agent의 대화 기억 구조의 아주 기본적인 형태입니다.


31. 특히 기억해야 할 Python 문법

이번 코드에서 앞으로 계속 만나게 될 것들입니다.

코드의미
history = [] 빈 리스트
history.append(x) 리스트 뒤에 추가
history[1] 리스트 두 번째 요소
msg["role"] 딕셔너리의 role 값
msg["content"] 딕셔너리의 content 값
dict.items() key/value를 함께 순회
.upper() 대문자로 변환
.strip() 앞뒤 공백 제거
[-2:] 뒤에서 2개 가져오기
role:<9 9칸 왼쪽 정렬
role:^9 9칸 가운데 정렬
[x for x in ...] 리스트 컴프리헨션
try/except 오류 처리

32. 그리고 가장 중요한 것: history는 AI의 "기억"인가?

여기서 개념적으로 하나 구분해야 합니다.

이 코드에서:

 
history = []
 

에 저장된 것은 LLM 자체의 기억(memory)이라기보다는,

애플리케이션이 LLM에게 다시 전달해주는 대화 기록

이라고 이해하는 것이 정확합니다.

예를 들어 사용자가:

뉴욕까지 가는 항공기를 찾아줘.
 

라고 하면 프로그램이 history에 저장합니다.

다음 질문:

저녁에 출발하는 것만 보여줘.
 

가 들어오면 프로그램은 이전 history와 현재 질문을 합쳐서 LLM에게 전달할 수 있습니다.

SYSTEM:
너는 예약 에이전트야.

USER:
뉴욕까지 가는 항공기를 찾아줘.

ASSISTANT:
지금 비행편을 확인합니다.

USER:
참, 저녁에 출발하는 비행기만 부탁해.
 

그러면 LLM은 앞의 대화 내용을 보고 "뉴욕행 항공편 중 저녁 출발"이라는 의미를 이해할 수 있습니다.

즉:

Agent의 기억 = history를 저장하고, 적절하게 잘라서 다시 LLM에게 전달하는 애플리케이션의 로직

이라고 생각하시면 상당히 정확합니다.


🌟 지금까지 공부한 코드와 연결하면

지금까지 보신 코드들을 하나로 연결하면 Agent의 구조가 상당히 선명해집니다.

[환경]
sys / os / virtual environment
        ↓
[Prompt]
f-string / system prompt / build_prompt()
        ↓
[대화]
history
        ↓
[대화 관리]
add()
trim()
        ↓
[LLM 호출]
call_model()
        ↓
[LLM 응답]
response
        ↓
[응답 분석]
strip()
lower()
split()
startswith()
        ↓
[Tool]
calculator
search
read_file
        ↓
[다시 history에 저장]
        ↓
[다음 Agent Loop]
 

그리고 바로 이 구조를 이해하면, 최근에 공부하셨던 Agent.run() → call_model() → Tool 실행 → history 추가 → MAX_STEPS까지 반복하는 코드가 훨씬 쉽게 보이기 시작합니다.

반응형