-
이코에코(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. 현재 아키텍처 분석
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 현재 스키마 구조
스키마 테이블 용도 usersaccounts사용자 기본 정보 userssocial_accountsOAuth 연동 usersuser_characters캐릭터 소유권 charactercharacters캐릭터 마스터 publiccheckpointsLangGraph 상태 (세션별) 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 response5. 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 = None7. 구현 로드맵
7.1 Phase 1: 기반 구축
DB 마이그레이션 user_memories, memory_settings 테이블 생성 P0 도메인 모델 UserMemory, MemoryCategory 등 P0 Repository 구현 PostgreSQLMemoryRepository P0 단위 테스트 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-settings P1 프론트엔드 메모리 관리 설정 페이지 P2 개인정보 동의 메모리 수집 동의 UI P2 7.5 Phase 5: 고도화
Vector Search pgvector 임베딩 검색 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 해시 유니크 인덱스 '이코에코(Eco²) > Plans' 카테고리의 다른 글
ADR: Chat LangGraph Eval Pipeline (0) 2026.02.09 이코이코(Eco²) Agent: Event Router, SSE-Gateway 무결성 개선 (0) 2026.01.23 이코에코(Eco²) Agent: Multi-Intent E2E Test Plan (0) 2026.01.19 ADR: LangGraph Channel Separation (1) 2026.01.18 ADR: Info Service 3-Tier Memory Architecture (0) 2026.01.17