RAG 기본 정리

twonezero·4일 전

RAG

기본적인 RAG 코드


from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS # NEW
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough


# 1. Loading a PDF doc
pdf_path = "파일 이름" 
loader = PyPDFLoader(pdf_path, mode = "single")
doc = loader.load()                    

# 2. split into coherent chunks
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,  
    chunk_overlap=100, 
    separators=["\n\n", "\n", ".", "!", "?", " "],  
)
chunks = splitter.split_documents(doc)

# 3. Embeddings + Vector‑Store
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectordb = FAISS.from_documents(chunks, embeddings)
retriever = vectordb.as_retriever(search_kwargs={"k": 4})

# 4. LLM
llm = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0,
)


# 5. Prompt‑Template
qa_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a business analyst and helpful assistant."),
    ("human", "Answer the following question as accurate and diligent as possible."
              "Do not speculate or invent facts, rely only on the provided text."
              "\n\nContext:\n{context}\n\nQuestion: {question}\nAnswer:")
])



def format_docs(docs):
    return "\n\n---\n\n".join(doc.page_content for doc in docs)


# LCEL
rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | qa_prompt
    | llm
    | StrOutputParser()
)


question = input("❓ Your Question for the Bot: ")
result = rag_chain.invoke(question)

print("\nResponse:\n", result)

RAG가 필요한 이유

  • Input 크기에 따라 확장 가능: 관련된 몇 개의 문단만 검색하므로, 위키/드라이브가 커지더라도 컨텍스트 윈도우 제한에 도달하지 않습니다.
  • 더 낮은 비용 및 지연 시간: 전체 문서를 매번 프롬프트에 붙여넣는 것보다 훨씬 적은 토큰을 사용합니다.
  • 더 높은 신호 대 잡음 비율: 모델이 짧고 관련성 높은 발췌문에 집중합니다. 답변을 인용과 함께 더 쉽게 근거를 제시할 수 있습니다.
  • 최신 정보 + 권한 인식: 문서가 변경되면 재인덱싱하고 사용자 권한으로 필터링할 수 있으며, 모델을 재학습할 필요가 없습니다.

RAG의 장단점

  • 더 많은 구성 요소: 수집, 청킹, 임베딩, 벡터 저장소/리랭커, 모니터링이 필요합니다.
  • ”검색 실패” 위험: 청킹이 부족하거나 검색기가 약하면 올바른 문단을 찾지 못할 수 있습니다 (튜닝/평가 필요).
  • 운영 작업: 접근 제어 매핑, 개인정보 처리, 품질 테스트가 지속적인 과제가 됩니다.

”모든 문서를 프롬프트에 넣는 방식”의 장점

  • 구현이 간단: 파이프라인이나 인프라가 필요 없습니다.
  • 검색 실패 없음: 프롬프트 컨텍스트에 있는 내용은 모델이 볼 수 있습니다.
  • 작고 고정된 컨텍스트에 적합: 예: 짧은 핸드북 또는 단일 정책 문서.

전체 문서를 프롬프트에 넣는 방식의 단점

  • 확장성 부족: 토큰 제한에 빠르게 도달하며 문서 크기에 비례해 비용이 증가합니다.
  • 주의력 희석: 크고 노이즈가 많은 컨텍스트는 답변 품질을 저하시킵니다.
  • 최소 권한 적용이 어려움: 모델/API에 과도한 콘텐츠를 공유할 수 있습니다.
  • 출처 추적이 약함: 명확한 소스 발췌문을 보여주기 어렵고, 감사가 복잡합니다.

일반적인 지침

  • Input 컨텍스트가 약 20~50페이지이고, 정기적으로 변경되며, 사용자별 권한이 있는 경우 RAG를 사용하세요 (선택적으로 리랭킹 및 캐싱과 함께).
  • 작고 고정되어 있으며 신뢰 경계가 단순한 경우, 컨텍스트 스터핑이 적합하고 시작하기에 가장 저렴합니다.

chunk_size

