가이드 목록으로
아키텍처·2026년 5월 25일·최종 업데이트 2026년 9월 23일·14분 분량

RAG 구현 가이드 — Claude와 Gemini를 활용한 프로덕션 검색 증강 생성

실제로 작동하는 프로덕션 RAG 시스템을 구축하세요 — 100줄짜리 데모가 아닙니다. 청킹 전략, 임베딩 모델 선택, 검색 순위 지정, 프롬프트 구조, 환각 제어, 일일 10,000건 쿼리에서도 월 비용을 세 자릿수로 유지하는 방법을 다룹니다.

마지막 검토일: .

대부분의 RAG 데모는 프로덕션에서 실패합니다. 10개 질문으로 구성된 데모 자료에서는 작동하지만 사용자가 새로운 질문을 하는 순간 망가집니다. 이 가이드는 실제로 견고한 아키텍처와 패턴을 다룹니다. 의미 경계를 존중하는 청킹, 퍼지 쿼리와 리터럴 쿼리를 모두 포착하는 하이브리드 검색, 환각을 방지하는 프롬프트 구조, 규모가 커져도 비용을 일정하게 유지하는 비용 관리입니다.

중요한 다섯 가지 결정

  1. 청킹 전략 — 모델보다 검색 품질에 더 큰 영향을 줍니다
  2. 임베딩 모델 — 재현율의 상한을 결정합니다
  3. 검색 전략 — 벡터만으로는 충분하지 않습니다
  4. 프롬프트 구조 — 모델이 환각을 일으킬지 결정합니다
  5. 생성 모델 + 캐싱 — 단위 경제성을 결정합니다

1. 청킹 — 영리함보다 단순함

이 부분을 지나치게 복잡하게 생각하지 마세요. 청크당 1000~1500자, 오버랩 100~200자를 사용하는 재귀적 문자 분할기를 쓰세요. 먼저 문단 경계에서 나누고, 그다음 문장, 마지막으로 단어 단위에서 나누려고 시도합니다. 도메인 전반에서 안정적입니다:

chunk.py
# Recursive character splitter with overlap — the boring choice that wins
from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1200,
    chunk_overlap=200,
    separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(document)
# 1200 chars ≈ 300-400 tokens — fits 5+ chunks in a Sonnet context window
# with room for system prompt + question + answer

더 영리해 보이지만 거의 도움이 되지 않는 것들: 문장 인식 모델, 마크다운 인식 분할기, 임베딩을 사용하는 슬라이딩 윈도우. 나쁜 방법은 아니지만, 엄청난 노력에 비해 개선 폭이 미미합니다. 그 노력은 대신 검색에 투자하세요.

2. 임베딩 모델 — text-embedding-3-large가 기본값

임베딩은 Kunavo에서 제공하지 않습니다. 이 단계에서는 임베딩 제공업체를 직접 호출하세요. /v1/embeddings은 그 뒤에 활성화된 모델이 없는 구현된 와이어 형식이므로 해당 요청은 실패합니다. 호출 가능한 항목의 기준은 항상 GET /v1/models입니다. 실제로는 RAG 파이프라인에 두 번째 키 외에는 아무것도 추가되지 않습니다. 임베딩 호출과 생성 호출은 어차피 별도의 요청이므로 OpenAI, Voyage 또는 Cohere에서 임베딩하고 Kunavo에서 생성하면 됩니다.

text-embedding-3-large은 대부분의 프로덕션 사용 사례에서 다국어 지원과 정확성의 최적 균형을 제공하며, OpenAI가 직접 공개한 요율이 적용됩니다. 여기서 확인하지 말고 OpenAI의 가격 페이지에서 확인하세요. 해당 서비스를 재판매하지 않으므로 그 가격을 인용해서는 안 됩니다. 청킹 전략이나 모델 자체를 변경하면 다시 임베딩하세요. 콘텐츠가 업데이트될 때는 다시 임베딩하지 말고 새 청크만 추가하면 됩니다.

더 작은 임베딩이 작동하는 예외: 순수 영어의 짧은 텍스트 검색(FAQ, 제품 카드). 이러한 경우 text-embedding-3-small은 품질 손실이 거의 없이 4배 저렴합니다. 다국어 또는 기술 콘텐츠라면 large를 유지하세요.

3. 검색 — 순수 벡터는 하이브리드에 뒤처집니다

