
# 고급 RAG 기법: 검색 증강 생성의 최신 기술과 구현

## 📋 학습 목표

이 모듈을 통해 다음을 학습하게 됩니다:

- RAG 시스템의 다양한 아키텍처 변형과 장단점을 이해합니다.
- 검색 품질 향상을 위한 고급 기법을 구현할 수 있습니다.
- 복잡한 질문에 정확하게 응답하는 RAG 파이프라인을 최적화할 수 있습니다.
- 프로덕션 환경에서의 RAG 성능 평가 및 모니터링 방법을 습득합니다.

## 1. RAG 아키텍처의 진화

### 1.1 기본 RAG vs. 고급 RAG 아키텍처

**기본 RAG 아키텍처의 한계:**
- 단순 의미 검색만으로는 복잡한 질문에 대응하기 어려움
- 컨텍스트 길이 제한으로 인한 정보 손실
- 연관성 높은 문서 선별의 어려움

**고급 RAG 아키텍처 특징:**
```python
# 기본 RAG 구현
from langchain.chains import RetrievalQA
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma

embeddings = OpenAIEmbeddings()
vectorstore = Chroma(embedding_function=embeddings, persist_directory="./chroma_db")
qa_chain = RetrievalQA.from_chain_type(
    llm=OpenAI(),
    retriever=vectorstore.as_retriever(search_kwargs={"k": 3})
)
```

### 1.2 Hybrid Search 구현

키워드 기반 검색(BM25)과 의미 기반 검색을 결합하여 검색 품질 향상

```python
# Hybrid Search 구현 예시
from langchain.retrievers import BM25Retriever, EnsembleRetriever

# 임베딩 기반 검색기
embedding_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# BM25 검색기 (키워드 기반)
bm25_retriever = BM25Retriever.from_documents(documents)
bm25_retriever.k = 3

# 앙상블 검색기
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, embedding_retriever],
    weights=[0.5, 0.5]
)
```

### 1.3 멀티 벡터 검색(Multi-Vector Retrieval)

문서를 다양한 관점에서 임베딩하여 검색 효율성 향상

```python
# 멀티 벡터 검색 구현 예시
from langchain.schema import Document
from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain.storage import InMemoryStore

# 문서 저장소
store = InMemoryStore()
id_key = "doc_id"

# 각 문서를 여러 청크로 분할하고 각 청크에 원본 문서 ID 연결
for i, doc in enumerate(documents):
    # 문서별 고유 ID 생성
    doc_id = f"doc_{i}"
    # 문서를 청크로 분할
    chunks = text_splitter.split_text(doc.page_content)
    
    # 각 청크에 원본 문서 ID 메타데이터 추가
    for j, chunk in enumerate(chunks):
        _id = f"{doc_id}_chunk_{j}"
        store.mset([(
            _id, 
            {"id": _id, "doc_id": doc_id, "text": chunk, "metadata": doc.metadata}
        )])

# 멀티 벡터 검색기 생성
retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    docstore=store,
    id_key=id_key,
)
```

## 2. 고급 RAG 파이프라인 최적화

### 2.1 쿼리 최적화 기법

#### 2.1.1 Query Expansion

```python
# 쿼리 확장 구현 예시
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

# 원본 검색기
base_retriever = vectorstore.as_retriever()

# LLM을 사용한 쿼리 확장
def expand_query(query):
    prompt = f"""
    원본 질문: {query}
    
    이 질문과 관련된 키워드와 관련 질문을 3개 생성해주세요. 
    결과는 쉼표로 구분된 목록으로 반환하세요.
    """
    response = llm.predict(prompt)
    expanded_terms = [term.strip() for term in response.split(',')]
    expanded_query = query + " " + " ".join(expanded_terms)
    return expanded_query

# 확장된 쿼리를 사용한 검색
def retrieval_with_query_expansion(query):
    expanded_query = expand_query(query)
    print(f"확장된 쿼리: {expanded_query}")
    return base_retriever.get_relevant_documents(expanded_query)
```

#### 2.1.2 HyDE (Hypothetical Document Embeddings)

```python
# HyDE 구현 예시
def hyde_retrieval(query):
    # 1. 가상 문서 생성
    hypothetical_doc_prompt = f"""
    다음 질문에 대한 이상적인 답변 문서를 생성해주세요:
    {query}
    
    답변 문서:
    """
    
    hypothetical_doc = llm.predict(hypothetical_doc_prompt)
    
    # 2. 가상 문서를 임베딩
    hyde_embedding = embeddings.embed_query(hypothetical_doc)
    
    # 3. 유사 문서 검색
    docs = vectorstore.similarity_search_by_vector(hyde_embedding, k=3)
    
    return docs
```

### 2.2 임베딩 모델 성능 비교

다양한 임베딩 모델의 성능과 비용 효율성 비교

