RAG 쉽게 이해하기 - 내 문서로 AI 챗봇 만드는 핵심 기술
RAG 쉽게 이해하기 - 내 문서로 AI 챗봇 만드는 핵심 기술
2026년 4월 기준 | AI 신기능 분석 · 실전 구현 가이드
요약: RAG(Retrieval-Augmented Generation)는 LLM이 내 문서를 검색해서 답변하게 만드는 기술입니다. GPT나 Claude에게 회사 내부 문서, 기술 문서, 논문 등을 "읽게" 하고 그 내용 기반으로 답변을 생성합니다. 환각(hallucination) 문제를 해결하는 가장 실용적인 방법이고, 파인튜닝 없이도 구현할 수 있어서 실무에서 가장 많이 쓰입니다.
RAG가 뭔가
RAG는 Retrieval-Augmented Generation의 약자입니다. 직역하면 "검색으로 보강된 생성"인데, 쉽게 말하면 AI가 답변하기 전에 관련 문서를 먼저 찾아보게 하는 기술입니다.
비유하자면 이렇습니다. 시험 볼 때 오픈북 시험과 닫힌 시험의 차이라고 생각하면 됩니다.
| 일반 LLM | RAG 적용 LLM |
| 학습한 내용만으로 답변 (닫힌 시험) | 관련 자료를 찾아본 뒤 답변 (오픈북 시험) |
| 최신 정보 반영 불가 | 문서 업데이트만 하면 최신 정보 반영 |
| 모르는 건 지어냄 (환각) | 출처 기반 답변, 환각 대폭 감소 |
제가 처음 RAG를 접한 건 사내 기술 문서 챗봇을 만들 때였습니다. 팀원들이 매번 Confluence에서 문서를 찾느라 시간을 쓰길래, "슬랙에서 질문하면 관련 문서를 찾아서 답변해주는 봇"을 만들어보자고 했는데, 그때 RAG라는 개념을 알게 됐습니다. 결론부터 말하면, 파인튜닝 없이 기존 API에 검색 기능만 얹어도 꽤 쓸만한 결과가 나왔습니다.
왜 RAG가 필요한가 - 환각 문제
LLM의 가장 큰 문제는 환각(hallucination)입니다. 모르는 내용도 자신 있게 지어내는 현상입니다.
예를 들어 GPT에게 "우리 회사 복지 제도 알려줘"라고 물으면, 학습 데이터에 우리 회사 정보가 없으니 그럴듯한 일반적인 복지 제도를 만들어서 답합니다. 연차 15일이라고 답하는데 실제로는 20일일 수도 있는 것입니다. 이게 내부 시스템에 들어가면 큰 문제가 됩니다.
환각이 발생하는 이유
LLM은 본질적으로 "다음 토큰 예측기"입니다. 학습 데이터에서 패턴을 학습해서 가장 그럴듯한 다음 단어를 생성하는 구조입니다. 정확한 사실을 저장하고 있는 데이터베이스가 아닙니다. 그래서 학습 데이터에 없는 내용은 패턴 기반으로 "그럴듯하게" 만들어냅니다.
RAG가 이 문제를 해결하는 방식
RAG는 이걸 구조적으로 해결합니다. LLM에게 질문을 던지기 전에, 관련 문서를 검색해서 프롬프트에 함께 넣어주는 것입니다.
# RAG 없이
프롬프트: "우리 회사 연차 제도 알려줘"
→ LLM이 지어낸 답변
# RAG 적용
프롬프트: """
아래 문서를 참고해서 답변해줘.
[검색된 문서]
- 인사규정 3조: 연차는 입사 1년차 15일, 2년차부터 매년 1일씩 추가...
- 복지규정 7조: 특별휴가는 경조사 시 3~7일 부여...
질문: 우리 회사 연차 제도 알려줘
"""
→ 문서 기반의 정확한 답변
이렇게 하면 LLM이 지어낼 이유가 없습니다. 참고할 문서가 바로 앞에 있으니까요. 실제로 RAG를 적용하면 환각률이 체감상 80% 이상 줄어듭니다. 물론 완전히 0%로 만들 수는 없지만, 출처 문서를 함께 보여주면 사용자가 검증도 가능합니다.
RAG vs 파인튜닝 비교
"그냥 파인튜닝하면 안 되나?"라는 질문을 많이 받습니다. 상황에 따라 다르지만, 대부분의 경우 RAG가 더 실용적입니다. 직접 둘 다 해보고 정리한 비교표입니다.
| 항목 | RAG | 파인튜닝 |
| 구현 난이도 | 중간 (API + 벡터DB) | 높음 (GPU, 학습 파이프라인) |
| 비용 (초기) | 낮음 (임베딩 비용만) | 높음 (GPU 비용, 학습 시간) |
| 비용 (운영) | 토큰 사용량 증가 (검색 결과 포함) | 일반 API 비용과 동일 |
| 데이터 업데이트 | 문서 추가만 하면 즉시 반영 | 재학습 필요 (수시간~수일) |
| 환각 제어 | 출처 명시 가능, 검증 용이 | 여전히 환각 가능성 있음 |
| 답변 스타일 변경 | 프롬프트로만 제어 (한계 있음) | 모델 자체가 스타일 학습 |
| 필요 데이터량 | 문서 몇 개부터 가능 | 최소 수백~수천 건 학습 데이터 |
| 적합한 경우 | 사내 문서 QA, 고객 지원, 기술 문서 검색 | 특정 도메인 전문 언어, 코드 생성 |
제 경험상 90%의 상황에서 RAG로 충분합니다. 파인튜닝이 필요한 건 "모델의 행동 자체를 바꿔야 할 때"뿐입니다. 예를 들어 의료 전문 용어를 자연스럽게 쓰게 하거나, 특정 코딩 스타일을 따르게 하는 경우입니다. 단순히 "우리 데이터를 알게 하고 싶다"면 RAG가 답입니다.
RAG 동작 원리 4단계
RAG 파이프라인은 크게 4단계로 나뉩니다.
1단계: 문서 분할 (Chunking)
긴 문서를 적절한 크기의 조각(chunk)으로 나눕니다. 보통 500~1000 토큰 단위로 나누는데, 너무 작으면 문맥이 끊기고, 너무 크면 검색 정확도가 떨어집니다. 이 chunk 크기를 정하는 게 생각보다 중요합니다. 저는 보통 512 토큰에 128 토큰 오버랩을 줍니다.
2단계: 임베딩 (Embedding)
각 chunk를 벡터(숫자 배열)로 변환합니다. 임베딩 모델이 텍스트의 의미를 고차원 벡터 공간에 매핑하는 것입니다. 의미가 비슷한 텍스트는 벡터 공간에서 가까이 위치하게 됩니다. OpenAI의 text-embedding-3-small이나 한국어 특화 모델인 Ko-Sentence-BERT 등을 쓸 수 있습니다.
3단계: 벡터 검색 (Retrieval)
사용자 질문도 같은 방식으로 임베딩한 뒤, 벡터DB에서 가장 가까운(유사한) chunk들을 찾아옵니다. 이게 "검색" 단계입니다. ChromaDB, Pinecone, Weaviate, FAISS 같은 벡터DB를 사용합니다.
4단계: 생성 (Generation)
검색된 chunk들을 프롬프트에 넣고, LLM에게 이 내용을 참고하여 답변하라고 요청합니다. 여기서 프롬프트 설계가 중요합니다. "주어진 문서에 없는 내용은 '모르겠습니다'라고 답해줘"를 추가하면 환각을 더 줄일 수 있습니다.
흐름 정리: 문서 → 분할(Chunk) → 임베딩(Vector) → 벡터DB 저장 → 질문 입력 → 질문 임베딩 → 유사 chunk 검색 → 프롬프트에 합쳐서 LLM 호출 → 답변 생성
파이썬으로 RAG 직접 구현하기
이론은 그만하고 코드를 보겠습니다. ChromaDB를 벡터DB로 쓰고, OpenAI API로 임베딩과 생성을 하는 간단한 RAG 파이프라인입니다. 실제로 제가 사이드 프로젝트에서 쓰는 구조를 단순화한 것입니다.
라이브러리 설치
pip install chromadb openai tiktoken
1단계: 문서 준비 및 분할
import tiktoken
def chunk_text(text, chunk_size=512, overlap=128):
"""텍스트를 토큰 기준으로 chunk로 분할"""
encoder = tiktoken.encoding_for_model("gpt-4")
tokens = encoder.encode(text)
chunks = []
start = 0
while start < len(tokens):
end = start + chunk_size
chunk_tokens = tokens[start:end]
chunk_text = encoder.decode(chunk_tokens)
chunks.append(chunk_text)
start += chunk_size - overlap # 오버랩 적용
return chunks
# 예시: 기술 문서 로드
with open("my_document.txt", "r", encoding="utf-8") as f:
raw_text = f.read()
chunks = chunk_text(raw_text)
print(f"총 {len(chunks)}개 chunk 생성됨")
오버랩을 주는 이유는 chunk 경계에서 문맥이 잘리는 걸 방지하기 위해서입니다. 128 토큰 정도 겹치게 하면 대부분의 경우 문맥이 유지됩니다.
2단계: ChromaDB에 임베딩 저장
import chromadb
from openai import OpenAI
client = OpenAI() # OPENAI_API_KEY 환경변수 필요
chroma_client = chromadb.PersistentClient(path="./chroma_db")
# 컬렉션 생성 (테이블 같은 개념)
collection = chroma_client.get_or_create_collection(
name="my_docs",
metadata={"hnsw:space": "cosine"} # 코사인 유사도 사용
)
def get_embedding(text):
"""OpenAI 임베딩 API 호출"""
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
# chunk들을 임베딩해서 ChromaDB에 저장
for i, chunk in enumerate(chunks):
embedding = get_embedding(chunk)
collection.add(
ids=[f"chunk_{i}"],
embeddings=[embedding],
documents=[chunk],
metadatas=[{"source": "my_document.txt", "chunk_index": i}]
)
print(f"{len(chunks)}개 chunk 저장 완료")
여기서 text-embedding-3-small은 OpenAI의 최신 임베딩 모델입니다. 1536 차원 벡터를 생성하고, 비용이 저렴합니다. 100만 토큰당 $0.02 수준이라 문서 수천 페이지를 임베딩해도 몇 백 원밖에 안 듭니다.
3단계: 검색 + 답변 생성
def rag_query(question, top_k=3):
"""RAG 파이프라인: 질문 → 검색 → 생성"""
# 질문을 임베딩
question_embedding = get_embedding(question)
# ChromaDB에서 유사한 chunk 검색
results = collection.query(
query_embeddings=[question_embedding],
n_results=top_k
)
# 검색된 문서를 컨텍스트로 구성
context = "\n\n---\n\n".join(results["documents"][0])
# LLM에게 컨텍스트와 함께 질문
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": """주어진 참고 문서를 기반으로 질문에 답변하세요.
규칙:
1. 참고 문서에 있는 내용만 사용하세요.
2. 문서에 없는 내용은 "해당 정보를 찾을 수 없습니다"라고 답하세요.
3. 가능하면 어떤 문서에서 정보를 가져왔는지 언급하세요."""
},
{
"role": "user",
"content": f"참고 문서:\n{context}\n\n질문: {question}"
}
],
temperature=0.1 # 낮은 temperature로 사실 기반 답변 유도
)
answer = response.choices[0].message.content
return {
"answer": answer,
"sources": results["metadatas"][0], # 출처 정보
"chunks_used": results["documents"][0] # 사용된 chunk
}
# 실행
result = rag_query("프로젝트 배포 절차가 어떻게 되나요?")
print("답변:", result["answer"])
print("출처:", result["sources"])
temperature=0.1로 설정한 이유는 창의적인 답변이 아니라 사실 기반 답변을 원하기 때문입니다. RAG에서는 temperature를 낮게 잡는 게 기본입니다.
전체 코드를 한 파일로 정리
"""
간단한 RAG 파이프라인 - ChromaDB + OpenAI
사용법: python rag_pipeline.py
"""
import chromadb
import tiktoken
from openai import OpenAI
client = OpenAI()
chroma = chromadb.PersistentClient(path="./chroma_db")
collection = chroma.get_or_create_collection("my_docs")
encoder = tiktoken.encoding_for_model("gpt-4")
def chunk_text(text, size=512, overlap=128):
tokens = encoder.encode(text)
chunks = []
start = 0
while start < len(tokens):
chunk = encoder.decode(tokens[start:start+size])
chunks.append(chunk)
start += size - overlap
return chunks
def embed(text):
res = client.embeddings.create(model="text-embedding-3-small", input=text)
return res.data[0].embedding
def index_file(filepath):
with open(filepath, "r", encoding="utf-8") as f:
chunks = chunk_text(f.read())
for i, c in enumerate(chunks):
collection.add(
ids=[f"{filepath}_{i}"],
embeddings=[embed(c)],
documents=[c],
metadatas=[{"source": filepath}]
)
print(f"[인덱싱 완료] {filepath} → {len(chunks)}개 chunk")
def ask(question, top_k=3):
results = collection.query(
query_embeddings=[embed(question)],
n_results=top_k
)
context = "\n---\n".join(results["documents"][0])
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "참고 문서를 기반으로 답변하세요. 문서에 없는 내용은 모른다고 하세요."},
{"role": "user", "content": f"참고 문서:\n{context}\n\n질문: {question}"}
],
temperature=0.1
)
return response.choices[0].message.content
if __name__ == "__main__":
# 1. 문서 인덱싱
index_file("my_document.txt")
# 2. 질문
while True:
q = input("\n질문 (종료: q): ")
if q.lower() == "q":
break
print("\n" + ask(q))
이게 RAG의 전부입니다. 50줄도 안 되는 코드로 동작하는 RAG 챗봇이 만들어집니다. 물론 프로덕션에 넣으려면 에러 처리, 배치 임베딩, chunk 크기 최적화 등이 필요하지만 핵심 구조는 이게 다입니다.
실전에서 삽질한 경험과 팁
위 코드를 돌려보면 "생각보다 잘 되네"라고 느낄 겁니다. 그런데 실제 서비스에 적용하면서 겪은 문제들이 있습니다.
1. chunk 크기가 답변 품질을 좌우한다
처음에 chunk를 200 토큰으로 잘게 잘랐더니 검색은 정확한데 문맥이 부족해서 LLM이 제대로 된 답을 못 했습니다. 반대로 2000 토큰으로 크게 잡으니 관련 없는 내용까지 같이 들어가서 답변이 산만해졌습니다.
제가 찾은 최적값은 512 토큰, 128 토큰 오버랩입니다. 물론 문서 성격에 따라 다릅니다. 기술 문서는 512가 적당하고, 법률 문서처럼 문단이 긴 경우 1024까지 올려도 됩니다.
2. 임베딩 모델 선택이 중요하다
한국어 문서를 다룬다면 임베딩 모델 선택에 신경 써야 합니다. 실제 테스트 결과입니다.
| 임베딩 모델 | 한국어 검색 정확도 | 비용 | 비고 |
| text-embedding-3-small | 상 | $0.02/1M토큰 | 가성비 최고, 대부분 이걸 씀 |
| text-embedding-3-large | 상 | $0.13/1M토큰 | 정확도 소폭 향상, 비용 6배 |
| Ko-Sentence-BERT | 중상 | 무료 (로컬) | API 비용 없음, GPU 필요 |
| multilingual-e5-large | 상 | 무료 (로컬) | 다국어 지원 좋음, 무거움 |
개인 프로젝트나 소규모 서비스라면 text-embedding-3-small로 충분합니다. 비용 대비 성능이 압도적입니다.
3. 검색 결과 수(top_k) 조절
top_k를 3으로 시작하는 걸 추천합니다. 5 이상으로 올리면 관련 없는 chunk가 섞여 들어가서 오히려 답변 품질이 떨어지는 경우가 많았습니다. LLM의 컨텍스트 윈도우도 고려해야 합니다. chunk 5개 x 512 토큰 = 2560 토큰이 컨텍스트로 들어가는데, 여기에 시스템 프롬프트와 질문까지 더하면 입력 토큰이 꽤 커집니다.
4. 하이브리드 검색을 고려하라
벡터 검색만으로는 한계가 있습니다. "에러코드 E-4021"같은 정확한 키워드 검색은 벡터 검색보다 BM25 같은 전통적인 키워드 검색이 더 정확합니다. 실무에서는 벡터 검색 + 키워드 검색을 합치는 하이브리드 검색을 쓰는 게 좋습니다.
# 하이브리드 검색 간단 예시
from rank_bm25 import BM25Okapi
# BM25 키워드 검색 인덱스 구축
tokenized_chunks = [chunk.split() for chunk in chunks]
bm25 = BM25Okapi(tokenized_chunks)
def hybrid_search(question, top_k=3):
# 벡터 검색 결과
vector_results = collection.query(
query_embeddings=[embed(question)],
n_results=top_k
)
# BM25 키워드 검색 결과
bm25_scores = bm25.get_scores(question.split())
bm25_top = sorted(range(len(bm25_scores)),
key=lambda i: bm25_scores[i], reverse=True)[:top_k]
# 두 결과를 합쳐서 중복 제거 후 반환
combined = list(set(
vector_results["documents"][0] +
[chunks[i] for i in bm25_top]
))
return combined[:top_k]
5. 답변에 출처를 반드시 포함시켜라
RAG의 가장 큰 장점 중 하나는 출처를 보여줄 수 있다는 점입니다. "이 답변은 인사규정 3조를 참고했습니다"라고 출처를 달면 사용자의 신뢰도가 크게 올라갑니다. 프롬프트에 "반드시 참고한 문서명을 답변 끝에 표시하세요"를 추가하는 것만으로도 효과가 큽니다.
마무리
RAG는 2024년부터 지금까지 LLM 활용의 핵심 패턴으로 자리잡았습니다. 그 이유는 명확합니다.
- 환각을 구조적으로 줄일 수 있다 - 지어내는 대신 문서를 참고하게 만듦
- 파인튜닝 없이 내 데이터를 활용할 수 있다 - 비용과 시간 절약
- 데이터 업데이트가 즉시 반영된다 - 재학습 불필요
- 구현이 상대적으로 간단하다 - 위에서 본 것처럼 50줄이면 된다
시작하고 싶다면 위의 코드를 그대로 복사해서 돌려보세요. 본인의 문서 하나만 넣어도 "오 이게 이렇게 되네" 하는 순간이 옵니다. 거기서부터 chunk 크기 바꿔보고, top_k 조절해보고, 프롬프트 다듬어보면 됩니다. 이론보다 직접 돌려보는 게 100배 빠릅니다.
다음 글에서는 LangChain을 활용한 RAG 구현과 멀티모달 RAG(이미지, PDF 포함)에 대해 다뤄보겠습니다.
참고 자료: Anthropic RAG 가이드, OpenAI Embeddings 문서, ChromaDB 공식 문서, Lewis et al. "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks" (2020)