RAG (Retrieval-Augmented Generation) - 검색으로 환각을 막다

ChatGPT에게 “우리 회사 휴가 정책이 뭐야?”라고 물어본 적 있나요? 아마 이런 답변을 받으셨을 겁니다:

“죄송하지만, 귀사의 내부 정책에 대한 정보는 가지고 있지 않습니다.”

당연합니다. GPT는 공개 인터넷 데이터로 학습했지, 여러분 회사의 내부 문서는 본 적이 없으니까요.

그렇다면 “회사 문서로 Fine-tuning 하면 되지 않을까?”라고 생각할 수 있습니다. 하지만 현실적으로 어렵습니다:

  • Fine-tuning 비용이 매우 높음
  • 문서가 업데이트될 때마다 재학습 필요
  • 수천 개의 문서를 모두 모델에 넣을 수 없음

여기서 RAG (Retrieval-Augmented Generation)가 등장합니다. 모델을 재학습시키지 않고, 필요한 정보를 검색해서 답변에 활용하는 방법이죠.

왜 RAG를 이해해야 할까요?

1. LLM의 가장 큰 문제 해결

LLM의 가장 큰 문제는 환각(Hallucination)입니다. 없는 사실을 그럴듯하게 만들어내는 것이죠.

2023년 한 변호사가 ChatGPT가 만들어낸 가짜 판례를 법정에 제출했다가 징계를 받은 사건(출처: 중앙일보)이 있었습니다. 이런 문제를 방지하려면 모델이 사실 기반으로 답변하도록 해야 합니다.

RAG는 이 문제의 현실적인 해결책입니다:

  • 모델에게 “상상하지 말고, 이 문서를 바탕으로 답해”라고 지시
  • 검증 가능한 출처 제공
  • 최신 정보 반영 (재학습 없이)

2. 실무에서 가장 많이 쓰이는 패턴

2024년 기준, 실무에서 배포된 LLM 애플리케이션의 약 70% 이상이 RAG를 사용한다는 통계가 있습니다. (Gartner 보고서)

왜일까요? RAG는 실용적이기 때문입니다:

  • Fine-tuning보다 훨씬 저렴 (1/100 비용)
  • 데이터 업데이트가 즉시 반영
  • 구현이 상대적으로 간단

제가 본 대부분의 기업 AI 챗봇, 문서 검색 시스템, 고객 지원 도구들은 모두 RAG 기반이었습니다.

3. 도메인 특화 AI의 핵심

“우리 회사 데이터로 AI를 만들고 싶어요”라는 요구사항을 자주 들으셨나요?

Fine-tuning은 비싸고 느리지만, RAG는:

  • 내부 문서만 벡터 DB에 저장
  • 질문이 오면 관련 문서 검색
  • 검색된 내용을 바탕으로 답변

이 과정이 몇 초 안에 완료됩니다. 모델 재학습 없이요.

먼저, 기초부터 이해하기

RAG란 무엇인가?

RAG (Retrieval-Augmented Generation)는 두 단계로 동작합니다:

  1. Retrieval (검색): 관련 있는 문서를 찾음
  2. Generation (생성): 찾은 문서를 바탕으로 답변 생성

비유하자면, 오픈북 시험과 비슷합니다. 모든 걸 외우지 않고, 교과서를 보면서 답을 작성하는 거죠.

사용자 질문
    ↓
[1. Retrieval]
검색 → 관련 문서 찾기
    ↓
[2. Generation]
질문 + 문서 → LLM → 답변

Fine-tuning vs RAG - 어떤 차이가 있을까?

RAG를 이해하려면 먼저 Fine-tuning과 비교해야 합니다. 둘 다 “LLM을 우리 데이터에 맞게 만든다”는 목표는 같지만, 접근 방식이 완전히 다릅니다.

Fine-tuning이란?

Fine-tuning(미세 조정)은 이미 학습된 모델을 추가 학습시키는 방법입니다.

[GPT-3.5 기본 모델]
        ↓
[우리 회사 데이터로 추가 학습]
   (몇 시간~며칠)
        ↓
[회사 특화 모델]

비유하자면, 의사 면허가 있는 사람을 소아과 전문의로 만드는 “전문의 과정”과 비슷합니다.

예시:

