DOCS/ideas/기억_개선_5단계_계획.md
happybell80 3ea5929365 docs: AI 응답 개선 Phase 1 트러블슈팅 문서 추가
- async/await 처리 실수와 해결 과정
- 기억 개선 5단계 계획을 ideas로 이동
- Phase 1 성공적 구현 기록
2025-08-05 23:25:17 +09:00

12 KiB

rb10508_micro 기억 개선 5단계 계획

개요

rb10508_micro의 AI 응답 단조로움 문제를 해결하기 위한 캐시 기반 지능형 응답 시스템 구현 계획입니다.

핵심 목표

  • 단조로운 템플릿 응답 제거
  • Gemini API 비용 최적화
  • 자연스러운 대화 경험 제공
  • 기존 코드 최대한 재사용

기본 원칙

  • 최소한의 코드 수정으로 최대 효과
  • skill-embedding 서비스 활용 (포트 8015)
  • 점진적 구현으로 리스크 최소화

🏗️ 기본 설정 (모든 단계 공통)

# config.py에 추가
USE_GEMINI_CONVERSATION: bool = False  # 1단계 토글
USE_CONVERSATION_CACHE: bool = False   # 2-4단계 토글
USE_RESPONSE_VARIATION: bool = False   # 5단계 토글
CACHE_DISTANCE_THRESHOLD: float = 0.3  # 동적 조정 가능
CACHE_MAX_ITEMS_PER_USER: int = 1000  # 사용자당 캐시 제한
CACHE_TTL_DAYS: int = 30              # 캐시 유효 기간
GEMINI_FALLBACK_MODEL: str = "gemini-2.5-flash-lite"  # 쿼터 초과 시 대체

📌 1단계: Gemini 전면 도입 + 병렬화 + Fallback

목표

  • 응답 품질 즉시 개선
  • 안정성 확보

구현

async def _generate_conversational_response(self, message: str, memories: List[Dict]) -> str:
    if not settings.USE_GEMINI_CONVERSATION:
        return random.choice(self.responses)  # 기존 방식
    
    try:
        # 병렬 처리로 지연 최소화
        tasks = [
            self._generate_gemini_response(message, memories, EmotionState()),
            self.memory.check_novelty(message, self.current_user_id)  # 동시 실행
        ]
        response, novelty = await asyncio.gather(*tasks)
        return response
        
    except Exception as e:
        if "quota" in str(e).lower():
            # 쿼터 초과 시 대체 모델
            self.gemini_model = genai.GenerativeModel(settings.GEMINI_FALLBACK_MODEL)
            return await self._generate_gemini_response(message, memories, EmotionState())
        else:
            # 완전 실패 시 기본 응답
            logger.error(f"Gemini 호출 실패: {e}")
            return "죄송합니다. 잠시 후 다시 시도해주세요."

효과

  • 응답 다양성 100% 개선
  • API 비용 증가 (임시 감수)
  • 쿼터 초과 시 자동 대체

📌 2단계: 캐시 인프라 구축 + 중복 방지

목표

  • 안전한 캐시 구조 마련
  • 동시성 제어

구현

# memory.py
async def _init_collections(self):
    # 기존 컬렉션들...
    
    if settings.USE_CONVERSATION_CACHE:
        self.conversation_cache = self.client.get_or_create_collection(
            name=f"{settings.ROBING_ID}_conversation_cache",
            metadata={
                "type": "conversation_cache",
                "version": "1.0",
                "max_items": settings.CACHE_MAX_ITEMS_PER_USER
            },
            embedding_function=self.embedding_function
        )
        # 동시성 제어를 위한 간단한 락
        self._cache_locks = {}  # user_id: asyncio.Lock()

하드코딩 값 설명

  • "type": "conversation_cache": 다른 컬렉션과 구분하기 위한 메타데이터
  • "version": "1.0": 향후 마이그레이션을 위한 버전 관리

📌 3단계: 지능형 캐시 저장 + TTL 관리

목표

  • 중복 방지
  • 자동 정리
  • 사용자별 캐시 제한

구현

