본문 바로가기
IT/AI

RAG 완전 정복: 벡터DB 데이터 조회 파이프라인 전체 프로세스 상세 가이드

by twofootdog 2026. 6. 7.
반응형

1. RAG 조회 파이프라인이란

RAG 시스템은 크게 두 가지 파이프라인으로 나뉩니다. 문서를 사전에 준비하는 인덱싱(적재) 파이프라인과, 실제 질문에 실시간으로 답하는 조회(검색+생성) 파이프라인입니다.

이전 글(2026.06.07 - [IT/AI] - RAG 완전 정복: 벡터DB 데이터 적재 파이프라인 전체 프로세스 상세 가이드)에서 인덱싱 파이프라인을 다뤘다면, 이번 글은 조회 파이프라인에 대해 알아보겠습니다.

RAG 조회 파이프라인은 사용자가 질문을 입력하는 순간부터 최종 답변이 돌아오기까지의 전체 흐름입니다. 인덱싱 파이프라인이 "도서관 책장을 미리 잘 정리해두는 일"이라면, 조회 파이프라인은 "사서가 질문을 받고 가장 관련 있는 책을 찾아 읽은 뒤 답해주는 일"에 해당합니다.

※ 조회 파이프라인의 핵심 특성

인덱싱과 달리 조회는 실시간으로 이루어지기 때문에 속도가 절대적으로 중요합니다. 사용자는 보통 2~3초 이내의 응답을 기대합니다. 또한 검색의 정확도가 최종 답변 품질을 결정하기 때문에, 얼마나 관련성 높은 청크를 찾아오느냐가 성패를 가릅니다.

 


2. 조회 파이프라인 전체 흐름

질문이 들어오는 순간부터 답변이 나오기까지 5단계로 구성됩니다.

 

① 1단계: 질문 임베딩 (Query Embedding)

사용자가 입력한 질문을 벡터로 변환하는 단계입니다. 벡터DB에 저장된 문서 벡터들과 비교하려면, 질문도 같은 방식으로 벡터화해야 합니다.

 

* 절대 원칙: 적재 시와 동일한 임베딩 모델 사용

이 단계에서 가장 중요한 규칙이 하나 있습니다.

"적재(인덱싱) 시에 사용한 임베딩 모델과 반드시 동일한 모델을 사용해야 합니다."

문서를 text-embedding-3-small로 벡터화해서 저장했다면, 질문도 반드시 text-embedding-3-small로 변환해야 합니다. 모델이 달라지면 벡터가 표현하는 공간 자체가 달라져서 유사도 계산이 전혀 의미 없어집니다. 마치 한국어 지도와 영어 지도를 놓고 위치를 비교하는 것처럼 전혀 다른 기준으로 비교하게 됩니다.

 

* 질문 전처리(선택적)

질문을 임베딩하기 전에 간단한 전처리를 추가하면 검색 품질이 높아집니다.

기법 설명 효과
질문 정제 오탈자 수정, 불필요한 단어 제거 노이즈 감소
HyDE (가상 문서 생성) 질문에 대한 가상 답변을 먼저 생성 후 임베딩 검색 정확도 향상
질문 확장 동의어, 관련 키워드 추가 검색 범위 확대
쿼리 분해 복잡한 질문을 여러 하위 질문으로 분리 복합 질문 처리 향상

 

from langchain_openai import OpenAIEmbeddings

# 적재 시와 반드시 동일한 모델 사용
embed_model = OpenAIEmbeddings(model="text-embedding-3-small")

# 질문을 벡터로 변환
query = "3분기 매출 목표 달성률은 얼마입니까?"
query_vector = embed_model.embed_query(query)
# → [0.12, -0.34, 0.89, ...] (1536개 숫자)

# HyDE 기법: 가상 답변을 먼저 생성 후 임베딩 (고급 기법)
from langchain.chains import HypotheticalDocumentEmbedder
from langchain_anthropic import ChatAnthropic

llm = ChatAnthropic(model="claude-haiku-4-5")
hyde_embeddings = HypotheticalDocumentEmbedder.from_llm(
    llm=llm,
    base_embeddings=embed_model,
    custom_prompt="질문에 대한 짧은 답변을 작성하세요: {question}"
)
hyde_vector = hyde_embeddings.embed_query(query)