# OpenAI Fine-tuning
training_data = [
    {"messages": [
        {"role": "user", "content": "휴가 정책은?"},
        {"role": "assistant", "content": "연차 15일입니다"}
    ]},
    # ... 수백~수천 개
]

# 모델 학습 (시간 + 비용 소요)
fine_tuned_model = openai.FineTuning.create(
    training_file="data.jsonl",
    model="gpt-3.5-turbo"
)

Fine-tuning의 특징:

장점:

  • 모델이 회사 용어, 문체, 답변 스타일을 “학습”함
  • 반복적인 패턴이 많으면 효과적
  • 추론 시 추가 컨텍스트 불필요

단점:

  • 비용이 높음: $100~$1000+ (데이터 양에 따라)
  • 시간이 오래 걸림: 몇 시간~며칠
  • 업데이트가 어려움: 데이터 변경 시 재학습 필요
  • 환각 여전히 발생: 모델은 여전히 “상상”할 수 있음

RAG의 차이점

RAG는 모델을 전혀 학습시키지 않습니다. 대신 질문할 때마다 필요한 정보를 찾아서 제공합니다.

[GPT-3.5 기본 모델] ← 그대로 사용
        +
[벡터 DB: 회사 문서들] ← 여기만 준비
        ↓
질문 → 검색 → 문서 + 질문 → 답변

비유하자면, 의사에게 “이 의학 교과서를 참고해서 답해주세요”라고 하는 것과 비슷합니다.

RAG의 특징:

장점:

  • 저렴함: Fine-tuning의 1/100 비용
  • 빠름: 몇 분~몇 시간이면 구축 가능
  • 업데이트 쉬움: 문서만 추가/수정하면 즉시 반영
  • 환각 방지: 문서에 없으면 “모르겠습니다” 답변 가능
  • 출처 표시: “이 문서에 따르면…” 명시 가능

단점:

  • 매번 검색 비용 발생
  • 컨텍스트 길이 제한 (검색된 문서가 너무 길면 문제)
  • 복잡한 추론에는 Fine-tuning보다 약할 수 있음

언제 무엇을 사용할까?

Fine-tuning을 선택해야 할 때:

  • 특정 형식/스타일 답변이 중요 (예: 법률 문서 작성)
  • 반복적인 패턴이 많음 (예: 고객 응대 스크립트)
  • 추론 속도가 매우 중요 (검색 지연 없이)
  • 데이터가 자주 변하지 않음

RAG를 선택해야 할 때:

  • 지식 기반 Q&A (예: 회사 내부 문서 검색)
  • 데이터가 자주 업데이트됨 (예: 뉴스, 정책 변경)
  • 출처 표시가 중요 (예: 의료, 법률)
  • 빠른 프로토타입 필요
  • 대부분의 경우 ← 실무에서는 RAG가 훨씬 실용적

두 개를 함께 사용하기:

가장 강력한 조합은 Fine-tuned 모델 + RAG입니다!

[Fine-tuned GPT-3.5]
   (회사 톤앤매너 학습)
        +
[RAG: 최신 문서 검색]
   (실시간 정보 제공)
        =
    최고의 성능

예: Fine-tuning으로 “친절한 고객 지원 톤”을 학습시키고, RAG로 최신 제품 정보를 제공.

전통적 방식 vs RAG

차이를 명확히 이해하기 위해 비교해볼까요?

❌ 전통적 LLM 방식

# 질문만 던짐
question = "2023년 우리 회사 매출은?"
response = llm.generate(question)

# 결과
 "죄송하지만 해당 정보를 모릅니다."
# 또는 더 나쁜 경우
 "약 150억원입니다." (환각 - 틀린 정보!)

문제:

  • LLM은 학습 시점까지의 데이터만 알고 있음
  • 회사 내부 정보는 전혀 모름
  • 없는 정보를 만들어낼 위험

✅ RAG 방식

# 1단계: 관련 문서 검색
question = "2023년 우리 회사 매출은?"
relevant_docs = vector_db.search(question, top_k=3)

# 검색 결과:
# - "2023 Annual Report.pdf" (관련도: 0.92)
# - "Q4 2023 Financial Summary.pdf" (관련도: 0.87)

