RAG란 무엇인가 – 외부 지식을 AI에 실시간으로 주입하기
모델이 모르는 것을 알게 만드는 기술. RAG의 원리부터 프롬프트 설계까지 완전 정복.
들어가며: Layer 2가 비어있었다
10편에서 컨텍스트 윈도우를 네 개의 레이어로 나눴습니다. 시스템 프롬프트, 검색된 외부 문서, 대화 히스토리, 현재 사용자 메시지. 그리고 솔직하게 하나를 인정했습니다.
"Layer 2, 즉 검색된 외부 문서를 어떻게 채울 것인지는 11편에서 다룬다."
이 편이 그 약속을 지킵니다.
모델은 훈련 데이터 이후의 세계를 모릅니다. 회사 내부 문서도 모릅니다. 어제 갱신된 정책도, 오늘 발표된 뉴스도 알 수 없습니다. 아무리 잘 짜인 프롬프트도 모델이 접근할 수 없는 정보는 만들어낼 수 없습니다. 만들어낸다면 그건 할루시네이션입니다.
**RAG(Retrieval-Augmented Generation, 검색 증강 생성)**는 이 문제를 해결하는 기술입니다. 모델이 추론하기 전에 관련 문서를 찾아서 컨텍스트에 넣어줌으로써, 모델이 모르는 것을 알게 만듭니다.
이 편에서는 RAG가 무엇인지, 어떻게 작동하는지, 그리고 Layer 2를 어떻게 설계해야 모델이 가장 잘 활용하는지를 처음부터 끝까지 다룹니다.
1. RAG란 무엇인가
1.1. 이름부터 이해하기
RAG는 세 단어의 조합입니다.
- Retrieval: 외부 저장소에서 관련 문서를 검색한다
- Augmented: 그 문서로 컨텍스트를 보강한다
- Generation: 보강된 컨텍스트를 바탕으로 답변을 생성한다
한 문장으로 정리하면 이렇습니다.
"모델이 답하기 전에, 관련 문서를 찾아서 컨텍스트에 넣어준다."
단순하게 들릴 수 있지만 이 흐름이 LLM 활용의 패러다임을 바꿉니다. 모델 자체를 바꾸는 것이 아니라, 모델에게 주어지는 정보를 설계하는 것입니다. 10편에서 강조했던 맥락, "무엇을 말할 것인가가 아니라 모델이 추론할 때 무엇을 보게 할 것인가"가 RAG에서 가장 직접적으로 실현됩니다.
1.2. 왜 필요한가
LLM에는 두 가지 근본적인 한계가 있습니다.
첫 번째는 **지식 단절(Knowledge Cutoff)**입니다. 모델은 훈련이 끝난 시점 이후의 정보를 알 수 없습니다. GPT-4o의 지식은 특정 날짜에서 멈춰있고, 오늘 발표된 뉴스나 어제 변경된 제품 사양은 존재하지 않는 정보입니다. 이 문제를 프롬프트로 해결하려 해도 한계가 명확합니다. 모델이 모르는 것을 프롬프트가 만들어줄 수는 없습니다.
두 번째는 내부 지식 부재입니다. 회사 내부 문서, 개인 노트, 특정 도메인의 비공개 데이터는 훈련 데이터에 포함되지 않습니다. 법무팀의 계약서 데이터베이스, 지난 5년치 고객 응대 기록, 어제 작성된 회의록. 이런 정보들은 아무리 강력한 모델도 알 수 없습니다.
RAG는 이 두 한계를 동시에 해결합니다. 모델을 업데이트하는 것이 아니라, 필요한 정보를 추론 시점에 직접 주입하는 방식으로.
2. RAG의 3단계 파이프라인
모든 RAG 시스템은 동일한 흐름을 따릅니다. 검색하고, 정제하고, 주입하는 것입니다. 각 단계를 구체적으로 살펴보겠습니다.