HyDE(Hypothetical Document Embeddings)란?

일반적인 방식은 "질문 → 벡터 → 문서 검색"이지만, HyDE는 "질문 → 가상 답변 생성 → 가상 답변 벡터 → 문서 검색" 순서로 진행합니다. 질문보다 가상 답변이 실제 저장된 문서와 표현 방식이 더 유사하기 때문에, 검색 정확도가 크게 향상됩니다. 단, 가상 답변 생성에 추가 비용과 시간이 발생합니다.

 


② 2단계: 유사도 검색 (Similarity Search)

질문 벡터와 벡터DB에 저장된 수많은 문서 벡터들을 비교해서, 가장 유사한 청크 K개를 반환하는 단계입니다. 이 단계는 AI가 전혀 필요 없습니다. 순수한 수학 연산(벡터 간 거리 계산)으로 이루어집니다.

 

* 유사도 측정 방식

대부분의 텍스트 검색에서는 코사인 유사도가 표준입니다.

방식 수식 특징 적합한 경우
코사인 유사도 cos(θ) = A·B / (|A||B|) 방향 기반, 크기 무관 가장 일반적, 텍스트 검색
내적 (Dot Product) A·B 정규화된 벡터에서 코사인과 동일 정규화 임베딩 모델
유클리디안 거리 √Σ(Aᵢ-Bᵢ)² 절대 거리 기반 공간 좌표 검색

 

* 검색 방식: Dense vs Sparse vs 하이브리드

검색 방식 원리 장점 단점
Dense 검색 임베딩 벡터 유사도 의미 기반, 표현이 달라도 검색 가능 정확한 키워드 매칭 약함
Sparse 검색 (BM25) 키워드 빈도 기반 정확한 단어 매칭 강함 의미 유사성 파악 불가
하이브리드 검색 Dense + Sparse 결합 두 방식의 장점 동시 활용 구현 복잡도 증가

프로덕션 환경에서는 하이브리드 검색이 단일 방식보다 일반적으로 더 높은 품질을 보입니다. Dense 검색이 "의미가 비슷한" 문서를 찾는다면, Sparse 검색은 "정확히 그 단어가 있는" 문서를 찾습니다. 두 방식을 결합하면 서로의 약점을 보완합니다.

# 기본 유사도 검색 (Dense)
docs = vectorstore.similarity_search(query, k=10)

# 점수 포함 검색
docs_with_score = vectorstore.similarity_search_with_score(query, k=10)
for doc, score in docs_with_score:
    print(f"유사도: {score:.4f} | {doc.page_content[:80]}...")

# 하이브리드 검색 (Dense + BM25)
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever

# BM25 검색기 (키워드 기반)
bm25_retriever = BM25Retriever.from_documents(documents)
bm25_retriever.k = 10

# Dense 검색기 (의미 기반)
dense_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})

# 두 방식을 0.5:0.5 비율로 결합
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, dense_retriever],
    weights=[0.5, 0.5]
)
hybrid_docs = ensemble_retriever.invoke(query)

# 메타데이터 필터 적용 (특정 날짜, 카테고리 등)
filtered_docs = vectorstore.similarity_search(
    query,
    k=10,
    filter={"source": "2024_Q3_report.pdf"}  # 특정 문서만 검색
)

※ K값 설정 가이드

검색할 청크 수(K)는 상황에 따라 달라집니다. 보통 리랭킹 없이 바로 생성 단계로 넘어가면 3~5개, 리랭킹을 거치는 경우에는 1차로 10~20개를 검색한 뒤 최종 3~5개로 압축하는 방식이 일반적입니다. K값이 너무 크면 관련 없는 정보가 프롬프트에 섞여 오히려 답변 품질이 떨어질 수 있습니다.


③ 3단계: 리랭킹 (Re-ranking) — 선택적이지만 강력한 품질 향상

리랭킹은 1차 벡터 검색으로 뽑은 후보 청크들을 더 정밀하게 재정렬하는 단계입니다. "선택 단계"이지만, 도입 시 검색 품질이 눈에 띄게 향상됩니다.

 

* 왜 리랭킹이 필요한가

벡터 유사도 검색(Bi-encoder)은 질문과 문서를 각각 따로 벡터화한 뒤 비교합니다. 속도는 빠르지만, 질문과 문서를 함께 놓고 세밀하게 비교하지 못한다는 한계가 있습니다.

