Python 시리즈 RAG #3 RAG 성능 모니터링

MJ·2025년 8월 14일
post-thumbnail

RAG 시스템 운영 최적화 가이드: 성능 튜닝과 지속적 개선 (시리즈 3/3)

앞선 두 시리즈에서 RAG의 기본 개념과 실제 구현 사례를 살펴봤다면, 이번 마지막 시리즈에서는 운영 환경에서의 핵심 최적화 기법을 다뤄보겠습니다.

1. 성능 모니터링 시스템

1.1. 핵심 메트릭 추적

실제 운영에서는 다음 메트릭들을 반드시 모니터링해야 합니다:

@dataclass
class PerformanceMetrics:
    response_time: float
    retrieval_time: float
    llm_time: float
    total_tokens: int
    similarity_scores: List[float]
    memory_usage: float

class RAGMonitor:
    def __init__(self):
        self.metrics_history = deque(maxlen=1000)
        self.thresholds = {
            'response_time': 3.0,    # 3초
            'memory_usage': 85.0,    # 85%
        }
    
    def get_performance_summary(self) -> Dict:
        """성능 요약 생성"""
        recent = list(self.metrics_history)[-100:]
        
        return {
            "avg_response_time": sum(m.response_time for m in recent) / len(recent),
            "avg_similarity_score": sum(sum(m.similarity_scores)/len(m.similarity_scores) for m in recent) / len(recent),
            "total_tokens_used": sum(m.total_tokens for m in recent),
            "avg_memory_usage": sum(m.memory_usage for m in recent) / len(recent)
        }

1.2. 실시간 대시보드

간단한 HTML 대시보드로 실시간 모니터링:

def generate_dashboard_html(summary: Dict) -> str:
    return f"""
    <h1>RAG 시스템 성능 대시보드</h1>
    <div>평균 응답 시간: {summary['avg_response_time']:.2f}s</div>
    <div>메모리 사용량: {summary['avg_memory_usage']:.1f}%</div>
    <div>총 토큰 사용량: {summary['total_tokens_used']:,}</div>
    """

2. 검색 성능 최적화

2.1. 적응형 검색 전략

쿼리 특성에 따라 검색 전략을 동적으로 선택:

class AdaptiveRetriever:
    def __init__(self, vectorstore):
        self.vectorstore = vectorstore
        self.strategy_performance = {
            'semantic': {'avg_time': 0, 'count': 0},
            'hybrid': {'avg_time': 0, 'count': 0}
        }
    
    def adaptive_search(self, query: str, k: int = 5):
        """쿼리 분석 후 최적 전략 선택"""
        query_analysis = self._analyze_query(query)
        
        # 쿼리 길이가 길면 하이브리드, 짧으면 의미 검색
        if query_analysis['length'] > 10:
            return self._hybrid_search(query, k)
        else:
            return self._semantic_search(query, k)
    
    def _analyze_query(self, query: str) -> Dict:
        return {
            'length': len(query.split()),
            'has_keywords': any(word.isupper() for word in query.split()),
            'question_type': self._classify_question(query)
        }
    
    def _classify_question(self, query: str) -> str:
        query_lower = query.lower()
        if any(word in query_lower for word in ['누구', '캐릭터']):
            return 'character'
        elif any(word in query_lower for word in ['어디', '장소']):
            return 'location'
        return 'general'

2.2. 동적 청크 크기 조정

콘텐츠 타입에 따른 최적 청크 크기:

class DynamicChunking:
    def __init__(self):
        self.optimal_sizes = {
            'character': 800,   # NPC 정보
            'location': 1200,   # 장소 정보  
            'rule': 1500,       # 게임 규칙
            'general': 1000     # 기본값
        }
    
    def get_optimal_chunk_size(self, content_type: str) -> int:
        return self.optimal_sizes.get(content_type, 1000)

3. LLM 최적화

3.1. 컨텍스트 윈도우 관리

토큰 사용량을 효율적으로 관리:

class ContextManager:
    def __init__(self, max_tokens: int = 4000):
        self.max_tokens = max_tokens
        self.token_weights = {
            'system_prompt': 0.2,     # 20%
            'retrieved_docs': 0.5,    # 50%
            'conversation': 0.2,      # 20%
            'current_query': 0.1      # 10%
        }
    
    def optimize_context(self, system_prompt: str, docs: List, 
                        history: List, query: str) -> Dict:
        """컨텍스트 최적화"""
        allocated = {
            component: int(self.max_tokens * weight)
            for component, weight in self.token_weights.items()
        }
        
        # 문서 우선순위 정렬 후 토큰 할당량에 맞춰 선택
        optimized_docs = self._select_top_docs(docs, allocated['retrieved_docs'])
        
        # 최근 대화만 포함
        optimized_history = history[-5:] if len(history) > 5 else history
        
        return {
            'system_prompt': system_prompt,
            'retrieved_docs': optimized_docs,
            'conversation_history': optimized_history,
            'current_query': query
        }

3.2. 프롬프트 최적화

상황별 맞춤형 프롬프트:

class PromptOptimizer:
    def __init__(self):
        self.templates = {
            'character': "NPC {character_name}로서 {situation}에 반응해주세요.",
            'combat': "전투 상황에서 {action}의 결과를 묘사해주세요.",
            'general': "TRPG GM으로서 {context}를 바탕으로 응답해주세요."
        }
    
    def get_optimized_prompt(self, query_type: str, **kwargs) -> str:
        template = self.templates.get(query_type, self.templates['general'])
        return template.format(**kwargs)

4. 비용 최적화

4.1. 스마트 LLM 라우팅

쿼리 복잡도에 따른 적절한 모델 선택:

