02-types-memory

Dec 7, 2025

MemoryNode

📁 관련 코드: lib/types/nodes/memory.ts

시스템의 핵심 단위입니다. 이메일에서 추출된 의미 있는 정보를 저장합니다.

핵심 개념: 이메일 ≠ 메모리

┌─────────────────────────────────────────────────────────────┐
이메일 (Email)                                              
"안녕하세요, 내일 2시에 미팅하죠.                              │
그리고 보고서는 금요일까지 보내주세요."                        
└─────────────────────────────────────────────────────────────┘
                              
                              LLM 추출
              ┌───────────────┴───────────────┐
              
┌─────────────────────────┐     ┌─────────────────────────┐
Memory 1 (MEETING)      Memory 2 (TASK)        
"내일 2시 미팅 예정"      "보고서 제출 (금요일)"   
importance: 0.8         importance: 0.8        
└─────────────────────────┘     └─────────────────────────┘

왜 분리하는가?

접근 방식

장점

단점

이메일 = 메모리

단순함

검색 시 불필요한 정보 포함

이메일 → 여러 메모리

정밀한 검색, 관계 추적 가능

추출 로직 필요

선택 이유: 사용자가 "내일 미팅 몇 시야?"라고 물으면, 미팅 정보만 정확히 반환해야 합니다. 이메일 전체를 반환하면 LLM이 불필요한 정보까지 처리해야 합니다.

MemoryCategory

enum MemoryCategory {
  MEETING = 'meeting',    // 미팅, 일정
  DECISION = 'decision',  // 결정, 합의
  PROJECT = 'project',    // 프로젝트 현황
  TASK = 'task',          // 할일, 요청
  ADS = 'ads',            // 광고, 뉴스레터
  MISC = 'misc',          // 기타
}

분류 기준

카테고리

키워드/패턴

예시

MEETING

미팅, 회의, 일정, 시간, 장소

"내일 2시 회의실에서 만나요"

DECISION

결정, 확정, 합의, 승인

"Q1 예산 500만원으로 확정"

PROJECT

진행, 완료, 마일스톤, 릴리즈

"v2.0 다음 주 월요일 배포"

TASK

해주세요, 부탁, 요청, 까지

"금요일까지 보고서 보내주세요"

ADS

할인, 프로모션, 구독 해지

"50% 할인 이벤트!"

MISC

위 어디에도 해당 안 됨

"잘 지내시죠?"

분류 우선순위

하나의 이메일이 여러 카테고리에 해당할 수 있습니다:

TASK > DECISION > MEETING > PROJECT > ADS > MISC

예: "미팅에서 결정된 사항을 정리해서 보내주세요" → TASK (요청이 있으므로)

ImportanceFactor

interface ImportanceFactor {
  factor: string;    // "contains_money"
  weight: number;    // 0.3
  evidence?: string; // "500만원"
}

왜 점수 산정 근거를 저장하는가?

문제: 사용자가 "왜 이게 중요하다고 판단했어?"라고 물으면?

해결: importanceFactors 배열에 근거 저장

// 예시
importanceFactors: [
  { factor: 'contains_money', weight: 0.3, evidence: '500만원' },
  { factor: 'has_deadline', weight: 0.2, evidence: '금요일까지' },
  { factor: 'multi_person', weight: 0.1 },
]
// 총점: 0.4 (base) + 0.3 + 0.2 + 0.1 = 1.0 → CRITICAL

주요 Factor 목록

Factor

Weight

설명

contains_money

+0.3

금액 언급

has_deadline

+0.2

마감일 언급

multi_person

+0.1

여러 사람 참여

contains_action

+0.15

액션 아이템 포함

is_reply_chain

+0.05

답장 체인의 일부

from_important_person

+0.2

중요 인물로부터

시간 필드 구분