# 2단계: 문서와 함께 LLM에게 질문
prompt = f"""
다음 문서를 바탕으로 질문에 답하세요:

문서:
{relevant_docs[0].content}
{relevant_docs[1].content}

질문: {question}
"""

response = llm.generate(prompt)

# 결과
 "2023 Annual Report에 따르면, 2023년 매출은 157억원입니다."

장점:

  • 실제 문서 기반 답변
  • 출처 명시 가능
  • 환각 방지

RAG의 핵심 구성 요소

RAG 시스템을 이해하려면 세 가지 핵심 요소를 알아야 합니다.

1. 임베딩 (Embedding) - 텍스트를 숫자로

RAG의 첫 번째 마법은 임베딩입니다. 텍스트를 숫자 벡터로 변환하는 과정이죠.

왜 임베딩이 필요한가?

컴퓨터는 “유사도”를 직접 계산할 수 없습니다. 하지만 숫자 벡터로 바꾸면 가능합니다.

# 텍스트 → 벡터 변환
text1 = "강아지가 공원에서 놀고 있다"
text2 = "개가 야외에서 뛰어놀고 있다"
text3 = "주식 시장이 급락했다"

# 임베딩 (1536차원 벡터로 변환)
vec1 = embedding_model.encode(text1)  # [0.234, -0.123, 0.456, ...]
vec2 = embedding_model.encode(text2)  # [0.221, -0.118, 0.447, ...]
vec3 = embedding_model.encode(text3)  # [-0.567, 0.890, -0.234, ...]

# 유사도 계산 (코사인 유사도)
similarity(vec1, vec2)  # 0.95 (매우 유사)
similarity(vec1, vec3)  # 0.12 (거의 무관)

의미가 비슷한 텍스트는 벡터 공간에서 가까이 위치합니다!

임베딩 모델의 예

OpenAI text-embedding-3-small:

  • 1536차원 벡터
  • 비용: $0.00002 / 1K tokens
  • 다국어 지원

OpenAI text-embedding-3-large:

  • 3072차원 벡터
  • 더 정확하지만 비싸고 느림

오픈소스: sentence-transformers:

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('all-MiniLM-L6-v2')
embedding = model.encode("검색할 텍스트")

무료이고, 로컬에서 실행 가능!

2. 벡터 데이터베이스 (Vector Database) - 유사한 것을 빠르게 찾기

임베딩 벡터를 저장하고, 빠르게 검색하려면 특별한 데이터베이스가 필요합니다.

전통적 DB vs 벡터 DB

전통적 데이터베이스 (MySQL, PostgreSQL):

SELECT * FROM documents WHERE title = '매출 보고서';
-- 정확히 일치하는 것만 찾음

벡터 데이터베이스:

vector_db.search(
    query_vector,
    top_k=5,
    metric='cosine'
)
# "의미상 유사한" 문서 5개를 찾음

인기 있는 벡터 DB

Pinecone (클라우드):

  • 관리형 서비스
  • 확장성 좋음
  • 비용: $70/month부터

Chroma (오픈소스):

import chromadb

client = chromadb.Client()
collection = client.create_collection("my_docs")

# 문서 저장
collection.add(
    documents=["문서 내용 1", "문서 내용 2"],
    ids=["doc1", "doc2"]
)

# 검색
results = collection.query(
    query_texts=["질문"],
    n_results=3
)

무료이고 간단!

Weaviate, Qdrant, Milvus:

  • 각각 장단점이 있음
  • 대규모 프로덕션에 적합

3. 청킹 (Chunking) - 문서를 적절한 크기로 나누기

긴 문서를 통째로 임베딩하면 문제가 생깁니다. 문서가 “매출, 인사, 복지” 모두 다루면, 정작 필요한 부분을 찾기 어렵죠.

청킹 전략

방법 1: 고정 크기

def chunk_by_size(text, chunk_size=500):
    words = text.split()
    chunks = []
    for i in range(0, len(words), chunk_size):
        chunks.append(' '.join(words[i:i+chunk_size]))
    return chunks

장점: 간단 단점: 문장 중간에 끊길 수 있음

방법 2: 의미 단위 (추천)

# 문단, 섹션 단위로 분할
chunks = text.split('\n\n')  # 빈 줄 기준

