이번 블로그에서는 AI가 외부 문서를 참고해 답변하는 기술인 RAG(Retrieval-Augmented Generation)의 원리에 대한 글을 작성해 보겠습니다.
들어가며
ChatGPT나 Claude 같은 LLM에게 "우리 회사 규정이 어떻게 돼?"라고 물어보면 어떻게 될까요? 당연히 모릅니다. LLM은 학습 시점까지 공개된 데이터로만 학습되었기 때문에, 사내 문서나 최신 정보처럼 학습 데이터에 없는 내용은 알 수가 없습니다.
더 큰 문제는 모르는 내용을 모른다고 하지 않고 그럴듯하게 지어내는 경우가 있다는 점입니다. 이를 할루시네이션(Hallucination)이라고 합니다. 이 글에서는 이 문제를 해결하는 대표적인 방법인 RAG가 어떤 원리로 동작하는지, 그리고 Python으로 직접 간단한 RAG를 구현하는 과정을 정리했습니다.
LLM은 왜 모르는 것을 모를까?
LLM의 한계는 크게 세 가지로 정리할 수 있습니다.
- 지식의 시점 제한: 학습이 끝난 이후의 정보는 알지 못합니다.
- 비공개 정보 부재: 사내 위키, 내부 규정, 개인 문서처럼 공개되지 않은 데이터는 학습에 포함되지 않습니다.
- 근거 없는 생성: LLM은 "다음에 올 그럴듯한 단어"를 예측하는 모델이기 때문에, 사실 여부와 상관없이 자연스러운 문장을 만들어 냅니다.
이 문제를 해결하는 가장 직관적인 방법은 시험에 비유하면 이해하기 쉽습니다. 모든 내용을 외워서 시험을 보는 것이 기존 LLM이라면, RAG는 오픈북 시험입니다. 답을 쓰기 전에 교재에서 관련 페이지를 찾아 펼쳐 놓고, 그 내용을 근거로 답을 작성하는 방식입니다.
RAG란?
RAG는 Retrieval-Augmented Generation의 약자로, 우리말로는 검색 증강 생성이라고 부릅니다. 이름 그대로 세 단계로 나뉩니다.
- Retrieval(검색): 사용자의 질문과 관련된 문서를 외부 저장소에서 찾습니다.
- Augmented(증강): 찾은 문서를 질문과 함께 프롬프트에 넣어 LLM에게 전달합니다.
- Generation(생성): LLM은 전달받은 문서를 근거로 답변을 생성합니다.
핵심은 모델을 다시 학습시키지 않는다는 점입니다. 모델은 그대로 두고, 질문할 때마다 필요한 지식을 프롬프트에 끼워 넣어 주는 방식이기 때문에 문서만 바꾸면 최신 정보를 바로 반영할 수 있습니다.
RAG는 어떻게 동작하는가?
RAG의 전체 흐름은 미리 문서를 준비해 두는 인덱싱 단계와, 질문이 들어왔을 때 실행되는 검색·생성 단계로 나뉩니다.
[인덱싱 단계 - 사전 준비]
문서 → 청킹(Chunking) → 임베딩(Embedding) → 벡터 DB 저장
[검색·생성 단계 - 질문 시점]
질문 → 임베딩 → 벡터 DB에서 유사한 청크 검색(Top-K) → 질문 + 검색된 청크를 프롬프트로 구성 → LLM → 답변
1. 청킹(Chunking)
문서 전체를 한 번에 LLM에 넣을 수는 없기 때문에, 문서를 일정 크기의 조각(Chunk)으로 나눕니다. 보통 수백 토큰 단위로 자르고, 문맥이 끊기지 않도록 앞뒤 청크가 조금씩 겹치게(Overlap) 나눕니다.
2. 임베딩(Embedding)
임베딩은 텍스트를 숫자 벡터로 변환하는 과정입니다. 임베딩 모델은 의미가 비슷한 문장일수록 벡터 공간에서 가까운 위치에 놓이도록 학습되어 있습니다. 예를 들어 "재택근무는 주 몇 회 가능한가요?"와 "원격 근무 허용 횟수"는 사용된 단어가 달라도 비슷한 벡터로 변환됩니다.
3. 벡터 DB 저장 및 유사도 검색
임베딩된 청크는 벡터 DB(Pinecone, Chroma, pgvector 등)에 저장됩니다. 질문이 들어오면 질문도 같은 모델로 임베딩한 뒤, 저장된 벡터 중 가장 가까운 K개를 찾습니다. 이때 거리 계산에는 주로 코사인 유사도(Cosine Similarity)를 사용합니다.
cos(A, B) = (A · B) / (|A| × |B|)
두 벡터의 방향이 같을수록 1에 가까워지고, 관련이 없을수록 0에 가까워집니다.
4. 프롬프트 증강 및 생성
검색된 청크를 질문과 함께 프롬프트에 넣습니다. 이때 "아래 문서만 근거로 답하라", "문서에 없으면 모른다고 답하라"는 지시를 함께 넣어 주는 것이 중요합니다. 이 부분은 지난 글에서 다룬 프롬프트 엔지니어링이 그대로 적용되는 지점입니다.
실습: Python으로 간단한 RAG 만들기
벡터 DB 없이 NumPy만으로 RAG의 핵심 흐름을 구현해 보겠습니다. 예시로 가상의 회사 사내 규정 문서를 사용합니다.
1. 패키지 설치
pip install sentence-transformers numpy anthropic
export ANTHROPIC_API_KEY=<YOUR_API_KEY>
2. 문서 준비 및 청킹
실습에서는 문장 하나를 청크 하나로 사용합니다. 실제 환경에서는 긴 문서를 아래 함수처럼 일정 길이로 겹치게 잘라서 사용합니다.
def chunk_text(text, size=300, overlap=50):
chunks = []
start = 0
while start < len(text):
chunks.append(text[start:start + size])
start += size - overlap
return chunks
docs = [
"연차 휴가는 입사 1년 차부터 15일이 부여되며, 3년마다 1일씩 추가됩니다.",
"재택근무는 주 2회까지 가능하며, 팀장 승인 후 사내 포털에서 신청합니다.",
"법인카드는 1회 50만 원을 초과하는 경우 사전 결재가 필요합니다.",
"신규 입사자는 입사 첫 주에 보안 교육을 반드시 이수해야 합니다.",
]
3. 임베딩 생성
한국어 문장에 특화된 임베딩 모델을 사용합니다. normalize_embeddings=True로 벡터를 정규화해 두면, 내적(dot product)만으로 코사인 유사도를 계산할 수 있습니다.
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer("jhgan/ko-sroberta-multitask")
doc_embeddings = model.encode(docs, normalize_embeddings=True)
print(doc_embeddings.shape) # (4, 768)
4. 검색 함수 구현
질문을 임베딩한 뒤 모든 문서와의 유사도를 계산하고, 점수가 높은 순서대로 K개를 반환합니다.
def retrieve(query, k=2):
q = model.encode([query], normalize_embeddings=True)[0]
scores = doc_embeddings @ q
top = np.argsort(scores)[::-1][:k]
return [(docs[i], float(scores[i])) for i in top]
for doc, score in retrieve("일주일에 집에서 몇 번 일할 수 있어?"):
print(f"{score:.3f} | {doc}")
질문에 "재택근무"라는 단어가 없는데도, 재택근무 규정 문서가 가장 높은 점수로 검색되는 것을 확인할 수 있습니다. 키워드가 아닌 의미로 검색하기 때문입니다.
5. 프롬프트 증강 및 답변 생성
검색된 문서를 프롬프트에 넣어 LLM에게 전달합니다.
import anthropic
client = anthropic.Anthropic()
def answer(query):
contexts = retrieve(query)
context_text = "\n".join(f"- {doc}" for doc, _ in contexts)
prompt = f"""아래 [참고 문서]만 근거로 질문에 답하세요.
문서에 답이 없으면 "문서에서 찾을 수 없습니다"라고 답하세요.
[참고 문서]
{context_text}
[질문]
{query}"""
res = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=500,
messages=[{"role": "user", "content": prompt}],
)
return res.content[0].text
print(answer("일주일에 집에서 몇 번 일할 수 있어?"))
테스트: RAG 유무에 따라 답변이 어떻게 달라지는지 확인
테스트 1: RAG 없이 질문
검색 단계 없이 질문만 LLM에 전달하면, 모델은 해당 회사의 규정을 알 수 없기 때문에 "회사마다 다르다"는 일반적인 답변을 하거나, 그럴듯한 횟수를 지어내기도 합니다.
테스트 2: RAG 적용 후 질문
같은 질문에 대해 이번에는 주 2회까지 가능하며, 팀장 승인 후 사내 포털에서 신청해야 한다는 문서 기반의 정확한 답변이 돌아옵니다.
테스트 3: 문서에 없는 내용 질문
"사내 식당 운영 시간이 어떻게 돼?"처럼 문서에 없는 내용을 물어보면, 프롬프트의 지시에 따라 "문서에서 찾을 수 없습니다"라고 답합니다. 할루시네이션을 줄이는 데 이 한 줄의 지시가 큰 역할을 합니다.
RAG vs 파인튜닝
LLM에 새로운 지식을 넣는 또 다른 방법으로 파인튜닝(Fine-tuning)이 있습니다. 두 방식은 목적이 다릅니다.
| 구분 | RAG | 파인튜닝 |
| 지식 업데이트 | 문서만 교체하면 즉시 반영 | 재학습 필요 |
| 비용 | 상대적으로 저렴 | 학습 비용 발생 |
| 출처 제시 | 근거 문서 제시 가능 | 어려움 |
| 적합한 용도 | 사실·지식 기반 Q&A | 말투, 형식, 특정 작업 방식 학습 |
정리하면 무엇을 아는가는 RAG로, 어떻게 말하는가는 파인튜닝으로 해결하는 것이 일반적입니다.
주의할 점
- 검색이 틀리면 답변도 틀립니다. RAG의 성능은 LLM보다 검색 품질에 더 크게 좌우됩니다. 엉뚱한 청크가 검색되면 LLM은 그 내용을 근거로 틀린 답을 만듭니다.
- 청크 크기 조절이 중요합니다. 너무 작으면 문맥이 잘리고, 너무 크면 관련 없는 내용이 섞여 검색 정확도가 떨어집니다. 문서 특성에 맞게 실험해 보는 것이 좋습니다.
- 의미 검색만으로는 부족할 때가 있습니다. 제품 코드나 고유명사처럼 정확한 키워드 매칭이 필요한 경우, BM25 같은 키워드 검색을 함께 쓰는 하이브리드 검색이나 리랭커(Reranker)를 추가하는 방식을 고려해 볼 수 있습니다.
- 문서가 바뀌면 재인덱싱이 필요합니다. 원본 문서를 수정했다면 해당 청크의 임베딩도 다시 생성해야 합니다.
- RAG 시스템도 평가가 필요합니다. 검색 정확도와 답변 정확도를 각각 측정해야 어느 단계가 문제인지 알 수 있습니다. 평가 방법은 LLM 평가(Evaluation)의 원리 글을 참고해 주세요.
마무리
LLM은 학습하지 않은 정보를 알 수 없고, 모르는 내용을 지어내기도 합니다. RAG는 질문과 관련된 문서를 먼저 검색한 뒤 그 내용을 프롬프트에 넣어 주는 방식으로, 모델을 다시 학습시키지 않고도 최신 정보와 비공개 문서를 근거로 답변하게 만듭니다. 결국 RAG는 LLM에게 오픈북 시험을 보게 하는 기술이라고 정리할 수 있습니다.
이것으로 RAG의 동작 원리에 대한 글을 마치겠습니다. 감사합니다!
'AI' 카테고리의 다른 글
| AI 답변의 정확도를 어떻게 측정할까? LLM 평가(Evaluation)의 원리 (0) | 2026.10.21 |
|---|---|
| LLM에게 일을 잘 시키는 법, 프롬프트 엔지니어링 (0) | 2026.10.14 |
| AI 모델이 검색을 하는 방법 (0) | 2026.10.07 |
| Jev (TypeSafe AI의 System One 모델) (0) | 2026.09.23 |
| Claude Code Guide (0) | 2026.09.16 |