04. 메모리: 에이전트가 대화를 기억하는 법
- "메모리 없는" 에이전트와 "메모리 있는" 에이전트의 차이를 코드로 구현한다
- LLM이 사실은 상태가 없는(stateless) 모델임을 이해하고, 메모리가 어떻게 구현되는지 안다
- 대화 기록을 누적할 때 생기는 토큰·비용 문제를 인식한다
- 메모리 관리 전략(전체 보관 vs 요약 vs 윈도우)의 트레이드오프를 잡는다
비유로 시작하기: 화이트보드를 든 상담원
앞 장의 기억상실증 상담원을 떠올려 보세요. 이번에는 그에게 화이트보드를 줍니다. 통화 중 오간 말을 화이트보드에 계속 적고, 다음 발언을 할 때마다 화이트보드 전체를 다시 읽습니다. 그러면 "아까 이름이 민수라고 하셨죠"라고 말할 수 있습니다.
여기서 핵심 통찰이 나옵니다. LLM 자체는 여전히 아무것도 기억하지 못합니다. 기억하는 것은 우리가 관리하는 화이트보드(대화 기록)이고, 매 호출마다 그 전체를 모델에 다시 넣어줄 뿐입니다.
LLM은 상태가 없다
이 점을 분명히 해두는 것이 메모리를 이해하는 열쇠입니다. LLM API는 상태가 없습니다(stateless). 모델은 직전 호출을 기억하지 못합니다. "대화를 기억하는 것처럼" 보이게 하려면, 우리가 이전 메시지들을 모아 매번 함께 보내야 합니다.
flowchart LR
classDef user fill:#FFE082,stroke:#F9A825,color:#000
classDef mem fill:#A5D6A7,stroke:#2E7D32,color:#000
classDef agent fill:#80DEEA,stroke:#00838F,color:#000
U[새 사용자 입력]:::user --> M[대화 기록<br/>화이트보드]:::mem
M -->|기록 전체 + 새 입력| A[LLM 호출]:::agent
A -->|응답| M
M -.다음 턴에 재사용.-> A
메모리 없는 버전 (복습)
def stateless_agent(user_input: str) -> str: response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "너는 친절한 비서다."}, {"role": "user", "content": user_input}, ], ) return response.choices[0].message.content
매 호출이 system + 단일 user로 끝납니다. 과거가 없습니다.
메모리 있는 버전
핵심 변화는 대화 기록을 리스트로 유지하고, 매 턴마다 사용자 메시지와 모델 응답을 그 리스트에 누적하는 것입니다.
class MemoryAgent: def __init__(self, system_prompt: str): # 화이트보드 초기화: 시스템 프롬프트로 시작 self.history = [{"role": "system", "content": system_prompt}] def chat(self, user_input: str) -> str: # 1) 사용자 발언을 기록에 추가 self.history.append({"role": "user", "content": user_input}) # 2) 기록 '전체'를 모델에 전달 response = client.chat.completions.create( model="gpt-4o-mini", messages=self.history, ) reply = response.choices[0].message.content # 3) 모델 응답도 기록에 추가 (다음 턴에서 참조됨) self.history.append({"role": "assistant", "content": reply}) return reply agent = MemoryAgent("너는 친절한 비서다.") print(agent.chat("내 이름은 민수야.")) print(agent.chat("내 이름이 뭐라고 했지?")) # 이제 "민수"라고 답한다
세 단계가 메모리의 본질입니다. 사용자 발언을 기록에 추가하고, 기록 전체를 보내고, 응답을 다시 기록에 추가합니다. assistant 역할로 모델의 과거 응답을 넣는 것이 포인트입니다.
📌 핵심: 메모리 = "대화 기록 리스트를 우리가 들고 다니며 매번 통째로 모델에 다시 넣는 것". 모델이 기억하는 게 아니라, 우리가 기억을 떠먹여 주는 것입니다.
메모리의 대가: 토큰이 계속 늘어난다
화이트보드가 무한정 커지면 문제가 생깁니다. 대화가 길어질수록 매 호출에 보내는 토큰이 누적되어 비용이 선형으로 증가하고, 결국 모델의 컨텍스트 한도를 넘깁니다.
flowchart TD
classDef warn fill:#EF9A9A,stroke:#C62828,color:#000
classDef ok fill:#A5D6A7,stroke:#2E7D32,color:#000
A["턴 1: 50토큰"] --> B["턴 2: 120토큰"]
B --> C["턴 3: 210토큰"]
C --> D["...턴 N: 한도 초과 위험"]:::warn
⚠️ 흔한 실수: 메모리를 무한히 쌓아두는 것. 긴 대화에서 갑자기 비용이 폭증하거나 컨텍스트 초과 에러가 나는 주요 원인입니다.
메모리 관리 전략
세 가지 대표 전략이 있습니다. 작업 성격에 맞게 고릅니다.
| 전략 | 방법 | 장점 | 단점 |
|---|---|---|---|
| 전체 보관 | 모든 메시지를 그대로 유지 | 구현 단순, 완전한 맥락 | 길어지면 비용·한도 문제 |
| 슬라이딩 윈도우 | 최근 N개 메시지만 유지 | 비용 일정, 단순 | 오래된 맥락 상실 |
| 요약(summary) | 오래된 대화를 요약문으로 압축 | 맥락 유지 + 토큰 절약 | 요약 비용·정보 손실 |
실무에서는 윈도우와 요약을 섞어 씁니다. 최근 대화는 원문으로, 오래된 부분은 요약으로 보관하는 식입니다. 뒤에서 배울 프레임워크들은 이런 메모리 전략을 빌트인으로 제공합니다. LangGraph는 체크포인트와 상태로, CrewAI는 단기·장기·엔티티 메모리로, n8n은 메모리 노드로 구현합니다. 즉 이 장에서 손으로 만든 것을 각 프레임워크가 자동화해 줍니다.
💡 실습 아이디어(강의의 Travel Planner 대응): MemoryAgent의 시스템 프롬프트를 "너는 여행 플래너다. 사용자의 선호를 기억하며 일정을 다듬는다"로 바꾸고, 여러 턴에 걸쳐 "예산은 100만원", "바다가 좋아", "그럼 어디 추천해?"를 입력해 보세요. 메모리 덕분에 앞서 말한 예산과 취향을 반영합니다.
이 장에서 배운 것
- LLM API는 상태가 없다. "기억"은 우리가 대화 기록을 들고 다니며 매번 전체를 다시 넣어주는 것이다.
- 메모리 구현의 핵심 3단계: 사용자 발언 추가 → 기록 전체 전송 → 모델 응답 추가.
- 기록이 길어지면 토큰·비용·컨텍스트 한도 문제가 생긴다.
- 전체 보관·슬라이딩 윈도우·요약 전략을 작업에 맞게 조합한다. 프레임워크들은 이를 빌트인으로 제공한다.
✍️ 확인 문제
- "LLM이 대화를 기억한다"는 표현이 기술적으로 부정확한 이유를 설명하라.
- 메모리 에이전트 구현에서 모델의 응답을
assistant역할로 기록에 다시 넣지 않으면 어떤 문제가 생기는가? - 100턴이 넘는 긴 고객 상담 봇을 만든다. 전체 보관 전략이 부적절한 이유와, 대안 전략 하나를 그 이유와 함께 제시하라.
이전 장: 03. 가장 단순한 에이전트
다음 장: 05. 도구(Tools) — 이제 에이전트가 외부 세계와 상호작용하게 만듭니다.