Hot Path와 Cold Path를 결합한 듀얼 레이어 메모리 아키텍처의 설계, 하이브리드 검색, 메모리 라우팅, 비용-지연시간 최적화 전략을 다룹니다.
지금까지 다룬 단기 메모리, 장기 메모리, 에피소딕 메모리, 지식 그래프를 하나의 시스템으로 통합하는 것이 듀얼 레이어 아키텍처(Dual-Layer Architecture)입니다.
두 개의 경로로 구성됩니다.
Hot Path는 매 턴마다 항상 컨텍스트에 포함되는 정보입니다. 빠른 접근이 보장되어야 하며, 토큰 예산을 효율적으로 사용해야 합니다.
interface HotPathContext {
systemPrompt: string; // 에이전트 지시사항
userProfile: UserProfileCore; // 사용자 핵심 정보
conversationSummary: string; // 현재까지 대화 요약
recentMessages: Message[]; // 최근 N개 메시지
activeGoals: string[]; // 현재 진행 중인 목표
}
interface UserProfileCore {
name: string;
preferences: string[]; // 핵심 선호도 (최대 5개)
expertise: string[]; // 전문 분야
communicationStyle: string;
}
function buildHotPath(
session: Session,
userProfile: UserProfileCore,
tokenBudget: number
): HotPathContext {
const systemPromptTokens = countTokens(session.systemPrompt);
const profileTokens = countTokens(JSON.stringify(userProfile));
let remainingBudget = tokenBudget - systemPromptTokens - profileTokens;
// 대화 요약에 20% 할당
const summaryBudget = Math.floor(remainingBudget * 0.2);
const summary = truncateToTokens(
session.conversationSummary, summaryBudget
);
remainingBudget -= countTokens(summary);
// 나머지를 최근 메시지에 할당
const recentMessages = selectRecentMessages(
session.messages, remainingBudget
);
return {
systemPrompt: session.systemPrompt,
userProfile,
conversationSummary: summary,
recentMessages,
activeGoals: session.activeGoals,
};
}128K 토큰 모델 기준으로 Hot Path의 예산 배분 예시입니다.
전체 컨텍스트: 128,000 토큰
Hot Path (50%): 64,000 토큰
- 시스템 프롬프트: 8,000 (6.25%)
- 사용자 프로필: 2,000 (1.56%)
- 대화 요약: 12,000 (9.38%)
- 최근 메시지: 38,000 (29.69%)
- 활성 목표: 4,000 (3.13%)
Cold Path (25%): 32,000 토큰
- 벡터 검색 결과: 15,000
- 그래프 탐색 결과: 10,000
- 에피소딕 참조: 7,000
출력 예약 (25%): 32,000 토큰Cold Path는 필요할 때만 검색하여 컨텍스트에 추가하는 메모리입니다. 매 턴마다 검색하는 것이 아니라, 메모리 라우터가 필요하다고 판단할 때만 실행됩니다.
interface ColdPathTrigger {
type: "explicit" | "implicit" | "periodic";
condition: string;
}
function shouldSearchColdPath(
message: Message,
conversationState: ConversationState
): ColdPathTrigger | null {
const content = message.content.toLowerCase();
// 명시적 트리거: 사용자가 과거 정보를 요청
if (
content.includes("이전에") ||
content.includes("지난번") ||
content.includes("기억") ||
content.includes("전에 말한")
) {
return { type: "explicit", condition: "user_requested_recall" };
}
// 암묵적 트리거: 현재 맥락으로는 답변이 어려운 경우
if (conversationState.confidenceScore < 0.5) {
return { type: "implicit", condition: "low_confidence" };
}
// 주기적 트리거: 새 주제가 시작될 때
if (conversationState.topicChanged) {
return { type: "periodic", condition: "topic_change" };
}
return null;
}Cold Path에서는 여러 소스를 병렬로 검색하고 결과를 통합합니다.
interface RetrievalSource {
name: string;
search: (query: string, userId: string) => Promise<SearchResult[]>;
weight: number;
timeout: number; // 밀리초
}
async function hybridRetrieval(
query: string,
userId: string,
sources: RetrievalSource[],
maxResults: number = 10
): Promise<SearchResult[]> {
// 모든 소스에서 병렬 검색 (타임아웃 적용)
const searchPromises = sources.map((source) =>
Promise.race([
source.search(query, userId).then((results) =>
results.map((r) => ({ ...r, sourceWeight: source.weight }))
),
new Promise<SearchResult[]>((resolve) =>
setTimeout(() => resolve([]), source.timeout)
),
])
);
const allResults = await Promise.all(searchPromises);
const flatResults = allResults.flat();
// 가중 점수로 통합 순위 결정
return flatResults
.map((r) => ({
...r,
finalScore: r.similarity * r.sourceWeight,
}))
.sort((a, b) => b.finalScore - a.finalScore)
.slice(0, maxResults);
}
// 사용 예시
const sources: RetrievalSource[] = [
{
name: "vector_db",
search: vectorMemory.search,
weight: 0.5,
timeout: 2000,
},
{
name: "knowledge_graph",
search: graphMemory.search,
weight: 0.3,
timeout: 3000,
},
{
name: "episodic",
search: episodicMemory.search,
weight: 0.2,
timeout: 2000,
},
];Cold Path 검색에는 반드시 타임아웃을 설정하세요. 외부 서비스 장애 시 전체 응답이 지연되는 것을 방지합니다. 타임아웃이 발생하면 해당 소스 없이 Hot Path 정보만으로 응답을 생성해야 합니다.
메모리 라우터(Memory Router)는 사용자 메시지를 분석하여 어떤 메모리 소스를 검색할지 결정하는 구성 요소입니다.
type MemorySourceType = "vector" | "graph" | "episodic" | "none";
interface RoutingDecision {
sources: MemorySourceType[];
query: string; // 검색에 사용할 쿼리 (원본과 다를 수 있음)
priority: "high" | "normal" | "low";
}
async function routeMemoryRequest(
message: Message,
conversationContext: string,
llm: LLMClient
): Promise<RoutingDecision> {
const response = await llm.complete({
messages: [{
role: "system",
content: `사용자 메시지를 분석하여 어떤 메모리 소스를 검색해야 하는지 결정하세요.
사용 가능한 소스:
- vector: 사실, 선호도, 일반 지식 검색
- graph: 엔티티 간 관계, 조직 구조, 프로젝트 연관성
- episodic: 과거 상호작용 경험, 이전 해결 사례
- none: 메모리 검색 불필요 (일반 대화, 인사 등)
JSON 형식으로 응답:
{"sources": [...], "query": "최적화된 검색 쿼리", "priority": "high|normal|low"}`,
}, {
role: "user",
content: `현재 대화 맥락: ${conversationContext}\n\n사용자 메시지: ${message.content}`,
}],
});
return JSON.parse(response.content);
}LLM 기반 라우터는 정확하지만 매 턴마다 추가 호출이 발생합니다. 비용을 줄이기 위해 규칙 기반 경량 라우터를 1차로 사용하고, 불확실한 경우에만 LLM 라우터를 사용하는 2단계 방식이 효과적입니다.
function lightweightRoute(message: Message): RoutingDecision | null {
const content = message.content;
// 인사, 감사 등 -> 메모리 불필요
if (isGreetingOrAck(content)) {
return { sources: [], query: "", priority: "low" };
}
// "누가", "어느 팀" 등 관계 질문 -> 그래프
if (containsRelationalQuery(content)) {
return { sources: ["graph"], query: content, priority: "high" };
}
// "지난번에", "이전에" 등 회고 -> 에피소딕
if (containsRecallIndicator(content)) {
return { sources: ["episodic"], query: content, priority: "high" };
}
// 판단 불가 -> null 반환하여 LLM 라우터로 위임
return null;
}반복적으로 접근되는 메모리에 대한 캐싱은 비용과 지연시간을 크게 줄입니다.
interface MemoryCache {
l1: Map<string, CachedResult>; // 인메모리 (세션 내)
l2: RedisClient; // Redis (세션 간, TTL 적용)
}
class CachedMemoryStore {
private cache: MemoryCache;
private coldStore: ColdPathStore;
async search(
query: string,
userId: string
): Promise<SearchResult[]> {
const cacheKey = this.buildCacheKey(query, userId);
// L1 캐시 확인
const l1Result = this.cache.l1.get(cacheKey);
if (l1Result && !this.isExpired(l1Result)) {
return l1Result.data;
}
// L2 캐시 확인
const l2Result = await this.cache.l2.get(cacheKey);
if (l2Result) {
this.cache.l1.set(cacheKey, { data: l2Result, timestamp: Date.now() });
return l2Result;
}
// Cold Path 검색
const freshResult = await this.coldStore.search(query, userId);
// 캐시 갱신
this.cache.l1.set(cacheKey, { data: freshResult, timestamp: Date.now() });
await this.cache.l2.set(cacheKey, freshResult, { ex: 3600 }); // 1시간 TTL
return freshResult;
}
private buildCacheKey(query: string, userId: string): string {
// 쿼리의 임베딩 해시를 키로 사용하여 유사 쿼리도 캐시 히트
return `mem:${userId}:${hashEmbedding(query)}`;
}
}캐시 무효화 정책이 중요합니다. 사용자가 새로운 선호도를 표현하면 관련 캐시를 즉시 무효화해야 합니다. 그렇지 않으면 오래된 정보가 계속 제공될 수 있습니다.
프로덕션에서 메모리 시스템의 비용과 지연시간은 핵심 운영 지표입니다.
1. 선택적 검색
- 모든 턴에 Cold Path 검색을 실행하지 않음
- 메모리 라우터로 필요한 경우만 검색
- 예상 절감: 40-60% API 호출 감소
2. 임베딩 캐싱
- 동일/유사 쿼리의 임베딩을 캐싱
- 임베딩 API 호출 절감
- 예상 절감: 30-50% 임베딩 비용 감소
3. 계층적 압축 (6장)
- 오래된 메모리를 주기적으로 압축
- 벡터 DB 저장 용량 절감
- 검색 대상 감소로 쿼리 비용 절감
4. 토큰 예산 엄격 관리
- Hot/Cold Path별 토큰 상한 설정
- 예산 초과 시 우선순위 기반 절단Hot Path 구성과 Cold Path 검색을 병렬로 실행하면 전체 지연시간을 줄일 수 있습니다. Cold Path에 타임아웃을 설정하여 느린 소스가 전체를 지연시키지 않도록 합니다.
지금까지의 모든 요소를 통합한 전체 아키텍처입니다.
class DualLayerMemorySystem {
private hotPath: HotPathManager;
private coldPath: ColdPathManager;
private router: MemoryRouter;
private cache: CachedMemoryStore;
private compressor: MemoryCompressor;
async processMessage(
message: Message,
session: Session
): Promise<UnifiedContext> {
// 1. Hot Path 구성 (항상 실행)
const hotContext = this.hotPath.build(session);
// 2. 메모리 라우팅 결정
const routing = await this.router.route(message, session);
// 3. Cold Path 검색 (필요 시)
let coldContext: SearchResult[] = [];
if (routing.sources.length > 0) {
coldContext = await this.cache.searchWithFallback(
routing.query,
session.userId,
routing.sources
);
}
// 4. 컨텍스트 통합 및 토큰 예산 조정
const unified = this.assembleContext(
hotContext, coldContext, session.tokenBudget
);
// 5. 백그라운드: 메모리 저장 및 압축
this.backgroundTasks(message, session);
return unified;
}
private backgroundTasks(message: Message, session: Session): void {
// 비동기: 새 메모리 저장
this.coldPath.storeIfRelevant(message, session);
// 비동기: 주기적 압축 확인
this.compressor.checkAndConsolidate(session.userId);
}
}이번 장에서 다룬 듀얼 레이어 아키텍처의 핵심 내용을 정리합니다.
9장에서는 프로젝트 메모리와 코딩 에이전트를 다룹니다. CLAUDE.md 기반 프로젝트 메모리, 코드베이스 컨텍스트 지속, 세션 간 학습, 팀 메모리 등 코딩 에이전트에 특화된 메모리 패턴을 살펴봅니다.
이 글이 도움이 되셨나요?
CLAUDE.md 기반 프로젝트 메모리, 코드베이스 컨텍스트 지속, 세션 간 학습, 팀 메모리 설계 패턴 등 코딩 에이전트에 특화된 메모리 시스템을 다룹니다.
Mem0, Zep, Letta, LangChain/LangGraph의 메모리 시스템을 상세 비교하고, 프로젝트 요구사항에 맞는 프레임워크 선택 의사결정 트리를 제시합니다.
Mem0와 Zep을 활용한 듀얼 레이어 메모리 시스템 구축, 메모리 압축 파이프라인, 성능 벤치마킹, 프로덕션 운영 체크리스트까지 실전 가이드를 제공합니다.