리랭킹(Cross-encoder)은 질문과 문서를 동시에 입력받아 "이 문서가 이 질문에 얼마나 직접적으로 답할 수 있는가"를 정밀하게 평가합니다. 훨씬 정확하지만 속도가 느리기 때문에, 전체 문서가 아닌 1차 검색으로 줄인 소수의 후보에게만 적용합니다.

또한 리랭킹은 "Lost in the Middle" 문제도 해결합니다. AI는 프롬프트 맨 앞이나 맨 뒤에 있는 정보는 잘 활용하지만, 중간에 있는 정보는 놓치는 경향이 있습니다. 리랭킹으로 가장 중요한 청크를 앞쪽에 배치하면 이 문제를 완화할 수 있습니다.

 

* 주요 리랭킹 모델 비교

모델 제공사 방식 한국어 지원 비용
rerank-v3.5 Cohere API 호출 다국어 지원 유료
rerank-multilingual-v3.0 Cohere API 호출 한국어 강점 유료
bge-reranker-v2-m3 BAAI 로컬 실행 한국어 지원 무료
ms-marco-MiniLM-L-12-v2 HuggingFace 로컬 실행 영어 특화 무료
Ranking API Google API 호출 다국어 지원 유료
# Cohere 리랭킹 (API 방식, 다국어 지원)
from langchain_cohere import CohereRerank
from langchain.retrievers import ContextualCompressionRetriever

reranker = CohereRerank(
    model="rerank-multilingual-v3.0",
    top_n=4  # 최종 반환할 청크 수
)

# 기존 검색기에 리랭킹 레이어 추가
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 20})
compression_retriever = ContextualCompressionRetriever(
    base_compressor=reranker,
    base_retriever=base_retriever
)

# 20개 검색 → 리랭킹 → 상위 4개 반환
reranked_docs = compression_retriever.invoke(query)

# BGE 리랭킹 (로컬 실행, 무료, 한국어 지원)
from sentence_transformers import CrossEncoder

cross_encoder = CrossEncoder("BAAI/bge-reranker-v2-m3")

# 질문-문서 쌍 생성
pairs = [[query, doc.page_content] for doc in candidate_docs]

# 각 쌍의 관련도 점수 계산
scores = cross_encoder.predict(pairs)

# 점수 기준으로 정렬 후 상위 K개 선택
ranked_docs = sorted(
    zip(candidate_docs, scores),
    key=lambda x: x[1],
    reverse=True
)
top_docs = [doc for doc, score in ranked_docs[:4]]

 

* 리랭킹 도입 전후 파이프라인 비교

AWS 기술 블로그에 따르면, 벡터 검색만으로는 임베딩 과정에서 정보 손실이 발생하며 리랭킹은 이를 보완하는 핵심 기법입니다. 특히 한국어처럼 영어 중심 임베딩 모델에서 성능이 다소 떨어질 수 있는 언어에서 리랭킹의 효과가 더욱 두드러집니다.

[리랭킹 없이]
질문 → 벡터 검색(K=5) → 프롬프트 구성 → 답변 생성

[리랭킹 적용]
질문 → 벡터 검색(K=20) → 리랭킹 → 상위 4개 선택 → 프롬프트 구성 → 답변 생성

④ 4단계: 프롬프트 구성 (Prompt Construction)

검색된 청크들과 사용자의 질문을 합쳐서 생성 AI에게 전달할 프롬프트를 만드는 단계입니다. 이 단계는 수학 연산도, AI 호출도 아닌 텍스트 조합 작업입니다. 하지만 프롬프트를 어떻게 구성하느냐가 최종 답변 품질에 큰 영향을 미칩니다.

* 기본 프롬프트 구조

[시스템 프롬프트]
당신은 주어진 문서를 기반으로 질문에 답하는 어시스턴트입니다.
반드시 아래 제공된 컨텍스트 내에서만 답변하세요.
컨텍스트에 없는 내용은 "제공된 문서에서 찾을 수 없습니다"라고 답하세요.

[컨텍스트 - 검색된 청크들]
출처: 2024_Q3_report.pdf (12페이지)
내용: 3분기 매출은 150억으로 목표 대비 98.5% 달성했습니다...