chunk_size (토큰 ≈ 문자/4)장점단점적합한 용도
< 1,000 토큰 (≤ ~4,000 문자)매우 세분화됨; 특정 용어에 대한 높은 재현율청크가 많음 → 더 많은 임베딩, 더 큰 인덱스, 더 높은 지연 시간FAQ, 채팅 로그
1,000–2,500 토큰 (~4,000–10,000 문자)견고한 균형: 충분한 컨텍스트, 과도하지 않음여전히 긴 섹션을 분할할 수 있음기술 문서, 블로그 게시물
2,500–4,000 토큰 (~10,000–16,000 문자)섹션을 온전하게 유지 (더 적은 “고립된 정보”)키워드가 가장자리에 있을 경우 재현율이 낮아질 수 있음연간 보고서, 백서
> 4,000 토큰 (>~16,000 문자)최소한의 분할; 더 적은 검색 호출컨텍스트 예산을 초과할 위험; 청크당 더 많은 노이즈매우 큰 컨텍스트 모델과 드문 검색의 경우에만

10K 컨텍스트(긴 문서, 포멀하고 섹션이 구분된 구조): chunk 크기의 시작점으로는 약 1,800–2,400 tokens가 적절함

chunk_overlap

overlap사용 이유장단점
0–5%문단이 깔끔하게 구분될 때 빠르고 저렴함문장/테이블이 경계를 넘어 잘릴 위험
10–20% (일반적)제목, 각주, 테이블 간의 흐름을 유지더 많은 저장소 및 임베딩 비용
> 25%상호 참조가 많은 계약서/코드중복이 많음; 더 느리고 비쌈

긴 보고서의 경우:15% 중복 (또는 약 200 토큰)을 목표로 합니다.
20%도 괜찮지만, 일반적으로 컨텍스트를 잃지 않고 약 15%로 낮출 수 있습니다.


큰 청크 vs 작은 청크의 장단점

더 큰 chunk_size, 더 작은 중복더 작은 chunk_size, 더 큰 중복
장점더 적은 검색 호출; 더 자체 포함적인 컨텍스트; LLM이 더 적은 연결 작업 필요좁은 쿼리에서 더 높은 재현율; 더 정확한 인용
단점청크당 더 많은 “노이즈”; 중요한 세부 정보가 묻힐 수 있음더 높은 비용/지연 시간; 모델이 여러 청크를 함께 엮어야 함 (일관성 위험)

핵심 요약: 10K의 경우, 기준으로 청크당 약 8,000~10,000 문자에 약 15% 중복으로 시도한 다음, 평가 점수와 프롬프트 비용에 따라 조정하세요.


Retrieving 여러 방식

  1. 기본적인 유사도 검색

    top_docs = vectordb.similarity_search(query, k=4)
    or

    retriever = vectordb.as_retriever(
       search_type="similarity",          
       search_kwargs={"k": 4}             
    )
    top_docs = retriever.invoke(query)
  1. 유사도 점수 임계값 포함

    
    retriever = vectordb.as_retriever(
       search_type="similarity_score_threshold",
       search_kwargs={
           "score_threshold": 0.4,
           "k": 4 
       }
    )
  2. MMR(Max Marginal Relevance) Search

    1. 쿼리와 유사도가 높은 상위 z개 후보 문서(청크)를 넓게 가져온>다.
    2. 이후 유사도(similarity)와 다양성(diversity)를 함께 고려해, 중>복을 줄이면서 최종 k개를 재선정한다.
    3. lambda_mult (0~1) 값으로 유사도 중심(1에 가까움) ↔ 다양성 >중심(0에 가까움)의 균형을 조절한다.
    
    retriever = vectordb.as_retriever(
       search_type="mmr",                   
       search_kwargs={"k": 4,
                      "fetch_k": 20,
                      "lambda_mult": 0.5}  
    )

요약

