ARCH · MEMORY

Memory 시스템

💾 4가지 메모리 타입 🗄️ 벡터 스토어 통합 📌 LangGraph 체크포인터

AI Agent의 4가지 메모리 타입을 이해하고, LangGraph 체크포인터와 벡터 스토어를 결합한 장기 메모리 시스템을 구현합니다.

4가지 메모리 타입

📋 Working (단기 메모리) 현재 대화 컨텍스트 LLM 컨텍스트 윈도우 요청 간 자동 초기화 AgentState.messages 📖 Episodic (에피소딕 메모리) 과거 대화 이력 세션별 요약 사용자 선호도 LangGraph Checkpointer 🔍 Semantic (시맨틱 메모리) 도메인 지식 문서/KB 임베딩 의미 기반 검색 Vector Store (Chroma, Pinecone) ⚙️ Procedural (절차적 메모리) 도구 사용 방법 워크플로우 패턴 성공/실패 패턴 시스템 프롬프트 / Few-shot ← 단기 / 임시 영구 / 지속 → 그림 1. AI Agent 4가지 메모리 타입 — 보존 기간 기준

LangGraph 체크포인터 — 에피소딕 메모리

LangGraph 체크포인터는 그래프 상태를 스레드별로 영구 저장합니다. 같은 thread_id로 호출하면 이전 대화 컨텍스트가 자동으로 복원됩니다.

pythonepisodic_memory.py — Postgres 체크포인터
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from langgraph.graph import StateGraph, END, MessagesState
from langchain_anthropic import ChatAnthropic
import os

async def create_persistent_agent():
    # Postgres에 대화 상태 영구 저장
    checkpointer = await AsyncPostgresSaver.from_conn_string(
        os.getenv("DATABASE_URL")
    )
    await checkpointer.setup()  # 테이블 초기화

    llm = ChatAnthropic(model="claude-opus-4-7")

    async def agent_node(state: MessagesState):
        response = await llm.ainvoke(state["messages"])
        return {"messages": [response]}

    builder = StateGraph(MessagesState)
    builder.add_node("agent", agent_node)
    builder.set_entry_point("agent")
    builder.add_edge("agent", END)

    return builder.compile(checkpointer=checkpointer)

# 사용: thread_id로 대화 세션 구분
async def chat(agent, user_id: str, message: str) -> str:
    config = {"configurable": {"thread_id": user_id}}

    result = await agent.ainvoke(
        {"messages": [{"role": "user", "content": message}]},
        config=config
    )
    return result["messages"][-1].content

# 사용자별 대화 이력 조회
async def get_conversation_history(agent, user_id: str) -> list:
    config = {"configurable": {"thread_id": user_id}}
    state = await agent.aget_state(config)
    return state.values.get("messages", [])

벡터 스토어 — 시맨틱 메모리

대화 중 학습한 사실이나 문서를 벡터로 저장하고, 이후 대화에서 유사 내용을 검색해 주입합니다.

pythonsemantic_memory.py — 벡터 스토어 기반 장기 메모리
from langchain_chroma import Chroma
from langchain_anthropic import ChatAnthropic
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
from datetime import datetime

class SemanticMemory:
    def __init__(self, persist_dir: str = "./memory_store"):
        self.vectorstore = Chroma(
            embedding_function=OpenAIEmbeddings(),
            persist_directory=persist_dir,
            collection_name="agent_memory"
        )
        self.llm = ChatAnthropic(model="claude-haiku-4-5-20251001")

    async def remember(self, content: str, user_id: str, metadata: dict = {}) -> str:
        """대화에서 기억할 내용 추출 + 저장"""
        # LLM으로 기억할 핵심 정보 추출
        extraction = await self.llm.ainvoke([{
            "role": "user",
            "content": f"다음 대화에서 사용자에 대해 기억할 중요한 사실을 한 문장으로 추출하세요. 없으면 'SKIP':\n{content}"
        }])
        fact = extraction.content.strip()
        if fact == "SKIP":
            return

        doc = Document(
            page_content=fact,
            metadata={
                "user_id": user_id,
                "timestamp": datetime.now().isoformat(),
                **metadata
            }
        )
        self.vectorstore.add_documents([doc])
        return fact

    async def recall(self, query: str, user_id: str, k: int = 3) -> list[str]:
        """유사한 기억 검색"""
        docs = self.vectorstore.similarity_search(
            query,
            k=k,
            filter={"user_id": user_id}
        )
        return [d.page_content for d in docs]

    async def build_memory_context(self, query: str, user_id: str) -> str:
        """검색된 기억을 시스템 프롬프트용 텍스트로 변환"""
        memories = await self.recall(query, user_id)
        if not memories:
            return ""
        memory_list = "\n".join(f"- {m}" for m in memories)
        return f"\n[이 사용자에 대해 알고 있는 것]\n{memory_list}\n"

# Agent에서 사용
memory = SemanticMemory()

async def memory_agent_node(state):
    user_id = state["user_id"]
    query = state["messages"][-1].content

    # 관련 기억 검색
    mem_context = await memory.build_memory_context(query, user_id)

    llm = ChatAnthropic(model="claude-opus-4-7")
    response = await llm.ainvoke(
        state["messages"],
        system=f"당신은 개인화된 AI 어시스턴트입니다.{mem_context}"
    )

    # 새 기억 저장
    await memory.remember(query, user_id)
    return {"messages": [response]}

메모리 압축 — 컨텍스트 윈도우 관리

긴 대화가 진행될수록 컨텍스트 윈도우가 초과됩니다. 오래된 메시지를 요약해 압축하는 전략이 필요합니다.

pythonmemory_compression.py — 슬라이딩 윈도우 + 요약
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage

MAX_MESSAGES = 20     # 유지할 최근 메시지 수
SUMMARY_KEEP = 6      # 압축 후 유지할 최신 메시지 수

async def compress_messages(
    messages: list,
    llm: ChatAnthropic
) -> list:
    """메시지가 MAX_MESSAGES를 초과하면 요약 압축"""
    if len(messages) <= MAX_MESSAGES:
        return messages

    # 오래된 메시지를 요약
    old_messages = messages[:-SUMMARY_KEEP]
    recent_messages = messages[-SUMMARY_KEEP:]

    summary_response = await llm.ainvoke([
        SystemMessage(content="다음 대화를 핵심 정보 위주로 간결하게 요약하세요."),
        *old_messages
    ])
    summary_text = summary_response.content

    # 요약 + 최신 메시지 결합
    compressed = [
        SystemMessage(content=f"[이전 대화 요약]\n{summary_text}"),
        *recent_messages
    ]
    print(f"메시지 압축: {len(messages)} → {len(compressed)}")
    return compressed

# LangGraph 노드에서 사용
async def agent_with_compression(state):
    messages = state["messages"]
    llm = ChatAnthropic(model="claude-haiku-4-5-20251001")

    # 압축 후 LLM 호출
    compressed = await compress_messages(messages, llm)
    response = await ChatAnthropic(model="claude-opus-4-7").ainvoke(compressed)
    return {"messages": messages + [response]}  # 원본 상태에는 전체 보존

메모리 타입 조합 패턴

사용 사례조합구현
개인화 챗봇Working + Episodic + SemanticCheckpointer + Vector Store
코드 어시스턴트Working + Procedural시스템 프롬프트 + Few-shot
리서치 에이전트Working + SemanticRAG + Vector Store
장기 프로젝트 관리모든 4가지 타입Checkpointer + Vector Store + 시스템 프롬프트