본문 바로가기

AI Agent

AI 에이전트가 기억하는 법 — 단기/장기 메모리 아키텍처와 MemGPT 완전 정리

반응형

AI 에이전트를 쓰다 보면 이런 답답함이 생겨요.

"지난주에 분명히 말했는데 또 처음부터 설명해야 하네."

LLM은 기본적으로 상태가 없어요(stateless). 대화가 끝나면 모든 걸 잊어요. 컨텍스트 창 안에 있는 것만 기억하고, 창 밖으로 밀려나면 사라져요.

이걸 해결하는 게 에이전트 메모리 아키텍처예요. 이번 글에서는 메모리 타입 4가지, MemGPT 구조 분석, 실전 구현 방법까지 정리해 드릴게요.


메모리가 없으면 뭐가 문제인가

메모리 없는 에이전트는 세 가지 한계가 있어요.

세션 간 기억 불가 — 오늘 대화와 어제 대화가 완전히 단절돼요. 사용자가 매번 컨텍스트를 다시 설명해야 해요.

컨텍스트 창 한계 — 대화가 길어지면 앞부분이 잘려나가요. 1시간 전에 한 결정을 에이전트가 기억 못 해요.

개인화 불가 — 사용자의 선호도, 습관, 이전 경험을 누적할 수 없어요. 매번 처음 만나는 사람처럼 대해요.


메모리 타입 4가지

타입 1: 단기 메모리 (Short-term Memory)

현재 세션의 대화 기록이에요. 가장 단순하고 모든 챗봇이 이미 갖고 있어요.

# 단기 메모리 = 컨텍스트 창에 있는 메시지 리스트
messages = [
    {"role": "system", "content": "당신은 도움이 되는 어시스턴트입니다."},
    {"role": "user", "content": "파이썬 리스트 정렬 방법 알려줘"},
    {"role": "assistant", "content": "sorted() 함수나 .sort() 메서드를 쓰면 돼요."},
    {"role": "user", "content": "역순으로도 가능해?"},  # 현재 질문
]

한계 — 세션이 끝나면 사라지고, 컨텍스트 창이 꽉 차면 앞부분이 잘려나가요.

타입 2: 작업 메모리 (Working Memory)

에이전트가 현재 작업을 처리하는 동안 중간 결과와 추론 상태를 저장해요. 단기 메모리가 "뭘 말했는가"라면, 작업 메모리는 "지금 무엇을 생각하고 있는가"예요.

class WorkingMemory:
    def __init__(self):
        self.current_plan = []       # 현재 실행 계획
        self.intermediate_results = {}  # 중간 결과
        self.reasoning_steps = []    # 추론 과정

    def update_plan(self, step: str):
        self.current_plan.append(step)

    def store_result(self, key: str, value: any):
        self.intermediate_results[key] = value

# 사용 예시 — 여행 계획 에이전트
wm = WorkingMemory()
wm.update_plan("파리 명소 검색")
wm.store_result("attractions", ["에펠탑", "루브르", "베르사유"])
wm.update_plan("맛집 검색")
wm.store_result("restaurants", ["르 쥘 베른", "카페 드 플로르"])
# 다음 단계에서 이전 결과를 참조해서 일정 구성

타입 3: 에피소딕 메모리 (Episodic Memory)

과거 경험과 상호작용을 저장해요. "언제, 무엇을, 어떤 순서로" 했는지 기록해요.

from datetime import datetime
from dataclasses import dataclass

@dataclass
class Episode:
    timestamp: datetime
    session_id: str
    user_id: str
    summary: str      # 대화 요약
    key_facts: list   # 중요한 사실들
    outcome: str      # 대화 결과

# 에피소딕 메모리 저장
episode = Episode(
    timestamp=datetime.now(),
    session_id="session_001",
    user_id="user_123",
    summary="사용자가 파이썬 리스트 정렬을 물어봤고, sorted()와 .sort() 차이를 설명함",
    key_facts=["파이썬 초보자", "알고리즘 공부 중", "sorted()가 새 리스트 반환함을 이해함"],
    outcome="문제 해결 완료"
)

타입 4: 시맨틱 메모리 (Semantic Memory)

