반응형

들어가며
Retrieval-Augmented Generation(RAG)은 LLM의 한계를 극복하는 가장 효과적인 패턴 중 하나입니다. LLM이 모르는 최신 정보, 사내 문서, 도메인 특화 지식을 동적으로 제공하여 환각(hallucination)을 줄이고 정확도를 높입니다.
이 글은 LLM-RAG-Study 레포지토리를 바탕으로, RAG의 기초 개념을 정리합니다.
목차
RAG란 무엇인가?
핵심 개념
RAG는 다음 두 단계로 구성됩니다:
- Retrieval (검색): 질의와 관련된 문서를 벡터 DB에서 찾기
- Generation (생성): 검색된 문서를 컨텍스트로 LLM에 제공하여 답변 생성
왜 RAG가 필요한가?
LLM 고유의 한계:
| 문제 | LLM만 사용 시 | RAG 적용 시 |
|---|---|---|
| 시간적 한계 | 학습 데이터 시점 이후 정보 모름 (예: 2024년 이후 이벤트) | 최신 문서 검색으로 해결 |
| 환각 (Hallucination) | 모르는 내용을 그럴듯하게 지어냄 | 실제 문서 기반 응답으로 정확도 향상 |
| 도메인 지식 부족 | 사내 문서, 전문 분야 지식 제한적 | 자체 지식 베이스 활용 |
| 출처 추적 불가 | 답변의 근거 확인 어려움 | 검색된 문서 출처 제공 가능 |
RAG vs Fine-tuning
| 기준 | RAG | Fine-tuning |
|---|---|---|
| 지식 업데이트 | 실시간 (문서 추가 시) | 재학습 필요 (시간·비용 소모) |
| 초기 구축 비용 | 낮음 | 높음 (GPU, 데이터 준비) |
| 추론 지연 | 중간 (검색 오버헤드) | 낮음 |
| 도메인 적응성 | 매우 높음 | 높음 (특정 태스크) |
| 투명성 | 높음 (출처 확인 가능) | 낮음 (블랙박스) |
권장 전략: 대부분 RAG로 시작 → 특수 태스크만 Fine-tuning 고려
환경 설정과 빠른 시작
사전 준비
# 레포지토리 클론
git clone https://github.com/Chenjae-kr/LLM-RAG-Study.git
cd LLM-RAG-Study/01-basic-rag
# 가상환경 생성 (Python 3.8+)
python -m venv .venv
# 활성화 (macOS/Linux)
source .venv/bin/activate
# 활성화 (Windows)
.venv\Scripts\activate
# 의존성 설치
pip install -r requirements.txt
최소 요구사항
- Python: 3.8 이상
- 메모리: 최소 4GB (임베딩 모델 로딩)
- 디스크: 최소 2GB (모델 캐시)
- GPU: 선택사항 (CPU도 가능하지만 느림)
30초 데모
# 1. 샘플 문서 임베딩 (벡터 저장소 생성)
python ingest.py
# 출력 예시:
# Processing documents...
# Embedding 50 chunks...
# Vector store saved to ./vector_store
# 2. 질의 실행
python query_rag.py "인공지능이란 무엇인가요?"
# 출력:
# [Retrieved Context]
# - Document 1: "인공지능(AI)은 컴퓨터 시스템이 인간의 지능을 모방..."
#
# [Generated Answer]
# 인공지능은 기계가 인간처럼 학습하고 추론하는 기술입니다...
Basic RAG 파이프라인
1. 문서 수집 및 전처리 (Ingestion)
ingest.py 핵심 로직:
from pathlib import Path
from sentence_transformers import SentenceTransformer
import numpy as np
from utils import embed_texts, save_vector_store
# 1. 문서 로드
def load_documents(data_dir: str) -> list[str]:
"""텍스트 파일 읽기"""
documents = []
for file_path in Path(data_dir).glob("*.txt"):
with open(file_path, 'r', encoding='utf-8') as f:
documents.append(f.read())
return documents
# 2. 청킹 (Chunking)
def chunk_text(text: str, chunk_size: int = 512, overlap: int = 50) -> list[str]:
"""텍스트를 겹치는 청크로 분할"""
words = text.split()
chunks = []
for i in range(0, len(words), chunk_size - overlap):
chunk = ' '.join(words[i:i + chunk_size])
chunks.append(chunk)
return chunks
# 3. 임베딩 생성
def main():
documents = load_documents("./data")
# 모든 문서를 청크로 분할
all_chunks = []
for doc in documents:
all_chunks.extend(chunk_text(doc))
print(f"Total chunks: {len(all_chunks)}")
# 임베딩 모델 로드 (384차원)
embeddings = embed_texts(all_chunks, model_name="all-MiniLM-L6-v2")
# 벡터 저장소에 저장
save_vector_store(embeddings, all_chunks, "./vector_store")
print("✓ Vector store created successfully")
if __name__ == "__main__":
main()
청킹 전략 비교:
| 방법 | 장점 | 단점 | 권장 사용처 |
|---|---|---|---|
| 고정 크기 | 구현 간단, 빠름 | 문맥 경계 무시 | 구조화된 문서 |
| 문장 단위 | 문맥 보존 | 크기 불균일 | 자연어 텍스트 |
| 의미 기반 | 의미적 일관성 높음 | 느림, 복잡 | 고품질 요구 시 |
권장 설정:
chunk_size = 512 # 토큰 수 (약 300-400단어)
overlap = 50 # 10% 중첩으로 문맥 연결
2. 벡터 검색 (Retrieval)
utils.py의 검색 함수:
import numpy as np
from sentence_transformers import SentenceTransformer
def search(query: str, vector_store_path: str, top_k: int = 3):
"""코사인 유사도 기반 검색"""
# 1. 벡터 저장소 로드
embeddings, chunks = load_vector_store(vector_store_path)
# 2. 질의 임베딩
model = SentenceTransformer("all-MiniLM-L6-v2")
query_embedding = model.encode([query])[0]
# 3. 코사인 유사도 계산
similarities = np.dot(embeddings, query_embedding) / (
np.linalg.norm(embeddings, axis=1) * np.linalg.norm(query_embedding)
)
# 4. 상위 K개 반환
top_indices = np.argsort(similarities)[-top_k:][::-1]
results = [(chunks[i], similarities[i]) for i in top_indices]
return results
검색 품질 개선 팁:
- top_k 조정: 3-5개 권장 (너무 많으면 노이즈 증가)
- 유사도 임계값: 0.5 이하 결과는 필터링 고려
- 재랭킹: Cross-Encoder로 2차 정렬 (후술)
3. 프롬프트 구성 및 생성 (Generation)
query_rag.py:
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
from utils import search
def generate_answer(query: str, context: list[str], model_name: str = "google/flan-t5-small"):
"""컨텍스트 기반 답변 생성"""
# 1. 검색 결과를 프롬프트로 구성
context_text = "\n\n".join([f"[{i+1}] {text}" for i, text in enumerate(context)])
prompt = f"""다음 문서를 참고하여 질문에 답하세요.
문서:
{context_text}
질문: {query}
답변:"""
# 2. 모델 로드 및 생성
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
inputs = tokenizer(prompt, return_tensors="pt", max_length=1024, truncation=True)
outputs = model.generate(**inputs, max_new_tokens=256)
answer = tokenizer.decode(outputs[0], skip_special_tokens=True)
return answer
# 메인 실행
if __name__ == "__main__":
query = "인공지능의 주요 응용 분야는?"
# 검색
results = search(query, "./vector_store", top_k=3)
contexts = [text for text, score in results]
# 출력: 검색된 문서
print("=== Retrieved Documents ===")
for i, (text, score) in enumerate(results):
print(f"\n[{i+1}] (Score: {score:.3f})")
print(text[:200] + "...")
# 생성
answer = generate_answer(query, contexts)
print("\n=== Generated Answer ===")
print(answer)
프롬프트 엔지니어링 팁:
# ❌ 나쁜 프롬프트
prompt = f"{context}\n\n{query}"
# ✅ 좋은 프롬프트
prompt = f"""당신은 정확한 답변을 제공하는 전문가입니다.
주어진 문서에만 기반하여 답변하세요. 확실하지 않으면 "문서에 정보가 없습니다"라고 답하세요.
문서:
{context}
질문: {query}
답변 (문서 기반):"""반응형
'Develop' 카테고리의 다른 글
| Mac에서 BlueStacks + 메신저봇R로 카카오톡 오픈채팅 자동응답 봇 만들기 (0) | 2026.02.24 |
|---|---|
| Three.js로 시작하는 3D 웹 그래픽 (0) | 2026.02.12 |
| [Tibero] JDBC-8101:Column used in the ON clause cannot be updated. 오류 해결하기 (0) | 2022.11.08 |
| [Spring JPA] JPA를 배워야하는 이유 (0) | 2022.05.13 |
| [Eclipse] Tomcat 에러, Serval port(8005, 8888) required by Tomcat 해결하는 법 (0) | 2022.05.11 |