검색 유형작동 방식반환 결과후보 풀주요 파라미터 (일반적)장점 / 사용 시기주의 사항
similarity벡터 유사도에 따른 상위 k개의 최근접 이웃.정확히 k개의 Document (리트리버를 통한 점수 없음).일반적으로 k개 (추가 풀 없음).k (예: 4), 선택적 filter.간단하고 빠른 기준선; 가장 유사한 청크가 필요한 직접 QA에 적합.입력 컨텍스트에 노이즈가 많으면 저품질 일치 항목을 반환할 수 있음; 다양성 제어 없음.
similarity_score_threshold후보를 가져와 관련성(≈[0,1])으로 변환하고, score_threshold 미만인 항목을 제거한 다음 최대 k개를 반환.임계값을 충족하는 ≤ k개의 Document.임계값 적용에 충분한 후보를 확보하기 위해 일반적으로 k보다 큼 (내부/fetch_k 통해).score_threshold (예: 0.3–0.7), k, 선택적 filter.강력한 일치 항목만 유지; 환각이나 “주제 외” 컨텍스트를 피하는 데 유용.임계값이 너무 높으면 k개 미만 (또는 0개)을 반환할 수 있음; 점수는 원시 거리가 아니라 정규화된 관련성임을 기억할 것.
mmr (Max Marginal Relevance)쿼리 유사도와 다양성의 균형을 맞추기 위해 더 큰 후보 세트를 재순위화; 관련성이 있고 중복되지 않는 항목을 반복적으로 선택.정확히 k개의 Document, 더 다양함.적절한 풀을 위해 fetch_k > k (예: 20)를 사용한 다음 k개를 선택.k, fetch_k (예: 4–5×k), lambda_mult (약 0.3–0.7; 높을수록 더 많은 관련성, 낮을수록 더 많은 다양성), 선택적 filter.요약, 긴 답변, 또는 상위 k개가 거의 중복일 때 훌륭함; 하위 주제의 적용 범위 개선.fetch_k가 너무 작으면 다양성에 해로움; 극단적인 lambda_mult는 과도할 수 있음: 1.0에 가까우면 일반 유사도와 동일, 0.0에 가까우면 다양하지만 주제에서 벗어날 수 있음.

작업별 빠른 추천

작업검색 유형k (필터링 후 반환)fetch_k (후보 풀)score_threshold (관련성 0–1)lambda_mult (MMR)이유 / 참고 사항
집중 QA (정확한 사실 조회)similarity_score_threshold4–620–400.55–0.70 (0.60부터 시작)최대 정밀도; 임계값이 약한 일치 항목을 제거; 작은 k는 컨텍스트를 좁게 유지.
탐색적 QA / 다측면 답변mmr6–105×k (≈ 40–80)(선택적 후 필터) 0.40–0.550.4–0.6 (0.5부터 시작)관련성과 다양성의 균형을 맞춰 10개의 거의 중복된 결과를 방지.
요약 (map-reduce / 개요)mmr8–125×k (≈ 50–100)0.30–0.450.3–0.5더 넓은 범위를 탐색; 주변부지만 중요한 섹션을 포함하기 위해 낮은 임계값.
의미론적 검색/찾아보기 (사용자에게 결과 표시)similarity 또는 mmr10–203×k0.40–0.60 (필요하면 제거)0.4–0.6 (MMR인 경우)사용자가 결과를 읽는 경우 다양성이 도움이 됨; 다양성을 위해 MMR 고려.

문서 필터링: where_document 옵션

similarity_search 등의 검색 메서드에서 where_document 파라미터를 사용하여 문서 내용으로 필터링할 수 있습니다.

1. 문자열 포함 연산자

# 특정 문자열 포함 (대소문자 구분)
results = vectordb.similarity_search(
    "query",
    where_document={"$contains": "search_string"}
)

# 특정 문자열 미포함
results = vectordb.similarity_search(
    "query",
    where_document={"$not_contains": "unwanted_string"}
)

2. 정규표현식

# 정규표현식 패턴 매칭
results = vectordb.similarity_search(
    "query",
    where_document={"$regex": r"\bAPI\b"}
)

# 이메일 패턴 예시
results = vectordb.similarity_search(
    "query",
    where_document={"$regex": r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"}
)

3. 논리 연산자 ($and, $or 조합)

# AND 조건: 두 조건을 모두 만족하는 문서
results = vectordb.similarity_search(
    "query",
    where_document={
        "$and": [
            {"$contains": "machine learning"},
            {"$regex": "[0-9]+"}
        ]
    }
)

# OR 조건: 두 조건 중 하나라도 만족하는 문서
results = vectordb.similarity_search(
    "query",
    where_document={
        "$or": [
            {"$contains": "Python"},
            {"$contains": "JavaScript"}
        ]
    }
)

where vs where_document 비교

옵션용도지원 연산자
where메타데이터 필터링$eq, $ne, $gt, $gte, $lt, $lte, $in, $nin
where_document문서 내용 필터링$contains, $not_contains, $regex

사용 예시

# 메타데이터와 문서 내용을 동시에 필터링
results = vectordb.similarity_search(
    "AI 기술 질문",
    k=5,
    where={"source": "technical_doc.pdf"},  # 메타데이터 필터
    where_document={"$contains": "neural network"}  # 문서 내용 필터
)
profile
I Enjoy Learn-and-Run Vibe😊

0개의 댓글