벡터 데이터베이스에 메모리를 저장하고 임베딩 기반으로 검색하는 장기 메모리 시스템의 설계와 구현 전략을 다룹니다.
2장에서 다룬 단기 메모리는 세션이 종료되면 사라집니다. 하지만 실제 에이전트 서비스에서는 다음과 같은 요구사항이 있습니다.
이러한 요구를 충족하려면 세션을 넘어 지속되는 장기 메모리(Long-term Memory)가 필요합니다. 그리고 이 장기 메모리의 핵심 인프라가 바로 벡터 데이터베이스(Vector Database)입니다.
임베딩(Embedding)은 텍스트를 고차원 벡터 공간의 점으로 변환하는 과정입니다. 의미적으로 유사한 텍스트는 벡터 공간에서 서로 가까이 위치합니다.
import { EmbeddingModel } from "./embeddings";
import { VectorDB } from "./vector-db";
interface MemoryRecord {
id: string;
content: string;
metadata: {
userId: string;
sessionId: string;
timestamp: number;
type: "fact" | "preference" | "interaction";
};
embedding?: number[];
}
class LongTermMemory {
constructor(
private embedder: EmbeddingModel,
private db: VectorDB
) {}
async store(content: string, metadata: MemoryRecord["metadata"]): Promise<void> {
const embedding = await this.embedder.embed(content);
await this.db.upsert({
id: crypto.randomUUID(),
content,
metadata,
embedding,
});
}
async search(
query: string,
userId: string,
topK: number = 5
): Promise<MemoryRecord[]> {
const queryEmbedding = await this.embedder.embed(query);
return this.db.query({
vector: queryEmbedding,
filter: { userId },
topK,
});
}
}메모리를 효과적으로 저장하고 검색하려면 인덱싱 전략이 중요합니다.
긴 대화를 그대로 하나의 벡터로 저장하면 검색 정밀도가 떨어집니다. 적절한 단위로 분할(Chunking)하여 저장해야 합니다.
// 전략 1: 대화 턴 단위
function chunkByTurn(messages: Message[]): string[] {
const chunks: string[] = [];
for (let i = 0; i < messages.length; i += 2) {
const userMsg = messages[i];
const assistantMsg = messages[i + 1];
if (userMsg && assistantMsg) {
chunks.push(
`사용자: ${userMsg.content}\n에이전트: ${assistantMsg.content}`
);
}
}
return chunks;
}
// 전략 2: 주제 단위
function chunkByTopic(messages: Message[]): string[] {
// 주제 전환 감지 후 그룹화
// 실제로는 LLM이나 토픽 모델링으로 경계를 판단
return detectTopicBoundaries(messages).map(
(group) => group.map((m) => `[${m.role}]: ${m.content}`).join("\n")
);
}
// 전략 3: 추출된 사실 단위
function chunkByFact(messages: Message[]): string[] {
// LLM을 사용하여 대화에서 개별 사실을 추출
// "사용자는 TypeScript를 선호한다"
// "프로젝트 마감일은 4월 15일이다"
return extractFacts(messages);
}Mem0와 같은 프레임워크는 사실 단위 청킹을 자동으로 수행합니다. 대화에서 저장할 가치가 있는 정보를 LLM이 판단하고 추출하여 개별 메모리 항목으로 저장합니다.
벡터 검색만으로는 충분하지 않은 경우가 많습니다. 메타데이터 필터링을 병행하면 검색 정밀도가 크게 향상됩니다.
interface MemoryMetadata {
// 필수 필터링 필드
userId: string;
organizationId?: string;
// 시간 관련
createdAt: number; // Unix timestamp
lastAccessedAt: number;
accessCount: number;
// 분류
type: "fact" | "preference" | "decision" | "interaction";
topic: string; // 주제 태그
importance: number; // 0.0 ~ 1.0
// 출처 추적
sourceSessionId: string;
sourceMessageIndex: number;
}벡터 검색은 항상 결과를 반환합니다. 질문과 전혀 무관한 메모리라도 "가장 가까운" 결과로 반환될 수 있습니다. 따라서 유사도 임계값(Similarity Threshold) 설정이 필수입니다.
interface SearchResult {
content: string;
similarity: number; // 0.0 ~ 1.0
metadata: MemoryMetadata;
}
function filterAndRank(
results: SearchResult[],
options: {
minSimilarity: number; // 최소 유사도 임계값
recencyWeight: number; // 시간 가중치
importanceWeight: number; // 중요도 가중치
}
): SearchResult[] {
const now = Date.now();
return results
// 1. 임계값 이하 제거
.filter((r) => r.similarity >= options.minSimilarity)
// 2. 복합 스코어 계산
.map((r) => {
const daysSinceCreation =
(now - r.metadata.createdAt) / (1000 * 60 * 60 * 24);
const recencyScore = Math.exp(-daysSinceCreation / 30); // 30일 반감기
const compositeScore =
r.similarity * 0.5 +
recencyScore * options.recencyWeight +
r.metadata.importance * options.importanceWeight;
return { ...r, compositeScore };
})
// 3. 복합 스코어 기준 정렬
.sort((a, b) => b.compositeScore - a.compositeScore);
}동일한 유사도를 가진 메모리라도, 최근에 저장된 것이 더 관련성이 높을 가능성이 큽니다. 시간 감쇠(Time Decay) 함수를 적용하면 오래된 메모리의 우선순위를 자연스럽게 낮출 수 있습니다.
유사도 임계값은 도메인과 임베딩 모델에 따라 달라집니다. 일반적으로 코사인 유사도 0.7 이상을 기준으로 시작하되, 실제 데이터로 테스트하며 조정하는 것을 권장합니다.
2026년 기준 주요 벡터 데이터베이스의 특성을 비교합니다.
완전 관리형 서비스로, 인프라 관리 없이 벡터 검색을 사용할 수 있습니다. 서버리스 아키텍처를 지원하며 대규모 트래픽에서도 안정적입니다.
from pinecone import Pinecone
pc = Pinecone(api_key="your-api-key")
index = pc.Index("agent-memory")
# 메모리 저장
index.upsert(
vectors=[
{
"id": "mem_001",
"values": embedding_vector,
"metadata": {
"user_id": "user_123",
"type": "preference",
"content": "사용자는 TypeScript를 선호합니다",
},
}
]
)
# 메모리 검색
results = index.query(
vector=query_embedding,
filter={"user_id": "user_123"},
top_k=5,
include_metadata=True,
)Rust로 작성된 고성능 오픈소스 벡터 DB입니다. 셀프 호스팅이 가능하며, 풍부한 필터링 기능을 제공합니다. 복잡한 메타데이터 필터와 벡터 검색을 동시에 수행하는 데 강점이 있습니다.
PostgreSQL의 확장으로, 기존 PostgreSQL 인프라에 벡터 검색 기능을 추가합니다. 별도의 벡터 DB를 운영하지 않아도 되므로 인프라 복잡도를 줄일 수 있습니다.
-- pgvector 확장 활성화
CREATE EXTENSION IF NOT EXISTS vector;
-- 메모리 테이블 생성
CREATE TABLE agent_memories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id TEXT NOT NULL,
content TEXT NOT NULL,
embedding vector(1536), -- OpenAI ada-002 차원
memory_type TEXT NOT NULL,
importance FLOAT DEFAULT 0.5,
created_at TIMESTAMPTZ DEFAULT NOW(),
last_accessed_at TIMESTAMPTZ DEFAULT NOW(),
access_count INTEGER DEFAULT 0
);
-- IVFFlat 인덱스 생성 (대량 데이터 시 검색 가속)
CREATE INDEX ON agent_memories
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- 의미 검색 쿼리
SELECT content, importance,
1 - (embedding <=> $1::vector) AS similarity
FROM agent_memories
WHERE user_id = $2
AND 1 - (embedding <=> $1::vector) > 0.7
ORDER BY similarity DESC
LIMIT 5;| 기준 | Pinecone | Qdrant | pgvector |
|---|---|---|---|
| 운영 방식 | 완전 관리형 | 셀프/클라우드 | 기존 PG 활용 |
| 초기 도입 난이도 | 낮음 | 중간 | 낮음 (PG 사용 시) |
| 대규모 확장성 | 우수 | 우수 | 보통 |
| 비용 | 사용량 기반 | 무료 (셀프) | 무료 (셀프) |
| 메타데이터 필터링 | 좋음 | 매우 좋음 | SQL 활용 |
| 추천 시나리오 | 빠른 MVP, 서버리스 | 고급 필터링 필요 | 기존 PG 인프라 |
장기 메모리는 무한히 축적되면 검색 성능이 저하됩니다. 적절한 생명주기 관리가 필요합니다.
동일한 주제의 새로운 정보가 입력되면 기존 메모리를 갱신해야 합니다. "사용자가 Python을 선호한다"는 메모리가 있는데 "이제 TypeScript로 전환했다"는 정보가 들어오면, 기존 메모리를 업데이트해야 합니다.
유사한 내용의 메모리가 여러 개 존재하면 하나로 통합합니다. 이는 검색 시 중복 결과를 줄이고, 저장 공간을 절약합니다.
오래되고 접근 빈도가 낮은 메모리는 아카이브하거나 삭제합니다. 접근 횟수와 마지막 접근 시간을 기준으로 자동 만료 정책을 설정할 수 있습니다.
async function cleanupStaleMemories(
db: VectorDB,
userId: string,
maxAgeDays: number = 90,
minAccessCount: number = 2
): Promise<number> {
const cutoffDate = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
const staleMemories = await db.query({
filter: {
userId,
lastAccessedAt: { $lt: cutoffDate },
accessCount: { $lt: minAccessCount },
importance: { $lt: 0.3 },
},
});
await db.delete(staleMemories.map((m) => m.id));
return staleMemories.length;
}이번 장에서 다룬 장기 메모리의 핵심 내용을 정리합니다.
4장에서는 에피소딕 메모리를 다룹니다. 단순한 사실 저장을 넘어, 에이전트가 과거 경험에서 학습하고 유사한 상황에서 더 나은 의사결정을 내리는 메커니즘을 살펴봅니다.
이 글이 도움이 되셨나요?
에이전트가 과거 상호작용을 에피소드로 기록하고, 경험 기반 의사결정과 패턴 학습에 활용하는 에피소딕 메모리 시스템을 다룹니다.
슬라이딩 윈도우, 메시지 요약, 토큰 예산 관리, 중요도 기반 정리 등 에이전트 단기 메모리의 핵심 전략을 코드 예제와 함께 다룹니다.
Zep의 시간 인식 동적 지식 그래프를 중심으로, 엔티티 추출, 관계 생성, 시간적 추론 등 구조화된 메모리의 설계와 장점을 다룹니다.