# 또는 LangChain 사용
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,  # 중복 허용 (맥락 유지)
    separators=['\n\n', '\n', '.', ' ']
)

chunks = splitter.split_text(long_document)

장점: 의미가 유지됨 단점: 청크 크기가 불균일

최적 청크 크기는?

경험상:

  • 너무 작으면 (< 200 tokens): 맥락 부족
  • 너무 크면 (> 1000 tokens): 노이즈 증가, 관련 없는 내용 포함
  • 추천: 500-800 tokens

실험을 통해 도메인에 맞게 조정해야 합니다.

RAG 시스템 구축하기

이론은 충분합니다. 실제로 만들어봅시다!

예제 1: 가장 간단한 RAG (50줄 이하)

from openai import OpenAI
import chromadb

client = OpenAI()

# 1. 벡터 DB 초기화
chroma_client = chromadb.Client()
collection = chroma_client.create_collection("knowledge_base")

# 2. 문서 저장 (임베딩 자동 생성)
documents = [
    "우리 회사 휴가 정책: 연차 15일, 병가 10일",
    "재택근무는 주 2회 가능합니다",
    "점심시간은 12시부터 1시까지입니다"
]

collection.add(
    documents=documents,
    ids=["doc1", "doc2", "doc3"]
)

# 3. RAG 함수
def rag_query(question):
    # 3-1. 관련 문서 검색
    results = collection.query(
        query_texts=[question],
        n_results=2
    )

    context = '\n'.join(results['documents'][0])

    # 3-2. LLM에게 질문 + 문서 제공
    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[
            {
                "role": "system",
                "content": "다음 문서를 바탕으로 답하세요. 문서에 없는 내용은 '모르겠습니다'라고 답하세요."
            },
            {
                "role": "user",
                "content": f"문서:\n{context}\n\n질문: {question}"
            }
        ]
    )

    return response.choices[0].message.content

# 4. 사용
answer = rag_query("재택근무 가능한가요?")
print(answer)
# → "네, 주 2회 재택근무가 가능합니다."

answer = rag_query("회사 주소가 어디인가요?")
print(answer)
# → "죄송하지만, 제공된 문서에 회사 주소 정보가 없습니다."

이게 RAG의 전부입니다! 검색 → 문서 + 질문 → LLM → 답변.

예제 2: PDF 문서 RAG 시스템

실무에서는 PDF, Word 등 다양한 파일을 처리해야 합니다.

from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chat_models import ChatOpenAI
from langchain.chains import RetrievalQA

# 1. PDF 로드
loader = PyPDFLoader("company_handbook.pdf")
pages = loader.load()

# 2. 청킹
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200
)
chunks = text_splitter.split_documents(pages)

print(f"총 {len(chunks)}개의 청크 생성")

# 3. 임베딩 + 벡터 DB 저장
embeddings = OpenAIEmbeddings()
vector_store = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"  # 디스크에 저장
)

# 4. RAG 체인 구성
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",  # 문서를 모두 프롬프트에 넣기
    retriever=vector_store.as_retriever(search_kwargs={"k": 3})
)

# 5. 사용
result = qa_chain.run("퇴직금 정책은 어떻게 되나요?")
print(result)

핵심:

  • PDF → 텍스트 추출
  • 긴 텍스트 → 청크로 분할
  • 청크 → 임베딩 → 벡터 DB
  • 질문 → 검색 → LLM

예제 3: 출처 표시하기 (Citation)

신뢰성을 위해 “어디서 찾았는지” 출처를 보여줘야 합니다.

def rag_with_citation(question):
    # 검색
    results = vector_store.similarity_search_with_score(
        question,
        k=3
    )

    # 문서 + 출처 정보 수집
    context_parts = []
    sources = []

    for i, (doc, score) in enumerate(results):
        context_parts.append(f"[출처 {i+1}]\n{doc.page_content}")
        sources.append({
            "id": i+1,
            "file": doc.metadata.get("source", "unknown"),
            "page": doc.metadata.get("page", "unknown"),
            "score": score
        })

    context = '\n\n'.join(context_parts)

    # LLM에게 출처 번호 사용하도록 지시
    prompt = f"""
다음 문서를 바탕으로 질문에 답하세요.
답변할 때 반드시 출처 번호를 명시하세요. (예: [출처 1]에 따르면...)

{context}

질문: {question}
"""

    response = llm.generate(prompt)

    return {
        "answer": response,
        "sources": sources
    }

