에이전트가 과거 상호작용을 에피소드로 기록하고, 경험 기반 의사결정과 패턴 학습에 활용하는 에피소딕 메모리 시스템을 다룹니다.
인간의 기억에서 에피소딕 메모리(Episodic Memory)는 특정 시간과 장소에서 겪은 개인적 경험의 기억입니다. "어제 카페에서 친구와 만나 이야기를 나눴다"와 같은 기억이 여기에 해당합니다.
AI 에이전트에서의 에피소딕 메모리도 같은 원리입니다. 단순히 "사용자는 TypeScript를 선호한다"라는 사실(장기 메모리)을 저장하는 것이 아니라, "2026년 3월 15일, 사용자가 프로젝트 기술 스택을 논의하면서 Python에서 TypeScript로 전환하기로 결정했고, 그 이유는 프론트엔드와의 코드 공유 때문이었다" 라는 전체 맥락을 기록합니다.
하나의 에피소드(Episode)는 하나의 완결된 상호작용 단위입니다. 에피소드를 효과적으로 저장하려면 체계적인 구조가 필요합니다.
interface Episode {
id: string;
userId: string;
// 시간 정보
startedAt: number;
endedAt: number;
duration: number; // 밀리초
// 맥락 정보
trigger: string; // 에피소드를 시작한 사용자 요청
goal: string; // 사용자의 목표
domain: string; // 주제 영역 (코딩, 분석, 창작 등)
// 진행 과정
steps: EpisodeStep[]; // 단계별 행동과 결과
toolsUsed: string[]; // 사용된 도구 목록
// 결과
outcome: "success" | "partial" | "failure";
resolution: string; // 최종 결과 요약
userSatisfaction?: number; // 사용자 만족도 (추정)
// 학습 포인트
lessonsLearned: string[]; // 이 에피소드에서 배운 점
tags: string[]; // 검색을 위한 태그
// 벡터 검색용
embedding?: number[]; // 에피소드 요약의 임베딩
}
interface EpisodeStep {
index: number;
action: string; // 에이전트가 취한 행동
reasoning: string; // 행동의 이유
result: string; // 행동의 결과
wasEffective: boolean;
}대화에서 에피소드의 시작과 끝을 자동으로 감지하는 것이 중요합니다.
function detectEpisodeBoundary(
currentMessage: Message,
previousMessages: Message[]
): "new_episode" | "continue" | "end_episode" {
const signals = {
// 새 에피소드 시작 신호
newTopicIndicators: [
"다른 질문", "새로운 주제", "그건 그렇고",
"다음으로", "별개의 문제",
],
// 에피소드 종료 신호
closingIndicators: [
"감사합니다", "해결됐습니다", "완료",
"이해했습니다", "좋습니다",
],
};
const content = currentMessage.content.toLowerCase();
// 종료 신호 감지
if (signals.closingIndicators.some((s) => content.includes(s))) {
return "end_episode";
}
// 새 에피소드 신호 감지
if (signals.newTopicIndicators.some((s) => content.includes(s))) {
return "new_episode";
}
// 장시간 공백 후 메시지
const lastMessage = previousMessages[previousMessages.length - 1];
if (lastMessage) {
const timeSinceLast = Date.now() - (lastMessage.timestamp ?? 0);
if (timeSinceLast > 30 * 60 * 1000) { // 30분 이상 공백
return "new_episode";
}
}
return "continue";
}실제 프로덕션에서는 단순 키워드 매칭보다 LLM을 활용한 주제 전환 감지가 더 정확합니다. 다만 매 메시지마다 LLM을 호출하면 비용이 발생하므로, 키워드 기반 1차 필터링 후 LLM으로 확인하는 2단계 방식이 효율적입니다.
에피소드가 종료되면 다음 파이프라인을 통해 저장됩니다.
async function finalizeEpisode(
episode: Episode,
llm: LLMClient,
embedder: EmbeddingModel,
episodeDB: VectorDB,
longTermMemory: LongTermMemory
): Promise<void> {
// 1. 에피소드 요약 생성
const summary = await llm.complete({
messages: [{
role: "system",
content: `다음 에피소드를 3-5문장으로 요약하세요.
사용자의 목표, 핵심 진행 과정, 최종 결과를 포함하세요.`,
}, {
role: "user",
content: JSON.stringify(episode.steps),
}],
});
// 2. 교훈 추출
const lessons = await llm.complete({
messages: [{
role: "system",
content: `이 에피소드에서 배울 수 있는 교훈을 추출하세요.
유사한 상황에서 참고할 수 있는 구체적인 인사이트를 포함하세요.`,
}, {
role: "user",
content: JSON.stringify({ steps: episode.steps, outcome: episode.outcome }),
}],
});
// 3. 임베딩 생성 및 저장
const embedding = await embedder.embed(summary.content);
await episodeDB.upsert({
id: episode.id,
content: summary.content,
metadata: {
userId: episode.userId,
outcome: episode.outcome,
domain: episode.domain,
tags: episode.tags,
lessonsLearned: lessons.content,
startedAt: episode.startedAt,
},
embedding,
});
// 4. 사실을 장기 메모리로 이관
const facts = await extractFacts(episode, llm);
for (const fact of facts) {
await longTermMemory.store(fact.content, fact.metadata);
}
}현재 상황과 유사한 과거 에피소드를 찾는 것이 에피소딕 메모리의 핵심 가치입니다.
async function findSimilarEpisodes(
currentContext: string,
userId: string,
episodeDB: VectorDB,
embedder: EmbeddingModel,
options: {
topK?: number;
minSimilarity?: number;
outcomeFilter?: "success" | "failure";
} = {}
): Promise<Episode[]> {
const { topK = 3, minSimilarity = 0.7, outcomeFilter } = options;
const queryEmbedding = await embedder.embed(currentContext);
const filter: Record<string, unknown> = { userId };
if (outcomeFilter) {
filter.outcome = outcomeFilter;
}
const results = await episodeDB.query({
vector: queryEmbedding,
filter,
topK,
});
return results.filter((r) => r.similarity >= minSimilarity);
}에피소딕 메모리의 진정한 가치는 과거 경험을 활용하여 현재 상황에서 더 나은 판단을 내리는 것입니다.
사용자가 비슷한 요청을 했을 때, 과거에 성공했던 접근 방식을 먼저 참조합니다.
async function enrichPromptWithExperience(
userRequest: string,
userId: string,
episodeDB: VectorDB,
embedder: EmbeddingModel
): Promise<string> {
// 성공한 유사 에피소드 검색
const successEpisodes = await findSimilarEpisodes(
userRequest, userId, episodeDB, embedder,
{ topK: 2, outcomeFilter: "success" }
);
// 실패한 유사 에피소드 검색
const failureEpisodes = await findSimilarEpisodes(
userRequest, userId, episodeDB, embedder,
{ topK: 1, outcomeFilter: "failure" }
);
let experienceContext = "";
if (successEpisodes.length > 0) {
experienceContext += "\n[과거 성공 경험]\n";
for (const ep of successEpisodes) {
experienceContext += `- ${ep.resolution}\n`;
experienceContext += ` 교훈: ${ep.lessonsLearned.join(", ")}\n`;
}
}
if (failureEpisodes.length > 0) {
experienceContext += "\n[과거 실패 경험 - 주의]\n";
for (const ep of failureEpisodes) {
experienceContext += `- ${ep.resolution}\n`;
experienceContext += ` 회피할 점: ${ep.lessonsLearned.join(", ")}\n`;
}
}
return experienceContext;
}과거에 실패했던 접근 방식을 반복하지 않도록 경고하는 것도 중요합니다.
[시스템 프롬프트에 주입되는 경험 컨텍스트]
과거 유사 경험:
- 3/18: Docker 빌드 실패 디버깅 시, 멀티스테이지 빌드로 전환하여 해결.
교훈: 의존성 캐싱 레이어를 분리하면 빌드 속도도 개선됨.
주의할 과거 실패:
- 3/10: 동일한 유형의 빌드 오류에서 캐시 삭제만 반복하여
30분을 소비. 근본 원인(베이스 이미지 버전)을 먼저 확인할 것.경험 기반 의사결정에서 과적합에 주의해야 합니다. 한두 번의 경험을 일반 원칙으로 확대 적용하면 오히려 잘못된 판단을 내릴 수 있습니다. 에피소드의 맥락적 유사성을 신중하게 평가해야 합니다.
개별 에피소드에서 반복되는 패턴을 추출하여 일반화된 지식으로 전환하는 과정도 필요합니다.
interface Pattern {
id: string;
description: string;
confidence: number; // 패턴의 신뢰도 (발생 빈도 기반)
episodeIds: string[]; // 근거가 되는 에피소드들
domain: string;
actionRecommendation: string;
}
async function extractPatterns(
episodes: Episode[],
llm: LLMClient
): Promise<Pattern[]> {
// 동일 도메인의 에피소드를 그룹화
const domainGroups = groupBy(episodes, (e) => e.domain);
const patterns: Pattern[] = [];
for (const [domain, domainEpisodes] of Object.entries(domainGroups)) {
if (domainEpisodes.length < 3) continue; // 최소 3개 에피소드 필요
const summaries = domainEpisodes.map((e) => ({
outcome: e.outcome,
resolution: e.resolution,
lessons: e.lessonsLearned,
}));
const response = await llm.complete({
messages: [{
role: "system",
content: `다음 에피소드들에서 반복되는 패턴을 찾으세요.
각 패턴에 대해: 설명, 발생 빈도, 권장 행동을 제시하세요.
최소 3개 에피소드에서 나타나는 패턴만 포함하세요.`,
}, {
role: "user",
content: JSON.stringify(summaries),
}],
});
// 파싱 후 patterns 배열에 추가
const extracted = parsePatterns(response.content, domain, domainEpisodes);
patterns.push(...extracted);
}
return patterns;
}추출된 패턴은 시스템 프롬프트에 포함되어 에이전트의 기본 행동 방침으로 작동합니다.
[학습된 행동 패턴]
1. 빌드 오류 (신뢰도: 높음, 근거: 5개 에피소드)
- 캐시 삭제 전 반드시 의존성 버전 충돌을 먼저 확인
- 에러 메시지의 첫 줄보다 마지막 줄이 근본 원인에 가까움
2. API 설계 논의 (신뢰도: 중간, 근거: 3개 에피소드)
- 사용자는 REST보다 GraphQL을 선호하는 경향
- 엔드포인트 설계 시 항상 페이지네이션을 먼저 제안에피소딕 메모리가 실시간으로 가치를 발휘하려면, 현재 대화가 과거 에피소드와 유사한 상황인지 자동으로 감지해야 합니다.
async function detectSimilarSituation(
currentMessages: Message[],
userId: string,
episodeDB: VectorDB,
embedder: EmbeddingModel
): Promise<{
isSimilar: boolean;
relevantEpisodes: Episode[];
suggestion: string | null;
}> {
// 현재 대화의 최근 3개 메시지로 맥락 구성
const recentContext = currentMessages
.slice(-3)
.map((m) => m.content)
.join(" ");
const similar = await findSimilarEpisodes(
recentContext, userId, episodeDB, embedder,
{ topK: 3, minSimilarity: 0.75 }
);
if (similar.length === 0) {
return { isSimilar: false, relevantEpisodes: [], suggestion: null };
}
// 가장 유사한 에피소드에서 제안 생성
const topEpisode = similar[0];
const suggestion = topEpisode.outcome === "failure"
? `이전에 유사한 상황에서 문제가 발생했습니다: ${topEpisode.lessonsLearned[0]}`
: `이전에 유사한 상황을 성공적으로 처리한 경험이 있습니다: ${topEpisode.resolution}`;
return {
isSimilar: true,
relevantEpisodes: similar,
suggestion,
};
}이번 장에서 다룬 에피소딕 메모리의 핵심 내용을 정리합니다.
5장에서는 지식 그래프 기반 메모리를 다룹니다. Zep의 시간 인식 동적 지식 그래프 아키텍처를 중심으로, 엔티티와 관계를 구조화하여 메모리를 조직하는 방법을 살펴봅니다.
이 글이 도움이 되셨나요?
Zep의 시간 인식 동적 지식 그래프를 중심으로, 엔티티 추출, 관계 생성, 시간적 추론 등 구조화된 메모리의 설계와 장점을 다룹니다.
벡터 데이터베이스에 메모리를 저장하고 임베딩 기반으로 검색하는 장기 메모리 시스템의 설계와 구현 전략을 다룹니다.
에이전트 메모리의 압축 기법, 3-6배 텍스트 압축과 5-40배 도구 호출 압축, 계층적 통합과 정보 손실 최소화 전략을 다룹니다.