class LLMRouter:
    def __init__(self):
        self.models = {
            'simple': {'name': 'deepseek', 'cost': 0.0000002, 'quality': 0.8},
            'complex': {'name': 'claude-sonnet', 'cost': 0.000015, 'quality': 0.95}
        }
    
    def select_model(self, query_complexity: str, budget_mode: str = 'balanced'):
        """쿼리 복잡도와 예산에 따른 모델 선택"""
        if budget_mode == 'cost_first':
            return min(self.models.keys(), key=lambda x: self.models[x]['cost'])
        elif query_complexity == 'high':
            return 'complex'
        else:
            return 'simple'
    
    def estimate_cost(self, model_type: str, tokens: int) -> float:
        model_info = self.models[model_type]
        return tokens * model_info['cost']

4.2. 응답 캐싱

유사한 쿼리에 대한 캐시 활용:

class ResponseCache:
    def __init__(self, ttl_hours: int = 24):
        self.cache = {}
        self.ttl = timedelta(hours=ttl_hours)
        self.hits = 0
        self.misses = 0
    
    def get_cached_response(self, query: str) -> Optional[str]:
        """캐시된 응답 검색"""
        query_hash = hashlib.md5(query.encode()).hexdigest()
        
        if query_hash in self.cache:
            cached_item = self.cache[query_hash]
            if datetime.now() - cached_item['timestamp'] < self.ttl:
                self.hits += 1
                return cached_item['response']
        
        self.misses += 1
        return None
    
    def store_response(self, query: str, response: str):
        """응답 캐시 저장"""
        query_hash = hashlib.md5(query.encode()).hexdigest()
        self.cache[query_hash] = {
            'response': response,
            'timestamp': datetime.now()
        }

5. 사용자 피드백 기반 개선

5.1. 피드백 수집 및 분석

class FeedbackSystem:
    def __init__(self):
        self.feedback_data = []
    
    def collect_feedback(self, query: str, response: str, rating: int, comments: str = ""):
        """피드백 수집"""
        feedback = {
            'timestamp': datetime.now(),
            'query': query,
            'response': response,
            'rating': rating,  # 1-5
            'comments': comments,
            'query_type': self._classify_query(query)
        }
        self.feedback_data.append(feedback)
        
        # 낮은 평점 즉시 분석
        if rating <= 2:
            self._analyze_negative_feedback(feedback)
    
    def generate_improvement_report(self, days: int = 7) -> Dict:
        """개선 리포트 생성"""
        recent = [f for f in self.feedback_data 
                 if f['timestamp'] > datetime.now() - timedelta(days=days)]
        
        if not recent:
            return {"message": "분석할 데이터 없음"}
        
        avg_rating = sum(f['rating'] for f in recent) / len(recent)
        
        # 타입별 성능
        type_ratings = defaultdict(list)
        for f in recent:
            type_ratings[f['query_type']].append(f['rating'])
        
        return {
            'period': f"최근 {days}일",
            'total_feedback': len(recent),
            'average_rating': avg_rating,
            'type_performance': {
                qtype: sum(ratings)/len(ratings)
                for qtype, ratings in type_ratings.items()
            }
        }

5.2. 자동 파라미터 튜닝

class AutoTuner:
    def __init__(self, feedback_system):
        self.feedback_system = feedback_system
        self.current_config = {
            'chunk_size': 1000,
            'search_k': 5,
            'temperature': 0.7
        }
    
    def auto_tune(self) -> Dict:
        """피드백 기반 자동 튜닝"""
        report = self.feedback_system.generate_improvement_report()
        
        if report.get('average_rating', 0) < 3.5:
            # 성능이 낮으면 파라미터 조정
            if report['average_rating'] < 3.0:
                self.current_config['search_k'] = min(self.current_config['search_k'] + 1, 8)
                self.current_config['chunk_size'] = max(self.current_config['chunk_size'] - 100, 600)
            
            return {'tuned': True, 'new_config': self.current_config}
        
        return {'tuned': False, 'reason': '성능 양호'}

6. 운영 환경 배포

6.1. Docker 컨테이너화

FROM python:3.13-slim

WORKDIR /app

# 의존성 설치
COPY pyproject.toml ./
RUN pip install uv && uv sync

# 애플리케이션 복사
COPY . .

# 헬스체크
HEALTHCHECK --interval=30s --timeout=10s \
  CMD curl -f http://localhost:8001/health || exit 1

EXPOSE 8001
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]

6.2. 서비스 헬스체크

class HealthChecker:
    async def check_system_health(self) -> Dict:
        """시스템 전체 상태 확인"""
        checks = {
            'database': self._check_db(),
            'embedding': self._check_embedding(),
            'llm': self._check_llm()
        }
        
        all_healthy = all(checks.values())
        
        return {
            'status': 'healthy' if all_healthy else 'unhealthy',
            'services': checks,
            'timestamp': datetime.now().isoformat()
        }

7. 마무리

운영 최적화 체크리스트

모니터링: 응답시간, 메모리, 토큰 사용량 추적
검색 최적화: 적응형 전략, 동적 청크 크기
비용 관리: 스마트 라우팅, 응답 캐싱
품질 개선: 피드백 수집, 자동 튜닝
배포 관리: 컨테이너화, 헬스체크

다음 단계

  1. 실시간 모니터링 대시보드 구축
  2. A/B 테스트를 통한 성능 비교
  3. 멀티모달 RAG로 확장
  4. 개인화 기능 추가

RAG 시스템은 지속적인 모니터링과 개선을 통해 더욱 강력해집니다. 이 가이드가 여러분의 RAG 시스템 운영에 도움이 되기를 바랍니다!

참고 자료:

profile
..

0개의 댓글