사실, 지식, 선호도 같은 일반적인 정보를 저장해요. 특정 사건이 아니라 "이 사용자에 대해 아는 것들"이에요.

semantic_memory = {
    "user_123": {
        "name": "셀",
        "occupation": "풀스택 개발자",
        "interests": ["AI 에이전트", "SEO", "모바일 게임 개발"],
        "preferred_language": "Python",
        "communication_style": "직접적이고 간결한 답변 선호",
        "projects": ["가계부 앱", "뚝딱스 블로그", "개발 블로그"]
    }
}

MemGPT 구조 분석

MemGPT는 운영체제의 가상 메모리 개념을 LLM에 적용한 아이디어예요. OS가 RAM과 디스크를 나눠서 관리하듯, MemGPT는 메모리를 두 레이어로 나눠요.

MemGPT의 2계층 구조

┌────────────────────────────────────┐
│         Main Context (RAM)          │
│  ┌──────────────┐ ┌─────────────┐  │
│  │  System      │ │   Core      │  │
│  │  Prompt      │ │   Memory    │  │  ← LLM이 직접 읽고 쓸 수 있음
│  │  (고정)      │ │   (가변)    │  │
│  └──────────────┘ └─────────────┘  │
│  ┌──────────────────────────────┐  │
│  │      Conversation History    │  │
│  └──────────────────────────────┘  │
└────────────────────────────────────┘
           페이징 (필요할 때)
┌────────────────────────────────────┐
│       External Context (Disk)       │
│  ┌──────────────┐ ┌─────────────┐  │
│  │   Recall     │ │  Archival   │  │  ← 검색해서 불러와야 함
│  │   Storage    │ │   Storage   │  │
│  │  (대화 기록) │ │  (지식 베이스)│ │
│  └──────────────┘ └─────────────┘  │
└────────────────────────────────────┘

Main Context (RAM에 해당)

  • System Prompt — 에이전트의 페르소나, 역할, 지시사항. 고정됨.
  • Core Memory — 에이전트가 직접 읽고 쓸 수 있는 중요 정보. 사용자 기본 정보, 현재 목표 같은 것들.
  • Conversation History — 최근 대화 기록.

External Context (Disk에 해당)

  • Recall Storage — 과거 대화 전체 기록. 검색으로 불러올 수 있음.
  • Archival Storage — 외부 지식 베이스. 문서, 데이터베이스 같은 것들.

MemGPT의 핵심 — 자기 주도 메모리 관리

MemGPT가 혁신적인 이유는 LLM이 스스로 메모리를 관리한다는 거예요. 별도 시스템이 아니라 LLM 자체가 툴 호출로 메모리를 읽고 쓰고 지워요.

# MemGPT 에이전트가 사용하는 메모리 툴들
memory_tools = [
    {
        "name": "core_memory_append",
        "description": "Core memory에 새 정보 추가",
        "usage": "사용자에 대한 중요한 사실을 저장할 때"
    },
    {
        "name": "core_memory_replace",
        "description": "Core memory의 기존 정보 수정",
        "usage": "사용자 정보가 바뀌었을 때"
    },
    {
        "name": "archival_memory_search",
        "description": "Archival storage에서 관련 정보 검색",
        "usage": "과거 기록이나 외부 지식이 필요할 때"
    },
    {
        "name": "archival_memory_insert",
        "description": "Archival storage에 새 정보 저장",
        "usage": "나중에 참고할 중요한 정보를 저장할 때"
    },
    {
        "name": "conversation_search",
        "description": "과거 대화에서 특정 내용 검색",
        "usage": "이전에 나눈 대화 내용을 찾을 때"
    }
]

실제 동작을 보면 이렇게 돼요.

사용자: "내 이름이 뭔지 기억해?"

에이전트 내부 추론:
  "사용자 이름이 core memory에 있는지 확인해야겠다"
  → core_memory_read() 호출
  → "셀"이라는 이름 발견

에이전트: "네, 셀 님이시죠!"

---

사용자: "아, 참고로 나 이제 서울로 이사했어"

