⚡ 중급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% 이하인가?
- [ ] 프로덕션 전환 시 키워드 검색을 벡터 검색으로 교체할 계획을 세웠는가?