# 사용
result = rag_with_citation("재택근무 정책은?")
print(result["answer"])
# → "[출처 1]에 따르면, 주 2회 재택근무가 가능합니다."

print("\n출처:")
for source in result["sources"]:
    print(f"  {source['id']}. {source['file']} (페이지 {source['page']})")

결과:

[출처 1]에 따르면, 주 2회 재택근무가 가능합니다.

출처:
  1. company_handbook.pdf (페이지 15)
  2. hr_policy.pdf (페이지 3)

사용자가 직접 확인할 수 있어 신뢰도가 높아집니다!

고급 RAG 기법

기본 RAG는 간단하지만, 실무에서는 더 정교한 기법이 필요합니다.

1. Hybrid Search - 키워드 + 의미 검색

벡터 검색만으로는 부족할 때가 있습니다.

문제 상황:

질문: "GPT-4의 가격은?"
문서: "GPT-4 pricing: $0.03/1K tokens"

벡터 검색: "GPT-4"와 "가격"의 의미를 이해하지만...
→ "AI 가격 정책", "모델 비용" 같은 문서도 검색됨 (노이즈)

해결: Hybrid Search

벡터 검색(의미) + BM25(키워드) 조합

from langchain.retrievers import BM25Retriever, EnsembleRetriever

# 벡터 검색기
vector_retriever = vector_store.as_retriever(search_kwargs={"k": 5})

# 키워드 검색기
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5

# 앙상블 (가중 평균)
ensemble_retriever = EnsembleRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    weights=[0.5, 0.5]  # 50:50 비율
)

# 사용
results = ensemble_retriever.get_relevant_documents("GPT-4 가격")

결과:

  • “GPT-4”라는 키워드가 정확히 있는 문서 우선
  • 의미상 관련 있는 문서도 포함

2. Re-ranking - 검색 후 재정렬

초기 검색에서 10개를 가져온 후, 더 정확하게 재정렬합니다.

from sentence_transformers import CrossEncoder

# 1단계: 벡터 검색 (빠르지만 덜 정확)
candidates = vector_store.similarity_search(question, k=20)

# 2단계: Re-ranking (느리지만 정확)
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

pairs = [[question, doc.page_content] for doc in candidates]
scores = reranker.predict(pairs)

# 점수 기준 재정렬
ranked_docs = sorted(
    zip(candidates, scores),
    key=lambda x: x[1],
    reverse=True
)

# 상위 3개만 사용
top_docs = [doc for doc, score in ranked_docs[:3]]

효과:

  • 검색 품질 10-30% 향상
  • 비용: 약간의 지연 시간 증가 (0.5초 정도)

3. Query Transformation - 질문 개선하기

사용자의 질문이 항상 명확하지는 않습니다.

문제:

사용자: "그거 어떻게 되는거야?"
→ 맥락 부족, 검색 불가능

해결 1: Query Expansion (질문 확장)

def expand_query(question, chat_history):
    # LLM에게 이전 대화 맥락 포함하여 질문 재작성 요청
    prompt = f"""
대화 기록:
{chat_history}

사용자 질문: {question}

위 질문을 검색 가능하도록 구체적으로 재작성하세요.
"""

    expanded_query = llm.generate(prompt)
    return expanded_query

# 예시
chat_history = """
사용자: 재택근무 정책 알려줘
AI: 주 2회 재택근무가 가능합니다.
사용자: 그거 언제부터 시행된거야?
"""

expanded = expand_query("그거 언제부터 시행된거야?", chat_history)
# → "재택근무 정책의 시행 날짜는 언제인가요?"

해결 2: Multi-Query (여러 질문 생성)

def generate_multiple_queries(question):
    prompt = f"""
다음 질문에 대해 3가지 다른 방식으로 물어보세요:

원본 질문: {question}

1.
2.
3.
"""

    queries = llm.generate(prompt)
    return queries.split('\n')

# 예시
queries = generate_multiple_queries("GPT-4 가격")
# 1. GPT-4의 비용은 얼마인가요?
# 2. GPT-4 API 요금은?
# 3. GPT-4 사용료는 어떻게 되나요?