에이전트 내부 추론:
  "중요한 정보다. core memory에 저장해야겠다"
  → core_memory_append("사용자 거주지: 서울 (이전: 이전 거주지 불명)") 호출

에이전트: "서울로 이사하셨군요! 기억해 둘게요."

실전 구현 — 계층적 메모리 시스템

세 가지 저장소를 조합한 실전 메모리 아키텍처예요.

from datetime import datetime
import json
from typing import Optional

class AgentMemorySystem:
    def __init__(self, user_id: str, vector_db, key_value_db):
        self.user_id = user_id
        self.vector_db = vector_db      # 시맨틱 검색용
        self.kv_db = key_value_db       # 빠른 조회용
        self.session_messages = []      # 단기 메모리

    # ── 단기 메모리 ──────────────────────────

    def add_message(self, role: str, content: str):
        """현재 세션 메시지 추가"""
        self.session_messages.append({
            "role": role,
            "content": content,
            "timestamp": datetime.now().isoformat()
        })

    def get_recent_messages(self, k: int = 10) -> list:
        """최근 K개 메시지 반환"""
        return self.session_messages[-k:]

    # ── 장기 메모리 ──────────────────────────

    def save_to_long_term(self, content: str, memory_type: str):
        """중요한 정보를 장기 메모리에 저장"""
        # 벡터 DB에 임베딩해서 저장 (시맨틱 검색용)
        self.vector_db.add(
            texts=[content],
            metadatas=[{
                "user_id": self.user_id,
                "type": memory_type,
                "timestamp": datetime.now().isoformat()
            }]
        )

    def retrieve_relevant(self, query: str, k: int = 5) -> list:
        """쿼리와 관련된 장기 메모리 검색"""
        results = self.vector_db.similarity_search(
            query=query,
            filter={"user_id": self.user_id},
            k=k
        )
        return [r.page_content for r in results]

    # ── 시맨틱 메모리 (사용자 프로필) ────────

    def update_user_profile(self, key: str, value: any):
        """사용자 프로필 정보 업데이트"""
        profile_key = f"profile:{self.user_id}"
        profile = self.kv_db.get(profile_key) or {}
        profile[key] = value
        profile["updated_at"] = datetime.now().isoformat()
        self.kv_db.set(profile_key, profile)

    def get_user_profile(self) -> dict:
        """사용자 프로필 조회"""
        return self.kv_db.get(f"profile:{self.user_id}") or {}

    # ── 세션 종료 시 에피소딕 메모리로 저장 ──

    def save_session(self, summary: str):
        """세션 종료 시 요약해서 에피소딕 메모리에 저장"""
        episode = {
            "session_id": f"session_{datetime.now().timestamp()}",
            "user_id": self.user_id,
            "summary": summary,
            "message_count": len(self.session_messages),
            "timestamp": datetime.now().isoformat()
        }
        # 에피소딕 메모리로 저장
        self.save_to_long_term(summary, memory_type="episode")
        # 세션 초기화
        self.session_messages = []

    # ── 통합 컨텍스트 구성 ────────────────────

    def build_context(self, current_query: str) -> str:
        """LLM에 넘길 통합 컨텍스트 구성"""
        profile = self.get_user_profile()
        relevant_memories = self.retrieve_relevant(current_query, k=3)
        recent_messages = self.get_recent_messages(k=5)

        context = f"""
[사용자 프로필]
{json.dumps(profile, ensure_ascii=False, indent=2)}

[관련 과거 기억]
{chr(10).join(relevant_memories)}

[최근 대화]
{json.dumps(recent_messages, ensure_ascii=False, indent=2)}
        """
        return context

메모리 저장 전략 — 뭘 언제 저장하나

모든 걸 저장하면 오히려 품질이 떨어져요. 관련 없는 메모리가 검색 결과를 오염시키거든요.

def should_save_to_long_term(message: str, llm) -> dict:
    """LLM이 저장 가치 판단"""
    prompt = f"""
    다음 대화 내용이 장기 기억으로 저장할 가치가 있는지 판단해줘.

    대화: {message}

    저장 기준:
    - 사용자 개인 정보 (이름, 직업, 위치, 선호도)
    - 반복적으로 참고할 사실
    - 중요한 결정이나 합의
    - 사용자의 장기 목표나 계획

    저장 가치 없는 것:
    - 일회성 질문
    - 일반 상식
    - 이미 알려진 정보

    JSON으로만 답해:
    {{"should_save": true/false, "reason": "이유", "memory_type": "profile/episode/knowledge"}}
    """
    result = json.loads(llm.invoke(prompt).content)
    return result

