ZeroClaw — Memory 시스템
Layer 2 학습. 실제 소스 코드 기반. 파일:
src/memory/traits.rs,src/memory/mod.rs,src/memory/retrieval.rs
전체 구조
Memory trait (계약)
│
├─ SqliteMemory ← 기본값. FTS5 + 벡터 하이브리드 검색
├─ LucidMemory ← SQLite 위에 추가 기능 레이어
├─ MarkdownMemory ← 파일 기반 (단순)
├─ PostgresMemory ← feature flag 필요
├─ QdrantMemory ← 외부 벡터 DB
└─ NoneMemory ← 비활성화 (noop)
RetrievalPipeline
├─ Stage 1: Hot Cache (in-memory LRU, TTL 300s)
├─ Stage 2: FTS (keyword, 점수 0.85 이상이면 early return)
└─ Stage 3: Vector (임베딩 유사도 + FTS 하이브리드 병합)
Memory trait — 계약
#[async_trait]
pub trait Memory: Send + Sync {
fn name(&self) -> &str;
async fn store(
&self, key: &str, content: &str,
category: MemoryCategory, session_id: Option<&str>
) -> anyhow::Result<()>;
async fn recall(
&self, query: &str, limit: usize,
session_id: Option<&str>,
since: Option<&str>, until: Option<&str>, // ISO 8601
) -> anyhow::Result<Vec<MemoryEntry>>;
async fn get(&self, key: &str) -> anyhow::Result<Option<MemoryEntry>>;
async fn forget(&self, key: &str) -> anyhow::Result<bool>;
async fn list(...) -> anyhow::Result<Vec<MemoryEntry>>;
async fn count(&self) -> anyhow::Result<usize>;
async fn health_check(&self) -> bool;
// default impl: noop (SQLite 등이 override)
async fn store_procedural(&self, messages: &[ProceduralMessage], ...) {}
async fn recall_namespaced(&self, namespace: &str, ...) {}
async fn store_with_metadata(&self, ..., namespace, importance) {}
}
MemoryEntry — 저장 단위
pub struct MemoryEntry {
pub id: String,
pub key: String, // 식별자 (예: "user_preference_language")
pub content: String, // 실제 내용
pub category: MemoryCategory,
pub timestamp: String, // RFC 3339
pub session_id: Option<String>,
pub score: Option<f64>, // recall 시 관련도 점수
pub namespace: String, // agent 간 격리 (기본값 "default")
pub importance: Option<f64>, // 0.0~1.0, 우선순위 검색용
pub superseded_by: Option<String>, // 충돌 시 대체된 항목 ID
}
MemoryCategory
pub enum MemoryCategory {
Core, // 장기 사실, 선호도, 결정 사항
Daily, // 일일 세션 로그
Conversation, // 대화 컨텍스트
Custom(String), // 사용자 정의 ("schedule", "assignment" 등)
}
내 프로젝트에서:
// 과제 정보 저장
memory.store(
"assignment_알고리즘설계_hw3",
"3주차 과제: 다익스트라 구현, 마감 2026-04-01",
MemoryCategory::Custom("assignment".into()),
Some(session_id),
).await?;
RetrievalPipeline — 3단계 검색
recall("다익스트라 과제")
│
├─ Stage 1: Hot Cache
│ TTL 5분 / LRU 256개
│ └─ 캐시 히트 → 즉시 반환
│
├─ Stage 2: FTS (SQLite FTS5 키워드 검색)
│ └─ 상위 score >= 0.85 → early return (Stage 3 스킵)
│ score < 0.85 → Stage 3로
│
└─ Stage 3: Vector (임베딩 유사도)
쿼리를 임베딩 벡터로 변환 → 코사인 유사도 계산
FTS 결과와 가중 합산:
final_score = vector_weight * vec_score
+ keyword_weight * fts_score
기본값: vector_weight=0.7, keyword_weight=0.3
FTS early return 이유: 키워드가 정확히 일치하면 굳이 벡터 임베딩 API 호출할 필요 없음. 비용과 지연 절약.
turn()과의 통합
// agent.rs - turn() 내부
// 1. 매 대화마다 관련 기억 로드
let context = memory_loader.load_context(
memory.as_ref(), user_message, session_id
).await.unwrap_or_default();
// 2. 기억을 user message 앞에 주입
let enriched = if context.is_empty() {
format!("[{now}] {user_message}")
} else {
format!("{context}[{now}] {user_message}")
};
// 3. auto_save = true면 user message 자동 저장
if self.auto_save {
memory.store("user_msg", user_message, Conversation, session_id).await;
}
DefaultMemoryLoader: 기본값 recall limit = 5, min_relevance_score = 0.3
백엔드 선택 흐름 (create_memory)
config.memory.backend = "sqlite"
│
├─ "sqlite" → SqliteMemory::with_embedder(...)
├─ "lucid" → LucidMemory(SqliteMemory) // 추가 기능 레이어
├─ "markdown" → MarkdownMemory(workspace_dir)
├─ "postgres" → PostgresMemory (feature flag)
├─ "qdrant" → QdrantMemory (외부 서버)
├─ "none" → NoneMemory (noop)
└─ 기타 → warn + fallback to MarkdownMemory
storage.provider.config.provider 설정이 있으면 그걸 우선함.
SqliteMemory — 내부 구조
brain.db (SQLite 파일)
├─ memories 테이블 (FTS5)
│ key, content, category, timestamp, session_id, namespace, importance
│
├─ embeddings 테이블
│ memory_id → embedding_vector (BLOB, f32 배열)
│
└─ FTS5 인덱스 (content 컬럼 전문 검색)
임베딩 provider 선택:
[memory]
embedding_provider = "openai" # openai, cohere, ollama, none
embedding_model = "text-embedding-3-small"
embedding_dimensions = 1536
vector_weight = 0.7
keyword_weight = 0.3
embedding_provider = "none" 이면 FTS 전용 (벡터 계산 없음). 로컬 환경에서 API 비용 없이 쓸 수 있음.
부가 기능들
Snapshot / Hydration:
brain.db 없음 + MEMORY_SNAPSHOT.md 있음
→ "Cold boot" 감지 → Markdown에서 SQLite로 자동 복원
Memory Hygiene:
오래된 Conversation 기억 자동 삭제 (retention_days)
중복/충돌 항목 병합 (conflict.rs)
Procedural Memory:
tool call 패턴을 "how to" 형태로 저장
다음 유사한 요청 시 절차적 기억으로 활용
Namespace 격리:
여러 agent가 같은 DB 공유할 때
namespace = "schedule_agent" vs "crawler_agent" 로 분리
내 프로젝트 설정
[memory]
backend = "sqlite"
auto_save = true
embedding_provider = "none" # 로컬 환경, 벡터 없이 FTS만
# embedding_provider = "ollama" # 로컬 임베딩 원하면
# embedding_model = "nomic-embed-text"
# embedding_dimensions = 768
vector_weight = 0.0
keyword_weight = 1.0
min_relevance_score = 0.3
max_history_messages = 50
LLM tool 로도 직접 조작:
memory_store(key, content, category) # 기억 저장
memory_recall(query, limit) # 기억 검색
memory_forget(key) # 기억 삭제
관련
- agent-loop — turn()에서 memory_loader.load_context() 호출
- tool-system — MemoryStoreTool, MemoryRecallTool, MemoryForgetTool
- config-schema — [memory] 섹션 전체
- overview — ZeroClaw 학습 지도