LangChain에서의 Hybrid RAG
요약
LangChain을 활용하여 검색 결과와 생성된 답변의 품질을 검증하는 Hybrid RAG 구현 방법을 소개합니다. 쿼리 강화와 검증 루프를 통해 RAG 시스템의 실패 사례를 방지하고 답변의 정확도를 높이는 워크플로우를 다룹니다.
핵심 포인트
- Hybrid RAG는 검색과 생성 단계에 검증 루프를 추가하여 품질을 보장함
- 쿼리 강화(Query Enhancement)를 통해 검색 효율성을 극대화함
- 검색된 문서가 질문에 답할 수 있는지 확인하는 검증 단계 포함
- 생성된 답변이 컨텍스트에 근거(grounded)하는지 재검증함
지금까지 다룬 두 가지 RAG 접근 방식은 모두 내재된 암묵적인 가정을 가지고 있습니다. 즉, 첫 번째 검색(retrieval) 시도가 충분히 좋은 문서를 반환할 것이며, 생성된 답변이 직접 반환하기에 충분히 훌륭할 것이라는 가정입니다.
대부분의 경우 이는 잘 작동합니다. 하지만 때로는 원래의 쿼리(query)가 모호하거나, 검색된 문서들이 느슨하게만 관련되어 있거나, 생성된 답변이 컨텍스트(context)가 실제로 말하는 내용에서 벗어나는 경우가 발생합니다.
Hybrid RAG는 이러한 실패 사례들이 사용자에게 도달하기 전에 이를 포착할 수 있도록 검증 루프(validation loops)를 추가합니다.
핵심 아이디어 (The Core Idea)
2-Step RAG는 검색에 단 한 번의 기회만 가집니다. Agentic RAG는 검색을 수행할지 여부를 결정합니다. Hybrid RAG는 이 두 가지를 모두 수행한 다음 다음과 같이 질문합니다: "이것이 정말로 충분했는가?"
만약 검색된 문서가 질문에 답할 수 없다면 — 더 나은 쿼리로 다시 시도합니다. 만약 생성된 답변이 컨텍스트에 근거(grounded)하지 않았다면 — 답변을 다시 생성합니다. 두 가지 검사를 모두 통과했을 때만 답변을 반환합니다.
워크플로우 (The Workflow)
Original Query
↓
Step 1: Query Enhancement
...
코드 살펴보기 (Walking Through the Code)
지식 베이스(Knowledge Base) 및 벡터 스토어(Vector Store)
docs = [
Document(page_content="LangChain is a framework for building LLM applications..."),
Document(page_content="Agents in LangChain use an LLM to decide which tools to call..."),
...
이전과 동일합니다 — 문서를 임베딩(embed)하고 FAISS에 저장합니다. 새로운 것은 없습니다.
Step 1: 쿼리 강화 (Query Enhancement)
def enhance_query(original: str) -> str:
prompt = f"Rewrite this question to make it better for searching a LangChain documentation knowledge base. Return ONLY the rewritten question.\n\nOriginal: {original}\n\nRewritten:"
result = free_llm.invoke(prompt)
...
검색하기 전에, LLM에게 쿼리를 문서와 일치할 가능성이 더 높은 형태로 다시 작성하도록 요청합니다.
예를 들어:
Original: "What is RAG?"
Enhanced: "Explain Retrieval-Augmented Generation and how it works in LangChain"
개선된 버전은 실제 문서 텍스트와 더 많은 중첩(overlap)을 가지며, 이는 FAISS가 관련 있는 결과를 반환할 가능성이 더 높음을 의미합니다.
Step 2: Retrieve (검색)
def retrieve(query: str, k: int = 2) -> list[Document]:
return vectorstore.similarity_search(query, k=k)
직관적인 유사도 검색(similarity search)입니다. (이제 개선된) 쿼리를 받아 이를 임베딩(embedding)하고, FAISS에서 가장 유사한 k개의 문서를 찾습니다.
Step 3: Retrieval Validation Loop (검색 검증 루프)
def is_sufficient(query: str, docs: list[Document]) -> bool:
prompt = f"Can the following documents answer this question? Answer ONLY 'yes' or 'no'.\n\nQuestion: {query}\n\nDocuments:\n{chr(10).join(d.page_content for d in docs)}\n\nAnswerable:"
result = free_llm.invoke(prompt)
...
이것이 첫 번째 검증 게이트(validation gate)입니다. LLM에게 질문과 검색된 문서들을 보여주고 한 가지만 묻습니다: "이 문서들을 통해 실제로 이 질문에 답할 수 있습니까?"
만약 답변이 'no'라면, 검색 루프(retrieval loop)가 작동합니다:
retrieved = retrieve(enhanced)
attempt = 1
...
각 실패 시도마다:
- 쿼리가 다시 작성됩니다. 이때 LLM이 더 타겟팅된 재작성을 하도록 유도하기 위해
(more specific)을 뒤에 붙입니다. - 다시 검색을 수행하며, 이번에는 조금 더 넓은 범위를 탐색하기 위해
k=3을 사용합니다. - 결과와 상관없이 최대 3번까지 시도합니다.
Step 4: Generate (생성)
def generate(query: str, context: str) -> str:
prompt = f"Answer the question using ONLY the context below.\n\nContext:\n{context}\n\nQuestion: {query}\n\nAnswer:"
result = free_llm.invoke(prompt)
...
2단계 RAG와 동일합니다. 검색된 컨텍스트(context)로 프롬프트를 구성하고 LLM에게 이를 바탕으로 답변하도록 요청합니다. ONLY 제약 조건은 LLM이 외부 지식을 가져오는 것을 방지합니다.
Step 5: Answer Validation (답변 검증)
def is_answer_good(query: str, answer: str, context: str) -> bool:
prompt = f"Is the following answer accurate and based ONLY on the context? Answer ONLY 'yes' or 'no'.\n\nQuestion: {query}\n\nContext:\n{context}\n\nAnswer:\n{answer}\n\nAccurate:"
result = free_llm.invoke(prompt)
...
두 번째 검증 게이트(validation gate)입니다. 답변을 생성한 후, LLM에게 자신의 출력을 검토하도록 요청합니다: "이 답변이 실제로 우리가 검색한 컨텍스트(context)에 근거하고 있는가?"
if is_answer_good(q, answer, context):
print(f" Validation: PASSED")
else:
...
검증에 실패하면 동일한 컨텍스트를 사용하여 한 번 더 재생성합니다. 첫 번째 생성 단계에서 내용이 벗어났을 수 있으므로, 동일한 컨텍스트로 두 번째 시도를 하면 더 근거 있는(grounded) 답변을 유지하는 경향이 있다는 아이디어입니다.
전체 코드 (Full Code)
import os
from dotenv import load_dotenv
...
샘플 출력 (Sample Output)
============================================================
Q: What is RAG?
1. Enhanced: Explain Retrieval-Augmented Generation in LangChain
...
다른 방식과의 비교 (How It Compares to the Others)
| 2-Step RAG | Agentic RAG | Hybrid RAG | |
|---|---|---|---|
| 쿼리 강화 (Query enhancement) | No | No | Yes |
| ... |
트레이드오프(tradeoff)는 명확합니다. 신뢰성이 높아질수록 더 많은 LLM 호출 비용이 발생합니다. 작은 지식 베이스와 간단한 질문의 경우에는 2-step RAG로도 충분합니다. Hybrid RAG는 잘못된 답변이 실제적인 결과를 초래할 수 있는 상황에서 의미가 있습니다.
주의 사항 (What to Keep in Mind)
각 검증 단계는 하나의 LLM 호출입니다. 단일 쿼리가 재시도 횟수에 따라 4~8번의 LLM 호출을 트리거할 수 있습니다. 대규모로 실행할 경우 지연 시간(latency)과 비용을 유의해야 합니다.
LLM이 자신의 작업물을 채점합니다. is_answer_good()은 답변을 생성했던 것과 동일한 LLM에게 검증을 요청합니다. 이는 검증이 아예 없는 것보다는 낫지만, 완벽하지는 않습니다. 잘못된 답변을 생성한 모델이 그 답변을 승인할 수도 있기 때문입니다. 실제 운영 시스템(production systems)에서는 검증을 수행하는 별도의 더 강력한 모델을 사용하는 것이 좋습니다.
재시도 루프(retry loop)에는 엄격한 제한이 있습니다. attempt < 3 제한은 지식 베이스(knowledge base)에 실제로 정답이 없는 경우 무한 루프에 빠지는 것을 방지합니다. 3번의 시도 후에는 무한히 반복하는 대신, 현재 가지고 있는 정보를 바탕으로 파이프라인이 다음 단계로 진행합니다.
Hybrid RAG를 사용해야 하는 경우
- 속도보다 답변의 정확도가 더 중요한 경우
- 쿼리(query)가 다양하고 때로는 모호한 경우
- 쿼리당 추가적인 LLM 호출 비용을 감당할 수 있는 경우
- 원샷(one-shot) 시스템 대신 자기 수정(self-correcting) 파이프라인을 원하는 경우
2단계 RAG(2-step RAG)로 시작하세요. 만약 잘못되거나 불완전한 답변을 반환한다는 것을 알게 된다면, Hybrid RAG가 자연스러운 다음 단계입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기