```python
# 여러 임베딩 모델 성능 비교
from langchain.embeddings import OpenAIEmbeddings, HuggingFaceEmbeddings
import time

models_to_compare = {
    "OpenAI Ada 002": OpenAIEmbeddings(),
    "BAAI/bge-large-en-v1.5": HuggingFaceEmbeddings(model_name="BAAI/bge-large-en-v1.5"),
    "Cohere": CohereEmbeddings(),
    "sentence-transformers/all-MiniLM-L6-v2": HuggingFaceEmbeddings(
        model_name="sentence-transformers/all-MiniLM-L6-v2"
    )
}

# 테스트 쿼리 세트
test_queries = ["인공지능의 윤리적 문제는 무엇인가요?", "파이썬에서 리스트와 튜플의 차이점은?"]

results = {}

for model_name, model in models_to_compare.items():
    print(f"Testing {model_name}...")
    
    # 속도 측정
    start_time = time.time()
    embeddings = [model.embed_query(q) for q in test_queries]
    elapsed = time.time() - start_time
    
    # 결과 저장
    results[model_name] = {
        "avg_time_per_query": elapsed / len(test_queries),
        "embedding_dimension": len(embeddings[0])
    }

# 결과 출력
for model, stats in results.items():
    print(f"{model}: {stats['avg_time_per_query']:.4f}초/쿼리, {stats['embedding_dimension']} 차원")
```

### 2.3 벡터 데이터베이스 성능 평가

```python
# 벡터 DB 성능 비교 예시 코드
import time

def benchmark_vector_db(db_name, db_instance, query, k=3):
    """벡터 데이터베이스 검색 성능 측정"""
    start_time = time.time()
    results = db_instance.similarity_search(query, k=k)
    query_time = time.time() - start_time
    
    return {
        "name": db_name,
        "query_time": query_time,
        "num_results": len(results)
    }

# 벤치마크 실행
benchmark_results = []
test_query = "머신러닝 모델 평가 방법은 무엇인가요?"

# Chroma 벤치마크
benchmark_results.append(benchmark_vector_db("Chroma", chroma_db, test_query))

# FAISS 벤치마크
benchmark_results.append(benchmark_vector_db("FAISS", faiss_db, test_query))

# 결과 출력
for result in benchmark_results:
    print(f"{result['name']}: {result['query_time']:.4f}초, {result['num_results']} 결과")
```

## 3. 맥락 관리 강화 기법

### 3.1 장기 기억 메커니즘

대화 이력을 요약하고 필요시 검색하는 메모리 시스템 구현

```python
# 대화 요약 메모리 구현 예시
from langchain.memory import ConversationSummaryBufferMemory
from langchain.chains import ConversationChain

# 대화 요약 메모리 초기화
memory = ConversationSummaryBufferMemory(
    llm=llm,
    max_token_limit=100,
    return_messages=True
)

# 대화 체인 생성
conversation = ConversationChain(
    llm=llm,
    memory=memory,
    verbose=True
)

# 대화 진행
conversation.predict(input="안녕하세요!")
conversation.predict(input="RAG 시스템에 대해 알려주세요.")

# 요약된 메모리 확인
print(memory.moving_summary_buffer)
```

### 3.2 대화 컨텍스트와 RAG 통합

사용자 질문과 대화 히스토리를 고려한 최적의 검색 쿼리 생성

```python
# 대화 컨텍스트 기반 쿼리 최적화
def contextual_query_generator(question, chat_history):
    # 대화 이력을 문자열로 변환
    history_text = "\n".join([f"Human: {h[0]}\nAI: {h[1]}" for h in chat_history[-3:]])
    
    prompt = f"""
    이전 대화:
    {history_text}
    
    사용자 질문: {question}
    
    위 대화 맥락을 고려하여, 벡터 데이터베이스 검색에 가장 효과적인 검색 쿼리를 생성해주세요.
    쿼리는 단순히 질문을 그대로 사용하는 것이 아니라, 현재 대화 컨텍스트와 사용자의 의도를 정확히 반영해야 합니다.
    
    최적화된 쿼리:
    """
    
    optimized_query = llm.predict(prompt).strip()
    return optimized_query

# 사용 예시
chat_history = [
    ("RAG란 무엇인가요?", "RAG는 Retrieval-Augmented Generation의 약자로..."),
    ("어떤 임베딩 모델이 좋나요?", "OpenAI의 text-embedding-ada-002와 같은 모델이..."),
]
question = "이를 개선하는 방법이 있을까요?"

optimized_query = contextual_query_generator(question, chat_history)
print(f"원본 질문: {question}")
print(f"최적화된 쿼리: {optimized_query}")
```

## 4. RAG 평가 및 최적화

### 4.1 RAGAS 평가 프레임워크 활용

