1 들어가며
대형 언어 모델(LLM)은 방대한 지식을 학습했지만, 여전히 근본적인 한계를 가지고 있다:
- 지식의 시간적 한계 (Knowledge Cutoff): 학습 데이터의 시점 이후 정보는 알지 못한다
- 도메인 특화 지식 부족: 기업 내부 문서, 전문 분야 자료 등에 접근 불가
- 할루시네이션 (Hallucination): 그럴듯하지만 사실이 아닌 정보를 생성
- 출처 불명확: 어디서 가져온 정보인지 추적 불가능
예를 들어, 다음과 같은 질문에는 답변하기 어렵다:
❌ "우리 회사의 2024년 신제품 출시 일정은?"
→ 내부 문서 없음
❌ "어제 발표된 신기술의 세부 사항은?"
→ Knowledge cutoff 이후
❌ "이 계약서의 3조 2항에 명시된 조건은?"
→ 특정 문서 접근 불가
Retrieval Augmented Generation (RAG)은 이러한 한계를 극복하는 핵심 기법이다. RAG는 외부 지식 베이스에서 관련 문서를 검색(Retrieve)하고, 이를 바탕으로 답변을 생성(Generate)한다.
RAG의 핵심 프로세스:
질문 → [1. 문서 검색] → 관련 문서 → [2. 문서+질문 전달] → LLM → [3. 답변 생성]
이번 포스트에서는 RAG의 동작 원리를 넘어, 프롬프트 엔지니어링 기법들(Chain-of-Thought, Self-Consistency, Prompt Chaining 등)과 RAG를 결합하는 다양한 패턴을 살펴본다. 또한 실무에서 마주치는 문제들(검색 실패, 정보 분산, 창의적 작업 부적합 등)과 해결책, 그리고 베스트 프랙티스와 최신 트렌드까지 다룬다.
2 프롬프트 엔지니어링과 RAG의 시너지
RAG와 프롬프트 엔지니어링을 결합하면 더 강력한 시스템을 만들 수 있다.
2.1 패턴 1: RAG + Chain-of-Thought
검색된 문서를 바탕으로 단계별 추론을 수행한다.
def rag_with_cot(query: str, rag: SimpleRAGSystem, top_k: int = 5) -> str:
"""
RAG + CoT 결합
"""
# Step 1: 문서 검색
documents = rag.retrieve(query, top_k=top_k)
# Step 2: CoT 프롬프트 구성
context = "\n\n".join([
f"[Document {i+1}]\n{doc['text']}"
for i, doc in enumerate(documents)
])
cot_prompt = f"""다음 문서들을 참고하여 질문에 답변하세요.
<documents>
{context}
</documents>
<question>
{query}
</question>
단계별로 생각하며 답변하세요:
1. 먼저, 질문을 이해합니다.
2. 문서에서 관련 정보를 찾습니다.
3. 정보들을 종합하여 결론을 도출합니다.
단계별 추론:"""
client = anthropic.Anthropic(api_key="your-api-key")
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1500,
temperature=0,
messages=[{"role": "user", "content": cot_prompt}]
)
return message.content[0].text효과: - 추론 과정이 명확 - 복잡한 질문에 강함 - 디버깅 용이
2.2 패턴 2: RAG + Self-Consistency
여러 답변을 생성하고 일관된 답변을 선택한다.
def rag_with_self_consistency(
query: str,
rag: SimpleRAGSystem,
num_samples: int = 5,
top_k: int = 5
) -> Dict:
"""
RAG + Self-Consistency
"""
# Step 1: 문서 검색 (한 번만)
documents = rag.retrieve(query, top_k=top_k)
# Step 2: 여러 답변 생성
answers = []
for i in range(num_samples):
result = rag.generate_answer(query, documents)
answers.append(result['answer'])
print(f"답변 {i+1}/{num_samples} 생성 완료")
# Step 3: 답변들 분석 및 다수결
consistency_prompt = f"""다음은 같은 질문에 대한 {num_samples}개의 답변입니다.
가장 일관되고 정확한 정보를 종합하여 최종 답변을 작성하세요.
질문: {query}
답변들:
"""
for i, ans in enumerate(answers, 1):
consistency_prompt += f"\n답변 {i}:\n{ans}\n"
consistency_prompt += "\n최종 답변 (가장 일관되고 정확한 내용):"
client = anthropic.Anthropic(api_key="your-api-key")
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1000,
temperature=0,
messages=[{"role": "user", "content": consistency_prompt}]
)
final_answer = message.content[0].text
return {
'query': query,
'individual_answers': answers,
'final_answer': final_answer
}효과: - ✅ 더 신뢰할 수 있는 답변 - ✅ 할루시네이션 감소 - ❌ 비용 증가 (num_samples배)
2.3 패턴 3: RAG + Prompt Chaining
복잡한 작업을 여러 단계로 나누고 각 단계에서 RAG 사용.
class RAGChain:
"""
RAG + Prompt Chaining
"""
def __init__(self, rag: SimpleRAGSystem):
self.rag = rag
self.client = anthropic.Anthropic(api_key="your-api-key")
def multi_hop_qa(self, query: str) -> Dict:
"""
Multi-hop QA: 여러 단계의 검색이 필요한 질문
예: "Anthropic의 CEO가 이전에 일했던 회사의 설립 연도는?"
→ Step 1: Anthropic CEO 찾기 (Dario Amodei)
→ Step 2: Dario가 이전에 일한 회사 찾기 (OpenAI)
→ Step 3: OpenAI 설립 연도 찾기 (2015)
"""
results = []
# Step 1: 질문 분해
decompose_prompt = f"""다음 질문을 답하기 위해 필요한 하위 질문들을 순서대로 나열하세요.
각 하위 질문의 답이 다음 하위 질문에 필요합니다.
질문: {query}
하위 질문들 (순서대로):
1."""
message = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=300,
temperature=0,
messages=[{"role": "user", "content": decompose_prompt}]
)
response = message.content[0].text
# 하위 질문 파싱
sub_queries = []
for line in response.split('\n'):
import re
line = re.sub(r'^\d+\.\s*', '', line.strip())
if line:
sub_queries.append(line)
print(f"🔍 하위 질문들:")
for i, sq in enumerate(sub_queries, 1):
print(f" {i}. {sq}")
print()
# Step 2: 각 하위 질문에 순차적으로 답변
accumulated_context = ""
for i, sub_query in enumerate(sub_queries, 1):
print(f"📝 하위 질문 {i} 처리 중: {sub_query}")
# 이전 답변을 컨텍스트로 포함
if accumulated_context:
enhanced_query = f"{sub_query}\n\n이전 답변 참고:\n{accumulated_context}"
else:
enhanced_query = sub_query
# RAG로 답변
docs = self.rag.retrieve(enhanced_query, top_k=3)
answer_result = self.rag.generate_answer(enhanced_query, docs)
answer = answer_result['answer']
print(f"✅ 답변: {answer}\n")
# 결과 저장
results.append({
'sub_query': sub_query,
'answer': answer,
'sources': docs
})
# 컨텍스트 누적
accumulated_context += f"Q: {sub_query}\nA: {answer}\n\n"
# Step 3: 최종 답변 종합
synthesis_prompt = f"""다음은 복잡한 질문에 대한 단계별 답변입니다.
이를 종합하여 원래 질문에 대한 간결한 최종 답변을 작성하세요.
원래 질문: {query}
단계별 답변:
{accumulated_context}
최종 답변:"""
message = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=500,
temperature=0,
messages=[{"role": "user", "content": synthesis_prompt}]
)
final_answer = message.content[0].text
return {
'query': query,
'sub_queries': results,
'final_answer': final_answer
}
# 사용 예시
rag_chain = RAGChain(rag)
result = rag_chain.multi_hop_qa(
"Anthropic의 CEO가 이전에 일했던 회사는 언제 설립되었나?"
)
print("=" * 80)
print("최종 답변:")
print(result['final_answer'])2.4 패턴 4: RAG + Few-Shot Learning
RAG로 검색된 예시를 Few-shot 프롬프트에 사용.
def rag_few_shot(query: str, rag: SimpleRAGSystem, num_examples: int = 3) -> str:
"""
RAG로 유사한 예시를 검색하고 Few-shot 프롬프트 구성
사용 사례: 특정 스타일로 글쓰기, 유사한 문제 해결 등
"""
# Step 1: 유사한 예시 검색
examples = rag.retrieve(query, top_k=num_examples)
# Step 2: Few-shot 프롬프트 구성
few_shot_prompt = "다음 예시들을 참고하여 비슷한 스타일로 답변하세요.\n\n"
for i, ex in enumerate(examples, 1):
few_shot_prompt += f"예시 {i}:\n{ex['text']}\n\n"
few_shot_prompt += f"이제 다음 질문에 답변하세요:\n{query}\n\n답변:"
client = anthropic.Anthropic(api_key="your-api-key")
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1000,
temperature=0.7,
messages=[{"role": "user", "content": few_shot_prompt}]
)
return message.content[0].text3 RAG의 한계와 대안
3.1 검색에 의존적
문제: 검색이 실패하면 답변도 실패
예시:
질문: "Python에서 리스트를 정렬하는 방법은?"
검색 결과: (검색 실패 - 관련 문서 없음)
답변: "제공된 문서에서 관련 정보를 찾을 수 없습니다."
→ 하지만 이건 LLM이 원래 알고 있는 내용!
해결책: Fallback to Model Knowledge
def rag_with_fallback(
query: str,
rag: SimpleRAGSystem,
confidence_threshold: float = 0.5
) -> Dict:
"""
RAG + Fallback 전략
1. RAG로 답변 시도
2. 신뢰도가 낮으면 모델 지식 사용
"""
# Step 1: RAG로 검색
documents = rag.retrieve(query, top_k=5)
# 검색 품질 평가
if not documents or max([1 - d['distance'] for d in documents]) < confidence_threshold:
print("⚠️ 검색 품질 낮음 - 모델 지식으로 답변")
# 모델 지식만 사용
fallback_prompt = f"""다음 질문에 답변하세요:
{query}
답변:"""
client = anthropic.Anthropic(api_key="your-api-key")
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1000,
temperature=0,
messages=[{"role": "user", "content": fallback_prompt}]
)
return {
'answer': message.content[0].text,
'mode': 'model_knowledge',
'sources': []
}
# Step 2: RAG 답변
result = rag.generate_answer(query, documents)
return {
'answer': result['answer'],
'mode': 'rag',
'sources': documents
}3.2 창의적 작업에 부적합
문제: RAG는 정보 검색 기반이므로 창의성이 제한됨
예시:
질문: "미래 도시에 대한 공상과학 소설을 써줘"
RAG: (문서 검색) → 기존 SF 소설 내용만 조합
→ 창의성 부족, 표절 위험
해결책: 작업 유형에 따라 선택적 RAG 사용
def adaptive_rag(query: str, rag: SimpleRAGSystem) -> Dict:
"""
작업 유형을 분석하고 적절한 방법 선택
"""
# Step 1: 작업 유형 분류
classify_prompt = f"""다음 질문을 분류하세요:
질문: {query}
카테고리:
- factual: 사실 기반 정보 요청
- creative: 창의적 콘텐츠 생성
- analytical: 분석 또는 추론
- conversational: 일상 대화
카테고리 (하나만):"""
client = anthropic.Anthropic(api_key="your-api-key")
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=20,
temperature=0,
messages=[{"role": "user", "content": classify_prompt}]
)
category = message.content[0].text.strip().lower()
# Step 2: 카테고리별 처리
if category == 'factual':
# RAG 사용
return rag_with_fallback(query, rag)
elif category == 'creative':
# 모델 지식만 사용 (RAG 스킵)
print("🎨 창의적 작업 - 모델 지식 사용")
creative_prompt = f"{query}\n\n창의적으로 답변하세요:"
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2000,
temperature=0.8, # 높은 temperature
messages=[{"role": "user", "content": creative_prompt}]
)
return {
'answer': message.content[0].text,
'mode': 'creative',
'sources': []
}
else:
# 기본: RAG + 모델 지식 결합
return rag_with_fallback(query, rag)3.3 문서 간 정보 종합 어려움
문제: 여러 문서에 분산된 정보를 종합하기 어려움
예시:
Doc 1: "Anthropic은 2021년 설립"
Doc 2: "Anthropic CEO는 Dario Amodei"
Doc 3: "Dario는 OpenAI 출신"
질문: "Anthropic의 설립 연도와 CEO의 이전 회사는?"
→ 3개 문서를 모두 참조해야 함
해결책: Iterative RAG
def iterative_rag(
query: str,
rag: SimpleRAGSystem,
max_iterations: int = 3
) -> Dict:
"""
반복적 RAG: 답변이 불완전하면 추가 검색
"""
accumulated_docs = []
accumulated_context = ""
for iteration in range(max_iterations):
print(f"\n🔄 Iteration {iteration + 1}/{max_iterations}")
# Step 1: 검색 (이전 컨텍스트 포함)
if accumulated_context:
enhanced_query = f"{query}\n\n이미 알고 있는 정보:\n{accumulated_context}"
else:
enhanced_query = query
docs = rag.retrieve(enhanced_query, top_k=5)
# 중복 제거하며 누적
new_docs = [d for d in docs if d not in accumulated_docs]
accumulated_docs.extend(new_docs)
print(f" 새로운 문서: {len(new_docs)}개")
# Step 2: 답변 생성
result = rag.generate_answer(query, accumulated_docs)
answer = result['answer']
# Step 3: 완성도 평가
completeness_prompt = f"""다음 답변이 질문에 완전히 답했는지 평가하세요.
질문: {query}
답변: {answer}
평가:
- complete: 질문에 완전히 답함
- incomplete: 추가 정보 필요
평가 (하나만):"""
client = anthropic.Anthropic(api_key="your-api-key")
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=20,
temperature=0,
messages=[{"role": "user", "content": completeness_prompt}]
)
completeness = message.content[0].text.strip().lower()
print(f" 완성도: {completeness}")
if 'complete' in completeness:
print("✅ 답변 완성")
break
# 다음 반복을 위해 현재 답변을 컨텍스트에 추가
accumulated_context = answer
return {
'query': query,
'answer': answer,
'iterations': iteration + 1,
'sources': accumulated_docs
}4 RAG + 다른 고급 기법 조합
4.1 RAG + Generate Knowledge
def rag_with_generated_knowledge(query: str, rag: SimpleRAGSystem) -> str:
"""
1. 모델이 배경 지식 생성
2. 생성된 지식으로 검색
3. 검색 결과 + 생성 지식으로 답변
"""
# Step 1: 배경 지식 생성
knowledge_prompt = f"""다음 질문과 관련된 배경 지식을 생성하세요:
질문: {query}
배경 지식:"""
client = anthropic.Anthropic(api_key="your-api-key")
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=300,
temperature=0.7,
messages=[{"role": "user", "content": knowledge_prompt}]
)
generated_knowledge = message.content[0].text
print(f"💡 생성된 배경 지식:\n{generated_knowledge}\n")
# Step 2: 생성된 지식을 쿼리에 추가하여 검색
enhanced_query = f"{query}\n\n관련 배경:\n{generated_knowledge}"
docs = rag.retrieve(enhanced_query, top_k=5)
# Step 3: 배경 지식 + 검색 결과로 답변
context = "\n\n".join([doc['text'] for doc in docs])
final_prompt = f"""다음 배경 지식과 문서들을 참고하여 질문에 답변하세요:
배경 지식:
{generated_knowledge}
검색된 문서:
{context}
질문: {query}
답변:"""
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1000,
temperature=0,
messages=[{"role": "user", "content": final_prompt}]
)
return message.content[0].text4.2 RAG + Tree of Thoughts
def rag_with_tot(query: str, rag: SimpleRAGSystem) -> str:
"""
RAG로 검색 → ToT로 추론
복잡한 분석 작업에 유용
"""
# Step 1: RAG로 관련 문서 검색
docs = rag.retrieve(query, top_k=5)
context = "\n\n".join([doc['text'] for doc in docs])
# Step 2: ToT 스타일 프롬프트
tot_prompt = f"""다음 문서들을 참고하여 질문에 답변하세요.
여러 접근 방법을 고려하고, 가장 좋은 방법을 선택하세요.
문서:
{context}
질문: {query}
3가지 접근 방법을 생각해보고, 각각을 평가한 후 최선의 답변을 제시하세요:
접근 1:
평가 1:
접근 2:
평가 2:
접근 3:
평가 3:
최종 답변:"""
client = anthropic.Anthropic(api_key="your-api-key")
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2000,
temperature=0.7,
messages=[{"role": "user", "content": tot_prompt}]
)
return message.content[0].text5 베스트 프랙티스 체크리스트
5.1 데이터 준비
- 문서 품질 확인: 오타, 형식 오류 제거
- 메타데이터 추가: 출처, 날짜, 카테고리 등
- 중복 제거: 동일하거나 매우 유사한 문서 제거
- 접근 제어: 민감 정보 필터링
5.2 청킹 전략
- 적절한 청크 크기: 512-1024 토큰 (일반적)
- 오버랩 설정: 50-100 토큰
- 구조 보존: 문장/문단 경계 존중
- 메타데이터 유지: 각 청크에 출처 정보
5.3 검색 최적화
- Hybrid Search 고려: 의미론 + 키워드
- 적절한 top_k: 3-5개 (일반적)
- Re-ranking 적용: 검색 품질 개선
- 쿼리 변환 활용: 확장, 분해, HyDE 등
5.4 프롬프트 설계
- 명확한 지시: “문서 기반 답변” 명시
- 할루시네이션 방지: “문서에 없으면 모른다고 답변”
- 출처 언급 유도: “어느 문서에서 가져왔는지 명시”
- 형식 지정: 원하는 답변 형식 예시 제공
5.5 성능 최적화
- 캐싱 활용: 반복 쿼리 캐싱
- 배치 처리: 임베딩 생성 시 배치
- 비동기 처리: 검색과 생성 파이프라인화
- 조기 종료: 충분히 좋은 답변 시 중단
5.6 평가 및 모니터링
- 검색 품질 측정: Precision, Recall, MRR
- 답변 품질 평가: Faithfulness, Relevance
- 지연 시간 모니터링: 검색/생성 시간 추적
- 사용자 피드백 수집: 썸업/썸다운, 상세 피드백
5.7 안전 및 보안
- 입력 검증: 악의적 쿼리 필터링
- 출력 검증: 유해 콘텐츠 탐지
- 접근 제어: 사용자별 문서 권한
- 개인정보 보호: PII 마스킹
6 일반적인 실수와 해결책
6.1 너무 큰 청크
- 청크 크기: 5000 토큰
- 결과: 관련 없는 내용이 많이 포함됨
- 해결책
- 청크 크기 줄이기 or
- 계층적 접근 or
- Parent Retriver
6.2 top_k 너무 크게 설정
- top_k = 50
- 결과: 노이즈가 많아 답변 품질 저하
- 해결책:
- 대부분의 경우: top_k = 3-5
- 또는 동적 결정
6.3 임베딩 모델 불일치
- 인덱싱: text-embedding-ada-002
- 검색: text-embedding-3-large
- 결과: 검색 품질 매우 나쁨
- 해결책: 항상 같은 Embedding 모델 사용
6.4 메타데이터 무시
- 모든 문서를 날짜 구분 없이 저장
- 결과: 오래된 정보가 검색됨
- 해결책: 메타데이터 활용하여 검색
7 정리 및 결론
7.1 RAG의 핵심 가치
- ✅ 지식의 시간적 한계 극복 (최신 정보)
- ✅ 도메인 특화 지식 부족 극복 (내부 문서)
- ✅ 할루시네이션 극복 (실제 출처 기반)
- ✅ 출처 추적 극복 (어디서 왔는지 명확)
7.2 언제 RAG를 사용할 것인가?
RAG 적합: - ✅ 사실 기반 Q&A - ✅ 문서 검색 및 분석 - ✅ 고객 지원 (FAQ, 매뉴얼) - ✅ 연구 보조 (논문, 보고서) - ✅ 내부 지식 관리
RAG 부적합: - ❌ 창의적 콘텐츠 생성 - ❌ 일반 상식 질문 (모델 지식으로 충분) - ❌ 복잡한 추론 (다른 기법 필요) - ❌ 실시간 대화 (검색 지연)
7.3 RAG vs Fine-tuning
| 특성 | RAG | Fine-tuning |
|---|---|---|
| 비용 | 낮음 | 높음 (재학습 필요) |
| 업데이트 | 쉬움 (문서만 추가) | 어려움 (재학습) |
| 출처 | 명확 | 불명확 |
| 도메인 적응 | 빠름 | 느림 |
| 추론 능력 | 제한적 | 강화 가능 |
| 추천 | 대부분의 경우 ⭐ | 특수 도메인만 |
7.4 향후 발전 방향
현재 트렌드: 1. Modular RAG: 각 컴포넌트를 교체 가능하게 2. Agentic RAG: LLM이 스스로 검색 전략 결정 3. Multi-modal RAG: 텍스트, 이미지, 테이블 통합 4. Contextual RAG: 대화 컨텍스트 고려 5. Self-improving RAG: 피드백으로 자동 개선
기대되는 발전: - 더 긴 컨텍스트 윈도우 → 검색 필요성 감소 - 더 똑똑한 검색 → 정확도 향상 - 더 빠른 처리 → 실시간 응답 가능 - 더 저렴한 비용 → 널리 보급
8 참고문헌
Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, N., … & Kiela, D. (2020). Retrieval-augmented generation for knowledge-intensive nlp tasks. Advances in Neural Information Processing Systems, 33, 9459-9474.
Gao, Y., Xiong, Y., Gao, X., Jia, K., Pan, J., Bi, Y., … & Wang, H. (2023). Retrieval-augmented generation for large language models: A survey. arXiv preprint arXiv:2312.10997.
Izacard, G., & Grave, E. (2021). Leveraging passage retrieval with generative models for open domain question answering. EACL 2021.
Borgeaud, S., et al. (2022). Improving language models by retrieving from trillions of tokens. ICML 2022.
Khattab, O., et al. (2022). Demonstrate-Search-Predict: Composing retrieval and language models for knowledge-intensive NLP. arXiv preprint arXiv:2212.14024.