벡터 유사도는 퍼지 의미 매칭("어떻게 취소하나요" → "구독 취소 정책")에 뛰어납니다. 리터럴 용어("SKU-A92837" 또는 "주문 #4729")에는 매우 약합니다. 해결책은 하이브리드입니다. BM25 키워드 검색을 병렬로 실행한 다음 reciprocal rank fusion을 적용하세요:

retrieve.py
# Hybrid retrieval: vector similarity + BM25 keyword scoring
# Pure vector misses literal keywords (product SKUs, IDs, dates)
def retrieve(question: str, k: int = 5) -> list[dict]:
    q_embed = embed([question])[0]
    vector_hits = vector_db.similarity_search(q_embed, k=k * 2)
    keyword_hits = bm25_search(question, k=k * 2)

    # Reciprocal rank fusion — simple, robust
    scores: dict[str, float] = {}
    for rank, hit in enumerate(vector_hits):
        scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)
    for rank, hit in enumerate(keyword_hits):
        scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)

    top_ids = sorted(scores, key=scores.get, reverse=True)[:k]
    return [chunk_by_id[i] for i in top_ids]

괜찮은 임베딩 모델을 선택한 후 얻을 수 있는 가장 큰 단일 품질 향상입니다. 보류된 100개 질문 평가 세트에서 recall@5를 추적하세요. BM25를 추가한 후 70%에서 90%로 올라갔다면 "모르겠습니다" 실패의 3분의 1을 제거한 것입니다.

4. 프롬프트 구조 — 환각을 방지하는 세 가지 패턴

answer.py
# Final RAG prompt structure — three sections, citations enforced
SYSTEM_PROMPT = """You answer based exclusively on the supplied Context.
- Cite the [doc_id] for each factual claim.
- If the context doesn't answer the question, say "I don't have that
  information" — do not extrapolate or use general knowledge.
- Be concise. No throat-clearing."""

def answer(question: str) -> dict:
    chunks = retrieve(question, k=5)
    context = "\n\n---\n\n".join(
        f"[doc:{c['id']}] {c['text']}" for c in chunks
    )
    resp = client.chat.completions.create(
        model="claude-sonnet-4-6",
        messages=[
            {"role": "system", "content": [{
                "type": "text",
                "text": SYSTEM_PROMPT,
                "cache_control": {"type": "ephemeral"},
            }]},
            {"role": "user", "content": f"# Context\n{context}\n\n# Question\n{question}"},
        ],
        max_tokens=600,
    )
    return {
        "text": resp.choices[0].message.content,
        "sources": [c["id"] for c in chunks],
    }

시스템 프롬프트에서 반드시 지켜야 할 세 가지 사항:

  • 출처 인용 — 모델이 각 주장에 [doc:42]을 첨부하도록 하세요. 인용된 ID가 실제가 아니라면 환각을 포착한 것입니다
  • 명시적 거부 — "컨텍스트가 답하지 못하면 정보가 없다고 말하세요." 이것이 없으면 모델은 세상에 대한 지식으로 답을 완성합니다
  • 간결한 출력 — 짧은 답변은 정확한 답변과 상관관계가 있습니다. 최대 600토큰은 좋은 프로덕션 기본값입니다

5. 생성 모델 + 캐싱 — 비용 계층

Claude Sonnet 4.6이 기본값입니다. 비용이 빠듯하다면 Claude Haiku 4.5가 저렴한 대체 옵션입니다. 시스템 프롬프트는 모든 호출에서 동일하므로 항상 시스템 프롬프트를 cache_control로 감싸세요. 캐싱을 사용하면 해당 부분의 비용이 입력 요율의 10%로 줄어듭니다.

프로덕션 규모에서의 현실적인 쿼리당 비용(컨텍스트 5K, 출력 500, 캐싱 사용):

모델쿼리당 비용하루 10,000개 쿼리 기준Sonnet 대비 품질
claude-sonnet-4-6~$0.007~$70/일기준선
claude-haiku-4-5~$0.001~$10/일~85%, 4배 저렴

하루 10,000개 쿼리 기준 → Sonnet은 $70/일, Haiku는 $10/일입니다. 중요한 사용 사례(고객 대상, 법률, 의료)에는 Sonnet을, 내부 또는 대량 처리에는 Haiku를 선택하세요.