# 각 질문으로 검색 → 결과 합치기
all_results = []
for query in queries:
    results = vector_store.search(query, k=2)
    all_results.extend(results)

# 중복 제거 + 상위 5개
unique_results = deduplicate(all_results)[:5]

다양한 관점에서 검색하므로 놓치는 문서가 줄어듭니다.

4. Contextual Compression - 문서 압축하기

검색된 문서가 너무 길면, LLM 컨텍스트 낭비 + 비용 증가.

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

# 압축기 설정
compressor = LLMChainExtractor.from_llm(llm)

compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=vector_store.as_retriever()
)

# 사용
compressed_docs = compression_retriever.get_relevant_documents(
    "재택근무 정책"
)

# 원본: 1000 tokens
# 압축: 200 tokens (질문과 관련된 부분만 추출)

효과:

  • 토큰 사용량 50-80% 감소
  • 답변 품질 유지 또는 향상 (노이즈 제거)

함정과 주의사항

RAG가 만능은 아닙니다. 실무에서 주의할 점들이 있습니다.

1. 검색 실패 - 관련 문서를 못 찾는 경우

RAG의 성능은 검색 품질에 달려 있습니다. 검색이 실패하면 답변도 틀립니다.

원인 1: 임베딩 모델의 한계

# 도메인 특화 용어
question = "우리 시스템의 QPS는?"
document = "초당 쿼리 수(Queries Per Second)는 10,000입니다."

# 일반 임베딩 모델은 "QPS"와 "Queries Per Second"의 관계를 모를 수 있음

해결:

  • 도메인 특화 임베딩 모델 사용
  • Fine-tuning (가능하면)
  • 용어집 추가 (QPS = Queries Per Second)

원인 2: 청킹 문제

# 잘못된 청킹
청크 1: "우리 회사의 매출은 2022년에 100억,"
청크 2: "2023년에는 150억원을 기록했습니다."

# 질문: "2023년 매출은?"
# → 청크 2만 검색되면 문맥 부족

해결:

  • Chunk overlap 사용 (예: 200 tokens)
  • 더 큰 청크 크기
  • Parent Document Retriever 사용
# Parent Document Retriever
from langchain.retrievers import ParentDocumentRetriever

# 작은 청크로 검색 (정확도↑)
# 하지만 원본 문서 반환 (맥락↑)
retriever = ParentDocumentRetriever(
    vectorstore=vector_store,
    docstore=InMemoryDocstore(),
    child_splitter=small_splitter,  # 검색용
    parent_splitter=large_splitter   # 반환용
)

2. 컨텍스트 길이 제한

LLM은 컨텍스트 길이에 제한이 있습니다.

GPT-3.5-turbo: 16K tokens
GPT-4: 8K tokens (기본) ~ 128K tokens (확장)
Claude 3: 200K tokens

문제는, 검색된 문서 5개가 각각 2000 tokens면:

  • 문서: 10,000 tokens
  • 질문 + 시스템 프롬프트: 500 tokens
  • 총: 10,500 tokens

GPT-3.5의 절반 이상을 사용합니다!

해결 방법:

1) Map-Reduce 패턴

# 1. 각 문서를 개별적으로 요약
summaries = []
for doc in retrieved_docs:
    summary = llm.generate(f"요약: {doc}")
    summaries.append(summary)

# 2. 요약들을 합쳐서 최종 답변
final_answer = llm.generate(f"""
요약들:
{summaries}

질문: {question}
""")

2) Refine 패턴

# 첫 문서로 초기 답변 생성
answer = llm.generate(f"{docs[0]}\n\n질문: {question}")

# 다음 문서들로 답변 개선
for doc in docs[1:]:
    answer = llm.generate(f"""
현재 답변: {answer}

추가 정보: {doc}

위 정보를 바탕으로 답변을 개선하세요.
""")

3) Contextual Compression (앞서 언급)

3. 비용 - 임베딩과 벡터 DB 비용

RAG는 “공짜”가 아닙니다.

임베딩 비용

# 1만 개 문서, 각 500 tokens
total_tokens = 10000 * 500 = 5,000,000 tokens

# OpenAI text-embedding-3-small: $0.00002 / 1K tokens
embedding_cost = (5000 / 1000) * 0.00002 = $0.1