async def store_conversation_cache(self, message: str, response: str, user_id: str, emotion_state: Dict):
    if not settings.USE_CONVERSATION_CACHE:
        return
    
    # 사용자별 락 획득
    if user_id not in self._cache_locks:
        self._cache_locks[user_id] = asyncio.Lock()
    
    async with self._cache_locks[user_id]:
        # 중복 체크
        existing = self.conversation_cache.query(
            query_texts=[message],
            n_results=1,
            where={"user_id": user_id}
        )
        
        if existing['distances'][0] and existing['distances'][0][0] < 0.1:  # 거의 동일
            logger.debug(f"중복 캐시 스킵: {message[:50]}...")
            return
        
        # 사용자별 캐시 개수 체크
        user_cache_count = self._get_user_cache_count(user_id)
        if user_cache_count >= settings.CACHE_MAX_ITEMS_PER_USER:
            # 오래된 것 삭제
            await self._cleanup_old_cache(user_id)
        
        # 저장
        cache_id = f"conv_{user_id}_{int(time.time())}_{hashlib.md5(message.encode()).hexdigest()[:8]}"
        
        self.conversation_cache.add(
            documents=[message],
            metadatas=[{
                "user_id": user_id,
                "response": response,
                "emotion_valence": emotion_state.get('valence', 0),
                "timestamp": datetime.utcnow().isoformat(),
                "ttl_date": (datetime.utcnow() + timedelta(days=settings.CACHE_TTL_DAYS)).isoformat(),
                "usage_count": 0,
                "last_used": datetime.utcnow().isoformat()
            }],
            ids=[cache_id]
        )

하드코딩 값 설명

  • 0.1: 중복 판단 임계값 (90% 이상 유사 시 중복)
  • cache_id 형식: 가독성과 유일성 보장

📌 4단계: 정밀 캐시 검색 + 다중 가중치

목표

  • 문맥에 맞는 캐시 활용
  • 감정, 시간, 유사도 종합 고려

구현

async def search_conversation_cache(self, query: str, user_id: str, emotion_valence: float = 0) -> List[Dict]:
    if not settings.USE_CONVERSATION_CACHE:
        return []
    
    # 기본 유사도 검색
    results = self.conversation_cache.query(
        query_texts=[query],
        n_results=10,  # 더 많이 가져와서 필터링
        where={
            "$and": [
                {"user_id": user_id},
                {"ttl_date": {"$gte": datetime.utcnow().isoformat()}}  # TTL 체크
            ]
        }
    )
    
    if not results['documents'][0]:
        return []
    
    # 다중 가중치 적용
    scored_results = []
    for i, doc in enumerate(results['documents'][0]):
        distance = results['distances'][0][i]
        metadata = results['metadatas'][0][i]
        
        # 거리 기반 점수 (0-1, 낮을수록 좋음)
        distance_score = distance
        
        # 감정 유사도 점수 (0-1, 낮을수록 좋음)
        emotion_diff = abs(emotion_valence - metadata.get('emotion_valence', 0))
        emotion_score = emotion_diff / 2  # 정규화
        
        # 시간 가중치 (최근일수록 좋음)
        days_old = (datetime.utcnow() - datetime.fromisoformat(metadata['timestamp'])).days
        time_score = min(days_old / settings.CACHE_TTL_DAYS, 1.0)
        
        # 종합 점수 (가중 평균)
        final_score = (
            0.6 * distance_score +  # 의미 유사도 60%
            0.2 * emotion_score +   # 감정 유사도 20%
            0.2 * time_score        # 시간 가중치 20%
        )
        
        if final_score < settings.CACHE_DISTANCE_THRESHOLD:
            scored_results.append({
                'content': doc,
                'metadata': metadata,
                'distance': distance,
                'final_score': final_score
            })
    
    # 점수순 정렬
    return sorted(scored_results, key=lambda x: x['final_score'])

하드코딩 값 설명

  • n_results=10: 충분한 후보군 확보
  • 가중치 비율 (0.6, 0.2, 0.2): 의미 유사도 중심, 감정과 시간 보조

📌 5단계: 자연스러운 응답 변형 + 모니터링

목표

  • 기계적이지 않은 변형
  • 효과 측정

구현