프로덕션에서 발생하는 문제와 포착 방법

  • 분포 드리프트: 학습 코퍼스가 실제 사용자 질문과 더 이상 일치하지 않습니다. 매주 100개 쿼리를 샘플링하고 recall@5를 수동으로 확인하세요
  • 오래된 임베딩: 원본 문서가 업데이트되었지만 임베딩이 갱신되지 않았습니다. 매주 인덱스 크기와 소스 크기를 추적하세요
  • 가짜 인용: 모델이 실제처럼 보이는 문서 ID를 만들어냅니다. 렌더링 전에 모든 [doc:N]을 실제 검색된 ID 목록과 대조해 검증하세요
  • 지연 시간 급증: 벡터 DB가 벡터 100만 개를 넘으면 급격히 느려집니다. HNSW 인덱싱을 사용하고 멀티테넌트라면 테넌트별로 분할하세요

4주 프로덕션 로드맵

  1. 1주 차: 문서 100개, 테스트 질문 10개로 프로토타입을 만들고 수동 평가
  2. 2주 차: 전체 코퍼스로 확장하고, 하이브리드 검색을 구축하며, 100개 질문 평가 세트 작성
  3. 3주 차: 평가를 반복하며 청킹 + 시스템 프롬프트를 조정하고 내부 사용자에게 출시
  4. 4주 차: 모니터링, 비용 예산, 출처 인용 UI를 포함한 공개 출시

자주 묻는 질문

프로덕션 RAG 시스템은 어떤 청크 크기를 사용해야 하나요?

청크당 1,000–1,500자, 오버랩은 100–200자로 설정하고, 문단 경계에서 먼저 나눈 다음 문장, 단어 순으로 나누는 재귀적 문자 분할기를 사용하세요. 문장 인식 모델, Markdown 인식 분할기 및 슬라이딩 윈도우는 훨씬 더 많은 작업을 들이면 조금 더 우수하지만, 그 노력은 검색에 투자하는 편이 더 큰 효과를 냅니다.

RAG 파이프라인은 어떤 임베딩 모델을 사용해야 하나요?

text-embedding-3-large는 다국어 및 기술 콘텐츠의 기본값입니다. FAQ와 제품 카드처럼 순수 영어의 짧은 텍스트 검색에는 text-embedding-3-small이 품질 손실이 거의 없이 약 4배 저렴합니다. 임베딩은 Kunavo에서 제공되지 않는다는 점에 유의하세요. /v1/embeddings는 구현된 와이어 형식이지만 뒤에서 활성화된 모델은 없으므로 임베딩 단계는 OpenAI, Voyage 또는 Cohere를 직접 호출하고 생성 단계는 Kunavo를 호출합니다. 두 요청은 어느 경우든 별개이므로 파이프라인에 추가되는 비용은 두 번째 키 하나뿐입니다.

벡터 검색만으로 RAG 검색을 충분히 처리할 수 있나요?

아니요. 벡터 유사도는 퍼지 의미 매칭에는 잘 작동하지만 SKU나 주문 번호 같은 리터럴 용어에는 실패합니다. BM25 키워드 검색을 병렬로 실행하고 reciprocal rank fusion으로 병합하면 일반적으로 recall@5가 약 70%에서 약 90%로 올라갑니다. 이는 괜찮은 임베딩 모델을 선택한 후 얻을 수 있는 가장 큰 단일 품질 향상입니다.

RAG 시스템이 환각을 일으키지 않도록 하려면 어떻게 해야 하나요?

시스템 프롬프트에 세 가지 패턴을 적용하세요. 모든 사실 주장에 [doc:N] 인용을 요구하고, 컨텍스트가 답을 제공하지 않으면 정보가 없다고 말하도록 모델에 지시하며, 답변을 짧게 유지하세요. 최대 600 출력 토큰은 좋은 프로덕션 기본값입니다. 그런 다음 렌더링 전에 인용된 모든 ID를 검색된 집합과 대조해 검증하세요. 실제로 존재하지 않는 ID는 포착된 환각입니다.

프로덕션 RAG 쿼리 비용은 얼마인가요?

컨텍스트 5K, 출력 500토큰, 프롬프트 캐싱 사용 기준: Claude Sonnet 4.6에서는 쿼리당 약 $0.007, Claude Haiku 4.5에서는 약 $0.001입니다(4배 저렴하며 품질은 대략 85%). 하루 10,000개 쿼리라면 Sonnet은 약 $70/일, Haiku는 약 $10/일입니다.