```python
# RAGAS를 사용한 RAG 시스템 평가
!pip install ragas

from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_relevancy,
    context_recall
)
from ragas.langchain import RagasEvaluator

# 평가 데이터셋 준비
eval_dataset = [
    {
        "question": "딥러닝과 머신러닝의 차이점은 무엇인가요?",
        "ground_truth": "머신러닝은 데이터에서 패턴을 학습하는 알고리즘이며, 딥러닝은 머신러닝의 하위 분야로 인공 신경망을 사용합니다."
    },
    # 더 많은 평가 데이터...
]

# 평가자 정의
evaluator = RagasEvaluator(
    metrics=[
        faithfulness, 
        answer_relevancy, 
        context_relevancy, 
        context_recall
    ]
)

# RAG 체인 평가
eval_results = evaluator.evaluate_queries(
    rag_chain=qa_chain,
    queries=[item["question"] for item in eval_dataset]
)

# 결과 분석
print(eval_results)
```

### 4.2 A/B 테스트 설계

다양한 RAG 설계 옵션을 체계적으로 비교하는 실험

```python
# RAG A/B 테스트 구현
import random
from collections import defaultdict

# 테스트할 RAG 변형들
rag_variants = {
    "basic": basic_rag_chain,
    "hybrid_search": hybrid_rag_chain,
    "query_expansion": query_expansion_rag_chain,
    "hyde": hyde_rag_chain
}

# 평가 질문 세트
evaluation_questions = [
    "딥러닝 모델 학습 시 과적합을 방지하는 방법은?",
    "트랜스포머 아키텍처의 주요 구성 요소는?",
    # 더 많은 질문...
]

# A/B 테스트 실행
results = defaultdict(list)

for question in evaluation_questions:
    # 각 변형에 대해 테스트
    for variant_name, variant_chain in rag_variants.items():
        response = variant_chain({"query": question})
        
        # 사용자 만족도 시뮬레이션 (실제로는 사용자 피드백으로 대체)
        # 1-5 점수 랜덤 할당 (실제 구현에서는 실제 평가 점수 사용)
        satisfaction_score = random.randint(1, 5)
        
        # 결과 저장
        results[variant_name].append({
            "question": question,
            "answer": response["result"],
            "satisfaction": satisfaction_score,
            "latency": response.get("latency", 0)  # 응답 시간
        })

# 결과 분석
for variant, data in results.items():
    avg_satisfaction = sum(item["satisfaction"] for item in data) / len(data)
    avg_latency = sum(item["latency"] for item in data) / len(data)
    print(f"{variant}: 만족도={avg_satisfaction:.2f}/5, 응답시간={avg_latency:.2f}초")
```

## 5. 산업 적용 사례 

### 5.1 내부 지식베이스 챗봇

```python
# 내부 문서 처리 및 챗봇 구현
from langchain.document_loaders import DirectoryLoader, TextLoader, PDFMinerLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import ConversationalRetrievalChain

# 다양한 형식의 문서 로더 설정
loaders = {
    "*.txt": DirectoryLoader("./company_docs", glob="**/*.txt", loader_cls=TextLoader),
    "*.pdf": DirectoryLoader("./company_docs", glob="**/*.pdf", loader_cls=PDFMinerLoader),
    # 다른 파일 형식 추가
}

# 모든 문서 로드
documents = []
for loader in loaders.values():
    documents.extend(loader.load())

# 문서 분할
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", ".", " ", ""]
)
chunks = text_splitter.split_documents(documents)

# 벡터 저장소 생성
vectorstore = Chroma.from_documents(chunks, embeddings)

# 대화형 RAG 체인 구현
qa_chain = ConversationalRetrievalChain.from_llm(
    llm=ChatOpenAI(temperature=0),
    retriever=vectorstore.as_retriever(search_kwargs={"k": 3}),
    return_source_documents=True,
    verbose=True
)

# 대화 예시
chat_history = []
query = "우리 회사의 휴가 정책은 어떻게 되나요?"
result = qa_chain({"question": query, "chat_history": chat_history})
chat_history.append((query, result["answer"]))

print(f"질문: {query}")
print(f"답변: {result['answer']}")
print("출처 문서:")
for doc in result["source_documents"]:
    print(f"- {doc.metadata.get('source', '알 수 없는 출처')}")
```

## 6. 실습 과제

1. 하이브리드 검색 구현하기: BM25와 임베딩 검색을 결합한 RAG 시스템을 구축하고 성능을 평가하세요.
2. 쿼리 최적화: HyDE 방식을 구현하여 기본 RAG와 성능을 비교하세요.
3. 대화형 RAG 시스템 개발: 대화 이력을 요약하고 활용하는 챗봇을 구현하세요.
4. RAGAS 평가: 자신이 구현한 RAG 시스템을 RAGAS 프레임워크로 평가하고 개선점을 도출하세요.

## 📚 추가 자료

- [RAG Paper (Lewis et al.)](https://arxiv.org/abs/2005.11401)
- [LangChain RAG Documentation](https://python.langchain.com/docs/use_cases/question_answering/)
- [Advanced RAG Techniques Blog](https://towardsdatascience.com/advanced-rag-techniques-beyond-basic-retrieval-for-complex-qa-systems-ce63a312ef71)
- [RAGAS: Evaluation Framework for RAG](https://github.com/explodinggradients/ragas) 