async def _vary_cached_response(self, original: str, cache_metadata: Dict) -> str:
    if not settings.USE_RESPONSE_VARIATION:
        return original
    
    # 사용 횟수에 따른 변형 강도 결정
    usage_count = cache_metadata.get('usage_count', 0)
    
    if usage_count < 3:
        # 3회 미만은 그대로 사용
        return original
    elif usage_count < 10:
        # 간단한 템플릿 변형
        return self._simple_variation(original)
    else:
        # 10회 이상 사용된 응답은 Gemini로 재생성
        prompt = f"""다음 응답을 의미는 유지하되 자연스럽게 다시 표현해주세요.
        원본: {original}
        
        규칙:
        - 핵심 의미는 동일하게
        - 어투나 표현만 살짝 변경
        - 1-2문장으로 간결하게"""
        
        try:
            response = await self.gemini_model.generate_content_async(prompt)
            varied = response.text.strip()
            
            # 변형된 응답도 캐시에 저장
            await self.store_conversation_cache(
                message=cache_metadata.get('original_message', ''),
                response=varied,
                user_id=cache_metadata.get('user_id', ''),
                emotion_state={'valence': cache_metadata.get('emotion_valence', 0)}
            )
            
            return varied
        except:
            return self._simple_variation(original)

def _simple_variation(self, text: str) -> str:
    """간단한 템플릿 기반 변형"""
    # 종결어미 변형 (더 자연스럽게)
    endings = {
        "입니다.": ["이에요.", "인 것 같아요.", "이네요."],
        "해요.": ["하는 편이에요.", "하고 있어요.", "해 보세요."],
        "네요.": ["는군요.", "네요!", "는 것 같아요."]
    }
    
    result = text
    for pattern, replacements in endings.items():
        if pattern in result and random.random() < 0.3:  # 30% 확률
            result = result.replace(pattern, random.choice(replacements), 1)
            break  # 한 번만 변형
    
    return result

하드코딩 값 설명

  • usage_count < 3: 신선한 응답은 그대로 유지
  • usage_count < 10: 적당한 사용 빈도는 간단 변형
  • 0.3 (30%): 변형 확률 - 너무 자주 변형하면 부자연스러움

📊 모니터링 지표

# 각 단계에서 로깅
logger.info(f"[CONV_CACHE] action=search user={user_id} query_len={len(query)} hits={len(results)} cache_used={cache_hit}")
logger.info(f"[CONV_CACHE] action=store user={user_id} msg_len={len(message)} cache_size={self._get_user_cache_count(user_id)}")
logger.info(f"[GEMINI_API] model={self.gemini_model.model_name} response_time={elapsed} fallback={is_fallback}")

📈 예상 효과

단계 기대 효과 구현 난이도 소요 시간 API 비용 변화
1단계 응답 다양성 100% 개선 매우 쉬움 5분 +100%
2단계 캐시 구조 준비 쉬움 30분 변화 없음
3단계 캐시 저장 시작 보통 1시간 변화 없음
4단계 API 비용 30% 절감 보통 2시간 -30%
5단계 API 비용 70% 절감 어려움 3시간 -70%

🔧 운영 고려사항

저장 한도

  • 사용자/팀 단위 max_items 지정으로 무제한 증가 방지

TTL 관리

  • 30일 이상 미사용 캐시 자동 삭제
  • 중요 대화는 요약 후 semantic 메모리로 이전

동시성

  • asyncio.Lock()으로 중복 저장 방지

배포 전략

  • 기능 토글로 단계별 롤아웃
  • 문제 발생 시 즉시 롤백 가능

성능 모니터링

  • 캐시 히트율 목표: 30% → 70% (3개월)
  • API 호출 감소율 추적
  • 응답 시간 개선 측정

🚀 구현 로드맵

  1. Week 1: 1단계 배포 및 모니터링
  2. Week 2: 2-3단계 구현 및 테스트
  3. Week 3: 4단계 배포 및 캐시 히트율 분석
  4. Week 4: 5단계 구현 및 전체 최적화

작성일: 2025-08-05
작성자: Claude & happybell80
프로젝트: rb10508_micro 응답 품질 개선