# 사용 예시
decision = should_save_to_long_term("아, 나 이제 프리랜서로 전환했어", llm)
# {"should_save": true, "reason": "직업 변경 중요 정보", "memory_type": "profile"}

if decision["should_save"]:
    memory_system.update_user_profile("occupation", "프리랜서")

망각 전략 — 오래된 메모리 관리

메모리는 무한정 쌓으면 안 돼요. 오래된 정보가 최신 정보를 방해하거든요.

from datetime import timedelta

class MemoryDecayManager:

    def apply_time_decay(self, memories: list, decay_factor: float = 0.1) -> list:
        """시간이 지날수록 메모리 가중치 감소"""
        now = datetime.now()
        scored = []
        for memory in memories:
            age_days = (now - datetime.fromisoformat(memory["timestamp"])).days
            score = memory["relevance_score"] * (1 - decay_factor * age_days)
            scored.append({**memory, "adjusted_score": max(0, score)})
        return sorted(scored, key=lambda x: x["adjusted_score"], reverse=True)

    def consolidate_memories(self, memories: list, llm) -> str:
        """비슷한 메모리들을 하나로 합치기"""
        prompt = f"""
        다음 메모리들을 하나의 간결한 요약으로 합쳐줘.
        중복을 제거하고 최신 정보를 우선시해줘.

        메모리들:
        {json.dumps(memories, ensure_ascii=False, indent=2)}
        """
        return llm.invoke(prompt).content

    def prune_outdated(self, memories: list, max_age_days: int = 90) -> list:
        """오래된 메모리 제거"""
        cutoff = datetime.now() - timedelta(days=max_age_days)
        return [m for m in memories
                if datetime.fromisoformat(m["timestamp"]) > cutoff]

메모리 타입별 저장소 선택

메모리 타입 적합한 저장소 이유

단기 메모리 인메모리 (Python dict) 빠른 읽기/쓰기, 세션 종료 시 삭제
작업 메모리 인메모리 작업 완료 시 삭제
에피소딕 메모리 벡터 DB (Qdrant, ChromaDB) 시맨틱 검색 필요
시맨틱 메모리 Key-Value DB (Redis) 빠른 조회, 구조화된 데이터
대규모 지식 베이스 벡터 DB + 관계형 DB 하이브리드 검색

실전 라이브러리

직접 구현 대신 쓸 수 있는 검증된 라이브러리들이에요.

Mem0 — 2025년 Series A $20M 투자 받은 프로덕션 메모리 레이어. 벡터 DB + KV 스토어 + 그래프 DB를 통합 API로 제공해요.

from mem0 import Memory

m = Memory()

# 메모리 저장
m.add("사용자는 파이썬 풀스택 개발자이고 AI 에이전트에 관심이 많다",
      user_id="user_123")

# 관련 메모리 검색
results = m.search("어떤 기술 스택을 쓰나요?", user_id="user_123")

Letta (MemGPT의 프로덕션 버전) — MemGPT 논문 팀이 만든 프레임워크. OS 스타일 계층적 메모리 + 자기 주도 메모리 관리를 지원해요.


마무리

에이전트 메모리 설계의 핵심 원칙은 세 가지예요.

첫째, 모든 메모리를 컨텍스트에 넣지 않는다. 필요한 것만 검색해서 넣어야 해요. 관련 없는 메모리는 오히려 품질을 떨어뜨려요.

둘째, 저장 가치를 판단한다. 모든 대화를 저장하지 말고, LLM이 중요도를 판단해서 선택적으로 저장해야 해요.

셋째, 망각도 기능이다. MemGPT가 보여준 것처럼 전략적 망각은 메모리 시스템의 필수 요소예요. 오래되고 관련 없는 정보는 지워야 검색 품질이 유지돼요. 😄


 

반응형