출처: 2024_Q3_report.pdf (15페이지)
내용: 4분기 목표는 전분기 대비 10% 성장한 165억으로...

[사용자 질문]
3분기 매출 목표 달성률은 얼마입니까?

 

  프롬프트 구성 시 핵심 고려사항

고려사항 내용 이유
컨텍스트 우선 명시 "컨텍스트만 활용하세요" 지시 할루시네이션 방지
출처 포함 각 청크에 파일명, 페이지 표시 답변 신뢰성 향상
토큰 제한 관리 청크 수와 크기로 토큰 조절 컨텍스트 윈도우 초과 방지
중요 청크 앞 배치 가장 관련 높은 청크를 먼저 Lost in the Middle 방지
불필요 내용 제거 중복 청크, 관련성 낮은 청크 제외 답변 품질 향상
from langchain_core.prompts import ChatPromptTemplate

# RAG 전용 프롬프트 템플릿
rag_prompt = ChatPromptTemplate.from_messages([
    ("system", """당신은 제공된 문서를 기반으로 질문에 정확하게 답하는 어시스턴트입니다.

규칙:
1. 반드시 아래 컨텍스트 내에서만 답변하세요.
2. 컨텍스트에 없는 내용은 '제공된 문서에서 확인할 수 없습니다'라고 답하세요.
3. 답변 마지막에 참고한 출처를 명시하세요.
4. 확실하지 않은 내용은 추측하지 마세요.

컨텍스트:
{context}"""),
    ("human", "{question}")
])

# 검색된 청크들을 컨텍스트 문자열로 변환
def format_context(docs):
    context_parts = []
    for i, doc in enumerate(docs, 1):
        source = doc.metadata.get("source", "출처 불명")
        page = doc.metadata.get("page", "")
        page_info = f" ({page}페이지)" if page else ""
        context_parts.append(
            f"[문서 {i}] 출처: {source}{page_info}\n{doc.page_content}"
        )
    return "\n\n".join(context_parts)

context_str = format_context(reranked_docs)
formatted_prompt = rag_prompt.format_messages(
    context=context_str,
    question=query
)

컨텍스트 윈도우 관리

생성 AI는 한 번에 처리할 수 있는 텍스트 양(토큰)에 제한이 있습니다. 청크가 너무 많거나 크면 컨텍스트 윈도우를 초과해 앞부분이 잘립니다. 보통 프롬프트 전체가 모델 컨텍스트 윈도우의 70% 이하가 되도록 청크 수와 크기를 조절하는 것이 좋습니다.


⑤ 5단계: 답변 생성 (Generation)

구성된 프롬프트를 생성 AI에 전달하고 최종 답변을 받는 단계입니다. 이 단계에서 처음으로 생성 AI(GPT-4o, Claude 등)가 호출됩니다. 앞선 4단계는 생성 AI 없이 처리됩니다.

 

* 답변 생성 후처리

답변을 그대로 반환하는 것보다, 다음과 같은 후처리를 추가하면 서비스 품질이 향상됩니다.

후처리 내용 효과

후처리 내용 효과
출처 표시 답변에 참고 문서 링크 첨부 신뢰성 향상
신뢰도 점수 검색 유사도 점수 표시 불확실성 전달
할루시네이션 감지 컨텍스트에 없는 내용 포함 여부 확인 오답 방지
스트리밍 응답 토큰 단위로 실시간 출력 체감 응답속도 향상
from langchain_anthropic import ChatAnthropic
from langchain.chains import RetrievalQA
from langchain import hub

# 생성 모델 설정
llm = ChatAnthropic(
    model="claude-sonnet-4-5",
    temperature=0,       # 일관된 답변을 위해 0 권장
    max_tokens=1000
)

# RAG 체인 구성 (검색 + 생성 통합)
rag_chain = RetrievalQA.from_chain_type(
    llm=llm,
    retriever=compression_retriever,   # 리랭킹 포함 검색기
    chain_type_kwargs={"prompt": rag_prompt},
    return_source_documents=True       # 출처 문서 함께 반환
)

# 질문 실행
result = rag_chain.invoke({"query": query})

print("=== 답변 ===")
print(result["result"])

print("\n=== 참고 출처 ===")
for doc in result["source_documents"]:
    print(f"- {doc.metadata.get('source')} ({doc.metadata.get('page')}페이지)")

