ARCH · RAG
Advanced RAG
기본 RAG를 넘어 Hybrid Search, HyDE, RAPTOR, Corrective RAG, Self-RAG 등 프로덕션 수준의 고급 검색-증강 패턴을 구현합니다.
RAG 진화 단계
| 단계 | 기법 | 핵심 개선 | 적합 케이스 |
|---|---|---|---|
| Basic RAG | Dense Vector Search | 시맨틱 검색 | 소규모 단일 도메인 |
| Advanced Retrieval | Hybrid Search, HyDE | 검색 품질 향상 | 다양한 질의 유형 |
| Adaptive RAG | Corrective RAG, Self-RAG | 검색 결과 검증 | 높은 정확도 요구 |
| Hierarchical RAG | RAPTOR | 다층 요약 인덱스 | 대규모 문서 컬렉션 |
Hybrid Search — Dense + Sparse 결합
벡터 검색(의미)과 BM25(키워드)를 결합해 각각의 단점을 보완합니다. 정확한 키워드 매칭이 필요한 코드 검색이나 고유명사에서 특히 효과적입니다.
pythonhybrid_search.py — Dense + BM25 앙상블
from langchain_chroma import Chroma
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
def build_hybrid_retriever(
documents: list[Document],
dense_weight: float = 0.6,
sparse_weight: float = 0.4,
k: int = 6
) -> EnsembleRetriever:
"""Dense + Sparse 검색기 결합"""
# Dense: 시맨틱 벡터 검색
vectorstore = Chroma.from_documents(
documents,
embedding=OpenAIEmbeddings()
)
dense_retriever = vectorstore.as_retriever(
search_type="mmr", # Maximum Marginal Relevance — 다양성 보장
search_kwargs={"k": k, "fetch_k": k * 3}
)
# Sparse: BM25 키워드 검색
bm25_retriever = BM25Retriever.from_documents(documents)
bm25_retriever.k = k
# 앙상블 (RRF: Reciprocal Rank Fusion)
return EnsembleRetriever(
retrievers=[dense_retriever, bm25_retriever],
weights=[dense_weight, sparse_weight]
)
# 사용
retriever = build_hybrid_retriever(docs)
results = retriever.invoke("LangGraph StateGraph 사용법")
HyDE — Hypothetical Document Embeddings
HyDE는 질문을 직접 임베딩하는 대신, LLM이 가상의 답변 문서를 먼저 생성하고 그것을 임베딩해 검색합니다. 질문-답변 임베딩 공간 불일치 문제를 해결합니다.
pythonhyde.py — 가상 문서 임베딩 검색
from langchain_anthropic import ChatAnthropic
from langchain_openai import OpenAIEmbeddings
import numpy as np
llm = ChatAnthropic(model="claude-haiku-4-5-20251001")
embeddings = OpenAIEmbeddings()
async def hyde_retrieve(
query: str,
vectorstore,
k: int = 5,
n_hypothetical: int = 3
) -> list:
"""HyDE: 가상 답변 N개 생성 → 임베딩 평균 → 검색"""
# 가상 답변 N개 생성
hypothetical_docs = []
for _ in range(n_hypothetical):
response = await llm.ainvoke([{
"role": "user",
"content": f"""다음 질문에 대한 이상적인 답변 문서를 작성하세요.
실제 데이터가 없어도 전문가가 작성할 법한 내용으로 작성합니다.
질문: {query}
답변 (3~5 문장):"""
}])
hypothetical_docs.append(response.content)
# 가상 문서들의 임베딩 평균
doc_embeddings = await embeddings.aembed_documents(hypothetical_docs)
avg_embedding = np.mean(doc_embeddings, axis=0).tolist()
# 평균 임베딩으로 검색
results = vectorstore.similarity_search_by_vector(avg_embedding, k=k)
return results
Corrective RAG — 검색 결과 검증 및 수정
검색 결과의 관련성을 LLM이 평가하고, 관련성이 낮으면 웹 검색으로 보완하는 자기교정 RAG 패턴입니다.
그림 1. Corrective RAG 흐름 — 검색 결과 검증 후 부족하면 웹 검색으로 보완
pythoncorrective_rag.py — LangGraph Corrective RAG
from langgraph.graph import StateGraph, END
from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
from typing import TypedDict, Literal
class RAGState(TypedDict):
question: str
documents: list
generation: str
web_search_needed: bool
llm = ChatAnthropic(model="claude-haiku-4-5-20251001")
web_search = TavilySearchResults(max_results=3)
async def retrieve_node(state: RAGState) -> dict:
docs = retriever.invoke(state["question"])
return {"documents": docs}
async def grade_documents_node(state: RAGState) -> dict:
"""각 문서의 관련성 평가"""
question = state["question"]
filtered_docs = []
web_needed = False
for doc in state["documents"]:
response = await llm.ainvoke([{
"role": "user",
"content": f"""문서가 질문에 관련이 있으면 'yes', 없으면 'no'만 답하세요.
질문: {question}
문서: {doc.page_content[:500]}"""
}])
if "yes" in response.content.lower():
filtered_docs.append(doc)
else:
web_needed = True
return {"documents": filtered_docs, "web_search_needed": web_needed}
async def web_search_node(state: RAGState) -> dict:
"""웹 검색으로 부족한 컨텍스트 보완"""
results = web_search.invoke(state["question"])
from langchain_core.documents import Document
web_docs = [Document(page_content=r["content"]) for r in results]
return {"documents": state["documents"] + web_docs}
async def generate_node(state: RAGState) -> dict:
context = "\n\n".join(d.page_content for d in state["documents"])
response = await ChatAnthropic(model="claude-opus-4-7").ainvoke([{
"role": "user",
"content": f"컨텍스트:\n{context}\n\n질문: {state['question']}"
}])
return {"generation": response.content}
def route_after_grading(state: RAGState) -> str:
if state["web_search_needed"] and not state["documents"]:
return "web_search"
return "generate"
builder = StateGraph(RAGState)
builder.add_node("retrieve", retrieve_node)
builder.add_node("grade_documents", grade_documents_node)
builder.add_node("web_search", web_search_node)
builder.add_node("generate", generate_node)
builder.set_entry_point("retrieve")
builder.add_edge("retrieve", "grade_documents")
builder.add_conditional_edges("grade_documents", route_after_grading, {
"web_search": "web_search",
"generate": "generate"
})
builder.add_edge("web_search", "generate")
builder.add_edge("generate", END)
crag_graph = builder.compile()
청크 전략 비교
| 전략 | 방법 | 장점 | 단점 |
|---|---|---|---|
| Fixed Size | N토큰 고정 분할 | 단순, 균일 | 문맥 단절 가능성 |
| Recursive Character | 문단→문장→단어 순 분할 | 자연스러운 경계 | 청크 크기 불균일 |
| Semantic Chunking | 임베딩 유사도 기반 | 의미 보존 | 비용 높음 |
| Parent-Child | 큰 청크 검색 + 작은 청크 임베딩 | 정밀도 + 컨텍스트 | 구현 복잡 |
| Sliding Window | 오버랩 포함 분할 | 경계 손실 감소 | 중복 저장 |
💡 RAG 파이프라인 선택 기준
- 정확한 키워드 검색 중요 → Hybrid Search (BM25 + Dense)
- 질문이 단답형/짧음 → HyDE로 임베딩 공간 매칭 개선
- 검색 정확도 최우선 → Corrective RAG + 크로스인코더 리랭킹
- 수천 페이지 이상 문서 → RAPTOR (계층적 요약 트리)