k
korAI
중급 전체
중급2026-05-187분

RAG 입문: 문서를 어떻게 자르고 어떻게 넣어야 AI가 틀리지 않는가

RAG(검색 증강 생성)의 핵심은 모델 선택이 아니라 문서 청킹과 컨텍스트 주입 전략에 있습니다. 가장 흔한 실수와 실용적인 설계 패턴을 소개합니다.

RAGretrievalcontext-injection

RAG가 필요한 이유

Claude는 학습 데이터 이후의 정보를 모릅니다. 사내 문서, 최신 규정, 개인화된 데이터를 다루려면 검색 결과를 프롬프트에 직접 삽입하는 RAG 패턴이 필수입니다.

RAG의 3단계 파이프라인

[문서] → ① 청킹 → ② 벡터 검색 → ③ 컨텍스트 주입 → [Claude]

각 단계에서 실수가 쌓이면 모델이 정확한 문서를 받고도 틀린 답을 냅니다.

① 청킹: 가장 과소평가되는 단계

| 전략 | 설명 | 적합한 문서 | |---|---|---| | 고정 크기 | 500토큰씩 자름 | 균일한 구조의 로그, DB 레코드 | | 문장 경계 | 마침표·단락 기준 | 일반 문서, 뉴스 | | 의미 단위 | 섹션/제목 기준 | 매뉴얼, 법령, 기술 문서 | | 슬라이딩 윈도우 | 50% 겹치게 자름 | 맥락이 경계를 넘나드는 문서 |

황금 규칙: 청크 하나가 단독으로 읽혔을 때 의미를 가져야 합니다. "위에서 설명한 것처럼"으로 시작하는 청크는 실패합니다.

② 컨텍스트 주입과 Claude 호출

아래는 검색된 청크를 Claude에 주입하는 Python 예시입니다.

import anthropic
from typing import List

client = anthropic.Anthropic()

def build_rag_prompt(query: str, retrieved_chunks: List[dict]) -> str:
    """
    retrieved_chunks: [{"source": "doc_name", "content": "...", "score": 0.92}]
    """
    # 관련도 낮은 청크 필터링 (임계값: 0.7)
    filtered = [c for c in retrieved_chunks if c["score"] >= 0.7]

    if not filtered:
        return "관련 문서를 찾지 못했습니다. 질문을 바꿔보세요."

    # 출처 명시 → 환각 방지의 핵심
    context_block = "\n\n".join(
        f"[출처: {c['source']}]\n{c['content']}"
        for c in filtered[:3]  # 상위 3개만 (토큰 절약)
    )

    return f"""아래 문서만을 근거로 질문에 답하세요.
문서에 없는 내용은 "문서에서 확인할 수 없습니다"라고 답하세요.

=== 참고 문서 ===
{context_block}
=================

질문: {query}"""


def ask_with_rag(query: str, retrieved_chunks: List[dict]) -> str:
    prompt = build_rag_prompt(query, retrieved_chunks)

    response = client.messages.create(
        model="claude-sonnet-4-6",  # 긴 컨텍스트 이해엔 Sonnet 권장
        max_tokens=1024,
        system="당신은 주어진 문서만을 기반으로 정확하게 답하는 어시스턴트입니다.",
        messages=[{"role": "user", "content": prompt}],
    )

    return response.content[0].text


# 사용 예시
chunks = [
    {"source": "휴가규정_2026.pdf", "content": "연차는 입사 1년 후 15일이 발생하며...", "score": 0.91},
    {"source": "취업규칙.pdf", "content": "반차는 오전/오후로 구분되며...", "score": 0.74},
    {"source": "급여명세서.pdf", "content": "2025년 12월 급여 내역...", "score": 0.42},  # 필터링됨
]

answer = ask_with_rag("반차 신청은 어떻게 하나요?", chunks)
print(answer)

③ 흔한 실패 패턴과 해결책

| 증상 | 원인 | 해결책 | |---|---|---| | 엉뚱한 문서 기반 답변 | 유사도 임계값 없음 | score ≥ 0.7 필터 추가 | | "위에서처럼" 오류 | 청크 경계 문제 | 슬라이딩 윈도우 또는 섹션 단위 청킹 | | 토큰 초과 오류 | 청크 수 무제한 | 상위 3~5개로 제한 | | 환각 답변 | 프롬프트에 제약 없음 | "문서에 없으면 모른다고 해" 명시 |

체크리스트

  • [ ] 청크 하나를 맥락 없이 읽어도 의미 전달이 되는가?
  • [ ] 유사도 임계값 필터가 적용되어 있는가?
  • [ ] 프롬프트에 "문서에 없으면 모른다고 해" 지시가 있는가?
  • [ ] 출처(source) 를 청크와 함께 모델에 전달하는가?
  • [ ] 주입하는 청크 수를 토큰 한도에 맞게 제한하는가?
  • [ ] 긴 컨텍스트 처리에 Sonnet 계열을 사용하고 있는가?