나를 기억하는 회고 컴패니언 — Mem0 논문(arXiv:2504.19413)의 장기 기억 아키텍처를 학습 목적으로 재구현한 개인 프로젝트입니다.
매일의 회고를 대화로 남기면, memoir가 핵심 사실만 추출해 기억하고 —
"작년 이맘때 나 무슨 고민 했었지?" "요즘 운동 얼마나 자주 했어?"
같은 질문에 타임스탬프 기반 시간 추론으로 답합니다. 모순되는 새 정보가 오면(이직, 취향 변화, 이사) 기존 기억을 스스로 갱신·무효화합니다.
나 › 요즘은 디카페인만 마셔. 카페인 때문에 잠을 못 자겠더라
memoir › 커피를 좋아하셨는데 디카페인으로 바꾸셨군요. 수면이 좀 나아졌나요?
╭─ 🧠 메모리 연산 ──────────────────────────────────────╮
│ ↻ UPDATE 사용자는 커피를 좋아한다 │
│ → 사용자는 커피를 좋아하며 디카페인만 마신다 │
│ + ADD 사용자는 2026-07경 카페인으로 수면 문제를 겪음 │
╰──────────────────────────────────────────────────────╯
턴마다 ADD / UPDATE / DELETE / NOOP 연산 로그가 표시되어, 논문의 메모리 파이프라인이 실제로 어떻게 동작하는지 볼 수 있습니다.
대화 (mₜ₋₁, mₜ)
│
▼ [쓰기 경로]
① Extraction ── 요약 S + 최근 m개 메시지 컨텍스트로 사실 추출 ── pipeline/extraction.py
② Update ── 유사 메모리 top-s 검색 → LLM tool call로 ── pipeline/update.py
ADD/UPDATE/DELETE/NOOP 선택 (Algorithm 1)
│
▼
Qdrant (벡터, 로컬) + SQLite (대화·이력·요약)
▲
│ [읽기 경로]
③ Retrieval ── 의미 검색 → 타임스탬프 포함 컨텍스트로 응답 ── retrieval + cli.py
| 논문 | 이 구현 | 비고 |
|---|---|---|
| 추출 프롬프트 P = (S, 최근 m개, 쌍) | pipeline/extraction.py |
m=10 (논문 설정) |
| 비동기 요약 갱신 모듈 | pipeline/summary.py |
N턴마다 asyncio.create_task |
| Tool call 기반 4연산 (Algorithm 1) | pipeline/update.py |
s=10, tool_choice="required" |
| DELETE (물리 삭제) | soft delete로 변경 | LLM 오판 복구 가능 (Mem0ᵍ의 invalid 방식 차용) |
| GPT-4o-mini | 동일 | temperature=0 |
| OpenAI 임베딩 | bge-m3 로컬 (dim=1024) | 한국어 성능, 무료·오프라인 |
| 벡터 DB | Qdrant 로컬 임베디드 모드 | 서버·도커 불필요 |
| Mem0ᵍ 2단계 추출 (엔티티→관계) | graph/pipeline.py |
취향 그래프: (사용자)-[prefers]->(디카페인) |
| 노드 해소 (유사도 ≥ t) | GraphMemory._resolve_node |
t=0.85 |
| 충돌 감지 → invalid 표시 | LLM update resolver | 물리 삭제 없음, temporal 보존 |
| 이중 검색 (entity-centric + semantic triplet) | GraphMemory.search |
결과 병합 |
| Neo4j | NetworkX 기본 + Neo4j 옵셔널 | 같은 BaseGraphStore 인터페이스, 백엔드 교체 가능 |
원 논문과 달리 한국어 상대 시간 정규화("지지난주", "재작년" → 절대 날짜)를 추출 단계에 내장했습니다. 논문에서 OpenAI 메모리가 temporal 질문에서 J<15%로 폭락한 원인이 타임스탬프 누락이었기 때문에, 시간 정보 보존을 1급 요구사항으로 다룹니다.
# Python 3.10+
pip install -e ".[dev]"
cp .env.example .env # OPENAI_API_KEY 입력
memoir # 챗 시작 (최초 실행 시 bge-m3 모델 ~2GB 다운로드)명령어: /memories 저장된 기억 · /graph 관계 그래프 · /stats p50/p95 지연 통계 · /history <id> 변경 이력 · /forget <id> 삭제 · /exit
옵션 (.env):
MEMOIR_ASYNC_WRITE=1— 쓰기(추출·갱신)를 백그라운드 큐로 분리 (Phase 3, 응답 지연 최소화)MEMOIR_GRAPH_ENABLED=0— 그래프 메모리 끄기 (턴당 LLM 호출·비용 절감)MEMOIR_GRAPH_BACKEND=neo4j— Neo4j 백엔드로 전환:
pip install -e ".[graph]"
docker compose up -d # http://localhost:7474 에서 그래프 시각화채팅 + 기억 카드 + 관계 그래프 시각화(vis-network) + 지연 통계를 브라우저에서:
pip install -e ".[web]"
memoir-web # http://127.0.0.1:8787Qdrant 로컬 모드는 단일 프로세스 전용이라 웹 UI와 CLI를 동시에 띄울 수 없습니다.
가짜 LLM/임베더로 파이프라인 로직을 검증합니다 (API 키·torch 불필요):
pytest- 중복 사실 → NOOP (중복 방지)
- 모순 정보 → soft DELETE, 검색 제외 + 데이터 보존
- UPDATE → 병합 + ADD→UPDATE 감사 이력
- LLM이 존재하지 않는 memory_id 지목(환각) → 안전한 NOOP 폴백
- user_id 격리 — 다른 사용자 메모리가 검색·갱신 후보에 절대 노출되지 않음
한국어 회고 시나리오 페르소나 5개 × 20문항 = 100문항 (single-hop / temporal / multi-hop / preference 각 25개). 시나리오별로 다른 user_id로 주입해 격리를 겸사겸사 검증하며, 세션 주입 → 답변 생성 → LLM-as-a-Judge(논문 부록 A) 채점 → 유형별·시나리오별 J + 95% 신뢰구간 + 오답 목록 + 지연 리포트를 출력합니다:
python -m eval.run_eval --runs 3 # Mem0ᵍ (그래프 포함), judge 3회
python -m eval.run_eval --no-graph # Mem0 기본형과 비교
python -m eval.run_eval --scenario eval/scenarios/01_developer.json # 단일 시나리오
python -m eval.judge cases.jsonl # 임의 QA 채점100문항 기준 표본 95% CI는 약 ±8%p — "유형별 난이도 패턴이 논문과 일치한다" 수준의 주장이 가능한 규모입니다. 논문의 Mem0 vs Mem0ᵍ 2%p 차이 검증에는 LOCOMO 규모(~2,000문항)가 필요합니다. 평가 데이터는 임시 디렉토리에 격리되어 개인 메모리를 오염시키지 않습니다.
- Phase 1 — 추출 + 저장 + 검색 (동작하는 기억 챗봇)
- Phase 2 — 갱신 파이프라인 (ADD/UPDATE/DELETE/NOOP, 요약 비동기 갱신, 감사 로그)
- Phase 3 — 쓰기 경로 큐 분리 (
BackgroundWriter), p50/p95 지연 계측 (/stats) - Phase 4 — 취향 그래프 (Mem0ᵍ): 2단계 추출, 노드 해소, 충돌 무효화, 이중 검색 (NetworkX 기본 + Neo4j 옵셔널)
- Phase 5 — 한국어 평가셋 100문항 + LLM-as-a-Judge 러너 (J 77% → 프롬프트 개선 → 93%)
- LOCOMO 서브셋 평가 연동 (
python -m eval.locomo) — 논문 수치와 직접 비교 - 웹 UI (
memoir-web) — 채팅·기억 카드·그래프 시각화·지연 통계
- 논문: Chhikara et al., Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory (2025)
- 원저자 공식 구현: mem0ai/mem0 — 본 프로젝트는 학습·커스터마이징 목적의 독립 재구현입니다
MIT