# 스트리밍 응답 (실시간 출력)
from langchain_core.output_parsers import StrOutputParser

streaming_chain = rag_prompt | llm | StrOutputParser()

for chunk in streaming_chain.stream({
    "context": context_str,
    "question": query
}):
    print(chunk, end="", flush=True)

※ temperature 값 설정 가이드

RAG 기반 QA 시스템에서는 temperature=0을 권장합니다. Temperature가 높을수록 창의적이고 다양한 답변이 나오지만, 사실 기반 질의응답에서는 오히려 일관성이 떨어지고 할루시네이션 위험이 높아집니다. 문서에 있는 내용을 정확하게 전달하는 것이 목적이라면 낮게 유지하는 것이 좋습니다.

 


3. 전체 조회 파이프라인 최종 정리

* 단계별 요약표

단계 작업 핵심 기술 AI 필요여부 비고
① 질문 임베딩 질문 → 벡터 변환 text-embedding-3, BGE-M3 임베딩 모델 적재 시 동일 모델 필수
② 유사도 검색 벡터DB 검색 코사인 유사도, BM25, 하이브리드 불필요 순수 수학 연산
③ 리랭킹 검색 결과 재정렬 Cohere Rerank, BGE-reranker Cross-encoder 선택적, 품질 향상 효과 큼
④ 프롬프트 구성 컨텍스트 + 질문 조합 LangChain PromptTemplate 불필요 할루시네이션 방지 지시 포함
⑤ 답변 생성 최종 답변 생성 Claude, GPT-4o 필수 유일한 생성 AI 호출 지점

 

* 조회 파이프라인 핵심 포인트 4가지

첫째, 생성 AI는 마지막 단계에서만 호출됩니다. 유사도 검색은 수학 연산, 프롬프트 구성은 텍스트 조합으로 AI 없이 처리됩니다. 비용과 속도 최적화의 핵심입니다.

둘째, 임베딩 모델은 적재 시와 반드시 동일해야 합니다. 조금이라도 다른 모델을 쓰면 벡터 공간이 달라져 검색이 완전히 실패합니다.

셋째, 검색 품질이 답변 품질을 결정합니다. 관련 없는 청크가 프롬프트에 들어가면 아무리 좋은 생성 AI도 틀린 답변을 냅니다. 리랭킹과 하이브리드 검색으로 검색 품질을 높이는 것이 전체 시스템 품질 향상의 핵심입니다.

넷째, 프롬프트에 "컨텍스트만 사용" 지시를 반드시 포함하세요. 이 지시가 없으면 AI가 검색된 문서 외에 학습 데이터에서 정보를 끌어와 할루시네이션이 발생할 수 있습니다.

 

* 인덱싱 파이프라인과 비교

구분 인덱싱(적재) 파이프라인 조회 파이프라인
실행 시점 파일 업로드 시 (사전) 사용자 질문 시 (실시간)
속도 느려도 괜찮음 빨라야 함 (2~3초 이내)
주요 단계 파싱→전처리→청킹→임베딩→저장 임베딩→검색→리랭킹→생성
생성 AI 사용 선택적 (파싱 시) 필수 (답변 생성)
반복 여부 한 번 처리 후 재사용 매 질문마다 실행

 

 

* RAG 인덱싱(적재)/조회 프로세스 정리

지금까지 살펴본 RAG 적재/조회 프로세스를 최종 정리하면 다음과 같습니다.

단계  작업 핵심기술 AI 여부 파이프라인
파싱 파일 → 텍스트 Unstructured, PyMuPDF 선택적 적재
전처리 노이즈 제거 regex, ftfy 불필요 적재
클렌징 품질/보안 처리 Presidio, MinHash 불필요 적재
청킹 적절한 크기로 분할 LangChain Splitter 불필요 적재
임베딩 텍스트 → 벡터 text-embedding-3, BGE-M3 임베딩 모델 적재+조회
벡터DB 저장 벡터+메타데이터 저장 Pinecone, ChromaDB 불필요 적재
유사도 검색 질문과 유사한 청크 탐색 코사인 유사도 불필요 조회
답변 생성 컨텍스트 기반 답변 GPT-4o, Claude 필수 조회

 

 


4. 참고 자료

 

 

반응형

댓글