k
korAI
중급 전체
중급2026-06-287분

내 문서를 Claude에 연결하는 가장 빠른 RAG 입문: 검색→주입→응답 3단계

RAG(Retrieval-Augmented Generation)는 모델이 '모르는 내용'을 외부 문서에서 실시간으로 꺼내 답변에 활용하는 패턴입니다. 벡터 DB 없이도 작은 문서셋으로 RAG 흐름을 직접 구현해 봅니다.

ragretrievalcontext-injection

RAG가 필요한 순간

Claude는 학습 시점 이후의 정보, 사내 문서, 개인 노트를 알지 못합니다. RAG는 '모델 재학습 없이 최신·전용 지식을 주입' 하는 가장 현실적인 방법입니다. 핵심 흐름은 단 3단계입니다.

사용자 질문 → [검색] 관련 청크 추출 → [주입] 컨텍스트로 삽입 → [생성] Claude 응답

단계 1 — 문서 청킹과 검색

실제 서비스에서는 벡터 DB(Pinecone, Weaviate 등)를 쓰지만, 원리를 먼저 익히려면 TF-IDF 수준의 단순 키워드 검색으로 충분합니다. 중요한 건 청크 크기입니다.

| 청크 크기 | 장점 | 단점 | |-----------|------|------| | 너무 작음(< 100 토큰) | 정밀한 검색 | 문맥 단절 | | 적당함(200~500 토큰) | 균형 | — | | 너무 큼(> 1000 토큰) | 풍부한 문맥 | 노이즈, 비용 증가 |

단계 2 — 컨텍스트 주입 전략

검색된 청크를 프롬프트에 넣을 때 출처 메타데이터를 함께 제공하면 모델이 더 정확하게 인용합니다. 또한 청크가 여러 개라면 관련도 높은 순으로 정렬해 앞에 배치하세요(모델은 앞부분에 더 주의를 기울입니다).

실전 코드: 사내 FAQ RAG

import anthropic
from typing import TypedDict

client = anthropic.Anthropic()

# 1) 문서 청크 (실제 환경에서는 DB에서 로드)
DOCS: list[dict] = [
    {"id": "faq-1", "source": "인사규정.md", "text": "연차 휴가는 입사 1년 후 15일이 발생하며, 매년 1일씩 추가됩니다. 최대 25일까지 누적됩니다."},
    {"id": "faq-2", "source": "인사규정.md", "text": "반차는 오전(09:00~13:00)과 오후(14:00~18:00)로 구분되며, 0.5일 차감됩니다."},
    {"id": "faq-3", "source": "복지안내.md", "text": "건강검진 비용은 연 1회 1인당 최대 20만원까지 회사가 지원합니다."},
    {"id": "faq-4", "source": "복지안내.md", "text": "점심 식대는 1일 8,000원이며 월말 급여와 함께 지급됩니다."},
]


def retrieve(query: str, top_k: int = 2) -> list[dict]:
    """단순 키워드 매칭 검색 (실제 서비스에서는 벡터 검색으로 교체)"""
    query_tokens = set(query.lower().split())
    scored = []
    for doc in DOCS:
        overlap = len(query_tokens & set(doc["text"].lower().split()))
        scored.append((overlap, doc))
    scored.sort(key=lambda x: x[0], reverse=True)
    return [doc for _, doc in scored[:top_k] if _ > 0]


def build_rag_prompt(question: str, chunks: list[dict]) -> str:
    if not chunks:
        context = "(관련 문서를 찾지 못했습니다)"
    else:
        context = "\n\n".join(
            f"[출처: {c['source']}]\n{c['text']}" for c in chunks
        )
    return f"""아래 문서를 바탕으로 질문에 답하세요.
문서에 없는 내용은 '문서에서 확인할 수 없습니다'라고 말하세요.

=== 참고 문서 ===
{context}

=== 질문 ===
{question}"""


def rag_answer(question: str) -> str:
    chunks = retrieve(question)
    prompt = build_rag_prompt(question, chunks)

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=512,
        messages=[{"role": "user", "content": prompt}],
    )
    return response.content[0].text


if __name__ == "__main__":
    questions = [
        "연차는 몇 일이나 받을 수 있나요?",
        "점심값 지원이 얼마예요?",
        "주식 옵션은 어떻게 되나요?",  # 문서에 없는 질문
    ]
    for q in questions:
        print(f"Q: {q}")
        print(f"A: {rag_answer(q)}\n{'-'*40}")

적용 전 체크리스트

  • [ ] 청크 크기를 200~500 토큰 범위로 설정했는가?
  • [ ] 각 청크에 출처(파일명, 섹션) 메타데이터가 포함되어 있는가?
  • [ ] 검색 결과가 0건일 때 폴백 메시지를 처리했는가?
  • [ ] 모델에게 '모르면 모른다고 말하라'는 지시를 명시했는가?
  • [ ] 컨텍스트 전체 토큰이 모델 한도(claude-sonnet-4-6: 200k)의 50% 이하인가?
  • [ ] 프로덕션 전환 시 키워드 검색을 벡터 검색으로 교체할 계획을 세웠는가?