interface MemoryNode {
  createdAt: Date;    // 언제 저장했나
  updatedAt: Date;    // 언제 수정했나
  occurredAt: Date;   // 언제 발생했나 ← 핵심!
  validFrom?: Date;   // 언제부터 유효
  validUntil?: Date;  // 언제까지 유효
}

각 필드의 의미

이메일 수신: 2024-01-15 09:00
이메일 내용: "다음 주 월요일(1/22) 미팅, 제안서는 1/25까지 제출"

┌────────────────────────────────────────────┐
Memory: 미팅 예정                           
├────────────────────────────────────────────┤
createdAt:  2024-01-15 09:05 (저장 시점)    
occurredAt: 2024-01-22 (미팅 날짜)          
validFrom:  null                           
validUntil: null                           
└────────────────────────────────────────────┘

┌────────────────────────────────────────────┐
Memory: 제안서 제출 기한                     
├────────────────────────────────────────────┤
createdAt:  2024-01-15 09:05 (저장 시점)    
occurredAt: 2024-01-15 (요청 받은 )        
validFrom:  2024-01-15                     
validUntil: 2024-01-25 (마감일)             
└────────────────────────────────────────────┘

왜 occurredAt이 중요한가?

시나리오: "지난 주에 무슨 일이 있었어?"

// ❌ 잘못된 쿼리 (저장 시점 기준)
memories.filter(m => m.createdAt >= lastWeekStart);

// ✅ 올바른 쿼리 (발생 시점 기준)
memories.filter(m => m.occurredAt >= lastWeekStart);

Embedding 필드

embedding?: number[];  // 예: [0.123, -0.456, 0.789, ...]

왜 optional인가?

  1. Lazy Computation: 모든 메모리에 즉시 임베딩 생성하면 비용 큼

  2. Batch Processing: 나중에 일괄 생성 가능

  3. Model Update: 임베딩 모델 변경 시 재생성 필요

사용 시점

// 저장 시: 임베딩 없이 저장
const memory = createMemoryNode({ ... });
saveToGraph(memory);

// 검색 요청 시: 임베딩 생성
if (!memory.embedding) {
  memory.embedding = await generateEmbedding(memory.content);
  updateInGraph(memory);
}

Version Control

version: number;           // 1, 2, 3, ...
previousVersionId?: string; // 이전 버전 ID

왜 버전 관리가 필요한가?

시나리오: 미팅 시간이 변경됨

이메일 1 (1/15): "1/22 2시에 미팅합시다"
이메일 2 (1/18): "미팅 3시로 변경됐습니다"

버전 관리 없이:

  • 두 개의 별개 메모리 → "미팅이 2시야 3시야?" 혼란

버전 관리 있으면:

Memory v2: "1/22 3시 미팅"
  └── previousVersionId Memory v1: "1/22 2시 미팅"

버전 업데이트 로직

function updateMemory(existing: MemoryNode, newContent: string): MemoryNode {
  return {
    ...existing,
    id: generateUUID(),           // 새 ID
    content: newContent,
    version: existing.version + 1,
    previousVersionId: existing.id,
    updatedAt: new Date(),
  };
}

Factory Helper

const memory = createMemoryNode({
  id: generateUUID(),
  content: "1월 22일 2시 프로젝트 킥오프 미팅",
  category: MemoryCategory.MEETING,
  importance: ImportanceTier.HIGH,
  occurredAt: new Date('2024-01-22T14:00:00'),
  sourceType: SourceType.BOOTSTRAPPED,
  sourceId: 'gmail-msg-12345',
  confidence: 0.95,
});

왜 Factory 함수를 제공하는가?

  1. 기본값 자동 설정: createdAt, updatedAt, version

  2. 타입 안전성: nodeType: NodeType.MEMORY 자동 설정

  3. 일관성 보장: 모든 메모리가 같은 방식으로 생성

다음 문서

  • → PersonNode: 사람 정보 스키마

  • → base.md: BaseNode, ImportanceTier 상세