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가 필요한 이유
RAG의 장단점
”모든 문서를 프롬프트에 넣는 방식”의 장점
전체 문서를 프롬프트에 넣는 방식의 단점
일반적인 지침
| 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가 적절함
| overlap | 사용 이유 | 장단점 |
|---|---|---|
| 0–5% | 문단이 깔끔하게 구분될 때 빠르고 저렴함 | 문장/테이블이 경계를 넘어 잘릴 위험 |
| 10–20% (일반적) | 제목, 각주, 테이블 간의 흐름을 유지 | 더 많은 저장소 및 임베딩 비용 |
| > 25% | 상호 참조가 많은 계약서/코드 | 중복이 많음; 더 느리고 비쌈 |
긴 보고서의 경우: 약 15% 중복 (또는 약 200 토큰)을 목표로 합니다.
20%도 괜찮지만, 일반적으로 컨텍스트를 잃지 않고 약 15%로 낮출 수 있습니다.
더 큰 chunk_size, 더 작은 중복 | 더 작은 chunk_size, 더 큰 중복 | |
|---|---|---|
| 장점 | 더 적은 검색 호출; 더 자체 포함적인 컨텍스트; LLM이 더 적은 연결 작업 필요 | 좁은 쿼리에서 더 높은 재현율; 더 정확한 인용 |
| 단점 | 청크당 더 많은 “노이즈”; 중요한 세부 정보가 묻힐 수 있음 | 더 높은 비용/지연 시간; 모델이 여러 청크를 함께 엮어야 함 (일관성 위험) |
핵심 요약: 10K의 경우, 기준으로 청크당 약 8,000~10,000 문자에 약 15% 중복으로 시도한 다음, 평가 점수와 프롬프트 비용에 따라 조정하세요.
top_docs = vectordb.similarity_search(query, k=4)
orretriever = vectordb.as_retriever( search_type="similarity", search_kwargs={"k": 4} ) top_docs = retriever.invoke(query)
유사도 점수 임계값 포함
retriever = vectordb.as_retriever( search_type="similarity_score_threshold", search_kwargs={ "score_threshold": 0.4, "k": 4 } )
MMR(Max Marginal Relevance) Search
- 쿼리와 유사도가 높은 상위 z개 후보 문서(청크)를 넓게 가져온>다.
- 이후 유사도(similarity)와 다양성(diversity)를 함께 고려해, 중>복을 줄이면서 최종 k개를 재선정한다.
- 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_threshold | 4–6 | 20–40 | 0.55–0.70 (0.60부터 시작) | — | 최대 정밀도; 임계값이 약한 일치 항목을 제거; 작은 k는 컨텍스트를 좁게 유지. |
| 탐색적 QA / 다측면 답변 | mmr | 6–10 | 5×k (≈ 40–80) | (선택적 후 필터) 0.40–0.55 | 0.4–0.6 (0.5부터 시작) | 관련성과 다양성의 균형을 맞춰 10개의 거의 중복된 결과를 방지. |
| 요약 (map-reduce / 개요) | mmr | 8–12 | 5×k (≈ 50–100) | 0.30–0.45 | 0.3–0.5 | 더 넓은 범위를 탐색; 주변부지만 중요한 섹션을 포함하기 위해 낮은 임계값. |
| 의미론적 검색/찾아보기 (사용자에게 결과 표시) | similarity 또는 mmr | 10–20 | 3×k | 0.40–0.60 (필요하면 제거) | 0.4–0.6 (MMR인 경우) | 사용자가 결과를 읽는 경우 다양성이 도움이 됨; 다양성을 위해 MMR 고려. |
where_document 옵션similarity_search 등의 검색 메서드에서 where_document 파라미터를 사용하여 문서 내용으로 필터링할 수 있습니다.
# 특정 문자열 포함 (대소문자 구분)
results = vectordb.similarity_search(
"query",
where_document={"$contains": "search_string"}
)
# 특정 문자열 미포함
results = vectordb.similarity_search(
"query",
where_document={"$not_contains": "unwanted_string"}
)
# 정규표현식 패턴 매칭
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,}$"}
)
# 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"} # 문서 내용 필터
)