{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "c55bc01a",
   "metadata": {},
   "source": [
    "# 고급 RAG 기법: 검색 증강 생성의 최신 기술과 구현\n",
    "\n",
    "**⬅️ 이전 시간: [Part 13: 생성형 AI 및 AI 에이전트 심화](./part_13_generative_ai.md)**\n",
    "**➡️ 다음 시간: [AI 에이전트 심화](./part_13.2_advanced_ai_agents.md)**\n",
    "\n",
    "## 📋 학습 목표\n",
    "\n",
    "이 모듈을 통해 다음을 학습하게 됩니다:\n",
    "\n",
    "- RAG 시스템의 다양한 아키텍처 변형과 장단점을 이해합니다.\n",
    "- 검색 품질 향상을 위한 고급 기법을 구현할 수 있습니다.\n",
    "- 복잡한 질문에 정확하게 응답하는 RAG 파이프라인을 최적화할 수 있습니다.\n",
    "- 프로덕션 환경에서의 RAG 성능 평가 및 모니터링 방법을 습득합니다.\n",
    "\n",
    "## 1. RAG 아키텍처의 진화\n",
    "\n",
    "### 1.1 기본 RAG vs. 고급 RAG 아키텍처\n",
    "\n",
    "**기본 RAG 아키텍처의 한계:**\n",
    "- 단순 의미 검색만으로는 복잡한 질문에 대응하기 어려움\n",
    "- 컨텍스트 길이 제한으로 인한 정보 손실\n",
    "- 연관성 높은 문서 선별의 어려움\n",
    "\n",
    "**고급 RAG 아키텍처 특징:**\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "867440d8",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 기본 RAG 구현\n",
    "from langchain.chains import RetrievalQA\n",
    "from langchain.embeddings import OpenAIEmbeddings\n",
    "from langchain.vectorstores import Chroma\n",
    "\n",
    "embeddings = OpenAIEmbeddings()\n",
    "vectorstore = Chroma(embedding_function=embeddings, persist_directory=\"./chroma_db\")\n",
    "qa_chain = RetrievalQA.from_chain_type(\n",
    "    llm=OpenAI(),\n",
    "    retriever=vectorstore.as_retriever(search_kwargs={\"k\": 3})\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "147dab3b",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "### 1.2 Hybrid Search 구현\n",
    "\n",
    "키워드 기반 검색(BM25)과 의미 기반 검색을 결합하여 검색 품질 향상\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aec52dc4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Hybrid Search 구현 예시\n",
    "from langchain.retrievers import BM25Retriever, EnsembleRetriever\n",
    "\n",
    "# 임베딩 기반 검색기\n",
    "embedding_retriever = vectorstore.as_retriever(search_kwargs={\"k\": 3})\n",
    "\n",
    "# BM25 검색기 (키워드 기반)\n",
    "bm25_retriever = BM25Retriever.from_documents(documents)\n",
    "bm25_retriever.k = 3\n",
    "\n",
    "# 앙상블 검색기\n",
    "ensemble_retriever = EnsembleRetriever(\n",
    "    retrievers=[bm25_retriever, embedding_retriever],\n",
    "    weights=[0.5, 0.5]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2c8a6561",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "### 1.3 멀티 벡터 검색(Multi-Vector Retrieval)\n",
    "\n",
    "문서를 다양한 관점에서 임베딩하여 검색 효율성 향상\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6bae55c4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 멀티 벡터 검색 구현 예시\n",
    "from langchain.schema import Document\n",
    "from langchain.retrievers.multi_vector import MultiVectorRetriever\n",
    "from langchain.storage import InMemoryStore\n",
    "\n",
    "# 문서 저장소\n",
    "store = InMemoryStore()\n",
    "id_key = \"doc_id\"\n",
    "\n",
    "# 각 문서를 여러 청크로 분할하고 각 청크에 원본 문서 ID 연결\n",
    "for i, doc in enumerate(documents):\n",
    "    # 문서별 고유 ID 생성\n",
    "    doc_id = f\"doc_{i}\"\n",
    "    # 문서를 청크로 분할\n",
    "    chunks = text_splitter.split_text(doc.page_content)\n",
    "    \n",
    "    # 각 청크에 원본 문서 ID 메타데이터 추가\n",
    "    for j, chunk in enumerate(chunks):\n",
    "        _id = f\"{doc_id}_chunk_{j}\"\n",
    "        store.mset([(\n",
    "            _id, \n",
    "            {\"id\": _id, \"doc_id\": doc_id, \"text\": chunk, \"metadata\": doc.metadata}\n",
    "        )])\n",
    "\n",
    "# 멀티 벡터 검색기 생성\n",
    "retriever = MultiVectorRetriever(\n",
    "    vectorstore=vectorstore,\n",
    "    docstore=store,\n",
    "    id_key=id_key,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f5e8b295",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "## 2. 고급 RAG 파이프라인 최적화\n",
    "\n",
    "### 2.1 쿼리 최적화 기법\n",
    "\n",
    "#### 2.1.1 Query Expansion\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ad2f5ccd",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 쿼리 확장 구현 예시\n",
    "from langchain.retrievers import ContextualCompressionRetriever\n",
    "from langchain.retrievers.document_compressors import LLMChainExtractor\n",
    "\n",
    "# 원본 검색기\n",
    "base_retriever = vectorstore.as_retriever()\n",
    "\n",
    "# LLM을 사용한 쿼리 확장\n",
    "def expand_query(query):\n",
    "    prompt = f\"\"\"\n",
    "    원본 질문: {query}\n",
    "    \n",
    "    이 질문과 관련된 키워드와 관련 질문을 3개 생성해주세요. \n",
    "    결과는 쉼표로 구분된 목록으로 반환하세요.\n",
    "    \"\"\"\n",
    "    response = llm.predict(prompt)\n",
    "    expanded_terms = [term.strip() for term in response.split(',')]\n",
    "    expanded_query = query + \" \" + \" \".join(expanded_terms)\n",
    "    return expanded_query\n",
    "\n",
    "# 확장된 쿼리를 사용한 검색\n",
    "def retrieval_with_query_expansion(query):\n",
    "    expanded_query = expand_query(query)\n",
    "    print(f\"확장된 쿼리: {expanded_query}\")\n",
    "    return base_retriever.get_relevant_documents(expanded_query)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "832ad1ad",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "#### 2.1.2 HyDE (Hypothetical Document Embeddings)\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8b4e8527",
   "metadata": {},
   "outputs": [],
   "source": [
    "# HyDE 구현 예시\n",
    "def hyde_retrieval(query):\n",
    "    # 1. 가상 문서 생성\n",
    "    hypothetical_doc_prompt = f\"\"\"\n",
    "    다음 질문에 대한 이상적인 답변 문서를 생성해주세요:\n",
    "    {query}\n",
    "    \n",
    "    답변 문서:\n",
    "    \"\"\"\n",
    "    \n",
    "    hypothetical_doc = llm.predict(hypothetical_doc_prompt)\n",
    "    \n",
    "    # 2. 가상 문서를 임베딩\n",
    "    hyde_embedding = embeddings.embed_query(hypothetical_doc)\n",
    "    \n",
    "    # 3. 유사 문서 검색\n",
    "    docs = vectorstore.similarity_search_by_vector(hyde_embedding, k=3)\n",
    "    \n",
    "    return docs"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b2c44541",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "### 2.2 임베딩 모델 성능 비교\n",
    "\n",
    "다양한 임베딩 모델의 성능과 비용 효율성 비교\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2ed01302",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 여러 임베딩 모델 성능 비교\n",
    "from langchain.embeddings import OpenAIEmbeddings, HuggingFaceEmbeddings\n",
    "import time\n",
    "\n",
    "models_to_compare = {\n",
    "    \"OpenAI Ada 002\": OpenAIEmbeddings(),\n",
    "    \"BAAI/bge-large-en-v1.5\": HuggingFaceEmbeddings(model_name=\"BAAI/bge-large-en-v1.5\"),\n",
    "    \"Cohere\": CohereEmbeddings(),\n",
    "    \"sentence-transformers/all-MiniLM-L6-v2\": HuggingFaceEmbeddings(\n",
    "        model_name=\"sentence-transformers/all-MiniLM-L6-v2\"\n",
    "    )\n",
    "}\n",
    "\n",
    "# 테스트 쿼리 세트\n",
    "test_queries = [\"인공지능의 윤리적 문제는 무엇인가요?\", \"파이썬에서 리스트와 튜플의 차이점은?\"]\n",
    "\n",
    "results = {}\n",
    "\n",
    "for model_name, model in models_to_compare.items():\n",
    "    print(f\"Testing {model_name}...\")\n",
    "    \n",
    "    # 속도 측정\n",
    "    start_time = time.time()\n",
    "    embeddings = [model.embed_query(q) for q in test_queries]\n",
    "    elapsed = time.time() - start_time\n",
    "    \n",
    "    # 결과 저장\n",
    "    results[model_name] = {\n",
    "        \"avg_time_per_query\": elapsed / len(test_queries),\n",
    "        \"embedding_dimension\": len(embeddings[0])\n",
    "    }\n",
    "\n",
    "# 결과 출력\n",
    "for model, stats in results.items():\n",
    "    print(f\"{model}: {stats['avg_time_per_query']:.4f}초/쿼리, {stats['embedding_dimension']} 차원\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8ad7df4a",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "### 2.3 벡터 데이터베이스 성능 평가\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7204e505",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 벡터 DB 성능 비교 예시 코드\n",
    "import time\n",
    "\n",
    "def benchmark_vector_db(db_name, db_instance, query, k=3):\n",
    "    \"\"\"벡터 데이터베이스 검색 성능 측정\"\"\"\n",
    "    start_time = time.time()\n",
    "    results = db_instance.similarity_search(query, k=k)\n",
    "    query_time = time.time() - start_time\n",
    "    \n",
    "    return {\n",
    "        \"name\": db_name,\n",
    "        \"query_time\": query_time,\n",
    "        \"num_results\": len(results)\n",
    "    }\n",
    "\n",
    "# 벤치마크 실행\n",
    "benchmark_results = []\n",
    "test_query = \"머신러닝 모델 평가 방법은 무엇인가요?\"\n",
    "\n",
    "# Chroma 벤치마크\n",
    "benchmark_results.append(benchmark_vector_db(\"Chroma\", chroma_db, test_query))\n",
    "\n",
    "# FAISS 벤치마크\n",
    "benchmark_results.append(benchmark_vector_db(\"FAISS\", faiss_db, test_query))\n",
    "\n",
    "# 결과 출력\n",
    "for result in benchmark_results:\n",
    "    print(f\"{result['name']}: {result['query_time']:.4f}초, {result['num_results']} 결과\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "489ece17",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "## 3. 맥락 관리 강화 기법\n",
    "\n",
    "### 3.1 장기 기억 메커니즘\n",
    "\n",
    "대화 이력을 요약하고 필요시 검색하는 메모리 시스템 구현\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "baefb474",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 대화 요약 메모리 구현 예시\n",
    "from langchain.memory import ConversationSummaryBufferMemory\n",
    "from langchain.chains import ConversationChain\n",
    "\n",
    "# 대화 요약 메모리 초기화\n",
    "memory = ConversationSummaryBufferMemory(\n",
    "    llm=llm,\n",
    "    max_token_limit=100,\n",
    "    return_messages=True\n",
    ")\n",
    "\n",
    "# 대화 체인 생성\n",
    "conversation = ConversationChain(\n",
    "    llm=llm,\n",
    "    memory=memory,\n",
    "    verbose=True\n",
    ")\n",
    "\n",
    "# 대화 진행\n",
    "conversation.predict(input=\"안녕하세요!\")\n",
    "conversation.predict(input=\"RAG 시스템에 대해 알려주세요.\")\n",
    "\n",
    "# 요약된 메모리 확인\n",
    "print(memory.moving_summary_buffer)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cf13a3d3",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "### 3.2 대화 컨텍스트와 RAG 통합\n",
    "\n",
    "사용자 질문과 대화 히스토리를 고려한 최적의 검색 쿼리 생성\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "864fa919",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 대화 컨텍스트 기반 쿼리 최적화\n",
    "def contextual_query_generator(question, chat_history):\n",
    "    # 대화 이력을 문자열로 변환\n",
    "    history_text = \"\\n\".join([f\"Human: {h[0]}\\nAI: {h[1]}\" for h in chat_history[-3:]])\n",
    "    \n",
    "    prompt = f\"\"\"\n",
    "    이전 대화:\n",
    "    {history_text}\n",
    "    \n",
    "    사용자 질문: {question}\n",
    "    \n",
    "    위 대화 맥락을 고려하여, 벡터 데이터베이스 검색에 가장 효과적인 검색 쿼리를 생성해주세요.\n",
    "    쿼리는 단순히 질문을 그대로 사용하는 것이 아니라, 현재 대화 컨텍스트와 사용자의 의도를 정확히 반영해야 합니다.\n",
    "    \n",
    "    최적화된 쿼리:\n",
    "    \"\"\"\n",
    "    \n",
    "    optimized_query = llm.predict(prompt).strip()\n",
    "    return optimized_query\n",
    "\n",
    "# 사용 예시\n",
    "chat_history = [\n",
    "    (\"RAG란 무엇인가요?\", \"RAG는 Retrieval-Augmented Generation의 약자로...\"),\n",
    "    (\"어떤 임베딩 모델이 좋나요?\", \"OpenAI의 text-embedding-ada-002와 같은 모델이...\"),\n",
    "]\n",
    "question = \"이를 개선하는 방법이 있을까요?\"\n",
    "\n",
    "optimized_query = contextual_query_generator(question, chat_history)\n",
    "print(f\"원본 질문: {question}\")\n",
    "print(f\"최적화된 쿼리: {optimized_query}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a2b57e98",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "## 4. RAG 평가 및 최적화\n",
    "\n",
    "### 4.1 RAGAS 평가 프레임워크 활용\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "42f56f9c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# RAGAS를 사용한 RAG 시스템 평가\n",
    "!pip install ragas\n",
    "\n",
    "from ragas.metrics import (\n",
    "    faithfulness,\n",
    "    answer_relevancy,\n",
    "    context_relevancy,\n",
    "    context_recall\n",
    ")\n",
    "from ragas.langchain import RagasEvaluator\n",
    "\n",
    "# 평가 데이터셋 준비\n",
    "eval_dataset = [\n",
    "    {\n",
    "        \"question\": \"딥러닝과 머신러닝의 차이점은 무엇인가요?\",\n",
    "        \"ground_truth\": \"머신러닝은 데이터에서 패턴을 학습하는 알고리즘이며, 딥러닝은 머신러닝의 하위 분야로 인공 신경망을 사용합니다.\"\n",
    "    },\n",
    "    # 더 많은 평가 데이터...\n",
    "]\n",
    "\n",
    "# 평가자 정의\n",
    "evaluator = RagasEvaluator(\n",
    "    metrics=[\n",
    "        faithfulness, \n",
    "        answer_relevancy, \n",
    "        context_relevancy, \n",
    "        context_recall\n",
    "    ]\n",
    ")\n",
    "\n",
    "# RAG 체인 평가\n",
    "eval_results = evaluator.evaluate_queries(\n",
    "    rag_chain=qa_chain,\n",
    "    queries=[item[\"question\"] for item in eval_dataset]\n",
    ")\n",
    "\n",
    "# 결과 분석\n",
    "print(eval_results)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f22e40b",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "### 4.2 A/B 테스트 설계\n",
    "\n",
    "다양한 RAG 설계 옵션을 체계적으로 비교하는 실험\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c71338fc",
   "metadata": {},
   "outputs": [],
   "source": [
    "# RAG A/B 테스트 구현\n",
    "import random\n",
    "from collections import defaultdict\n",
    "\n",
    "# 테스트할 RAG 변형들\n",
    "rag_variants = {\n",
    "    \"basic\": basic_rag_chain,\n",
    "    \"hybrid_search\": hybrid_rag_chain,\n",
    "    \"query_expansion\": query_expansion_rag_chain,\n",
    "    \"hyde\": hyde_rag_chain\n",
    "}\n",
    "\n",
    "# 평가 질문 세트\n",
    "evaluation_questions = [\n",
    "    \"딥러닝 모델 학습 시 과적합을 방지하는 방법은?\",\n",
    "    \"트랜스포머 아키텍처의 주요 구성 요소는?\",\n",
    "    # 더 많은 질문...\n",
    "]\n",
    "\n",
    "# A/B 테스트 실행\n",
    "results = defaultdict(list)\n",
    "\n",
    "for question in evaluation_questions:\n",
    "    # 각 변형에 대해 테스트\n",
    "    for variant_name, variant_chain in rag_variants.items():\n",
    "        response = variant_chain({\"query\": question})\n",
    "        \n",
    "        # 사용자 만족도 시뮬레이션 (실제로는 사용자 피드백으로 대체)\n",
    "        # 1-5 점수 랜덤 할당 (실제 구현에서는 실제 평가 점수 사용)\n",
    "        satisfaction_score = random.randint(1, 5)\n",
    "        \n",
    "        # 결과 저장\n",
    "        results[variant_name].append({\n",
    "            \"question\": question,\n",
    "            \"answer\": response[\"result\"],\n",
    "            \"satisfaction\": satisfaction_score,\n",
    "            \"latency\": response.get(\"latency\", 0)  # 응답 시간\n",
    "        })\n",
    "\n",
    "# 결과 분석\n",
    "for variant, data in results.items():\n",
    "    avg_satisfaction = sum(item[\"satisfaction\"] for item in data) / len(data)\n",
    "    avg_latency = sum(item[\"latency\"] for item in data) / len(data)\n",
    "    print(f\"{variant}: 만족도={avg_satisfaction:.2f}/5, 응답시간={avg_latency:.2f}초\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e44ae634",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "## 5. 산업 적용 사례 \n",
    "\n",
    "### 5.1 내부 지식베이스 챗봇\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f96527f7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 내부 문서 처리 및 챗봇 구현\n",
    "from langchain.document_loaders import DirectoryLoader, TextLoader, PDFMinerLoader\n",
    "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
    "from langchain.chains import ConversationalRetrievalChain\n",
    "\n",
    "# 다양한 형식의 문서 로더 설정\n",
    "loaders = {\n",
    "    \"*.txt\": DirectoryLoader(\"./company_docs\", glob=\"**/*.txt\", loader_cls=TextLoader),\n",
    "    \"*.pdf\": DirectoryLoader(\"./company_docs\", glob=\"**/*.pdf\", loader_cls=PDFMinerLoader),\n",
    "    # 다른 파일 형식 추가\n",
    "}\n",
    "\n",
    "# 모든 문서 로드\n",
    "documents = []\n",
    "for loader in loaders.values():\n",
    "    documents.extend(loader.load())\n",
    "\n",
    "# 문서 분할\n",
    "text_splitter = RecursiveCharacterTextSplitter(\n",
    "    chunk_size=1000,\n",
    "    chunk_overlap=200,\n",
    "    separators=[\"\\n\\n\", \"\\n\", \".\", \" \", \"\"]\n",
    ")\n",
    "chunks = text_splitter.split_documents(documents)\n",
    "\n",
    "# 벡터 저장소 생성\n",
    "vectorstore = Chroma.from_documents(chunks, embeddings)\n",
    "\n",
    "# 대화형 RAG 체인 구현\n",
    "qa_chain = ConversationalRetrievalChain.from_llm(\n",
    "    llm=ChatOpenAI(temperature=0),\n",
    "    retriever=vectorstore.as_retriever(search_kwargs={\"k\": 3}),\n",
    "    return_source_documents=True,\n",
    "    verbose=True\n",
    ")\n",
    "\n",
    "# 대화 예시\n",
    "chat_history = []\n",
    "query = \"우리 회사의 휴가 정책은 어떻게 되나요?\"\n",
    "result = qa_chain({\"question\": query, \"chat_history\": chat_history})\n",
    "chat_history.append((query, result[\"answer\"]))\n",
    "\n",
    "print(f\"질문: {query}\")\n",
    "print(f\"답변: {result['answer']}\")\n",
    "print(\"출처 문서:\")\n",
    "for doc in result[\"source_documents\"]:\n",
    "    print(f\"- {doc.metadata.get('source', '알 수 없는 출처')}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "294289f8",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "## 6. 실습 과제\n",
    "\n",
    "1. 하이브리드 검색 구현하기: BM25와 임베딩 검색을 결합한 RAG 시스템을 구축하고 성능을 평가하세요.\n",
    "2. 쿼리 최적화: HyDE 방식을 구현하여 기본 RAG와 성능을 비교하세요.\n",
    "3. 대화형 RAG 시스템 개발: 대화 이력을 요약하고 활용하는 챗봇을 구현하세요.\n",
    "4. RAGAS 평가: 자신이 구현한 RAG 시스템을 RAGAS 프레임워크로 평가하고 개선점을 도출하세요.\n",
    "\n",
    "## 📚 추가 자료\n",
    "\n",
    "- [RAG Paper (Lewis et al.)](https://arxiv.org/abs/2005.11401)\n",
    "- [LangChain RAG Documentation](https://python.langchain.com/docs/use_cases/question_answering/)\n",
    "- [Advanced RAG Techniques Blog](https://towardsdatascience.com/advanced-rag-techniques-beyond-basic-retrieval-for-complex-qa-systems-ce63a312ef71)\n",
    "- [RAGAS: Evaluation Framework for RAG](https://github.com/explodinggradients/ragas) "
   ]
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 5
}
