본문 바로가기
AI

RAG 란??

by journeylabs 2025. 12. 18.
728x90
반응형

목차

     

    RAG(Retrieval-Augmented Generation) 란 ??

    한 줄 요약: RAG는 LLM이 답변하기 전에 관련 문서를 검색해서 가져오고, 그 문서를 근거로만 답변하게 만드는 아키텍처입니다.


    1. RAG란 무엇인가?

    1.1 기본 개념

    RAG(Retrieval-Augmented Generation)는 두 가지 핵심 컴포넌트로 구성됩니다:

     

    🔍 Retriever (검색기)

    • 사용자 질문과 관련된 문서나 정보 조각(chunk)을 찾아옵니다
    • 벡터 검색, 키워드 검색, 하이브리드 검색 등 다양한 방식이 있습니다

    ✍️ Generator (생성기)

    • 검색된 문서를 근거로 최종 답변을 생성합니다
    • 대부분 LLM(Large Language Model)이 이 역할을 담당합니다

    1.2 간단한 비유

    전통적인 LLM 사용

    • 학생에게 교과서 없이 시험을 보게 하는 것과 같습니다
    • 기억에만 의존하므로 틀린 답이나 헷갈린 답을 할 가능성이 높습니다

    RAG 시스템

    • 학생에게 관련 자료를 찾아서 보고 답하게 하는 오픈북 시험과 같습니다
    • 근거 자료를 참고하므로 정확도가 높고, 출처도 명확합니다

    2. 왜 RAG가 필요한가?

    2.1 환각(Hallucination) 문제 해결

    환각이란? LLM이 그럴듯하지만 사실이 아닌 정보를 자신있게 생성하는 현상입니다.

     

    예시:

    질문: "우리 회사 2024년 하계 휴가 정책은?"
    
    ❌ LLM만 사용 (환각 발생)
    답변: "일반적으로 기업들은 5일의 하계 휴가를 제공합니다..."
    → 회사마다 다른데 일반론을 답변
    
    ✅ RAG 사용
    답변: "인사팀 공지(2024.05.15)에 따르면, 올해는 7월 29일~8월 2일 
    5일간 하계 휴가입니다. [출처: 2024_하계휴가_공지.pdf]"
    → 실제 문서 기반으로 정확한 답변
    

    2.2 최신 정보 반영

    LLM은 학습 시점까지의 데이터만 알고 있습니다. RAG를 사용하면:

    • 실시간으로 업데이트되는 문서 활용 가능
    • 학습 없이도 최신 정보 제공 가능
    • 회사 내부 정보, 프로젝트별 문서 등 특수 지식 활용 가능

    2.3 출처 투명성

    RAG는 "어떤 문서를 근거로 답했는지" 명확히 알 수 있어:

    • 답변 신뢰도 향상
    • 감사(Audit) 및 컴플라이언스 대응 용이
    • 사용자가 원본 문서를 직접 확인 가능

    3. RAG의 핵심 개념

    3.1 Parametric vs Non-parametric Knowledge

    Parametric Knowledge (파라메트릭 지식)

     

    정의: 모델의 가중치(파라미터) 안에 암묵적으로 저장된 지식

    특징:

    • ✅ 빠른 추론 속도
    • ❌ 업데이트가 어려움 (재학습 필요)
    • ❌ 최신 정보 반영 불가
    • ❌ 출처 불명확

    예시:

    • "프랑스의 수도는 어디야?" → "파리입니다" (모델이 학습으로 알고 있음)

    Non-parametric Knowledge (논-파라메트릭 지식)

    정의: 외부 데이터베이스, 문서, API 등에서 검색으로 가져오는 지식

    특징:

    • ✅ 실시간 업데이트 가능
    • ✅ 출처 명확
    • ✅ 권한 관리 가능
    • ❌ 검색 시간 추가

    예시:

    • "우리 팀 올해 Q3 목표는?" → 사내 문서 검색 → "팀 목표 문서.pdf를 기반으로..."

    3.2 RAG가 해결하는 문제들

    문제 전통적 LLM  RAG 시스템
    최신 정보 ❌ 학습 시점까지만 ✅ 실시간 업데이트
    사내 기밀 정보 ❌ 학습 불가능/위험 ✅ 안전하게 활용
    환각 ⚠️ 높은 위험 ✅ 크게 감소
    출처 제공 ❌ 불가능 ✅ 명확한 출처
    업데이트 비용 ❌ 재학습 필요 ✅ 문서만 업데이트

    4. RAG 아키텍처 상세

    4.1 전체 데이터 흐름

    flowchart TB
        A[사용자 질문] --> B[질문 전처리/재작성]
        B --> C[Retriever 검색기]
        C --> D[(벡터 데이터베이스)]
        D --> C
        C --> E[Top-K 문서 선택]
        E --> F[컨텍스트 패키징]
        F --> G[Generator LLM]
        G --> H[답변 + 출처]
        
        style A fill:#e1f5ff
        style H fill:#e1ffe1
        style C fill:#fff4e1
        style G fill:#ffe1f5
    

    4.2 핵심 컴포넌트 상세 설명

    🧮 Component 1: Embedder (임베딩 모델)

    역할: 텍스트를 의미를 담은 숫자 벡터로 변환

    작동 원리:

    입력: "연차 이월 규정"
         ↓ (임베딩 모델)
    출력: [0.23, -0.45, 0.67, ..., 0.12]  ← 768차원 벡터
    

    왜 필요한가?

    • 단순 키워드 매칭을 넘어 의미적 유사도 검색 가능
    • "연차 이월"과 "휴가 이월" 같이 다른 단어지만 비슷한 의미를 파악

    실무 예시:

    질문: "법인카드 분실했는데 어떻게 해야 돼?"
    
    키워드 검색: "법인카드 분실" 정확히 일치하는 문서만 찾음
    임베딩 검색: "회사 카드 도난/분실", "법인 신용카드 재발급" 등도 찾음
    

    주요 모델:

    • OpenAI: text-embedding-3-large, text-embedding-3-small
    • 오픈소스: bge-large-en-v1.5, multilingual-e5-large

    🔍 Component 2: Retriever (검색기)

     

    역할: 질문과 관련된 문서/청크를 찾아오기

     

    검색 방식 비교:

    1) Dense Retrieval (벡터 검색)

    # 의미 기반 검색
    query = "퇴사할 때 뭐 해야 돼?"
    # → "퇴직 절차", "이직 프로세스" 등 의미가 비슷한 문서 검색
    
    • ✅ 의미적 유사도가 높음
    • ❌ 정확한 용어/숫자 검색에 약함

    2) Sparse Retrieval (BM25, 키워드 검색)

    # 키워드 기반 검색
    query = "연봉 3000만원 이상 경력 5년"
    # → "3000만원", "경력 5년" 정확히 일치하는 문서 우선
    
    • ✅ 정확한 키워드/숫자 검색에 강함
    • ❌ 동의어, 의미적 유사성 파악 약함

    3) Hybrid Retrieval (권장)

    # 두 방식 조합
    검색 1단계: Dense로 의미상 관련 문서 30개 추출
    검색 2단계: Sparse로 키워드 일치도 재평가
    최종 결과: 두 점수를 가중 평균하여 Top-5 선택
    

     

    실전 파라미터:

    retriever = vectorstore.as_retriever(
        search_type="mmr",  # Maximum Marginal Relevance (중복 제거)
        search_kwargs={
            "k": 5,              # 최종 반환 문서 수
            "fetch_k": 20,       # 1차 후보 문서 수
            "lambda_mult": 0.7   # 다양성 vs 유사도 균형
        }
    )
    

     

     

    ✍️ Component 3: Generator (생성기)

     

    역할: 검색된 문서를 근거로 최종 답변 생성

    프롬프트 설계가 90%

     

    나쁜 프롬프트:

    질문: {question}
    문서: {context}
    
    답변해줘.
    

    → 모델이 문서를 무시하고 자기 지식으로 답할 수 있음

     

    좋은 프롬프트:

    당신은 회사 규정 전문가입니다.
    
    규칙:
    1. 반드시 제공된 [문서]만을 근거로 답변하세요
    2. 문서에 없는 내용은 "문서에서 해당 정보를 찾을 수 없습니다"라고 답하세요
    3. 답변 형식:
       - 핵심 답변 (2-3문장)
       - 근거: [문서명, 페이지/섹션]
       - 추가 참고사항 (있는 경우)
    
    [문서]
    {context}
    
    [질문]
    {question}
    
    [답변]
    

    주요 파라미터:

    llm = ChatOpenAI(
        model="gpt-4o-mini",
        temperature=0,        # 0 = 결정론적, 1 = 창의적
        max_tokens=500,       # 답변 길이 제한
    )
    

     

    4.3 문서 청킹(Chunking) 전략

    청킹이란? 긴 문서를 작은 조각으로 나누는 과정

    왜 필요한가?

    • LLM 컨텍스트 길이 제한 (GPT-4: ~128K 토큰)
    • 관련 없는 부분 제외하고 핵심만 전달
    • 검색 정확도 향상

    청킹 방법 비교

    1) 고정 길이 청킹

    # 800자마다 자르기
    chunk_size = 800
    overlap = 150  # 앞뒤 문맥 유지
    
    • ✅ 간단함
    • ❌ 문장/문단 중간에서 잘릴 수 있음

    2) 재귀적 청킹 (권장)

    RecursiveCharacterTextSplitter(
        chunk_size=800,
        chunk_overlap=150,
        separators=["\n\n", "\n", ". ", " ", ""]  # 우선순위대로 자르기
    )
    
    • ✅ 자연스러운 분할
    • ✅ 문맥 보존

    3) 의미 기반 청킹

    # 제목, 섹션, 리스트 단위로 자르기
    SemanticChunker(
        breakpoint_threshold_type="percentile",
        breakpoint_threshold_amount=85
    )
    
    • ✅ 논리적 구조 유지
    • ❌ 처리 시간 증가

    실전 청킹 팁

    문서 유형별 전략:

    문서 유형 권장 chunk_size 권장 방법 이유

    문서 유형  권장 chunk_size 권장 방법  이유
    FAQ 200-400 질문-답변 쌍 단위 각 QA는 독립적
    절차/가이드 600-1000 섹션/스텝 단위 순서가 중요
    계약서/규정 800-1200 조항 단위 법적 맥락 보존
    기술 문서 500-800 코드 블록 유지 예제 완전성

     

    메타데이터 추가 (중요!)

    for chunk in chunks:
        chunk.metadata = {
            "source": "인사규정.pdf",
            "page": 3,
            "section": "제2장 휴가",
            "last_updated": "2024-06-15",
            "department": "인사팀",
            "access_level": "전직원"  # 권한 관리용
        }
    

    5. 실패 유형과 해결책

    5.1 검색 실패 (Retrieval Failure)

    문제: 관련 문서가 있는데 검색에서 못 찾아옴

    케이스 1: 용어 불일치

    ❌ 실패 케이스:
    질문: "법인카드 분실 시 처리 절차는?"
    문서: "회사 신용카드 도난 및 분실 대응 매뉴얼"
    → "법인카드"와 "회사 신용카드" 매칭 실패
    
    ✅ 해결책:
    1. 하이브리드 검색 (Dense + BM25)
    2. 동의어 사전 구축
    3. 질문 리라이팅: "법인카드" → "회사 카드, 법인 신용카드" 등 확장
    

    케이스 2: 청킹 오류

    ❌ 실패 케이스:
    원본 문서: 
    "법인카드 분실 절차:
    1. 즉시 카드사 분실 신고 (1588-xxxx)
    2. 법무팀 이메일로 분실 보고
    3. 재발급 신청서 제출"
    
    나쁜 청킹:
    chunk1: "법인카드 분실 절차: 1. 즉시 카드사"
    chunk2: "분실 신고 (1588-xxxx) 2. 법무팀"
    → 각 청크가 불완전
    
    ✅ 해결책:
    - 리스트 구조 인식하는 청킹 사용
    - chunk_size 늘리기 (절차는 통째로 유지)
    - 제목+내용 함께 청킹
    

    케이스 3: 메타데이터 필터링 오류

    ❌ 실패 케이스:
    질문: "우리 팀 2024년 Q3 목표는?"
    검색 결과: 다른 팀의 목표 문서들 반환
    
    ✅ 해결책:
    retriever = vectorstore.as_retriever(
        search_kwargs={
            "k": 5,
            "filter": {
                "team": "사용자의 팀명",
                "year": 2024,
                "quarter": "Q3"
            }
        }
    )
    

     

    5.2 생성 실패 (Generation Failure)

    문제: 문서는 정확히 가져왔는데 LLM이 잘못 답변

    케이스 1: 문서 무시하고 자기 지식으로 답변

    ❌ 실패 케이스:
    문서: "우리 회사는 연차를 최대 2년까지 이월 가능"
    질문: "연차 이월 규정은?"
    LLM 답변: "일반적으로 연차는 1년 이월이 가능합니다"
    → 문서 내용 무시하고 일반 상식으로 답변
    
    ✅ 해결책:
    시스템 프롬프트에 명시:
    "당신의 일반 지식은 무시하세요. 오직 제공된 [문서]만 근거로 답하세요.
    문서와 당신의 지식이 충돌하면, 반드시 문서를 우선하세요."
    

    케이스 2: 환각 (문서에 없는 내용 추가)

    ❌ 실패 케이스:
    문서: "법인카드 분실 시 카드사에 신고하세요"
    질문: "법인카드 잃어버렸어요"
    LLM 답변: "카드사 신고 후 경찰서에 분실 신고도 필수입니다"
    → "경찰서 신고"는 문서에 없는 내용
    
    ✅ 해결책:
    1. temperature=0 (무작위성 제거)
    2. 답변 형식 강제:
       """
       답변 형식:
       1. 핵심 답변
       2. 근거: [문서의 정확한 인용문]
       3. 확신도: (높음/중간/낮음)
       
       문서에 명시되지 않은 내용은 절대 추가하지 마세요.
       """
    

    케이스 3: 예외 조건 누락

    ❌ 실패 케이스:
    문서: "연차는 2년 이월 가능. 단, 계약직은 이월 불가"
    질문: "계약직도 연차 이월 되나요?"
    LLM 답변: "네, 2년까지 이월 가능합니다"
    → 예외 조건 놓침
    
    ✅ 해결책:
    프롬프트에 추가:
    "문서의 '단,', '예외', '제외', '다만' 등의 조건을 반드시 확인하고
    질문이 예외 케이스에 해당하는지 먼저 판단하세요."
    

     

    5.3 종합 디버깅 체크리스트

    단계 체크 항목 해결 방법
    검색 Top-K에 정답 문서가 있나? k 늘리기, 하이브리드 검색
      검색된 문서가 관련있나? 쿼리 리라이팅, 임베딩 모델 변경
      문서 순서가 적절한가? Re-ranking 추가
    생성 LLM이 문서를 보고 있나? 프롬프트에 문서 강조
      환각이 발생하나? temperature↓, 형식 강제
      예외 처리를 하나? 조건문 명시 프롬프트

    6. 실습: LangChain으로 RAG 구축

    6.1 환경 설정

    # 필수 패키지 설치
    pip install langchain langchain-community langchain-openai
    pip install chromadb pypdf python-dotenv
    
    # 또는 한 번에
    pip install -U langchain langchain-community langchain-openai chromadb pypdf python-dotenv
    

    6.2 기본 RAG 파이프라인

    import os
    from dotenv import load_dotenv
    from langchain_openai import OpenAIEmbeddings, ChatOpenAI
    from langchain_community.document_loaders import PyPDFLoader, TextLoader
    from langchain_text_splitters import RecursiveCharacterTextSplitter
    from langchain_community.vectorstores import Chroma
    from langchain.chains import RetrievalQA
    from langchain.prompts import PromptTemplate
    
    # 환경 변수 로드
    load_dotenv()
    os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY")
    
    # ========== 1단계: 문서 로드 ==========
    print("📂 문서 로딩 중...")
    documents = []
    
    # PDF 파일 로드
    pdf_loader = PyPDFLoader("data/company_handbook.pdf")
    documents.extend(pdf_loader.load())
    
    # 텍스트 파일 로드
    txt_loader = TextLoader("data/faq.txt", encoding="utf-8")
    documents.extend(txt_loader.load())
    
    print(f"✅ 총 {len(documents)}개 문서 로드 완료")
    
    # ========== 2단계: 문서 청킹 ==========
    print("✂️ 문서 청킹 중...")
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=800,           # 청크 크기
        chunk_overlap=150,        # 겹치는 부분 (문맥 유지)
        length_function=len,
        separators=["\n\n", "\n", ". ", " ", ""]  # 우선순위대로 분리
    )
    
    chunks = text_splitter.split_documents(documents)
    print(f"✅ {len(chunks)}개 청크 생성 완료")
    
    # 청크 예시 출력
    print(f"📄 청크 샘플:\n{chunks[0].page_content[:200]}...\n")
    
    # ========== 3단계: 임베딩 + 벡터 DB 생성 ==========
    print("🔢 벡터 임베딩 생성 중...")
    embeddings = OpenAIEmbeddings(
        model="text-embedding-3-large"  # 또는 text-embedding-3-small
    )
    
    vectorstore = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory="./chroma_db"  # 로컬에 저장
    )
    print("✅ 벡터 DB 생성 완료\n")
    
    # ========== 4단계: Retriever 설정 ==========
    retriever = vectorstore.as_retriever(
        search_type="mmr",  # Maximum Marginal Relevance (중복 제거)
        search_kwargs={
            "k": 5,              # 최종 반환 문서 수
            "fetch_k": 20,       # 1차 후보 문서 수
        }
    )
    
    # ========== 5단계: 프롬프트 템플릿 (중요!) ==========
    prompt_template = """당신은 회사 규정 전문 AI 어시스턴트입니다.
    
    규칙:
    1. 반드시 아래 [문서]만을 근거로 답변하세요
    2. 문서에 없는 내용은 "제공된 문서에서 해당 정보를 찾을 수 없습니다"라고 답하세요
    3. 답변 시 출처(문서명, 페이지)를 함께 제시하세요
    4. 예외 조건("단,", "다만", "제외")을 반드시 확인하세요
    
    [문서]
    {context}
    
    [질문]
    {question}
    
    [답변]"""
    
    PROMPT = PromptTemplate(
        template=prompt_template,
        input_variables=["context", "question"]
    )
    
    # ========== 6단계: LLM + RAG 체인 구성 ==========
    llm = ChatOpenAI(
        model="gpt-4o-mini",     # 또는 gpt-4o
        temperature=0            # 일관된 답변
    )
    
    qa_chain = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",      # 모든 문서를 한 번에 전달
        retriever=retriever,
        return_source_documents=True,  # 출처 문서 반환
        chain_type_kwargs={"prompt": PROMPT}
    )
    
    # ========== 7단계: 질의응답 ==========
    def ask_question(question):
        print(f"\n💬 질문: {question}")
        print("-" * 80)
        
        result = qa_chain.invoke({"query": question})
        
        # 답변 출력
        print(f"🤖 답변:\n{result['result']}\n")
        
        # 출처 문서 출력
        print("📚 참조한 문서:")
        for i, doc in enumerate(result['source_documents'], 1):
            source = doc.metadata.get('source', '알 수 없음')
            page = doc.metadata.get('page', '?')
            print(f"  {i}. {source} (페이지 {page})")
            print(f"     내용: {doc.page_content[:100]}...\n")
    
    # ========== 테스트 질문들 ==========
    questions = [
        "연차 이월 규정이 어떻게 돼?",
        "법인카드 분실하면 어떻게 해야 돼?",
        "재택근무 신청 방법은?",
    ]
    
    for q in questions:
        ask_question(q)
        print("=" * 80)
    

    6.3 고급 기능: 하이브리드 검색

    from langchain.retrievers import EnsembleRetriever
    from langchain_community.retrievers import BM25Retriever
    
    # Dense 검색 (벡터)
    dense_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
    
    # Sparse 검색 (BM25)
    bm25_retriever = BM25Retriever.from_documents(chunks)
    bm25_retriever.k = 10
    
    # 하이브리드 (50:50 가중치)
    hybrid_retriever = EnsembleRetriever(
        retrievers=[dense_retriever, bm25_retriever],
        weights=[0.5, 0.5]
    )
    
    # 하이브리드 retriever 사용
    qa_chain_hybrid = RetrievalQA.from_chain_type(
        llm=llm,
        retriever=hybrid_retriever,  # 기존 retriever 대신
        return_source_documents=True
    )
    

    6.4 메타데이터 필터링

    # 메타데이터 추가 예시
    for chunk in chunks:
        chunk.metadata.update({
            "department": "HR",
            "access_level": "all_employees",
            "last_updated": "2024-06-15",
            "document_type": "policy"
        })
    
    # 필터링 검색
    filtered_retriever = vectorstore.as_retriever(
        search_kwargs={
            "k": 5,
            "filter": {
                "department": "HR",
                "access_level": "all_employees"
            }
        }
    )
    

    7. 실습: LlamaIndex로 RAG 구축

    7.1 왜 LlamaIndex인가?

    LangChain vs LlamaIndex

    특징 LangChain LlamaIndex
    학습 곡선 중간 쉬움
    유연성 높음 (다양한 체인) 중간
    RAG 최적화 범용적 RAG 특화
    문서 처리 수동 설정 많음 자동화 많음
    추천 용도 복잡한 워크플로우 빠른 RAG 프로토타입

    7.2 기본 RAG 파이프라인

    import os
    from dotenv import load_dotenv
    from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
    from llama_index.llms.openai import OpenAI
    from llama_index.embeddings.openai import OpenAIEmbedding
    
    # 환경 변수 로드
    load_dotenv()
    os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY")
    
    # ========== 1단계: 전역 설정 ==========
    Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
    Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-large")
    Settings.chunk_size = 800
    Settings.chunk_overlap = 150
    
    # ========== 2단계: 문서 로드 (폴더 전체) ==========
    print("📂 문서 로딩 중...")
    documents = SimpleDirectoryReader(
        input_dir="data/",
        required_exts=[".pdf", ".txt", ".docx"]  # 지원 형식
    ).load_data()
    
    print(f"✅ {len(documents)}개 문서 로드 완료")
    
    # ========== 3단계: 인덱스 생성 (자동 청킹+임베딩) ==========
    print("🔢 인덱스 생성 중...")
    index = VectorStoreIndex.from_documents(
        documents,
        show_progress=True  # 진행률 표시
    )
    print("✅ 인덱스 생성 완료\n")
    
    # ========== 4단계: Query Engine 생성 ==========
    query_engine = index.as_query_engine(
        similarity_top_k=5,           # Top-K 문서
        response_mode="compact",      # 응답 모드
    )
    
    # ========== 5단계: 질의응답 ==========
    def ask(question):
        print(f"💬 질문: {question}")
        print("-" * 80)
        
        response = query_engine.query(question)
        
        print(f"🤖 답변:\n{response}\n")
        
        # 출처 확인
        if response.source_nodes:
            print("📚 참조 문서:")
            for i, node in enumerate(response.source_nodes, 1):
                print(f"  {i}. {node.node.metadata.get('file_name', '알 수 없음')}")
                print(f"     점수: {node.score:.3f}")
                print(f"     내용: {node.node.text[:100]}...\n")
    
    # 테스트
    questions = [
        "연차 이월 규정은?",
        "재택근무 신청 방법 알려줘",
        "법인카드 분실 시 처리 절차는?"
    ]
    
    for q in questions:
        ask(q)
        print("=" * 80)
    

    7.3 커스텀 프롬프트

    from llama_index.core import PromptTemplate
    
    # 커스텀 프롬프트 정의
    qa_prompt_tmpl = PromptTemplate(
        "당신은 회사 규정 전문 AI입니다.\n\n"
        "규칙:\n"
        "1. 아래 [컨텍스트]만을 근거로 답변하세요\n"
        "2. 컨텍스트에 없으면 '정보를 찾을 수 없습니다'라고 하세요\n"
        "3. 출처를 명시하세요\n\n"
        "컨텍스트:\n"
        "---------------------\n"
        "{context_str}\n"
        "---------------------\n\n"
        "질문: {query_str}\n"
        "답변: "
    )
    
    # Query Engine에 적용
    query_engine = index.as_query_engine(
        text_qa_template=qa_prompt_tmpl,
        similarity_top_k=5
    )
    

    7.4 고급: 인덱스 영속화 (저장/로드)

    # 인덱스 저장
    index.storage_context.persist(persist_dir="./storage")
    print("💾 인덱스 저장 완료")
    
    # ========== 나중에 다시 로드 ==========
    from llama_index.core import StorageContext, load_index_from_storage
    
    storage_context = StorageContext.from_defaults(persist_dir="./storage")
    loaded_index = load_index_from_storage(storage_context)
    
    query_engine = loaded_index.as_query_engine(similarity_top_k=5)
    print("📂 인덱스 로드 완료")
    

    7.5 LlamaIndex의 강력한 기능들

    (1) 자동 메타데이터 추출

    from llama_index.core.extractors import TitleExtractor, KeywordExtractor
    
    # 메타데이터 자동 추출
    from llama_index.core.node_parser import SimpleNodeParser
    
    node_parser = SimpleNodeParser.from_defaults(
        chunk_size=800,
        chunk_overlap=150,
        metadata_extractor=[
            TitleExtractor(),          # 제목 추출
            KeywordExtractor(keywords=10)  # 키워드 추출
        ]
    )
    
    nodes = node_parser.get_nodes_from_documents(documents)
    index = VectorStoreIndex(nodes)
    

    (2) 쿼리 변환 (Query Transformation)

    from llama_index.core.indices.query.query_transform import HyDEQueryTransform
    
    # HyDE: 질문 → 가상 답변 생성 → 답변으로 검색
    hyde = HyDEQueryTransform(include_original=True)
    
    query_engine = index.as_query_engine(
        similarity_top_k=5,
        query_transform=hyde
    )
    
    # "연차 이월 규정?"
    # → HyDE가 먼저 가상 답변 생성
    # → 그 답변과 유사한 문서 검색
    

    8. RAG 평가하기: RAGAS

    8.1 왜 평가가 중요한가?

    RAG의 함정:

    • 개발 중에는 잘 작동하는 것 같지만...
    • 특정 질문에서 갑자기 무너짐
    • 사용자가 발견하기 전에 미리 찾아야 함

    평가 없이 배포하면:

    ❌ "이 시스템 믿을 수 없어요" (신뢰도 하락)
    ❌ "엉뚱한 답변만 해요" (사용자 이탈)
    ❌ "출처가 틀렸어요" (법적 리스크)
    

    8.2 RAGAS 핵심 메트릭 3가지

    flowchart LR
        A[사용자 질문] --> B[Retriever]
        B --> C[검색된 문서들]
        C --> D[Generator LLM]
        D --> E[최종 답변]
        
        C -.Context Recall.-> F[평가 1]
        C -.Context Precision.-> G[평가 2]
        E -.Faithfulness.-> H[평가 3]
        
        style F fill:#ffe1e1
        style G fill:#fff4e1
        style H fill:#e1ffe1
    

    메트릭 1: Context Recall (컨텍스트 재현율)

    정의: "정답을 만드는 데 필요한 정보가 검색 결과에 포함되었는가?"

    예시:

    질문: "연차는 몇 년까지 이월 가능?"
    정답 (Ground Truth): "2년까지 이월 가능"
    
    검색된 문서:
    Doc1: "연차 사용 기간은 발생일로부터 1년"
    Doc2: "미사용 연차는 최대 2년까지 이월"  ← 정답 포함!
    Doc3: "연차 사용 시 사전 승인 필요"
    
    Context Recall = 1.0 (정답 정보가 Doc2에 있음)
    

     

    낮은 경우 해결책:

    • Retriever 개선 (하이브리드 검색)
    • Top-K 늘리기
    • 청킹 전략 변경
    • 쿼리 리라이팅

    메트릭 2: Context Precision (컨텍스트 정밀도)

    정의: "검색된 문서들이 얼마나 관련성 높은가? (노이즈가 적은가?)"

    예시:

    질문: "법인카드 분실 시 처리 절차는?"
    
    검색 결과 (순서대로):
    1. "법인카드 분실 신고 및 재발급 절차" ✅ 관련 높음
    2. "개인정보 유출 시 대응 매뉴얼"     ❌ 관련 없음
    3. "법인카드 사용 가이드라인"         ⚠️ 약간 관련
    4. "출장 경비 처리 규정"             ❌ 관련 없음
    5. "법인카드 분실 사례 및 처벌"       ✅ 관련 높음
    
    Context Precision = 낮음 (노이즈가 많고, 관련 문서가 분산)
    

     

    낮은 경우 해결책:

    • Re-ranking 추가
    • Top-K 줄이기
    • 메타데이터 필터링 강화
    • 임베딩 모델 업그레이드

    메트릭 3: Faithfulness (충실도)

    정의: "생성된 답변이 검색된 문서에 근거하는가? (환각 없는가?)"

    예시:

    검색된 문서:
    "법인카드 분실 시 즉시 카드사(1588-xxxx)에 신고하세요."
    
    ❌ 낮은 Faithfulness (환각 발생):
    "법인카드 분실 시 카드사 신고 후 경찰서에도 분실 신고를 해야 합니다."
    → "경찰서 신고"는 문서에 없는 내용!
    
    ✅ 높은 Faithfulness:
    "법인카드 분실 시 즉시 카드사(1588-xxxx)에 신고하세요. 
     [출처: 법인카드 사용 규정 3.2절]"
    → 문서 내용만 사용
    

     

    낮은 경우 해결책:

    • Temperature = 0
    • 프롬프트에 "문서에만 근거" 명시
    • 답변 형식 강제 (출처 필수)
    • 모델 업그레이드 (GPT-4 등)

    8.3 RAGAS 실습 코드

    # 설치
    pip install ragas datasets
    
    from datasets import Dataset
    from ragas import evaluate
    from ragas.metrics import (
        faithfulness,
        context_recall,
        context_precision,
        answer_relevancy
    )
    
    # ========== 평가 데이터셋 준비 ==========
    # 실제로는 QA 시스템에서 자동 수집
    evaluation_data = {
        "question": [
            "연차는 몇 년까지 이월 가능?",
            "법인카드 분실 시 처리 절차는?",
            "재택근무 신청 방법은?"
        ],
        "answer": [
            "미사용 연차는 최대 2년까지 이월 가능합니다. [출처: 인사규정 12조]",
            "즉시 카드사(1588-xxxx)에 신고 후 법무팀에 이메일 보고하세요.",
            "사내 그룹웨어 → 근태관리 → 재택근무 신청서 제출하세요."
        ],
        "contexts": [
            # 각 질문에 대해 검색된 문서 조각들
            [
                "제12조 (연차 이월) 미사용 연차는 익년도로 최대 2년까지 이월할 수 있다.",
                "단, 계약직 및 인턴은 이월 대상에서 제외된다."
            ],
            [
                "법인카드 분실 시 즉시 카드사(1588-xxxx)로 분실 신고해야 한다.",
                "분실 신고 후 24시간 내 법무팀(legal@company.com)에 이메일 보고",
                "재발급 신청서는 인트라넷에서 다운로드 가능"
            ],
            [
                "재택근무는 사내 그룹웨어 근태관리 메뉴에서 신청한다.",
                "신청 시 업무 계획서 및 팀장 승인이 필요하다."
            ]
        ],
        "ground_truth": [
            # 정답 (있으면 더 정확한 평가 가능)
            "연차는 최대 2년까지 이월 가능하며, 계약직과 인턴은 제외됩니다.",
            "카드사 즉시 신고 후 24시간 내 법무팀에 이메일 보고해야 합니다.",
            "그룹웨어 근태관리에서 신청하며 팀장 승인이 필요합니다."
        ]
    }
    
    # Dataset 변환
    dataset = Dataset.from_dict(evaluation_data)
    
    # ========== RAGAS 평가 실행 ==========
    print("🔍 RAGAS 평가 시작...\n")
    
    result = evaluate(
        dataset,
        metrics=[
            faithfulness,         # 충실도
            context_recall,       # 검색 재현율
            context_precision,    # 검색 정밀도
            answer_relevancy      # 답변 관련성
        ]
    )
    
    print("=" * 80)
    print("📊 평가 결과:")
    print("=" * 80)
    print(result)
    print()
    
    # 개별 점수 확인
    print("📈 메트릭별 점수:")
    print(f"  Faithfulness (충실도):        {result['faithfulness']:.3f}")
    print(f"  Context Recall (검색 재현율):  {result['context_recall']:.3f}")
    print(f"  Context Precision (검색 정밀도): {result['context_precision']:.3f}")
    print(f"  Answer Relevancy (답변 관련성): {result['answer_relevancy']:.3f}")
    

    8.4 결과 해석 및 액션

    # 점수별 해석 가이드
    def interpret_scores(result):
        scores = {
            'faithfulness': result['faithfulness'],
            'context_recall': result['context_recall'],
            'context_precision': result['context_precision'],
            'answer_relevancy': result['answer_relevancy']
        }
        
        print("\n🎯 개선 방향:")
        print("-" * 80)
        
        # Faithfulness 낮음
        if scores['faithfulness'] < 0.7:
            print("⚠️ Faithfulness 낮음 (< 0.7) → 환각 발생!")
            print("   해결책:")
            print("   - temperature를 0으로 설정")
            print("   - 프롬프트에 '문서에만 근거' 명시")
            print("   - 답변 형식 강제 (출처 필수)")
            print()
        
        # Context Recall 낮음
        if scores['context_recall'] < 0.7:
            print("⚠️ Context Recall 낮음 (< 0.7) → 필요한 정보 못 찾음!")
            print("   해결책:")
            print("   - Top-K 늘리기 (5 → 10)")
            print("   - 하이브리드 검색 (Dense + BM25)")
            print("   - 청킹 크기 조정")
            print("   - 쿼리 리라이팅 추가")
            print()
        
        # Context Precision 낮음
        if scores['context_precision'] < 0.7:
            print("⚠️ Context Precision 낮음 (< 0.7) → 노이즈 많음!")
            print("   해결책:")
            print("   - Re-ranking 추가")
            print("   - Top-K 줄이기")
            print("   - 메타데이터 필터링")
            print("   - 임베딩 모델 업그레이드")
            print()
        
        # Answer Relevancy 낮음
        if scores['answer_relevancy'] < 0.7:
            print("⚠️ Answer Relevancy 낮음 (< 0.7) → 질문과 답변 불일치!")
            print("   해결책:")
            print("   - 프롬프트 개선 (질문 강조)")
            print("   - LLM 모델 업그레이드")
            print()
        
        # 모두 좋음
        if all(score >= 0.8 for score in scores.values()):
            print("✅ 모든 메트릭이 우수합니다! (≥ 0.8)")
            print("   현재 시스템을 프로덕션에 배포할 준비가 되었습니다.")
            print()
    
    # 실행
    interpret_scores(result)
    

    8.5 실전 평가 워크플로우

    # 1) 테스트셋 구축 (실제 사용자 질문 수집)
    test_questions = [
        "우리 회사 연차 규정이 어떻게 돼?",
        "출장 중 경비 처리는 어떻게 해?",
        # ... 50~100개 질문
    ]
    
    # 2) RAG 시스템으로 답변 생성
    results = []
    for q in test_questions:
        response = qa_chain.invoke({"query": q})
        results.append({
            "question": q,
            "answer": response['result'],
            "contexts": [doc.page_content for doc in response['source_documents']]
        })
    
    # 3) 사람이 ground_truth 레이블링 (정답 작성)
    # → CSV나 DB에 저장
    
    # 4) 주기적으로 RAGAS 평가 실행
    # → 성능 모니터링 대시보드 구축
    

    9. 실전 사용 사례

    9.1 사내 문서 기반 Q&A 챗봇

    구현 예시:

    from langchain.memory import ConversationBufferMemory
    from langchain.chains import ConversationalRetrievalChain
    
    # 대화 히스토리 관리
    memory = ConversationBufferMemory(
        memory_key="chat_history",
        return_messages=True,
        output_key="answer"
    )
    
    # 대화형 RAG
    conversational_chain = ConversationalRetrievalChain.from_llm(
        llm=llm,
        retriever=retriever,
        memory=memory,
        return_source_documents=True
    )
    
    # 연속 대화
    print(conversational_chain.invoke({"question": "연차 규정 알려줘"})["answer"])
    print(conversational_chain.invoke({"question": "그럼 이월은 가능해?"})["answer"])
    # → "그럼"이 앞 대화 맥락을 참조
    

     

    주요 기능:

    • 인사/법무/IT/보안 규정 통합 검색
    • 문서 버전 관리 (최신 규정만 검색)
    • 권한 기반 필터링 (직급별 접근 제어)
    • 출처 링크 제공 (SharePoint/Confluence)

    효과:

    • 📉 HR 팀 단순 문의 70% 감소
    • ⏱️ 직원 정보 검색 시간 80% 단축
    • 📚 온보딩 기간 50% 단축

    9.2 고객 지원 자동 응답 시스템

    아키텍처:

    flowchart LR
        A[고객 문의] --> B{의도 분류}
        B -->|FAQ| C[RAG 검색]
        B -->|복잡| D[상담사 연결]
        C --> E[자동 답변]
        E --> F{고객 만족?}
        F -->|Yes| G[티켓 종료]
        F -->|No| D
    

     

    코드 예시: 의도 분류 + RAG

    def classify_intent(question):
        """질문 복잡도 분류"""
        prompt = f"""
        이 질문이 FAQ로 답변 가능한지 판단하세요.
        
        질문: {question}
        
        답변 형식 (JSON):
        {{"can_answer_with_faq": true/false, "reason": "이유"}}
        """
        
        response = llm.predict(prompt)
        return eval(response)  # 실제로는 JSON 파싱
    
    # 워크플로우
    def handle_customer_query(question):
        # 1) 의도 분류
        intent = classify_intent(question)
        
        if intent["can_answer_with_faq"]:
            # 2) RAG로 자동 답변
            response = qa_chain.invoke({"query": question})
            return {
                "type": "automated",
                "answer": response["result"],
                "sources": response["source_documents"]
            }
        else:
            # 3) 상담사 연결
            return {
                "type": "escalate",
                "reason": intent["reason"]
            }
    
    # 테스트
    print(handle_customer_query("배송 조회 방법은?"))  # FAQ 가능
    print(handle_customer_query("제품이 고장났는데 AS 받고 싶어요"))  # 상담사 필요
    

     

    실전 팁:

    • FAQ + 매뉴얼 + 릴리즈 노트 통합 검색
    • 티켓 자동 분류 (긴급도/카테고리)
    • 다국어 지원 (번역 + 각 언어별 임베딩)
    • 답변 신뢰도 점수 표시 (낮으면 상담사 연결)

    9.3 개발자 도구 (DevEx)

    사례: "내부 기술 문서 검색 봇"

    # 다양한 소스 통합
    sources = [
        "Architecture Decision Records (ADR)",
        "API 문서 (Swagger/OpenAPI)",
        "코딩 컨벤션",
        "런북 (Runbook)",
        "장애 대응 매뉴얼",
        "온보딩 가이드"
    ]
    
    # 코드 검색 특화 임베딩
    from langchain.embeddings import HuggingFaceEmbeddings
    
    code_embeddings = HuggingFaceEmbeddings(
        model_name="microsoft/codebert-base"  # 코드 특화 모델
    )
    
    # 메타데이터 필터링
    def search_dev_docs(query, filters=None):
        """
        filters 예시:
        {
            "doc_type": "runbook",
            "service": "payment-api",
            "severity": "critical"
        }
        """
        retriever = vectorstore.as_retriever(
            search_kwargs={"k": 5, "filter": filters}
        )
        return qa_chain.invoke({"query": query})
    
    # 사용 예시
    print(search_dev_docs(
        "결제 API 타임아웃 에러 발생 시 대응 방법은?",
        filters={"doc_type": "runbook", "service": "payment-api"}
    ))
    

     

    주요 질문 유형:

    • "이 에러 코드는 무슨 뜻이야?"
    • "X 서비스 배포 롤백 절차는?"
    • "새 팀원 온보딩 체크리스트 알려줘"
    • "API 인증 방식이 어떻게 돼?"

    효과:

    • 📚 문서 검색 시간 90% 단축
    • 🚨 장애 대응 시간 50% 감소
    • 👨‍💻 온보딩 기간 2주 → 3일

    9.4 규정/정책 준수 (Compliance)

    사례: "규정 위반 사전 예방 시스템"

    # 계약서 검토 자동화
    def review_contract(contract_text):
        """계약서가 회사 정책을 준수하는지 검토"""
        
        prompt = f"""
        아래 계약서 초안이 회사 법무 정책을 위반하는지 검토하세요.
        
        [회사 정책] (검색된 문서)
        {{context}}
        
        [계약서 초안]
        {contract_text}
        
        출력 형식:
        1. 위반 사항: (있으면 명시, 없으면 "없음")
        2. 근거 조항: (정책 문서의 해당 조항)
        3. 수정 권장사항:
        """
        
        # RAG 체인 실행
        review = qa_chain.invoke({"query": prompt})
        return review["result"]
    
    # 예시
    contract = """
    계약 기간: 2024.1.1 ~ 2026.12.31 (3년)
    위약금: 계약 금액의 50%
    ...
    """
    
    print(review_contract(contract))
    # 출력:
    # 위반 사항: 계약 기간이 정책 위반 (최대 2년)
    # 근거 조항: 계약 관리 규정 제5조
    # 수정 권장사항: 계약 기간을 2년으로 단축 필요
    

     

    적용 분야:

    • 계약서 자동 검토
    • 이메일 발송 전 정보 유출 체크
    • 코드 보안 정책 준수 검증
    • 규제 대응 (GDPR, 개인정보보호법)

    10. 결론: RAG는 "똑똑한 검색 시스템"이다

    10.1 핵심 정리

    RAG를 한 문장으로 요약하면:

    "LLM에게 Google을 쥐어준 것"

    더 정확히 말하면:

    • LLM = 글쓰기 엔진 (문장 생성의 달인)
    • RAG = 검색 시스템 + 글쓰기 엔진 (근거 기반 답변)

    10.2 RAG의 진짜 가치

    ✅ 할 수 있는 것

    1. 최신 정보 제공: 학습 없이도 새 문서 반영
    2. 환각 대폭 감소: 근거 문서로 사실 검증 가능
    3. 출처 투명성: "어디서 가져온 정보인지" 명확
    4. 도메인 특화: 사내/전문 지식 활용 가능
    5. 비용 효율적: 재학습보다 훨씬 저렴

    ❌ 할 수 없는 것

    1. 완벽한 정확도: 검색 실패 시 답변 불가
    2. 창의적 생성: 문서 기반이므로 창작 어려움
    3. 실시간 계산: "현재 주가는?" 같은 API 필요 질문
    4. 추론: 복잡한 논리/수학 문제는 약함

    10.3 성공하는 RAG의 3요소

    graph TD
        A[성공적인 RAG] --> B[좋은 검색]
        A --> C[좋은 생성]
        A --> D[좋은 평가]
        
        B --> B1[청킹 전략]
        B --> B2[하이브리드 검색]
        B --> B3[메타데이터 관리]
        
        C --> C1[프롬프트 설계]
        C --> C2[환각 방지]
        C --> C3[출처 강제]
        
        D --> D1[RAGAS 메트릭]
        D --> D2[사용자 피드백]
        D --> D3[지속 개선]
        
        style A fill:#e1f5ff
        style B fill:#ffe1e1
        style C fill:#e1ffe1
        style D fill:#fff4e1
    

     

    1. 좋은 검색 (70%의 중요도)

    • 관련 문서를 못 찾으면 아무리 좋은 LLM도 소용없음
    • 청킹 + 임베딩 + 하이브리드 검색 + Re-ranking

    2. 좋은 생성 (20%의 중요도)

    • 프롬프트로 LLM의 행동 통제
    • 근거 강제 + 환각 방지 + 출처 필수

    3. 좋은 평가 (10%의 중요도)

    • RAGAS로 정량적 측정
    • 사용자 피드백으로 정성적 개선
    • 지속적인 모니터링과 개선

    10.4 RAG vs Fine-tuning: 언제 무엇을 쓸까?

    상황 RAG Fine-tuning
    최신 정보 필요 ✅ 적합 ❌ 부적합
    문서가 자주 바뀜 ✅ 적합 ❌ 부적합
    출처 명시 필요 ✅ 적합 ❌ 불가능
    특정 스타일/톤 학습 ❌ 부적합 ✅ 적합
    도메인 용어 이해 △ 가능 ✅ 더 좋음
    비용 💰 저렴 💰💰💰 비쌈
    유지보수 ✅ 쉬움 ❌ 어려움

     

    결론: 대부분의 기업 유스케이스에서는 RAG가 더 적합합니다.


    11. 마치며

    RAG는 "마법"이 아닙니다. 잘 설계된 검색 시스템입니다.

    성공의 열쇠는:

    • 📚 좋은 문서 관리 (청킹, 메타데이터, 버전)
    • 🔍 정확한 검색 (하이브리드, Re-ranking, 필터링)
    • ✍️ 통제된 생성 (프롬프트, 환각 방지, 출처 강제)
    • 📊 지속적인 평가 (RAGAS, 모니터링, 개선)

    기억하세요:

    "RAG 시스템의 품질 = 검색 품질 × 생성 품질"

    검색이 0점이면, 아무리 좋은 LLM도 0점입니다.

    이제 여러분도 프로덕션 수준의 RAG 시스템을 만들 수 있습니다.

    728x90
    반응형