# 한 번만 하면 됨 (업데이트 제외)

저렴합니다!

벡터 DB 비용

Pinecone:

  • Standard: $70/month (1GB 저장)
  • 1만 개 문서 (1536차원 벡터): 약 60MB
  • → $70/month

오픈소스 (Chroma, Weaviate):

  • 무료
  • 하지만 서버 운영 비용 발생

검색 비용

매 질문마다:

  • 임베딩 (질문): $0.00002 (거의 무료)
  • LLM 생성 (문서 + 질문): $0.001 ~ $0.01 (모델에 따라)
# 하루 1000 질문
# 각 질문당 평균 2000 tokens (문서 포함)

# GPT-3.5-turbo: $0.0005 / 1K input tokens
daily_cost = 1000 * (2000/1000) * 0.0005 = $1/day
monthly_cost = $30/month

Fine-tuning ($1000+)보다는 훨씬 저렴하지만, 무시할 수 없는 비용입니다.

최적화:

# 1. 캐싱
cache = {}
def cached_rag(question):
    if question in cache:
        return cache[question]

    answer = rag_query(question)
    cache[question] = answer
    return answer

# 2. 더 작은 모델 사용
# GPT-4 → GPT-3.5 (10배 저렴)

# 3. 청크 수 제한
retriever = vector_store.as_retriever(search_kwargs={"k": 3})
# k=5 → k=3 (40% 비용 절감)

4. 데이터 프라이버시 - 내부 문서 보안

회사 내부 문서를 외부 API(OpenAI, Pinecone)에 보내는 것이 안전한가요?

우려 사항:

  • 문서가 OpenAI 서버를 거침
  • 학습 데이터로 사용될 수 있음
  • 규제 준수 (GDPR, HIPAA 등)

해결 방법:

1) API 설정 확인

# OpenAI는 API 데이터를 학습에 사용하지 않음 (명시)
# 하지만 서버를 거치는 건 사실

2) 온프레미스 솔루션

# 로컬 임베딩 모델
from sentence_transformers import SentenceTransformer
embeddings = SentenceTransformer('all-MiniLM-L6-v2')

# 로컬 벡터 DB
import chromadb
chroma_client = chromadb.Client()

# 로컬 LLM (오픈소스)
from langchain.llms import LlamaCpp
llm = LlamaCpp(model_path="./llama-2-7b.bin")

장점: 데이터가 외부로 나가지 않음 단점: 성능 저하, 인프라 비용

3) Azure OpenAI Service

  • 전용 인스턴스
  • 데이터 격리 보장
  • 엔터프라이즈 계약 가능

5. 최신성 vs 정확성 트레이드오프

RAG의 장점은 “최신 정보”인데, 여기에도 함정이 있습니다.

시나리오:

오늘: 2025-11-26
질문: "현재 CEO는 누구인가요?"

데이터베이스:
- 2023-01-01 문서: "CEO는 김철수입니다"
- 2024-05-15 문서: "신임 CEO 박영희 취임"
- 2025-11-20 문서: "CEO 박영희, 사임 발표"

검색 결과: 3개 문서 모두 검색됨

LLM 답변: "CEO는 박영희입니다" (틀림! 이미 사임함)

해결:

1) 메타데이터 필터링

# 최신 문서만 검색
results = vector_store.similarity_search(
    question,
    k=5,
    filter={"date": {"$gte": "2025-01-01"}}  # 2025년 이후만
)

2) 날짜 가중치

from datetime import datetime

def rerank_by_date(docs):
    now = datetime.now()

    scored_docs = []
    for doc in docs:
        doc_date = datetime.fromisoformat(doc.metadata['date'])
        days_old = (now - doc_date).days

        # 최신일수록 높은 점수
        recency_score = 1 / (1 + days_old / 30)  # 한 달마다 감소

        # 유사도 + 최신성
        final_score = doc.score * 0.7 + recency_score * 0.3
        scored_docs.append((doc, final_score))

    return sorted(scored_docs, key=lambda x: x[1], reverse=True)

3) 명시적 지시

prompt = f"""
다음 문서들을 바탕으로 답하세요.
주의: 날짜를 확인하고 가장 최신 정보를 사용하세요.

{context}

질문: {question}
"""