2.1. 1단계 — 검색 (Retrieve)
사용자의 질문과 관련된 문서를 외부 저장소에서 찾아옵니다. 여기서 "외부 저장소"는 일반적으로 벡터 데이터베이스입니다. 질문과 의미적으로 가장 가까운 문서들을 찾아오는 것이 이 단계의 목표입니다.
사용자: "Claude의 컨텍스트 윈도우 크기가 어떻게 되나요?"
→ 벡터 DB에서 의미 유사 문서 검색
→ [Doc 1] Claude 모델 사양 문서 (유사도: 0.94)
→ [Doc 2] LLM 컨텍스트 크기 비교표 (유사도: 0.87)
→ [Doc 3] Claude 최신 업데이트 노트 (유사도: 0.81)
검색의 품질이 전체 RAG 시스템의 품질을 결정합니다. 잘못된 문서를 찾아오면, 아무리 좋은 모델도 잘못된 답을 생성합니다. 이 단계에 대해서는 3절에서 더 자세히 다룹니다.
2.2. 2단계 — 정제 (Refine)
검색된 문서를 그대로 컨텍스트에 넣으면 토큰이 폭발합니다. 30페이지짜리 PDF 세 개를 통째로 넣으면 컨텍스트 한도를 훌쩍 넘깁니다. 이 단계에서는 관련도가 낮은 부분을 걷어내고 핵심만 남깁니다.
[Doc 1] 전체 38페이지 → 관련 단락 3개만 추출 → 약 800 토큰
[Doc 2] 전체 15페이지 → 관련 표 1개만 추출 → 약 300 토큰
[Doc 3] 유사도 0.81 → 임계값(0.85) 미만 → 제외
임계값 이하의 문서를 걸러내는 것이 중요합니다. 10편에서 다뤘던 컨텍스트 오염 문제가 여기서도 적용됩니다. 관련 없는 문서를 주입하면 모델의 주의가 분산되고 오히려 정확도가 떨어집니다.
2.3. 3단계 — 주입 (Inject)
정제된 문서를 컨텍스트 Layer 2에 구조적으로 배치하고, 모델이 그것을 근거로 답변을 생성합니다. 단순히 텍스트를 붙여넣는 것이 아니라, 모델이 각 문서의 역할을 명확히 인식할 수 있도록 구조를 설계해야 합니다.
<knowledge>
<doc id="1" relevance="0.94" source="claude-specs.pdf">
Claude Sonnet 4.6의 컨텍스트 윈도우는 200K 토큰입니다.
이는 약 15만 단어 분량의 텍스트를 한 번에 처리할 수 있는 크기입니다...
</doc>
<doc id="2" relevance="0.87" source="llm-comparison.xlsx">
| 모델 | 최대 컨텍스트 |
| Claude Sonnet 4.6 | 200K 토큰 |
| GPT-4o | 128K 토큰 |
| Gemini 2.5 Pro | 1M 토큰 |
</doc>
</knowledge>
relevance와 source 속성을 포함하면 모델이 각 문서의 신뢰도와 출처를 함께 고려해 답변을 생성합니다. 주입 설계에 대해서는 5절에서 자세히 다룹니다.
3. 벡터 검색: 왜 키워드 검색으로는 부족한가
3.1. 키워드 검색의 근본적인 한계
전통적인 검색 엔진은 단어의 일치를 기반으로 작동합니다. 질문에 있는 단어가 문서에 있으면 찾고, 없으면 못 찾습니다. RAG에 이 방식을 쓰면 심각한 문제가 생깁니다.
질문: "컨텍스트 윈도우 크기가 얼마나 돼?"
문서: "모델이 한 번에 처리할 수 있는 토큰 한도는 200K입니다."
→ 키워드 검색: "컨텍스트 윈도우"라는 단어가 없음 → 검색 실패
→ 벡터 검색: 의미가 같음 → 검색 성공
같은 개념을 다른 표현으로 쓴 문서는 키워드 검색에서 완전히 누락됩니다. 내부 문서일수록 표현이 다양하고 일관성이 없기 때문에 이 문제는 더 심각해집니다.
3.2. 벡터 검색의 원리
텍스트를 수백~수천 차원의 숫자 배열(벡터)로 변환하면, 의미가 비슷한 문장은 벡터 공간에서 가깝게 위치합니다. 이 변환 과정을 **임베딩(Embedding)**이라고 부릅니다.
"컨텍스트 윈도우 크기" → [0.23, 0.87, 0.41, -0.15, ...]
"토큰 처리 한도" → [0.21, 0.89, 0.44, -0.13, ...] ← 가깝다
"오늘 점심으로 뭘 먹을까" → [0.91, 0.12, 0.77, 0.62, ...] ← 멀다
두 벡터 사이의 각도(코사인 유사도)로 의미적 거리를 계산합니다. 단어가 달라도 의미가 같으면 유사도가 높게 나옵니다. 이것이 벡터 검색이 키워드 검색보다 RAG에 적합한 이유입니다.
3.3. 임베딩 실용 지식
임베딩은 문서를 저장하는 시점에 한 번 생성하고, 이후에는 재사용합니다. 검색 시에는 질문도 동일한 임베딩 모델로 변환해서 문서 벡터들과 비교합니다. 동일한 모델로 임베딩해야 벡터 공간이 일치합니다. 서로 다른 모델로 임베딩한 벡터를 비교하면 의미 있는 유사도가 나오지 않습니다.
| 항목 | 내용 |
|---|---|
| 임베딩 모델 | OpenAI text-embedding-3-small, Cohere Embed, BGE, E5 등 |
| 벡터 DB | Pinecone, Supabase Vector, Weaviate, Chroma |
| 문서 임베딩 | 저장 시 1회 생성, 이후 재사용 |
| 쿼리 임베딩 | 검색 시마다 생성, 동일 모델 필수 |
4. 청킹: 문서를 어떻게 자를 것인가
4.1. 청킹이 중요한 이유
벡터 검색에서 검색 단위는 문서 전체가 아니라 문서를 잘라낸 조각, 즉 **청크(Chunk)**입니다. 100페이지 PDF를 통째로 하나의 벡터로 만들면 어떤 일이 일어날지 생각해 보겠습니다.
그 PDF에는 도입부, 기술 사양, 가격 정책, FAQ, 면책 조항이 모두 섞여있습니다. 이것을 하나의 벡터로 압축하면, 어떤 질문에도 적당히 비슷하고 어떤 질문에도 정확하게 맞지 않는 평균적인 벡터가 됩니다. "가격 정책"을 물어봤을 때 이 PDF가 검색되더라도, 정작 가격 정책 단락은 100페이지 어딘가에 묻혀있습니다.
**청킹(Chunking)**은 이 문제를 해결하기 위해 문서를 의미 있는 조각으로 나누는 작업입니다. 청크가 작을수록 검색 정밀도가 높아지지만 문맥이 줄어들고, 클수록 문맥이 풍부해지지만 관련 없는 내용이 섞입니다. 적절한 크기를 찾는 것이 청킹의 핵심입니다.
4.2. 청킹 전략
고정 크기 청킹은 가장 단순한 방식입니다. 텍스트를 일정한 길이로 자릅니다. 구현이 쉽고 빠르지만, 문장이 청크 경계에서 잘릴 수 있습니다. overlap 파라미터로 경계 양쪽에 중첩 구간을 두면 문맥 연속성을 어느 정도 보완할 수 있습니다.
def fixed_chunk(text: str, size: int = 500, overlap: int = 50) -> list[str]:
chunks = []
for i in range(0, len(text), size - overlap):
chunks.append(text[i:i + size])
return chunks
의미 단위 청킹은 문서의 구조를 활용합니다. 마크다운이라면 ## 헤딩을 기준으로, HTML이라면 <section> 태그를 기준으로 나눕니다. 자연스러운 의미 경계를 따르기 때문에 검색 품질이 높습니다. 실제 서비스에서는 이 방식을 권장합니다.
def semantic_chunk(markdown: str) -> list[str]:
# ## 헤딩 단위로 분리
sections = re.split(r'\n## ', markdown)
# 너무 짧은 섹션은 이전 청크에 병합
return [s.strip() for s in sections if len(s.strip()) > 100]
계층형 청킹은 두 방식의 장점을 결합합니다. 문서를 큰 단위(섹션)와 작은 단위(단락)로 모두 저장합니다. 검색은 작은 단위로 정밀하게 하고, 실제로 컨텍스트에 주입할 때는 해당 단락이 속한 섹션 전체를 넣습니다. 검색 정밀도와 문맥 풍부함을 동시에 얻을 수 있습니다.
[부모 청크] ## 4. 가격 정책 (섹션 전체, ~1000 토큰)
├── [자식 청크] 기본 요금제 설명 (단락, ~200 토큰)
├── [자식 청크] 프리미엄 요금제 (단락, ~200 토큰)
└── [자식 청크] 환불 정책 (단락, ~150 토큰)
검색: "환불은 어떻게 되나요?" → 자식 청크 "환불 정책" 매칭
주입: 자식의 부모인 "가격 정책" 섹션 전체를 컨텍스트에 삽입
5. 주입 설계: 찾은 문서를 어떻게 넣을 것인가
검색하고 정제했다면, 이제 컨텍스트에 어떻게 배치할 것인지가 남습니다. 단순히 문서를 이어붙이는 것이 아니라, 모델이 가장 잘 활용할 수 있는 구조로 설계해야 합니다.
5.1. 배치 순서가 결과를 바꾼다
10편에서 다뤘던 Lost in the Middle 현상을 여기서 적용합니다. 모델은 컨텍스트의 앞과 뒤에 있는 정보를 더 잘 활용하고, 가운데에 있는 정보는 상대적으로 무시하는 경향이 있습니다.
이 때문에 관련도 순으로 단순하게 나열하면 가장 중요한 문서가 가운데에 묻혀 활용되지 않을 수 있습니다. 중요한 문서는 앞이나 뒤에 배치하고, 보조 자료는 가운데에 두는 것이 효과적입니다.
[시스템 프롬프트]
[★ 가장 관련도 높은 문서 → 앞에 배치]
[관련도 중간 수준의 문서들 → 가운데]
[★ 두 번째로 중요한 문서 → 뒤에 배치]
[사용자 메시지]
5.2. 태그 구조로 역할을 명시한다
문서를 컨텍스트에 넣을 때 구조화된 태그를 사용하면, 모델이 각 영역의 역할을 명확하게 인식합니다. 특히 Claude는 XML 태그를 기반으로 컨텍스트를 구분하는 것을 공식적으로 권장합니다.
<instructions>
아래 <knowledge> 태그 안의 문서들만을 근거로 답변하라.
문서에 없는 내용은 "확인이 필요합니다"라고 말하라.
모든 사실 주장에는 [Doc N] 형식으로 출처를 표기하라.
날짜가 충돌하는 경우 최신 문서를 우선하라.
</instructions>
<knowledge>
<doc id="1" relevance="0.94" date="2026-04">
...문서 내용...
</doc>
<doc id="2" relevance="0.87" date="2025-11">
...문서 내용...
</doc>
</knowledge>
<query>
사용자 질문
</query>
relevance와 date 속성을 포함하면 모델이 최신성과 관련도를 함께 판단합니다. 특히 같은 주제를 다루지만 날짜가 다른 문서가 여러 개 있을 때 date 속성이 있으면 모델이 최신 정보를 우선할 수 있습니다.
5.3. 출처 인용을 강제하라
RAG 시스템에서 할루시네이션을 억제하는 가장 효과적인 방법 중 하나는 출처 인용을 강제하는 것입니다. 모델이 "이 정보는 [Doc 1]에 있다"는 형식으로 출처를 표기하도록 지시하면, 자연스럽게 문서에 없는 내용을 만들어내는 것에 제약이 생깁니다. 근거를 표기할 수 없는 주장을 하기가 어려워지기 때문입니다.
# 인용 없는 지시 (피할 것)
"아래 문서를 참고해서 답하라."
# 인용 강제 지시 (권장)
"답변의 모든 사실 주장에는 [Doc N]을 표기하라.
문서에서 찾을 수 없는 내용은 절대 추가하지 말라.
문서에서 찾을 수 없으면 '문서에서 확인되지 않는 내용입니다'라고 명시하라."
6. RAG 실패 패턴
RAG를 도입했는데도 성능이 기대에 못 미치는 경우, 대부분은 다음 네 가지 패턴 중 하나입니다.
6.1. 잘못된 검색 — Garbage In, Garbage Out
가장 흔한 실패 패턴입니다. 관련 없는 문서가 검색되면, 모델은 그것을 근거로 답합니다. 검색 자체가 잘못되면 이후 단계는 의미가 없습니다.
질문: "환불 정책이 어떻게 되나요?"
검색 결과: 배송 정책 문서 (키워드 '정책'이 일치했기 때문)
→ 모델: 배송 정책의 내용을 환불 정책으로 혼동해 답변
유사도 임계값을 설정해서 일정 수준 이하의 문서를 제외하고, 필요하다면 Reranker 모델을 추가해 1차 검색 결과를 다시 정렬하는 방법이 효과적입니다.
6.2. 너무 많이 넣기 — 컨텍스트 오염
관련 문서를 최대한 많이 넣으면 더 좋을 것 같지만, 실제로는 반대입니다. 10편에서 다뤘듯 관련 없는 정보가 컨텍스트에 들어오면 모델의 주의가 분산됩니다. 연구에 따르면 관련 없는 문서를 5개 추가할 때마다 정확도가 15~20%씩 떨어집니다.
상위 3~5개 문서만 사용하고, 유사도 임계값으로 품질을 관리하는 것이 더 좋은 결과를 냅니다.
TOP_K = 5
RELEVANCE_THRESHOLD = 0.75
docs = vector_search(query, top_k=TOP_K)
docs = [d for d in docs if d.relevance >= RELEVANCE_THRESHOLD]
6.3. 청크 경계 문제 — 반쪽짜리 정보
청크가 중요한 문장 중간에서 잘리면 모델은 불완전한 정보를 받습니다. 이 경우 모델은 잘린 앞부분만으로 추론해서 잘못된 결론에 도달할 수 있습니다.
# 경계에서 잘린 청크
"환불은 구매일로부터 30일 이내에 신청할 수 있으며,
제품이 미개봉 상태인 경우에 한해..."
→ [청크 경계] → 조건의 나머지가 다음 청크에 있음
# 모델의 해석: 30일 이내면 조건 없이 환불 가능
청크에 overlap을 설정하거나, 문장 경계를 기준으로 청킹하거나, 앞서 설명한 계층형 청킹으로 이 문제를 완화할 수 있습니다.
6.4. 날짜 충돌 — 오래된 문서가 이기는 경우
같은 주제를 다루는 문서가 여러 개이고 날짜가 다를 때, 모델이 더 오래된 문서의 내용을 선택하는 경우가 있습니다. 가격 정보, 정책, 사양처럼 변경이 잦은 정보에서 특히 위험합니다.
date 속성을 명시하고 시스템 프롬프트에 "날짜가 충돌하면 최신 문서를 우선하라"는 지시를 추가하는 것이 가장 단순하고 효과적인 해결책입니다.
7. 실전 RAG 프롬프트 템플릿
지금까지의 내용을 하나의 템플릿으로 정리하면 다음과 같습니다. 7편에서 다뤘던 변수 시스템을 RAG에 그대로 적용한 구조입니다.
<instructions>
당신은 [서비스명]의 지식 베이스 AI입니다.
## 답변 규칙
- <knowledge> 태그 안의 문서들만을 근거로 답변한다
- 문서에 없는 내용은 "문서에서 확인되지 않는 내용입니다"라고 답한다
- 모든 사실 주장에는 [Doc N] 형식으로 출처를 표기한다
- 날짜가 충돌하면 최신 문서를 우선한다
- 문서의 내용을 요약하거나 해석하지 말고 그대로 전달한다
</instructions>
<knowledge>
{{retrieved_documents}}
</knowledge>
<history>
{{conversation_history}}
</history>
<query>
{{user_message}}
</query>
{{변수}}는 실행 시점에 동적으로 채워집니다. 검색된 문서, 이전 대화, 사용자 메시지가 각각의 자리에 들어가며 하나의 컨텍스트를 형성합니다.
결론: RAG는 컨텍스트 엔지니어링의 핵심 엔진
10편을 마치고 나서 한 가지 질문이 남아있었습니다. 컨텍스트를 4개의 레이어로 나누었는데, Layer 2를 어떻게 채울 것인가. RAG가 그 답입니다.
프롬프트를 잘 쓰는 것은 여전히 중요합니다. 하지만 프롬프트보다 더 근본적인 것이 있습니다. 모델이 추론할 때 어떤 정보를 보게 할 것인가. RAG는 그 Layer 2를 체계적으로 채우는 방법입니다.
모델을 더 크게 만들거나 파인튜닝하는 것보다, 올바른 정보를 올바른 방식으로 컨텍스트에 주입하는 것이 대부분의 실용적인 문제에서 더 빠르고 비용 효율적인 해결책입니다. 이것이 RAG가 2023년부터 급속도로 보편화된 이유입니다.
핵심 원칙 요약
| 원칙 | 핵심 |
|---|---|
| 벡터 검색 | 키워드가 아닌 의미로 찾는다. 표현이 달라도 의미가 같으면 찾는다 |
| 청킹 | 의미 단위로 자른다. overlap으로 경계를 보완한다 |
| 정제 | 임계값 이하는 제외한다. 3~5개 이하로 유지한다 |
| 배치 | 중요한 문서는 앞이나 뒤에. 가운데는 보조 자료 |
| 인용 강제 | 출처 표기를 의무화해 할루시네이션을 억제한다 |
12편을 향하여
RAG로 외부 지식을 주입하는 법을 익혔다면, 자연스럽게 다음 질문이 생깁니다.
"AI가 검색 자체를 스스로 판단하고 수행하게 할 수 있을까?"
지금까지의 RAG는 검색을 외부 시스템이 처리하고 결과를 모델에게 전달하는 방식이었습니다. 하지만 AI가 스스로 "어떤 도구를 쓸지"를 결정하고, 직접 검색하고, 결과를 보고 다음 행동을 판단하는 패턴이 있습니다. 이것이 ReAct — Reasoning and Acting입니다.
**[12편: ReAct 패턴 – 추론과 도구 사용을 엮는 에이전트의 핵심]**에서는 AI가 단순한 응답 기계에서 스스로 판단하고 행동하는 에이전트로 진화하는 과정을 다룹니다.
