ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이코에코(Eco²) Agent: Cross-Session Memory 고도화 방안
    이코에코(Eco²)/Plans 2026. 1. 19. 21:38
    Ref: https://openai.com/ko-KR/index/memory-and-new-controls-for-chatgpt/

    세션 간 사용자 정보 기억 기능 설계 및 메모리 계층 고도화

    작성일: 2026-01-19
    상태: Draft (설계 단계)
    관련 기능: ChatGPT Memory 유사 기능


    목차

    1. 현재 아키텍처 분석
    2. 글로벌 메모리 설계
    3. 데이터베이스 스키마
    4. 컴포넌트 설계
    5. LangGraph 통합
    6. API 설계
    7. 구현 로드맵

    1. 현재 아키텍처 분석

    1.1 기존 메모리 구조

    ┌─────────────────────────────────────────────────────────────────────────┐
    │                     현재 세션 내 멀티턴 구조                              │
    ├─────────────────────────────────────────────────────────────────────────┤
    │                                                                          │
    │   ┌─────────────────┐                                                   │
    │   │   Session A     │                                                   │
    │   │   thread_id: X  │                                                   │
    │   ├─────────────────┤                                                   │
    │   │ message[0]      │ → "페트병 어디 버려?"                              │
    │   │ message[1]      │ → "투명 페트병은..."                               │
    │   │ message[2]      │ → "라벨도 떼야 해?"                                │
    │   │ message[3]      │ → "네, 라벨은..."                                  │
    │   └────────┬────────┘                                                   │
    │            │                                                             │
    │            ▼                                                             │
    │   ┌─────────────────┐     ┌─────────────────┐                           │
    │   │ Redis (L1)      │ ←→  │ PostgreSQL (L2) │                           │
    │   │ TTL: 24h        │     │ checkpoints     │                           │
    │   └─────────────────┘     └─────────────────┘                           │
    │                                                                          │
    │   ═══════════════════════════════════════════════════════════           │
    │                         세션 경계 (분리됨)                                │
    │   ═══════════════════════════════════════════════════════════           │
    │                                                                          │
    │   ┌─────────────────┐                                                   │
    │   │   Session B     │  ← 이전 세션 정보 없음!                            │
    │   │   thread_id: Y  │                                                   │
    │   └─────────────────┘                                                   │
    │                                                                          │
    └─────────────────────────────────────────────────────────────────────────┘

    1.2 현재 스키마 구조

    스키마 테이블 용도
    users accounts 사용자 기본 정보
    users social_accounts OAuth 연동
    users user_characters 캐릭터 소유권
    character characters 캐릭터 마스터
    public checkpoints LangGraph 상태 (세션별)

    1.3 현재 ChatState 구조

    ChatState(TypedDict):
        # Core
        job_id: str
        user_id: str
        thread_id: str              # 세션 ID
        message: str
    
        # Context (세션 내에서만 유지)
        conversation_history: list  # 최근 10개
        summary: str                # 압축된 이전 대화
        intent_history: list[str]   # 의도 체인
    
        # 없음: 세션 간 공유 정보 ❌

    2. 글로벌 메모리 설계

    2.1 목표 아키텍처

    ┌─────────────────────────────────────────────────────────────────────────┐
    │                      글로벌 메모리 적용 후 구조                           │
    ├─────────────────────────────────────────────────────────────────────────┤
    │                                                                          │
    │   ┌─────────────────┐         ┌─────────────────────────────┐           │
    │   │   Session A     │         │    User Memory Store        │           │
    │   │   thread_id: X  │         │    (PostgreSQL)             │           │
    │   └────────┬────────┘         │                             │           │
    │            │                  │  user_id: abc-123           │           │
    │            │ 메모리 추출       │  memories:                  │           │
    │            ├─────────────────→│    - "강남구 거주"           │           │
    │            │                  │    - "투명 페트병 자주 질문"  │           │
    │            │                  │    - "친환경 관심"           │           │
    │                               └──────────────┬──────────────┘           │
    │   ═══════════════════════════════════════════│═══════════════           │
    │                         세션 경계             │                          │
    │   ═══════════════════════════════════════════│═══════════════           │
    │                                              │                           │
    │   ┌─────────────────┐                        │                           │
    │   │   Session B     │◄───────────────────────┘                           │
    │   │   thread_id: Y  │     메모리 주입                                    │
    │   │                 │                                                    │
    │   │ "강남구에서는   │  ← 세션 A의 정보 활용!                             │
    │   │  투명 페트병을  │                                                    │
    │   │  별도 분리..."  │                                                    │
    │   └─────────────────┘                                                    │
    │                                                                          │
    └─────────────────────────────────────────────────────────────────────────┘

    2.2 메모리 라이프사이클

    ┌──────────────────────────────────────────────────────────────────────┐
    │                        Memory Lifecycle                               │
    ├──────────────────────────────────────────────────────────────────────┤
    │                                                                       │
    │   1. EXTRACTION (추출)                                                │
    │   ┌─────────────────────────────────────────────────────────────┐    │
    │   │ 대화 완료 후 LLM이 기억할 정보 추출                           │    │
    │   │                                                              │    │
    │   │ User: "나 강남구 살아"                                        │    │
    │   │ AI: "강남구 분리배출 규정 안내해드릴게요..."                   │    │
    │   │                                                              │    │
    │   │ → Memory 추출: {"강남구 거주", confidence: 0.95}              │    │
    │   └─────────────────────────────────────────────────────────────┘    │
    │                              │                                        │
    │                              ▼                                        │
    │   2. STORAGE (저장)                                                   │
    │   ┌─────────────────────────────────────────────────────────────┐    │
    │   │ user_memories 테이블에 영구 저장                              │    │
    │   │                                                              │    │
    │   │ - Deduplication: 유사 메모리 병합                            │    │
    │   │ - Confidence Decay: 시간 경과 시 신뢰도 감소                 │    │
    │   │ - Capacity Limit: 사용자당 최대 50개                         │    │
    │   └─────────────────────────────────────────────────────────────┘    │
    │                              │                                        │
    │                              ▼                                        │
    │   3. RETRIEVAL (조회)                                                 │
    │   ┌─────────────────────────────────────────────────────────────┐    │
    │   │ 새 세션 시작 시 관련 메모리 로드                              │    │
    │   │                                                              │    │
    │   │ - Category Filter: location, preference, behavior            │    │
    │   │ - Relevance Score: 현재 질문과 연관성                        │    │
    │   │ - Recency Boost: 최근 사용된 메모리 우선                     │    │
    │   └─────────────────────────────────────────────────────────────┘    │
    │                              │                                        │
    │                              ▼                                        │
    │   4. INJECTION (주입)                                                 │
    │   ┌─────────────────────────────────────────────────────────────┐    │
    │   │ Answer Node 프롬프트에 메모리 컨텍스트 추가                   │    │
    │   │                                                              │    │
    │   │ System: "사용자 정보:\n- 강남구 거주\n- 분리배출 관심"        │    │
    │   │ User: "플라스틱 어디 버려?"                                   │    │
    │   │ AI: "강남구에서는 투명 페트병을..."                           │    │
    │   └─────────────────────────────────────────────────────────────┘    │
    │                              │                                        │
    │                              ▼                                        │
    │   5. UPDATE (갱신)                                                    │
    │   ┌─────────────────────────────────────────────────────────────┐    │
    │   │ 사용된 메모리 last_used_at 갱신                               │    │
    │   │ 새로운 정보로 기존 메모리 보강                                │    │
    │   └─────────────────────────────────────────────────────────────┘    │
    │                                                                       │
    └──────────────────────────────────────────────────────────────────────┘

    3. 데이터베이스 스키마

    3.1 신규 테이블:

    -- 글로벌 메모리 테이블
    CREATE TABLE users.user_memories (
        -- Primary Key
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        -- Foreign Key
        user_id UUID NOT NULL REFERENCES users.accounts(id) ON DELETE CASCADE,
    
        -- Memory Content
        content TEXT NOT NULL,                    -- "사용자는 강남구에 거주"
        content_embedding VECTOR(1536),           -- OpenAI ada-002 embedding (선택)
    
        -- Classification
        category memory_category NOT NULL,        -- ENUM
        subcategory TEXT,                         -- 세부 분류 (예: 'gangnam-gu')
    
        -- Confidence & Scoring
        confidence FLOAT NOT NULL DEFAULT 1.0     -- 0.0 ~ 1.0
            CHECK (confidence >= 0.0 AND confidence <= 1.0),
        relevance_score FLOAT DEFAULT 0.0,        -- 동적 계산용
    
        -- Provenance (출처 추적)
        source_type memory_source NOT NULL,       -- ENUM
        source_session_id UUID,                   -- 추출된 세션 ID
        source_message_preview TEXT,              -- 원본 메시지 미리보기 (100자)
    
        -- Lifecycle
        usage_count INTEGER NOT NULL DEFAULT 0,   -- 사용 횟수
        last_used_at TIMESTAMPTZ,                 -- 마지막 사용
        expires_at TIMESTAMPTZ,                   -- 만료 시점 (선택)
    
        -- Timestamps
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    
        -- Soft Delete
        is_active BOOLEAN NOT NULL DEFAULT TRUE
    );
    
    -- ENUM Types
    CREATE TYPE memory_category AS ENUM (
        'location',      -- 거주지, 자주 가는 곳
        'preference',    -- 선호도, 취향
        'behavior',      -- 반복 행동 패턴
        'context',       -- 상황 정보 (직업, 가족 등)
        'feedback'       -- 사용자 피드백 ("이 정보 기억해줘")
    );
    
    CREATE TYPE memory_source AS ENUM (
        'explicit',      -- 사용자가 명시적 요청 ("기억해줘")
        'inferred',      -- LLM이 대화에서 추론
        'system'         -- 시스템 자동 수집 (위치 등)
    );
    
    -- Indexes
    CREATE INDEX idx_user_memories_user_id
        ON users.user_memories(user_id)
        WHERE is_active = TRUE;
    
    CREATE INDEX idx_user_memories_category
        ON users.user_memories(user_id, category)
        WHERE is_active = TRUE;
    
    CREATE INDEX idx_user_memories_last_used
        ON users.user_memories(user_id, last_used_at DESC)
        WHERE is_active = TRUE;
    
    -- Partial Index for high-confidence memories
    CREATE INDEX idx_user_memories_high_confidence
        ON users.user_memories(user_id, confidence DESC)
        WHERE is_active = TRUE AND confidence >= 0.7;
    
    -- Vector Index (pgvector 사용 시)
    -- CREATE INDEX idx_user_memories_embedding
    --     ON users.user_memories USING ivfflat (content_embedding vector_cosine_ops)
    --     WITH (lists = 100);
    
    -- Unique constraint: 동일 사용자의 동일 내용 중복 방지
    CREATE UNIQUE INDEX idx_user_memories_unique_content
        ON users.user_memories(user_id, md5(content))
        WHERE is_active = TRUE;

    3.2 신규 테이블:

    -- 메모리 변경 감사 로그
    CREATE TABLE users.memory_audit_logs (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        -- Reference
        memory_id UUID NOT NULL,                  -- FK 없음 (삭제된 메모리도 추적)
        user_id UUID NOT NULL,
    
        -- Action
        action memory_audit_action NOT NULL,      -- ENUM
    
        -- Before/After
        old_content TEXT,
        new_content TEXT,
        old_confidence FLOAT,
        new_confidence FLOAT,
    
        -- Context
        session_id UUID,                          -- 변경 발생 세션
        trigger TEXT,                             -- 'llm_extraction', 'user_request', 'decay'
    
        -- Timestamp
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );
    
    CREATE TYPE memory_audit_action AS ENUM (
        'created',       -- 새 메모리 생성
        'updated',       -- 내용 또는 confidence 변경
        'merged',        -- 유사 메모리 병합
        'deactivated',   -- 소프트 삭제
        'reactivated',   -- 복구
        'expired'        -- TTL 만료
    );
    
    CREATE INDEX idx_memory_audit_logs_user
        ON users.memory_audit_logs(user_id, created_at DESC);
    
    CREATE INDEX idx_memory_audit_logs_memory
        ON users.memory_audit_logs(memory_id, created_at DESC);

    3.3 신규 테이블:

    -- 사용자별 메모리 설정
    CREATE TABLE users.memory_settings (
        user_id UUID PRIMARY KEY REFERENCES users.accounts(id) ON DELETE CASCADE,
    
        -- Feature Toggle
        is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
    
        -- Preferences
        allowed_categories memory_category[] DEFAULT ARRAY['location', 'preference']::memory_category[],
        auto_extraction BOOLEAN NOT NULL DEFAULT TRUE,  -- LLM 자동 추출 허용
    
        -- Limits
        max_memories INTEGER NOT NULL DEFAULT 50,
        retention_days INTEGER DEFAULT NULL,            -- NULL = 영구 보관
    
        -- Timestamps
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );

    3.4 ERD (Entity Relationship Diagram)

    ┌─────────────────────────────────────────────────────────────────────────┐
    │                              users schema                                │
    ├─────────────────────────────────────────────────────────────────────────┤
    │                                                                          │
    │   ┌─────────────────────┐                                               │
    │   │   accounts          │                                               │
    │   ├─────────────────────┤                                               │
    │   │ id (PK)             │◄─────────────────────────────────────────┐    │
    │   │ nickname            │                                          │    │
    │   │ email               │                                          │    │
    │   │ ...                 │                                          │    │
    │   └──────────┬──────────┘                                          │    │
    │              │                                                      │    │
    │              │ 1:N                                                  │    │
    │              │                                                      │    │
    │   ┌──────────▼──────────┐         ┌─────────────────────┐          │    │
    │   │   user_memories     │         │  memory_settings    │          │    │
    │   │   (NEW)             │         │  (NEW)              │          │    │
    │   ├─────────────────────┤         ├─────────────────────┤          │    │
    │   │ id (PK)             │         │ user_id (PK, FK)    │──────────┤    │
    │   │ user_id (FK)        │─────────│ is_enabled          │          │    │
    │   │ content             │         │ allowed_categories  │          │    │
    │   │ category            │         │ max_memories        │          │    │
    │   │ confidence          │         └─────────────────────┘          │    │
    │   │ source_type         │                                          │    │
    │   │ source_session_id   │─ ─ ─ ─ ─ ─ ┐                             │    │
    │   │ usage_count         │            │ (참조, FK 아님)              │    │
    │   │ last_used_at        │            │                             │    │
    │   │ is_active           │            │                             │    │
    │   └──────────┬──────────┘            │                             │    │
    │              │                       │                             │    │
    │              │ 1:N                   │                             │    │
    │              │                       │                             │    │
    │   ┌──────────▼──────────┐            │                             │    │
    │   │ memory_audit_logs   │            │                             │    │
    │   │ (NEW)               │            │                             │    │
    │   ├─────────────────────┤            │                             │    │
    │   │ id (PK)             │            │                             │    │
    │   │ memory_id           │◄───────────┤                             │    │
    │   │ user_id             │────────────│─────────────────────────────┘    │
    │   │ action              │            │                                  │
    │   │ old_content         │            │                                  │
    │   │ new_content         │            │                                  │
    │   └─────────────────────┘            │                                  │
    │                                      │                                  │
    └──────────────────────────────────────│──────────────────────────────────┘
                                           │
    ┌──────────────────────────────────────│──────────────────────────────────┐
    │                      LangGraph checkpoints                              │
    ├──────────────────────────────────────│──────────────────────────────────┤
    │                                      │                                  │
    │   ┌─────────────────────┐            │                                  │
    │   │   checkpoints       │◄─ ─ ─ ─ ─ ┘                                   │
    │   ├─────────────────────┤   thread_id = session_id                      │
    │   │ thread_id           │                                               │
    │   │ checkpoint_id       │                                               │
    │   │ checkpoint (JSONB)  │ → ChatState (세션 내 컨텍스트)                 │
    │   │ metadata            │                                               │
    │   └─────────────────────┘                                               │
    │                                                                          │
    └─────────────────────────────────────────────────────────────────────────┘

    4. 컴포넌트 설계

    4.1 도메인 모델

    # apps/chat_worker/domain/models/memory.py
    
    from dataclasses import dataclass
    from datetime import datetime
    from enum import Enum
    from uuid import UUID
    
    
    class MemoryCategory(str, Enum):
        LOCATION = "location"
        PREFERENCE = "preference"
        BEHAVIOR = "behavior"
        CONTEXT = "context"
        FEEDBACK = "feedback"
    
    
    class MemorySource(str, Enum):
        EXPLICIT = "explicit"
        INFERRED = "inferred"
        SYSTEM = "system"
    
    
    @dataclass
    class UserMemory:
        """사용자 글로벌 메모리 엔티티."""
    
        id: UUID
        user_id: UUID
        content: str
        category: MemoryCategory
        confidence: float
        source_type: MemorySource
    
        usage_count: int = 0
        last_used_at: datetime | None = None
        source_session_id: UUID | None = None
    
        is_active: bool = True
        created_at: datetime | None = None
        updated_at: datetime | None = None
    
        def touch(self) -> None:
            """메모리 사용 시 호출."""
            self.usage_count += 1
            self.last_used_at = datetime.utcnow()
    
        def decay(self, factor: float = 0.95) -> None:
            """시간 경과에 따른 신뢰도 감소."""
            self.confidence = max(0.1, self.confidence * factor)
    
    
    @dataclass
    class MemoryExtractionResult:
        """LLM 메모리 추출 결과."""
    
        memories: list[dict]
        # [{"content": "...", "category": "location", "confidence": 0.9}]

    4.2 포트 인터페이스

    # apps/chat_worker/application/ports/memory.py
    
    from abc import ABC, abstractmethod
    from uuid import UUID
    
    from chat_worker.domain.models.memory import (
        MemoryCategory,
        MemoryExtractionResult,
        UserMemory,
    )
    
    
    class MemoryRepositoryPort(ABC):
        """메모리 저장소 포트."""
    
        @abstractmethod
        async def get_user_memories(
            self,
            user_id: UUID,
            categories: list[MemoryCategory] | None = None,
            limit: int = 10,
            min_confidence: float = 0.5,
        ) -> list[UserMemory]:
            """사용자 메모리 조회."""
            ...
    
        @abstractmethod
        async def save_memory(self, memory: UserMemory) -> UserMemory:
            """메모리 저장 (upsert)."""
            ...
    
        @abstractmethod
        async def touch_memory(self, memory_id: UUID) -> None:
            """메모리 사용 기록."""
            ...
    
        @abstractmethod
        async def deactivate_memory(self, memory_id: UUID) -> bool:
            """메모리 비활성화 (소프트 삭제)."""
            ...
    
        @abstractmethod
        async def get_memory_settings(self, user_id: UUID) -> dict:
            """사용자 메모리 설정 조회."""
            ...
    
    
    class MemoryExtractorPort(ABC):
        """메모리 추출기 포트."""
    
        @abstractmethod
        async def extract_memories(
            self,
            conversation: list[dict],
            existing_memories: list[UserMemory],
        ) -> MemoryExtractionResult:
            """대화에서 메모리 추출."""
            ...

    4.3 인프라 어댑터

    # apps/chat_worker/infrastructure/memory/postgresql_memory_repository.py
    
    from uuid import UUID
    
    from sqlalchemy.ext.asyncio import AsyncSession
    
    from chat_worker.application.ports.memory import MemoryRepositoryPort
    from chat_worker.domain.models.memory import MemoryCategory, UserMemory
    
    
    class PostgreSQLMemoryRepository(MemoryRepositoryPort):
        """PostgreSQL 메모리 저장소 구현."""
    
        def __init__(self, session: AsyncSession):
            self._session = session
    
        async def get_user_memories(
            self,
            user_id: UUID,
            categories: list[MemoryCategory] | None = None,
            limit: int = 10,
            min_confidence: float = 0.5,
        ) -> list[UserMemory]:
            query = """
            SELECT * FROM users.user_memories
            WHERE user_id = :user_id
              AND is_active = TRUE
              AND confidence >= :min_confidence
            """
    
            if categories:
                query += " AND category = ANY(:categories)"
    
            query += """
            ORDER BY
                CASE WHEN last_used_at IS NOT NULL
                     THEN last_used_at
                     ELSE created_at
                END DESC,
                confidence DESC
            LIMIT :limit
            """
    
            result = await self._session.execute(
                text(query),
                {
                    "user_id": user_id,
                    "min_confidence": min_confidence,
                    "categories": [c.value for c in categories] if categories else None,
                    "limit": limit,
                }
            )
    
            return [self._to_entity(row) for row in result.fetchall()]
    
        async def save_memory(self, memory: UserMemory) -> UserMemory:
            """메모리 저장 (중복 시 confidence 갱신)."""
            query = """
            INSERT INTO users.user_memories (
                id, user_id, content, category, confidence,
                source_type, source_session_id
            ) VALUES (
                :id, :user_id, :content, :category, :confidence,
                :source_type, :source_session_id
            )
            ON CONFLICT (user_id, md5(content)) WHERE is_active = TRUE
            DO UPDATE SET
                confidence = GREATEST(user_memories.confidence, EXCLUDED.confidence),
                usage_count = user_memories.usage_count + 1,
                updated_at = NOW()
            RETURNING *
            """
    
            result = await self._session.execute(text(query), memory.__dict__)
            return self._to_entity(result.fetchone())

    4.4 메모리 추출기

    # apps/chat_worker/infrastructure/memory/llm_memory_extractor.py
    
    from chat_worker.application.ports.llm import LLMClientPort
    from chat_worker.application.ports.memory import MemoryExtractorPort
    from chat_worker.domain.models.memory import MemoryExtractionResult, UserMemory
    
    MEMORY_EXTRACTION_PROMPT = """
    다음 대화에서 사용자에 대해 기억할 만한 정보를 추출하세요.
    
    ## 대화
    {conversation}
    
    ## 이미 기억하고 있는 정보
    {existing_memories}
    
    ## 추출 기준
    - 사용자의 거주 지역 (예: "강남구에 살아요")
    - 선호도 및 관심사 (예: "친환경 제품 좋아해요")
    - 반복적으로 묻는 주제
    - 가족 구성원 정보 (예: "아이가 있어요")
    - 직업 또는 상황 (예: "자취생이에요")
    
    ## 주의사항
    - 민감한 개인정보(주민번호, 카드번호, 비밀번호)는 절대 추출하지 마세요
    - 이미 기억하고 있는 정보와 중복되면 추출하지 마세요
    - 추측이 아닌 명시적으로 언급된 정보만 추출하세요
    
    ## 응답 형식 (JSON)
    {{
      "memories": [
        {{"content": "사용자는 강남구에 거주", "category": "location", "confidence": 0.95}},
        {{"content": "친환경 제품에 관심이 많음", "category": "preference", "confidence": 0.8}}
      ]
    }}
    
    memories가 없으면 빈 배열을 반환하세요.
    """
    
    
    class LLMMemoryExtractor(MemoryExtractorPort):
        """LLM 기반 메모리 추출기."""
    
        def __init__(self, llm: LLMClientPort):
            self._llm = llm
    
        async def extract_memories(
            self,
            conversation: list[dict],
            existing_memories: list[UserMemory],
        ) -> MemoryExtractionResult:
            # 대화 포맷팅
            conv_text = "\n".join([
                f"{msg['role']}: {msg['content']}"
                for msg in conversation[-10:]  # 최근 10개만
            ])
    
            # 기존 메모리 포맷팅
            existing_text = "\n".join([
                f"- {m.content}" for m in existing_memories
            ]) or "없음"
    
            prompt = MEMORY_EXTRACTION_PROMPT.format(
                conversation=conv_text,
                existing_memories=existing_text,
            )
    
            # LLM 호출 (structured output)
            response = await self._llm.generate_structured(
                prompt=prompt,
                response_schema=MemoryExtractionResult,
            )
    
            return response

    5. LangGraph 통합

    5.1 ChatState 확장

    # apps/chat_worker/infrastructure/orchestration/langgraph/state.py
    
    class ChatState(TypedDict):
        # ... 기존 필드 ...
    
        # ═══════════════════════════════════════════════════════════════
        # Global Memory Layer (NEW)
        # ═══════════════════════════════════════════════════════════════
        user_memories: Annotated[list[dict], operator.add]
        # [{"content": "강남구 거주", "category": "location", "confidence": 0.9}]
    
        memory_context: str | None
        # 프롬프트 주입용 포맷된 문자열
        # "사용자 정보:\n- 강남구 거주\n- 분리배출 관심"
    
        extracted_memories: list[dict]
        # 현재 대화에서 추출된 새 메모리 (저장 대기)

    5.2 Memory Node 추가

    # apps/chat_worker/infrastructure/orchestration/langgraph/nodes/memory_node.py
    
    async def memory_retrieval_node(state: dict[str, Any]) -> dict[str, Any]:
        """세션 시작 시 사용자 메모리 로드."""
    
        user_id = state.get("user_id")
        if not user_id:
            return {"user_memories": [], "memory_context": None}
    
        # 메모리 설정 확인
        settings = await memory_repo.get_memory_settings(user_id)
        if not settings.get("is_enabled", True):
            return {"user_memories": [], "memory_context": None}
    
        # 메모리 조회
        memories = await memory_repo.get_user_memories(
            user_id=UUID(user_id),
            categories=settings.get("allowed_categories"),
            limit=10,
            min_confidence=0.5,
        )
    
        # 프롬프트 컨텍스트 생성
        if memories:
            memory_context = "## 사용자 정보 (이전 대화에서 기억)\n"
            memory_context += "\n".join([f"- {m.content}" for m in memories])
        else:
            memory_context = None
    
        return {
            "user_memories": [m.__dict__ for m in memories],
            "memory_context": memory_context,
        }
    
    
    async def memory_extraction_node(state: dict[str, Any]) -> dict[str, Any]:
        """대화 완료 후 메모리 추출."""
    
        user_id = state.get("user_id")
        settings = await memory_repo.get_memory_settings(user_id)
    
        if not settings.get("auto_extraction", True):
            return {"extracted_memories": []}
    
        # 대화 히스토리에서 메모리 추출
        conversation = state.get("messages", [])
        existing = state.get("user_memories", [])
    
        result = await memory_extractor.extract_memories(
            conversation=conversation,
            existing_memories=existing,
        )
    
        # 추출된 메모리 저장
        for mem_data in result.memories:
            memory = UserMemory(
                id=uuid4(),
                user_id=UUID(user_id),
                content=mem_data["content"],
                category=MemoryCategory(mem_data["category"]),
                confidence=mem_data["confidence"],
                source_type=MemorySource.INFERRED,
                source_session_id=UUID(state.get("thread_id")),
            )
            await memory_repo.save_memory(memory)
    
        return {"extracted_memories": result.memories}

    5.3 그래프 수정

    # apps/chat_worker/infrastructure/orchestration/langgraph/factory.py
    
    def create_chat_graph(...):
        graph = StateGraph(ChatState)
    
        # ═══════════════════════════════════════════════════════════════
        # 1. Memory Retrieval (첫 번째 노드)
        # ═══════════════════════════════════════════════════════════════
        graph.add_node("memory_retrieval", memory_retrieval_node)
    
        # 기존 노드들...
        graph.add_node("intent", intent_node)
        graph.add_node("router", dynamic_router)
        # ...
        graph.add_node("answer", answer_node)
    
        # ═══════════════════════════════════════════════════════════════
        # 2. Memory Extraction (마지막 노드)
        # ═══════════════════════════════════════════════════════════════
        graph.add_node("memory_extraction", memory_extraction_node)
    
        # Edge 설정
        graph.set_entry_point("memory_retrieval")
        graph.add_edge("memory_retrieval", "intent")
        # ... 기존 엣지 ...
        graph.add_edge("answer", "memory_extraction")
        graph.add_edge("memory_extraction", END)
    
        return graph.compile(checkpointer=checkpointer)

    5.4 Answer Node에 메모리 주입

    # apps/chat_worker/infrastructure/orchestration/langgraph/nodes/answer_node.py
    
    async def answer_node(state: dict[str, Any]) -> dict[str, Any]:
        # ...
    
        # 메모리 컨텍스트 주입
        memory_context = state.get("memory_context")
    
        input_dto = GenerateAnswerInput(
            # ... 기존 필드 ...
            user_memory_context=memory_context,  # NEW
        )
    
        # ...
    # apps/chat_worker/application/commands/generate_answer_command.py
    
    SYSTEM_PROMPT_TEMPLATE = """
    {character_context}
    
    {user_memory_context}
    
    {location_context}
    
    당신은 분리배출 도우미 AI입니다.
    ...
    """

    6. API 설계

    6.1 메모리 관리 엔드포인트

    # OpenAPI Spec
    
    /api/v1/users/me/memories:
      get:
        summary: 내 메모리 목록 조회
        responses:
          200:
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    memories:
                      type: array
                      items:
                        $ref: '#/components/schemas/UserMemory'
                    total:
                      type: integer
    
      delete:
        summary: 모든 메모리 삭제
        responses:
          204:
            description: 삭제 완료
    
    /api/v1/users/me/memories/{memory_id}:
      delete:
        summary: 특정 메모리 삭제
        responses:
          204:
            description: 삭제 완료
    
    /api/v1/users/me/memory-settings:
      get:
        summary: 메모리 설정 조회
        responses:
          200:
            content:
              application/json:
                schema:
                  $ref: '#/components/schemas/MemorySettings'
    
      patch:
        summary: 메모리 설정 변경
        requestBody:
          content:
            application/json:
              schema:
                type: object
                properties:
                  is_enabled:
                    type: boolean
                  auto_extraction:
                    type: boolean
                  allowed_categories:
                    type: array
                    items:
                      type: string
                      enum: [location, preference, behavior, context, feedback]

    6.2 DTO 스키마

    # apps/users/presentation/http/schemas/memory.py
    
    from pydantic import BaseModel
    
    
    class UserMemoryResponse(BaseModel):
        id: str
        content: str
        category: str
        confidence: float
        created_at: str
        last_used_at: str | None
        usage_count: int
    
    
    class MemoryListResponse(BaseModel):
        memories: list[UserMemoryResponse]
        total: int
    
    
    class MemorySettingsResponse(BaseModel):
        is_enabled: bool
        auto_extraction: bool
        allowed_categories: list[str]
        max_memories: int
    
    
    class UpdateMemorySettingsRequest(BaseModel):
        is_enabled: bool | None = None
        auto_extraction: bool | None = None
        allowed_categories: list[str] | None = None

    7. 구현 로드맵

    7.1 Phase 1: 기반 구축

    DB 마이그레이션user_memories, memory_settings 테이블 생성P0
    도메인 모델UserMemory, MemoryCategory 등P0
    Repository 구현PostgreSQLMemoryRepositoryP0
    단위 테스트Repository CRUD 테스트P0

    7.2 Phase 2: LangGraph 통합

    ChatState 확장user_memories, memory_context 필드P0
    Memory Retrieval Node세션 시작 시 메모리 로드P0
    Answer Node 수정메모리 컨텍스트 주입P0
    통합 테스트E2E 메모리 로드 테스트P0

    7.3 Phase 3: 메모리 추출

    LLM Extractor대화에서 메모리 추출P1
    Memory Extraction Node대화 완료 후 추출P1
    Deduplication중복 메모리 병합 로직P1
    감사 로그memory_audit_logs 기록P2

    7.4 Phase 4: API & UI

    REST API/users/me/memories 엔드포인트P1
    설정 API/users/me/memory-settingsP1
    프론트엔드메모리 관리 설정 페이지P2
    개인정보 동의메모리 수집 동의 UIP2

    7.5 Phase 5: 고도화

    Vector Searchpgvector 임베딩 검색P3
    Confidence Decay시간 경과 신뢰도 감소P3
    Relevance Scoring현재 질문 연관성 점수P3
    Analytics메모리 사용 통계P3

    8. 리스크 및 고려사항

    8.1 개인정보 보호

    민감정보 저장LLM 추출 시 필터링 프롬프트
    GDPR/개인정보보호법명시적 동의 UI, 삭제 권리 보장
    데이터 유출암호화 저장, 접근 로그

    8.2 성능

    조회 지연Redis 캐싱 (메모리 목록)
    LLM 추출 비용배치 처리, 비동기 추출
    저장소 증가사용자당 최대 50개 제한

    8.3 품질

    잘못된 추출confidence threshold, 사용자 피드백
    오래된 정보confidence decay, expires_at
    중복 메모리md5 해시 유니크 인덱스

     

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango