AI 에이전트 장기 메모리 비교: Mem0 vs Letta vs Zep — 세 가지 철학과 실전 선택 기준
LLM 기반 앱을 한 번이라도 만들어봤다면 반드시 이 벽에 부딪힌다. "지난 대화를 어떻게 기억하게 할까?" 컨텍스트 윈도우에 전체 대화를 욱여넣으면 되겠지 싶지만, 현실은 그렇지 않다. 토큰 비용이 폭발하고, 대화가 길어질수록 LLM의 집중력은 흐트러지며, 세션이 끊기면 모든 것이 사라진다. 이 문제를 해결하기 위해 에이전트 장기 메모리 시스템이 등장했는데, 2025~2026년 사이 이 공간이 급격히 성숙하면서 선택지가 세 가지로 뚜렷하게 나뉘었다.
Mem0, Letta, Zep — 이 셋은 GitHub 스타 수, 커뮤니티 채택 규모, 그리고 무엇보다 아키텍처 철학이 서로 완전히 다른 접근을 대표한다. 기존 스택에 최소한의 변경으로 꽂는 메모리 레이어, OS처럼 메모리를 자율 관리하는 에이전트 플랫폼, 시간의 흐름까지 기록하는 시계열 지식 그래프 — 세 가지 철학이 실제 기술 선택을 어떻게 바꿔놓는지 직접 써보면서 겪은 경험을 곁들여 풀어보려 한다. 이 글을 읽고 나면 자신의 서비스 성격에 맞는 메모리 시스템을 30분 안에 좁힐 수 있을 것이다. 각 시스템의 작동 원리, 실제 실행 가능한 코드 예시, 처음에 저지르기 쉬운 실수까지 순서대로 다룬다.
핵심 개념
왜 외부 메모리가 필요한가
LLM 컨텍스트 윈도우는 RAM과 비슷하다. 전원을 끄면 날아가고, 용량에는 한계가 있다. GPT-4o가 128k 토큰을 지원한다고 해도, 수십 번의 세션에 걸친 사용자 이력을 모두 담으면 비용이 감당이 안 된다. 장기 메모리 시스템은 이 문제를 외부 저장소로 해결한다. 중요한 사실만 골라 저장하고, 새 대화가 시작될 때 관련 기억만 꺼내 컨텍스트에 주입하는 방식이다.
장기 메모리(Long-Term Memory): AI 에이전트가 세션과 시간을 초월해 사용자 정보, 선호, 컨텍스트를 지속적으로 기억·갱신할 수 있게 해주는 인프라 계층. LLM의 컨텍스트 윈도우 한계를 외부 저장소로 보완하며, 메모리 시스템 자체도 LLM 호출을 수반하므로 추가 레이턴시와 비용이 발생한다는 점을 미리 염두에 두는 것이 좋다.
Mem0 — 기존 스택에 꽂는 메모리 레이어
Mem0의 접근법은 "최소 침습"이다. LangGraph, CrewAI, AutoGen 같이 이미 쓰고 있는 에이전트 프레임워크를 바꾸지 않고, 그 위에 메모리 레이어를 볼트온으로 연결한다.
볼트온(Bolt-on): 기존 시스템을 수정하지 않고 외부에서 기능을 추가로 연결하는 통합 방식. 플러그인처럼 붙였다 뗐다 할 수 있어 점진적 도입에 유리하다.
내부적으로는 벡터·그래프·키-값 세 가지 저장소를 혼합해서 쓴다. 대략 설명하면, 자연어 의미가 중요한 사실(선호, 감정, 설명)은 벡터에, 사람·조직·사물 사이의 관계는 그래프에, 자주 참조하는 키 기반 데이터는 키-값에 저장된다. 대화에서 사실을 추출할 때 LLM을 활용해 중복을 제거하고, 충돌이 생기면 최신 정보로 덮어쓴다.
2026년 4월 업데이트에서 단일 패스 계층 추출 알고리즘을 공개했다. 기존 방식이 여러 단계에 걸쳐 사실을 추출·분류했다면, 이 알고리즘은 한 번의 LLM 호출로 추출과 분류를 동시에 처리한다. 덕분에 시간적 쿼리 정확도가 +29.6p, 멀티홉 추론이 +23.1p 향상됐다는 벤치마크 결과를 보여줬다. GitHub 스타 48,000+, 2024년 시리즈 A 24M 달러 유치라는 숫자가 커뮤니티 반응을 대변한다. 솔직히 처음 수치를 봤을 때 "설마 이게 진짜야?" 싶었는데, 직접 써보니 간단한 고객 지원 시나리오에서는 체감이 됐다.
Letta — 메모리를 OS처럼 관리하는 에이전트 플랫폼
Letta(구 MemGPT)는 UC Berkeley의 MemGPT 논문에서 출발했다. 핵심 아이디어는 메모리를 컴퓨터 운영체제처럼 계층화한다는 것이다.
- Core Memory: RAM처럼 항상 컨텍스트에 올라가 있는 핵심 정보 (이름, 페르소나, 주요 사실)
- Recall Memory: 최근 대화 기록을 검색 가능한 형태로 보관하는 디스크 캐시
- Archival Memory: 방대한 장기 지식을 보관하는 콜드 스토리지
가장 독특한 점은 에이전트가 스스로 함수 호출로 이 계층들 사이에서 데이터를 이동·편집한다는 것이다. 사람이 개입하지 않아도 에이전트가 자율적으로 메모리를 관리한다. 처음에 메모리 함수 설계를 잡을 때 반나절을 날렸는데, 어떤 정보를 Core에 두고 어떤 정보를 Archival로 보낼지 기준을 먼저 정의하지 않으면 에이전트가 엉뚱한 계층에 저장하는 일이 생긴다. 그 설계 비용을 초기에 치르면 이후엔 오히려 투명하게 동작을 추적할 수 있다는 게 장점이다. 완전 오픈소스(MIT)이고, 2026년 1월에는 병렬 에이전트 간 공유 메모리를 지원하는 Conversations API도 출시됐다.
Zep — 시간의 흐름을 기억하는 지식 그래프
Zep의 핵심 엔진인 Graphiti는 접근법 자체가 다르다. 사실(fact)에 유효 시간(validity window)을 부여한다. 예를 들어 "사용자의 주소가 서울 강남구"라는 사실이 나중에 "부산 해운대구"로 바뀌면, 기존 기록을 삭제하지 않고 무효(invalidated) 처리한다. 이 무효화는 규칙 기반이 아니라 LLM이 새 에피소드의 의미를 파악해 기존 팩트와 충돌 여부를 판단하는 방식으로 이뤄진다. 덕분에 "2025년 3월 당시의 주소"를 물어봐도 정확하게 답할 수 있다.
Temporal Knowledge Graph (시계열 지식 그래프): 팩트의 변경 이력을 타임스탬프와 함께 보존하는 지식 그래프. 삭제 대신 무효화로 과거와 현재 상태를 동시에 추적한다. Neo4j 위에서 동작하며, 각 팩트 노드는
valid_at,invalid_at속성으로 유효 기간을 관리한다.
멀티홉 추론(Multi-hop Reasoning): "A의 상사인 B의 팀이 담당하는 프로젝트"처럼 여러 관계를 연결해야 도달하는 답을 추론하는 능력. 단순 벡터 검색으로는 이런 체이닝된 관계를 잘 처리하지 못하고, 그래프 구조가 있어야 효과적으로 탐색할 수 있다.
Graphiti 엔진을 Apache 2.0 오픈소스로 공개하면서 커뮤니티 채택이 빠르게 늘었다. DMR Benchmark(대화에서 팩트를 얼마나 정확하게 검색하는지 측정하는 기준)에서 94.8%라는 높은 정확도를 기록했고, SOC 2 Type 2, HIPAA, GDPR 인증도 갖추고 있어 엔터프라이즈 환경에서 존재감이 크다. Neo4j 세팅이 처음에는 진입 장벽처럼 느껴지지만, Graphiti만 독립으로 쓰면 생각보다 훨씬 가볍게 시작할 수 있다.
실전 적용
예시 1: Mem0로 고객 지원 챗봇에 메모리 붙이기
기존 OpenAI 챗봇에 Mem0를 추가하는 과정은 생각보다 간단하다. "이게 정말 이렇게 쉬워?" 싶었는데 실제로 해보면 그렇다. 다만 한 가지 — 신규 사용자의 첫 대화처럼 relevant_memories가 빈 경우를 명시적으로 처리하지 않으면 시스템 프롬프트에 빈 줄이 들어가서 약간 어색한 응답이 나온다. 아래 코드에서 그 부분도 함께 보여준다.
from mem0 import Memory
from openai import OpenAI
# Qdrant가 없다면 docker run -p 6333:6333 qdrant/qdrant 로 바로 띄울 수 있다
config = {
"vector_store": {
"provider": "qdrant",
"config": {
"collection_name": "customer_support",
"host": "localhost",
"port": 6333,
}
},
"llm": {
"provider": "openai",
"config": {"model": "gpt-4o", "temperature": 0}
}
}
memory = Memory.from_config(config)
client = OpenAI()
def chat_with_memory(user_id: str, user_message: str) -> str:
relevant_memories = memory.search(user_message, user_id=user_id)
# 신규 사용자 첫 대화 처리 — 빈 리스트면 메모리 섹션 자체를 생략
if relevant_memories:
memory_context = "\n".join([m["memory"] for m in relevant_memories])
memory_section = f"\n이 사용자에 대해 알고 있는 정보:\n{memory_context}"
else:
memory_section = ""
system_prompt = f"당신은 친절한 고객 지원 에이전트입니다.{memory_section}"
try:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
]
)
assistant_message = response.choices[0].message.content
# 대화 내용에서 중요 사실을 자동 추출·저장
# 실무에서는 여기에 재시도 로직과 저장 실패 알림을 추가하는 것을 권장한다
memory.add(
[
{"role": "user", "content": user_message},
{"role": "assistant", "content": assistant_message}
],
user_id=user_id
)
return assistant_message
except Exception as e:
# 메모리 저장 실패가 대화 자체를 막지 않도록 분리해서 처리하는 것이 좋다
raise
# 사용 예시
print(chat_with_memory("user_123", "저는 프리미엄 플랜 사용자인데 결제 문제가 있어요"))
# 다음 세션에서 "결제 문제 있었던 프리미엄 사용자"라는 맥락이 자동으로 주입됨
print(chat_with_memory("user_123", "아까 말씀드린 문제가 해결됐나요?"))| 코드 포인트 | 설명 |
|---|---|
memory.search() |
사용자 ID 스코프로 관련 기억만 벡터 검색해 반환 |
memory_section 분기 |
신규 사용자 첫 대화에서 빈 컨텍스트 블록이 프롬프트에 들어가지 않도록 처리 |
memory.add() |
대화 쌍을 넘기면 LLM이 중요 사실을 자동 추출·저장 |
user_id 파라미터 |
사용자별 메모리 격리 — 다른 사용자 기억이 섞이지 않음 |
예시 2: Letta로 장기 코딩 에이전트 구성하기
단순한 고객 지원 챗봇이라면 Mem0로 충분하다. Letta를 선택하는 이유는 에이전트가 수일에 걸쳐 자율적으로 작업할 때다. 코딩 에이전트가 오늘 파악한 "인증 방식 변경" 사실을 일주일 뒤 작업에서도 기억하고, 스스로 어느 메모리 계층에 저장할지 판단하는 그 자율성이 핵심이다.
from letta import create_client
from letta.schemas.memory import ChatMemory
client = create_client()
# Core Memory에 초기 정보 설정 — 항상 컨텍스트에 포함되는 기본값
# human과 persona 설계가 에이전트 동작의 토대가 되므로 첫 설계를 신중하게 잡는 것이 좋다
agent = client.create_agent(
name="coding-assistant",
memory=ChatMemory(
human="개발자, Python/TypeScript 주로 사용, FastAPI 프로젝트 진행 중",
persona="경험 많은 시니어 개발자 어시스턴트. 코드 리뷰와 디버깅을 돕는다."
),
)
# 에이전트가 Core/Recall/Archival 중 어디에 저장할지 스스로 판단한다
response = client.send_message(
agent_id=agent.id,
role="user",
message="우리 프로젝트의 API 인증 방식이 JWT에서 OAuth2로 바뀐 거 기억해둬"
)
print(response.messages[-1].text)
# 며칠 후 새 세션에서도 기억이 유지됨 — 같은 agent_id가 서버에 상태를 영구 보존
response2 = client.send_message(
agent_id=agent.id,
role="user",
message="인증 관련 코드 짤 때 주의사항이 뭐였지?"
)
print(response2.messages[-1].text)
# OAuth2로 바뀌었다는 맥락을 기억하고 답변| 코드 포인트 | 설명 |
|---|---|
ChatMemory(human, persona) |
Core Memory 초기화 — 매 컨텍스트에 항상 포함되는 기본 정보 |
agent_id 재사용 |
서버에 저장된 에이전트 상태를 불러옴 — 세션 재시작 후에도 연속성 유지 |
| 에이전트 자율 판단 | 어느 계층에 저장할지는 에이전트가 내부 함수 호출로 결정 — 로그에서 추적 가능 |
예시 3: Graphiti(Zep 엔진)로 시계열 팩트 관리하기
금융이나 CRM처럼 "이전 상태"와 "현재 상태"를 명확히 구분해야 하는 도메인에서 Graphiti가 빛난다. Mem0나 Letta는 최신 정보로 덮어쓰거나 갱신하지만, Graphiti는 이전 팩트를 무효화하면서도 보존한다. 아래 코드는 async 함수 안에서 실행해야 하므로 asyncio.run()으로 감싸는 부분까지 포함했다.
import asyncio
from graphiti_core import Graphiti
from graphiti_core.nodes import EpisodeType
from datetime import datetime
async def main():
# Neo4j가 없다면: docker run -p 7687:7687 -e NEO4J_AUTH=neo4j/password neo4j
graphiti = Graphiti(
neo4j_uri="bolt://localhost:7687",
neo4j_user="neo4j",
neo4j_password="password"
)
# 인덱스와 제약 조건 초기화 — 최초 1회만 실행하면 된다
await graphiti.build_indices_and_constraints()
# 팩트 추가 — 에피소드 단위로 시간 정보와 함께 저장
await graphiti.add_episode(
name="고객 주소 변경",
episode_body="고객 김철수의 주소가 서울 강남구에서 부산 해운대구로 변경됨",
source=EpisodeType.text,
reference_time=datetime(2025, 6, 1), # 실제 변경 시점 — 저장 시점과 구분됨
source_description="CRM 시스템 업데이트"
)
# 시간적 쿼리 — 현재 주소든, 특정 시점의 주소든 정확하게 응답
results = await graphiti.search("김철수의 주소", num_results=5)
for edge in results:
print(f"팩트: {edge.fact}")
print(f"유효 시작: {edge.valid_at}")
print(f"유효 종료: {edge.invalid_at or '현재까지 유효'}")
print("---")
# 출력 예시:
# 팩트: 김철수의 주소는 서울 강남구
# 유효 시작: 2024-01-15
# 유효 종료: 2025-06-01
# ---
# 팩트: 김철수의 주소는 부산 해운대구
# 유효 시작: 2025-06-01
# 유효 종료: 현재까지 유효
await graphiti.close()
asyncio.run(main())| 코드 포인트 | 설명 |
|---|---|
async def main() + asyncio.run() |
Graphiti API가 전부 비동기 — 최상위에서 직접 await 호출은 안 된다 |
reference_time |
팩트가 실제 발생한 시점 — 저장 시점과 분리해 정확한 시계열 추적 가능 |
invalid_at |
팩트가 무효화된 시점 — None이면 현재도 유효 |
add_episode() |
삭제 없이 새 에피소드를 추가하고, LLM이 기존 팩트와 충돌 여부를 판단해 자동 무효화 |
어떤 상황에서 무엇을 고르나
아래 매트릭스가 선택을 가장 빠르게 좁히는 방법이다. 행이 하나라도 "예"라면 그 시스템을 우선 탐색해보는 것을 권장한다.
| 상황 | Mem0 | Letta | Zep / Graphiti |
|---|---|---|---|
| 기존 LangChain/CrewAI 스택을 유지하고 싶다 | ✅ | ❌ | △ |
| 빠른 프로토타입, 당장 오늘 붙여보고 싶다 | ✅ | △ | △ |
| 에이전트가 수일에 걸쳐 자율적으로 작업한다 | △ | ✅ | △ |
| 오픈소스 자체 호스팅이 필수다 | △ | ✅ | ✅ (Graphiti) |
| 시간적 상태 변화(주소 변경, 계약 갱신 등)가 핵심이다 | ❌ | △ | ✅ |
| HIPAA / SOC 2 / GDPR 인증이 필요하다 | △ | ❌ | ✅ |
| 멀티홉 관계 추론이 자주 필요하다 | △ | △ | ✅ |
| 메모리 동작을 코드 레벨에서 세밀하게 제어하고 싶다 | ❌ | ✅ | △ |
△ = 가능하지만 제한적이거나 추가 설정 필요
장단점 분석
장점
| 시스템 | 핵심 강점 |
|---|---|
| Mem0 | 기존 프레임워크 변경 없이 바로 연결 가능; p95 검색 지연 200ms, 토큰 사용량 91% 절감; 20개 벡터 스토어 백엔드 지원; 활발한 커뮤니티(GitHub 48k+) |
| Letta | 완전 오픈소스(MIT); 에이전트 자율 메모리 관리로 사실상 무한 컨텍스트 구현; 쓰기 즉시 읽기 가능한 트랜잭션 일관성; 메모리 동작 로그 추적 가능 |
| Zep | 시계열 팩트 관리로 시간적 쿼리 최강; SOC 2 / HIPAA / GDPR 인증; 하이브리드 검색(시맨틱+키워드+그래프)으로 DMR Benchmark 94.8%; Graphiti 엔진 Apache 2.0 독립 오픈소스 |
단점 및 주의사항
Mem0의 그래프 메모리가 클라우드 Pro($249/월) 전용이라는 점은 처음에 놓치기 쉬운 함정이다. 자체 호스팅하면 벡터 검색만 가능하고 관계 기반 멀티홉 추론은 쓸 수 없다. 멀티홉 추론이 핵심 요구사항이라면 처음부터 Graphiti를 직접 통합하는 쪽이 더 나은 선택일 수 있다.
Letta는 메모리 함수 설계 복잡도가 높다. 우리 팀이 처음에 저질렀던 실수는 Core Memory의 용량 제한을 무시하고 모든 것을 Core에 밀어 넣으려 한 것이었다. Core는 항상 컨텍스트에 올라가는 만큼 크기 제한이 있고, 그 설계를 초기에 잘 잡지 않으면 에이전트가 예상치 못한 계층에 데이터를 저장하기 시작한다. 소규모 프로토타입이라면 Letta Cloud로 시작해서 감을 잡은 뒤 자체 호스팅으로 이전하는 것을 권장한다.
Zep은 솔직히 토큰 소비량이 예상보다 많이 나올 수 있다. LLM이 팩트 충돌 여부를 매번 판단하는 구조라 대화 빈도가 높은 서비스에서는 비용 테스트를 먼저 돌려보는 것이 안전하다. 비용이 부담스럽다면 전체 Zep 플랫폼 대신 Graphiti 엔진만 독립으로 쓰는 방법도 있다.
| 시스템 | 단점 | 대응 방안 |
|---|---|---|
| Mem0 | 그래프 메모리는 클라우드 Pro 전용; 자체 호스팅 시 순수 벡터 검색만 가능 | 멀티홉 추론이 필수라면 Graphiti 직접 통합 고려 |
| Letta | 자체 호스팅 운영 오버헤드; Core Memory 용량 제한 설계 필요; 초기 학습 곡선 | 소규모 프로토타입은 Letta Cloud로 시작 후 이전 |
| Zep | LLM 기반 팩트 판단으로 토큰 소비 높을 수 있음; 전체 플랫폼은 SaaS 중심 | 비용 민감한 경우 Graphiti 엔진만 독립 사용 |
실무에서 가장 흔한 실수
-
모든 대화 내용을 무조건 저장하려는 것 — 우리 팀도 초기에 이 실수를 했다. 메모리 시스템도 LLM 호출을 수반하므로 추가 지연과 비용이 발생한다. "어떤 정보를 왜 저장하는가"의 기준(중요도 임계치, 정보 유형 필터)을 먼저 설계하는 것이 나중에 비용을 아끼는 길이다.
-
벤치마크 점수만 보고 선택하는 것 — LOCOMO(대화 메모리 정확도)나 LongMemEval(장기 메모리 추론 종합, ICLR 2025 채택) 점수가 실제 서비스 품질을 완전히 대변하지 않는다. 자신의 워크로드 특성(시간적 쿼리 비율, 멀티홉 추론 필요성, 세션 빈도)에 맞는 시나리오 10~20개로 자체 평가를 병행하는 것이 중요하다.
-
자체 호스팅 vs. 관리형 SaaS를 비용만으로 결정하는 것 — 데이터 주권, 규정 준수(HIPAA, GDPR), 운영 팀의 역량을 함께 고려해야 한다. 특히 Mem0의 그래프 메모리는 클라우드 전용이라 자체 호스팅 시 기능 차이가 크다는 점을 미리 확인해두는 것이 좋다.
마치며
개인적으로 새 프로젝트를 시작한다면 일단 Mem0로 빠르게 프로토타입을 만들고, "시간적 상태 변화가 중요한가" 또는 "에이전트가 수일에 걸쳐 자율 작업하는가" 두 질문에 "예"가 나오는 시점에 Graphiti나 Letta로 옮기는 흐름을 택할 것 같다. Mem0는 빠른 통합, Letta는 자율적인 에이전트 제어, Zep은 시간의 흐름까지 추적해야 하는 엔터프라이즈 환경 — 올바른 선택은 서비스의 성격과 팀의 운영 역량에 달려 있다.
지금 바로 시작해볼 수 있는 3단계:
-
Mem0로 30분 프로토타이핑 —
pip install mem0ai후 기존 챗봇 코드에memory.add()와memory.search()두 줄을 추가해보시면 장기 메모리의 감을 빠르게 잡을 수 있습니다. 공식 문서에 Getting Started 예제가 잘 정리되어 있습니다. -
Letta 또는 Graphiti 탐색 — "에이전트가 수일에 걸쳐 자율적으로 작업하는가"라면 Letta를, "시간적 상태 변화(주소 변경, 계약 갱신 등)가 핵심인가"라면 Graphiti를 Docker로 바로 띄워서 위 예시 코드를 실행해보시면 좋습니다. 두 질문 모두 "아니오"라면 Mem0에서 더 깊게 파는 것이 더 효율적일 수 있습니다.
-
자체 평가셋 구성 — 공개 벤치마크 외에 실제 서비스 시나리오 10~20개를 직접 작성해서 각 시스템의 검색 정확도와 지연시간을 측정해보시기를 권장합니다. Letta는 이를 위한 오픈소스 평가 프레임워크 Letta Evals도 제공하고 있습니다.
참고 자료
- Mem0 오픈소스 개요 | docs.mem0.ai — Getting Started 및 공식 API 레퍼런스
- Mem0 GitHub | mem0ai/mem0 — 소스 코드 및 이슈 트래커
- Mem0 논문 | arXiv:2504.19413 — 단일 패스 계층 추출 알고리즘 원문
- State of AI Agent Memory 2026 | Mem0 블로그 — 벤치마크 수치 및 시장 현황
- Letta 공식 사이트 | letta.com — Letta Cloud 및 Evals 프레임워크
- Letta GitHub | letta-ai/letta — MIT 오픈소스 전체 코드
- Letta 메모리 관리 문서 | docs.letta.com — Core/Recall/Archival 계층 심화 설명
- Agent Memory: How to Build Agents that Learn and Remember | Letta 블로그 — 에이전트 메모리 설계 아키텍처
- Zep 공식 사이트 | getzep.com — 엔터프라이즈 플랜 및 인증 정보
- Zep 논문 | arXiv:2501.13956 — Temporal Knowledge Graph 아키텍처 원문
- Graphiti GitHub | getzep/graphiti — Apache 2.0 독립 오픈소스 엔진
- Graphiti: Knowledge Graph Memory for an Agentic World | Neo4j 블로그 — Neo4j 연동 아키텍처 심화
- Mem0 + AWS 레퍼런스 아키텍처 | AWS 블로그 — ElastiCache + Neptune Analytics 완전 관리형 구성
- Mem0 vs Zep vs Letta 비교 | HydraDB — 세 시스템 기능 비교표
- Agent Memory at Scale 2026 비교 | AgentMarketCap — 시장 벤더 현황 및 벤치마크 종합
- Zep vs Mem0: Benchmarks and Pricing | Atlan — 성능 및 비용 구체적 비교