에이전트 메모리의 압축 기법, 3-6배 텍스트 압축과 5-40배 도구 호출 압축, 계층적 통합과 정보 손실 최소화 전략을 다룹니다.
메모리 통합(Memory Consolidation)은 여러 개의 메모리를 하나의 압축된 표현으로 결합하는 과정입니다. 인간의 수면 중 기억 통합 과정에서 영감을 받은 개념으로, 에이전트가 축적한 방대한 메모리를 관리 가능한 크기로 유지하면서도 핵심 정보를 보존합니다.
에이전트가 수백 시간의 대화를 누적하면, 메모리 저장소의 크기가 급격히 증가합니다. 이는 두 가지 문제를 일으킵니다.
일반 대화 텍스트의 경우, 적절한 요약 기법으로 3-6배 압축이 가능합니다. 10,000 토큰의 대화를 2,000-3,000 토큰으로 줄이면서도 핵심 정보를 유지할 수 있습니다.
원본 텍스트에서 중요한 문장을 선별하여 유지하는 방식입니다. 원문의 표현을 그대로 보존하므로 정보 왜곡의 위험이 낮습니다.
interface CompressedMemory {
original: string;
compressed: string;
compressionRatio: number;
preservedFacts: string[];
droppedFacts: string[];
}
async function extractiveCompress(
conversation: Message[],
llm: LLMClient,
targetRatio: number = 0.3 // 원본의 30%로 압축
): Promise<CompressedMemory> {
const originalText = conversation
.map((m) => `[${m.role}]: ${m.content}`)
.join("\n");
const originalTokens = countTokens(originalText);
const targetTokens = Math.floor(originalTokens * targetRatio);
const response = await llm.complete({
messages: [{
role: "system",
content: `다음 대화에서 가장 중요한 정보를 추출하세요.
규칙:
1. 결정사항, 사실, 수치를 우선 보존
2. 인사, 감사, 확인 등 의례적 발화는 제거
3. ${targetTokens} 토큰 이내로 압축
4. 원문의 핵심 표현을 최대한 유지
5. 제거된 정보의 목록도 함께 제공`,
}, {
role: "user",
content: originalText,
}],
});
return parseCompressionResult(response.content, originalText);
}LLM이 원본의 의미를 이해하고 새로운 요약문을 생성하는 방식입니다. 더 높은 압축률을 달성할 수 있지만, 원문에 없는 내용이 삽입(환각)될 위험이 있습니다.
const ABSTRACTIVE_PROMPT = `
다음 대화를 구조화된 요약으로 변환하세요.
출력 형식:
[목표] 사용자가 달성하려는 것
[결정] 합의된 사항 목록
[사실] 확인된 사실 정보
[미해결] 아직 결정되지 않은 사항
[행동] 다음에 수행할 작업
각 항목은 한 줄로 간결하게 작성합니다.
`;원본 대화: 8,500 토큰
추출적 압축: 2,800 토큰 (3.0배 압축)
- 정보 보존율: 약 95%
- 원문 표현 유지
생성적 압축: 1,400 토큰 (6.0배 압축)
- 정보 보존율: 약 85-90%
- 구조화된 요약일반적으로 추출적 압축은 정확성이 중요한 경우(기술 논의, 계약 조건 등)에, 생성적 압축은 전반적인 맥락 파악이 중요한 경우(일상 대화, 아이디어 브레인스토밍 등)에 적합합니다.
에이전트가 도구(Tool)를 사용한 기록은 일반 텍스트보다 훨씬 높은 압축률을 달성할 수 있습니다. 도구 호출의 입력/출력은 구조화되어 있고, 중간 과정보다 최종 결과가 중요하기 때문입니다.
코딩 에이전트의 경우, 파일 읽기, 검색, 편집 등의 도구 호출이 대화 토큰의 대부분을 차지할 수 있습니다.
도구 호출 시퀀스 (원본: 15,000 토큰)
1. file_search("*.ts") -> 50개 파일 목록 반환 (2,000 토큰)
2. file_read("src/index.ts") -> 파일 전체 내용 (3,000 토큰)
3. file_read("src/utils.ts") -> 파일 전체 내용 (2,500 토큰)
4. file_edit("src/index.ts") -> 수정된 전체 내용 (3,200 토큰)
5. build_check() -> 빌드 로그 전체 (4,300 토큰)
압축 후 (750 토큰, 20배 압축):
- src/index.ts: import 경로 수정 (line 15, 23)
- src/utils.ts: 참조만 확인, 수정 없음
- 빌드 결과: 성공interface ToolCall {
tool: string;
input: Record<string, unknown>;
output: string;
tokenCount: number;
}
interface CompressedToolSequence {
summary: string;
filesModified: string[];
filesRead: string[];
finalOutcome: string;
originalTokens: number;
compressedTokens: number;
}
function compressToolCalls(calls: ToolCall[]): CompressedToolSequence {
const filesModified: string[] = [];
const filesRead: string[] = [];
let finalOutcome = "";
for (const call of calls) {
switch (call.tool) {
case "file_read":
filesRead.push(call.input.path as string);
break;
case "file_edit":
case "file_write":
filesModified.push(call.input.path as string);
break;
case "build_check":
case "test_run":
// 최종 결과만 보존
finalOutcome = extractOutcome(call.output);
break;
}
}
const summary = generateToolSummary(calls);
const originalTokens = calls.reduce((s, c) => s + c.tokenCount, 0);
const compressedTokens = countTokens(summary);
return {
summary,
filesModified,
filesRead,
finalOutcome,
originalTokens,
compressedTokens,
};
}도구 호출 압축은 코딩 에이전트에서 특히 효과적입니다. 파일의 전체 내용 대신 변경된 부분만, 검색 결과 전체 대신 관련 파일명만 보존하면 5-40배의 압축률을 달성할 수 있습니다.
계층적 통합(Hierarchical Consolidation)은 메모리를 여러 수준의 세밀도로 관리하는 전략입니다.
| 계층 | 보존 기간 | 용도 | 압축률 |
|---|---|---|---|
| L0: 원본 | 24-48시간 | 최근 맥락 참조 | 1x (압축 없음) |
| L1: 세션 요약 | 30일 | 최근 상호작용 회고 | 3-6x |
| L2: 주간/월간 통합 | 1년 | 장기 패턴 파악 | 10-20x |
| L3: 사용자 프로필 | 영구 | 핵심 개인화 정보 | 50-100x |
async function runConsolidationPipeline(
userId: string,
memoryStore: MemoryStore,
llm: LLMClient
): Promise<void> {
// L0 -> L1: 24시간 이상 된 원본을 세션 단위로 요약
const staleRawMemories = await memoryStore.getLevel0({
userId,
olderThan: Date.now() - 24 * 60 * 60 * 1000,
});
if (staleRawMemories.length > 0) {
const sessionGroups = groupBySession(staleRawMemories);
for (const [sessionId, memories] of sessionGroups) {
const summary = await summarizeSession(memories, llm);
await memoryStore.storeLevel1({ userId, sessionId, summary });
await memoryStore.archiveLevel0(memories.map((m) => m.id));
}
}
// L1 -> L2: 7일 이상 된 세션 요약을 주간 통합
const staleSessionSummaries = await memoryStore.getLevel1({
userId,
olderThan: Date.now() - 7 * 24 * 60 * 60 * 1000,
});
if (staleSessionSummaries.length >= 3) {
const weeklyDigest = await consolidateWeekly(staleSessionSummaries, llm);
await memoryStore.storeLevel2({ userId, digest: weeklyDigest });
await memoryStore.archiveLevel1(staleSessionSummaries.map((s) => s.id));
}
// L2 -> L3: 월간 통합으로 사용자 프로필 갱신
const monthlyDigests = await memoryStore.getLevel2({
userId,
count: 4, // 최근 4주
});
if (monthlyDigests.length >= 4) {
const profileUpdate = await updateUserProfile(monthlyDigests, llm);
await memoryStore.updateLevel3({ userId, profile: profileUpdate });
}
}압축에서 가장 중요한 것은 핵심 정보를 잃지 않는 것입니다.
압축 전에 각 정보 조각의 중요도를 평가하고, 높은 중요도의 정보는 압축 과정에서 반드시 보존합니다.
interface TaggedFact {
content: string;
importance: "critical" | "high" | "medium" | "low";
category: "decision" | "fact" | "preference" | "context";
}
async function tagAndCompress(
memories: string[],
llm: LLMClient
): Promise<string> {
// 1단계: 사실 추출 및 중요도 태깅
const taggedFacts = await extractAndTagFacts(memories, llm);
// 2단계: 중요도별 처리
const critical = taggedFacts.filter((f) => f.importance === "critical");
const high = taggedFacts.filter((f) => f.importance === "high");
const medium = taggedFacts.filter((f) => f.importance === "medium");
// low는 통합 요약에만 반영
// 3단계: critical과 high는 원문 보존, medium은 요약
const preserved = [...critical, ...high].map((f) => f.content);
const summarized = await summarizeFacts(medium, llm);
return [
"[보존된 핵심 정보]",
...preserved,
"",
"[요약된 부가 정보]",
summarized,
].join("\n");
}압축 후 원본과 비교하여 정보 손실을 평가하는 검증 단계를 추가합니다.
async function validateCompression(
original: string,
compressed: string,
llm: LLMClient
): Promise<{
score: number; // 0-1 사이의 정보 보존 점수
missingFacts: string[];
distortedFacts: string[];
}> {
const response = await llm.complete({
messages: [{
role: "system",
content: `원본 텍스트와 압축된 텍스트를 비교하세요.
평가 항목:
1. 누락된 중요 사실
2. 왜곡된 정보
3. 전체적인 정보 보존율 (0-1)
원본:
${original}
압축본:
${compressed}`,
}],
});
return parseValidationResult(response.content);
}압축 품질 검증을 생략하면 시간이 지남에 따라 메모리의 정확도가 저하될 수 있습니다. 특히 계층적 통합에서 L2, L3 수준의 압축은 반드시 검증을 거쳐야 합니다. 왜곡된 프로필 정보가 이후 모든 상호작용에 영향을 미치기 때문입니다.
통합 파이프라인은 적절한 시점에 실행되어야 합니다.
interface ConsolidationSchedule {
l0ToL1: {
trigger: "age"; // 시간 기반
threshold: number; // 24시간
minBatchSize: number; // 최소 5개 메모리
};
l1ToL2: {
trigger: "count"; // 개수 기반
threshold: number; // 세션 요약 7개 이상
maxAge: number; // 최대 30일
};
l2ToL3: {
trigger: "periodic"; // 주기적
interval: number; // 30일마다
};
}
const defaultSchedule: ConsolidationSchedule = {
l0ToL1: { trigger: "age", threshold: 24 * 60 * 60 * 1000, minBatchSize: 5 },
l1ToL2: { trigger: "count", threshold: 7, maxAge: 30 * 24 * 60 * 60 * 1000 },
l2ToL3: { trigger: "periodic", interval: 30 * 24 * 60 * 60 * 1000 },
};프로덕션 환경에서는 이 파이프라인을 백그라운드 작업으로 실행하여 사용자 경험에 영향을 주지 않도록 합니다.
이번 장에서 다룬 메모리 압축과 통합의 핵심 내용을 정리합니다.
7장에서는 메모리 프레임워크 비교와 선택을 다룹니다. Mem0, Zep, Letta, LangChain/LangGraph 메모리의 특성을 상세히 비교하고, 프로젝트 요구사항에 맞는 프레임워크를 선택하는 의사결정 트리를 제시합니다.
이 글이 도움이 되셨나요?
Mem0, Zep, Letta, LangChain/LangGraph의 메모리 시스템을 상세 비교하고, 프로젝트 요구사항에 맞는 프레임워크 선택 의사결정 트리를 제시합니다.
Zep의 시간 인식 동적 지식 그래프를 중심으로, 엔티티 추출, 관계 생성, 시간적 추론 등 구조화된 메모리의 설계와 장점을 다룹니다.
Hot Path와 Cold Path를 결합한 듀얼 레이어 메모리 아키텍처의 설계, 하이브리드 검색, 메모리 라우팅, 비용-지연시간 최적화 전략을 다룹니다.