실전 활용: RAG 시스템 구축 체크리스트

이제 실무에서 RAG 시스템을 구축한다면, 어떤 순서로 해야 할까요?

Phase 1: MVP (Minimum Viable Product)

목표: 1-2주 안에 작동하는 프로토타입

# 1. 간단한 구성
- 임베딩: OpenAI text-embedding-3-small
- 벡터 DB: Chroma (로컬)
- LLM: GPT-3.5-turbo
- 프레임워크: LangChain

# 2. 핵심 기능만
- 문서 업로드
- 질문-답변
- 출처 표시

# 3. 평가
- 10 질문  준비
- 수동으로 답변 품질 확인

Phase 2: 품질 개선

목표: 답변 정확도 80% 이상

# 1. 검색 개선
- Hybrid search 도입
- Re-ranking 추가
- 청킹 전략 최적화

# 2. 프롬프트 엔지니어링
- System prompt 튜닝
- Few-shot examples 추가
- 답변 형식 가이드

# 3. 평가 자동화
def evaluate_rag():
    test_cases = [
        {"question": "...", "expected": "...", "source": "..."},
        # 50개 이상
    ]

    correct = 0
    for case in test_cases:
        answer = rag_query(case["question"])
        if is_correct(answer, case["expected"]):
            correct += 1

    accuracy = correct / len(test_cases)
    print(f"정확도: {accuracy:.1%}")

Phase 3: 프로덕션 준비

목표: 안정적인 서비스

# 1. 인프라
- 벡터 DB: Pinecone / Weaviate (확장성)
- 모니터링: LangSmith, Weights & Biases
- 로깅: 모든 질문-답변 저장

# 2. 성능 최적화
- 캐싱 (Redis)
- 비동기 처리
- 배치 임베딩

# 3. 안전장치
- Rate limiting
- 컨텐츠 필터링
- Fallback 메커니즘

Phase 4: 지속적 개선

# 1. 사용자 피드백 수집
def collect_feedback(question, answer, feedback):
    # 엄지 up/down
    db.save({
        "question": question,
        "answer": answer,
        "rating": feedback,
        "timestamp": now()
    })

# 2. 주기적 분석
- 낮은 평가 받은 답변 검토
- 자주 묻는 질문 파악
- 문서  발견

# 3. 모델 업그레이드
-  임베딩 모델 A/B 테스트
- LLM 모델 업그레이드

마치며

RAG는 LLM을 실용적으로 만드는 핵심 기술입니다. Fine-tuning처럼 비싸지도 않고, Prompt Engineering처럼 불안정하지도 않습니다.

핵심을 정리하면:

  1. 검색 + 생성: 필요한 정보를 찾아서 답변에 활용
  2. 환각 방지: 문서 기반 답변으로 신뢰성 확보
  3. 최신성: 재학습 없이 새 정보 반영
  4. 비용 효율: Fine-tuning의 1/100 비용

실무에서 성공하려면:

  • 좋은 검색이 80% - 임베딩, 청킹, 벡터 DB에 집중
  • 프롬프트 엔지니어링이 15% - LLM에게 명확한 지시
  • 나머지 5% - 모니터링과 지속적 개선

RAG는 “설정하고 잊기” 방식이 아닙니다. 계속 모니터링하고, 사용자 피드백을 받고, 개선해야 합니다. 하지만 제대로 구축하면, 회사 지식을 AI로 만드는 가장 현실적인 방법입니다.

다음 단계:

  • 간단한 프로토타입 만들어보기 (예제 1 활용)
  • 실제 문서 10개로 테스트
  • 검색 품질 평가
  • 점진적으로 고도화

RAG는 이론보다 실습이 중요합니다. 직접 만들어보면서 체득하는 것이 가장 빠른 학습 방법입니다.

참고 자료

논문

공식 문서 & 튜토리얼

벡터 데이터베이스 비교

  • Pinecone - 관리형 클라우드
  • Weaviate - 오픈소스, 확장성 좋음
  • Chroma - 오픈소스, 간단함
  • Qdrant - 오픈소스, 러시아산
  • Milvus - 오픈소스, 대규모 데이터

실습 자료

블로그 & 케이스 스터디

댓글