""" 폴백 RAG 체인 구현 (기본적인 기능만 포함) - 직접 DeepSeek API 호출 방식 """ import os import logging import time from typing import List, Dict, Any, Optional, Tuple from langchain.schema import Document # 직접 DeepSeek 클라이언트 사용 from direct_deepseek import DirectDeepSeekClient # 설정 가져오기 from config import ( LLM_MODEL, USE_OPENAI, USE_DEEPSEEK, DEEPSEEK_API_KEY, DEEPSEEK_ENDPOINT, DEEPSEEK_MODEL, TOP_K_RETRIEVAL ) # 로깅 설정 logger = logging.getLogger("FallbackRAGChain") class FallbackRAGChain: """ 기본적인 RAG 체인 구현 (단순화된 버전, 문제 해결용) 직접 DeepSeek API 호출 방식 사용 """ def __init__(self, vector_store): """ RAG 체인 초기화 Args: vector_store: 벡터 스토어 인스턴스 """ logger.info("폴백 RAG 체인 초기화...") self.vector_store = vector_store # DeepSeek 모델 직접 초기화 if USE_DEEPSEEK and DEEPSEEK_API_KEY: logger.info(f"DeepSeek 모델 직접 초기화: {DEEPSEEK_MODEL}") try: self.client = DirectDeepSeekClient( api_key=DEEPSEEK_API_KEY, model_name=DEEPSEEK_MODEL ) logger.info("DeepSeek 모델 직접 초기화 성공") except Exception as e: logger.error(f"DeepSeek 모델 초기화 실패: {e}") # 오프라인 모드로 폴백 self.client = None logger.warning("LLM이 초기화되지 않아 오프라인 모드로 동작합니다.") else: # LLM이 설정되지 않음 logger.warning("LLM이 설정되지 않아 오프라인 모드로 동작합니다.") self.client = None logger.info("폴백 RAG 체인 초기화 완료") def _retrieve(self, query: str) -> str: """ 쿼리에 대한 관련 문서 검색 및 컨텍스트 구성 Args: query: 사용자 질문 Returns: 검색 결과를 포함한 컨텍스트 문자열 """ if not query or not query.strip(): return "검색 쿼리가 비어있습니다." try: # 벡터 검색 수행 logger.info(f"벡터 검색: '{query[:30]}...'") docs = self.vector_store.similarity_search(query, k=TOP_K_RETRIEVAL) if not docs: return "관련 문서를 찾을 수 없습니다." # 검색 결과 컨텍스트 구성 context_parts = [] for i, doc in enumerate(docs, 1): source = doc.metadata.get("source", "알 수 없는 출처") page = doc.metadata.get("page", "") source_info = f"{source}" if page: source_info += f" (페이지: {page})" context_parts.append(f"[참고자료 {i}] - 출처: {source_info}\n{doc.page_content}\n") context = "\n".join(context_parts) # 컨텍스트 길이 제한 (토큰 수 제한) if len(context) > 6000: logger.warning(f"컨텍스트가 너무 깁니다 ({len(context)} 문자). 제한합니다.") context = context[:2500] + "\n...(중략)...\n" + context[-2500:] return context except Exception as e: logger.error(f"검색 중 오류: {e}") return f"검색 중 오류 발생: {str(e)}" def _generate_prompt(self, query: str, context: str) -> List[Dict[str, str]]: """ 프롬프트 생성 (DeepSeek API 형식) Args: query: 사용자 질문 context: 검색 결과 컨텍스트 Returns: DeepSeek API용 messages 형식 """ # 시스템 프롬프트 system_prompt = """다음 정보를 기반으로 질문에 정확하게 답변해주세요. 참고 정보에 답이 있으면 반드시 그 정보를 기반으로 답변하세요. 참고 정보에 답이 없는 경우에는 일반적인 지식을 활용하여 답변할 수 있지만, "제공된 문서에는 이 정보가 없으나, 일반적으로는..." 식으로 시작하세요. 답변은 정확하고 간결하게 제공하되, 가능한 참고 정보에서 근거를 찾아 설명해주세요. 참고 정보의 출처도 함께 알려주세요.""" # 사용자 프롬프트 (질문과 컨텍스트 포함) user_prompt = f"""질문: {query} 참고 정보: {context}""" # DeepSeek API에 맞는 메시지 포맷 messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ] return messages def _generate_simple_response(self, query: str, context: str) -> str: """ 간단한 오프라인 응답 생성 (LLM이 없을 때 사용) """ # 기본 제공 응답 (일반적인 질문에 대한 정해진 응답) predefined_answers = { "대한민국의 수도": "대한민국의 수도는 서울입니다.", "수도": "대한민국의 수도는 서울입니다.", "누구야": "저는 RAG 기반 질의응답 시스템입니다. 문서를 검색하고 관련 정보를 찾아드립니다.", "안녕": "안녕하세요! 무엇을 도와드릴까요?", "뭐해": "사용자의 질문에 답변하기 위해 문서를 검색하고 있습니다. 무엇을 알려드릴까요?" } # 질문에 맞는 미리 정의된 응답이 있는지 확인 for key, answer in predefined_answers.items(): if key in query.lower(): return answer # 미리 정의된 응답이 없으면 검색 결과만 표시 return f""" 현재 LLM API 연결에 문제가 있어 검색 결과만 표시합니다. 질문: {query} 검색된 관련 문서: {context} [참고] 관련 정보를 찾으셨나요? API 연결 문제로 인해 자동 요약이 제공되지 않습니다. 다시 시도하거나 다른 질문을 해보세요. """ def run(self, query: str) -> str: """ 사용자 쿼리에 대한 RAG 파이프라인 실행 Args: query: 사용자 질문 Returns: 모델 응답 문자열 """ if not query or not query.strip(): return "질문이 비어있습니다. 질문을 입력해 주세요." try: logger.info(f"RAG 체인 실행: '{query[:30]}...'") # 문서 검색 context = self._retrieve(query) # LLM이 초기화되지 않은 경우 오프라인 응답 if self.client is None: logger.warning("LLM이 초기화되지 않아 오프라인 응답 생성") return self._generate_simple_response(query, context) # 프롬프트 구성 messages = self._generate_prompt(query, context) # 응답 생성 (최대 3회 시도) max_retries = 3 retry_delay = 1.0 for attempt in range(max_retries): try: logger.info(f"응답 생성 시도 ({attempt+1}/{max_retries})") # 직접 DeepSeek API 호출 response = self.client.chat(messages) if response["success"]: logger.info(f"응답 생성 성공 (길이: {len(response['response'])})") return response["response"] else: logger.error(f"응답 생성 실패: {response['message']}") if attempt < max_retries - 1: logger.info(f"{retry_delay}초 후 재시도...") time.sleep(retry_delay) retry_delay *= 2 else: # 모든 시도 실패 시 오프라인 응답 logger.warning("최대 재시도 횟수 초과, 오프라인 응답 생성") return self._generate_simple_response(query, context) except Exception as e: logger.error(f"응답 생성 중 오류: {e}") if attempt < max_retries - 1: logger.info(f"{retry_delay}초 후 재시도...") time.sleep(retry_delay) retry_delay *= 2 else: # 모든 시도 실패 시 오프라인 응답 생성 return self._generate_simple_response(query, context) except Exception as e: logger.error(f"RAG 체인 실행 중 오류: {e}") return f"질문 처리 중 오류가 발생했습니다: {str(e)}"