
ChatGPT의 등장 이후, LLM(Large Language Model)을 자사 서비스에 통합하려는 움직임이 폭발적으로 늘어났습니다. 하지만 LLM에 단순히 질문을 던지는 것만으로는 정확한 답변을 기대하기 어렵습니다. 할루시네이션(환각) 문제, 학습 데이터의 시점 한계, 도메인 특화 지식 부족 등을 해결하기 위해 등장한 것이 바로 RAG(Retrieval-Augmented Generation) 패턴입니다. 이 글에서는 RAG 파이프라인의 전체 아키텍처부터 벡터 DB 연동, 청킹 전략, LangChain 활용까지 실전 코드와 함께 다룹니다.
RAG 아키텍처란 무엇인가?
RAG(Retrieval-Augmented Generation)는 LLM이 답변을 생성하기 전에, 외부 지식 소스에서 관련 정보를 검색하여 컨텍스트로 제공하는 패턴입니다. 이를 통해 LLM의 응답 정확도를 크게 향상시킬 수 있습니다.
RAG의 기본 흐름
1. 사용자 질문 입력
2. 질문을 임베딩 벡터로 변환
3. 벡터 DB에서 유사한 문서 청크 검색
4. 검색된 문서 + 원본 질문을 LLM에 전달
5. LLM이 컨텍스트 기반으로 답변 생성
# RAG 파이프라인의 핵심 흐름 (간략 버전)
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.chains import RetrievalQA
# 1. 임베딩 모델 초기화
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
api_key="YOUR_OPENAI_API_KEY"
)
# 2. 벡터 DB에서 리트리버 생성
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings
)
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 5}
)
# 3. LLM + 리트리버 결합
llm = ChatOpenAI(
model="gpt-4o",
temperature=0.1,
api_key="YOUR_OPENAI_API_KEY"
)
# 4. RAG 체인 구성
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True
)
# 5. 질문-응답
result = qa_chain.invoke({"query": "Spring Boot에서 트랜잭션 전파란?"})
print(result["result"])
RAG vs 파인튜닝: 언제 무엇을 선택할까?
| 항목 | RAG | 파인튜닝 |
|---|---|---|
| 지식 업데이트 | 문서 추가만으로 즉시 반영 | 재학습 필요 (비용+시간) |
| 비용 | 벡터 DB 운영비 | GPU 학습 비용 |
| 정확도 | 검색 품질에 의존 | 도메인 특화 가능 |
| 환각 방지 | 출처 기반 답변 가능 | 제한적 |
| 적합한 경우 | FAQ, 문서 QA, 최신 정보 | 특정 스타일/톤, 분류 작업 |
대부분의 엔터프라이즈 사용 사례에서는 RAG가 더 실용적입니다. 파인튜닝은 특정 도메인에서 모델 자체의 행동을 변경해야 할 때 고려합니다.

벡터 DB 이해하기: ChromaDB와 Pinecone
RAG의 핵심은 유사도 검색이며, 이를 효율적으로 수행하는 것이 벡터 데이터베이스의 역할입니다.
ChromaDB: 로컬 개발에 최적
ChromaDB는 오픈소스 벡터 DB로, 로컬 환경에서 빠르게 프로토타이핑할 수 있습니다.
import chromadb
from chromadb.config import Settings
# ChromaDB 클라이언트 생성
client = chromadb.PersistentClient(path="./chroma_db")
# 컬렉션 생성
collection = client.get_or_create_collection(
name="tech_documents",
metadata={"hnsw:space": "cosine"} # 코사인 유사도
)
# 문서 추가
collection.add(
documents=[
"Spring Boot는 스프링 프레임워크 기반의 웹 애플리케이션 프레임워크입니다.",
"Redis는 인메모리 데이터 스토어로 캐싱에 주로 사용됩니다.",
"Docker 컨테이너는 애플리케이션의 이식성을 높여줍니다."
],
metadatas=[
{"source": "spring-docs", "chapter": 1},
{"source": "redis-docs", "chapter": 1},
{"source": "docker-docs", "chapter": 1}
],
ids=["doc1", "doc2", "doc3"]
)
# 유사도 검색
results = collection.query(
query_texts=["스프링 부트 사용법"],
n_results=2,
include=["documents", "metadatas", "distances"]
)
print(results)
Pinecone: 프로덕션 환경을 위한 관리형 벡터 DB
from pinecone import Pinecone, ServerlessSpec
# Pinecone 초기화
pc = Pinecone(api_key="YOUR_PINECONE_API_KEY")
# 인덱스 생성
pc.create_index(
name="tech-knowledge-base",
dimension=1536, # text-embedding-3-small 차원
metric="cosine",
spec=ServerlessSpec(
cloud="aws",
region="us-east-1"
)
)
index = pc.Index("tech-knowledge-base")
# 벡터 업서트
index.upsert(
vectors=[
{
"id": "doc-001",
"values": embedding_vector, # 1536차원 벡터
"metadata": {
"source": "spring-docs",
"text": "Spring Boot 트랜잭션 관리...",
"category": "backend"
}
}
],
namespace="tech-docs"
)
# 유사도 검색
query_result = index.query(
vector=query_embedding,
top_k=5,
include_metadata=True,
namespace="tech-docs",
filter={"category": {"$eq": "backend"}}
)

벡터 DB 비교
| 항목 | ChromaDB | Pinecone | Weaviate | Milvus |
|---|---|---|---|---|
| 호스팅 | 로컬/셀프 | 관리형(SaaS) | 둘 다 | 셀프호스팅 |
| 설정 난이도 | 매우 쉬움 | 쉬움 | 보통 | 보통 |
| 확장성 | 제한적 | 뛰어남 | 뛰어남 | 뛰어남 |
| 비용 | 무료 | 종량제 | 무료+유료 | 무료 |
| 적합 시나리오 | 프로토타입 | 프로덕션 | 프로덕션 | 대규모 |
임베딩과 청킹 전략: RAG 성능의 핵심
RAG 파이프라인의 성능은 어떻게 문서를 분할(청킹)하고, 어떤 임베딩 모델을 사용하느냐에 크게 좌우됩니다.
임베딩 모델 선택
# OpenAI 임베딩 (가장 널리 사용)
from langchain_openai import OpenAIEmbeddings
embeddings_small = OpenAIEmbeddings(
model="text-embedding-3-small", # 1536차원, 저비용
api_key="YOUR_OPENAI_API_KEY"
)
embeddings_large = OpenAIEmbeddings(
model="text-embedding-3-large", # 3072차원, 고정밀
api_key="YOUR_OPENAI_API_KEY"
)
# 한국어 특화: multilingual-e5-large
from langchain_huggingface import HuggingFaceEmbeddings
korean_embeddings = HuggingFaceEmbeddings(
model_name="intfloat/multilingual-e5-large",
model_kwargs={"device": "cuda"}, # GPU 사용
encode_kwargs={"normalize_embeddings": True}
)
| 모델 | 차원 | 한국어 성능 | 비용 | 용도 |
|---|---|---|---|---|
| text-embedding-3-small | 1536 | 양호 | $0.02/1M 토큰 | 범용 |
| text-embedding-3-large | 3072 | 우수 | $0.13/1M 토큰 | 고정밀 |
| multilingual-e5-large | 1024 | 우수 | 무료(로컬) | 한국어 특화 |
| bge-m3 | 1024 | 우수 | 무료(로컬) | 다국어 |
청킹(Chunking) 전략
문서를 적절한 크기로 분할하는 것은 RAG 성능에 결정적인 영향을 미칩니다. 청크가 너무 크면 노이즈가 많고, 너무 작으면 맥락을 잃습니다.
1. 고정 크기 청킹 (Fixed-size Chunking)
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 가장 기본적이고 안정적인 방식
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 청크 크기 (문자 수)
chunk_overlap=50, # 오버랩 (문맥 유지)
separators=["\n\n", "\n", ".", " ", ""],
length_function=len
)
chunks = text_splitter.split_text(document_text)
2. 시맨틱 청킹 (Semantic Chunking)
의미적으로 관련된 문장을 그룹화하는 방식으로, 더 높은 검색 정확도를 제공합니다.
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(api_key="YOUR_OPENAI_API_KEY")
semantic_splitter = SemanticChunker(
embeddings,
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95
)
semantic_chunks = semantic_splitter.split_text(document_text)
3. 마크다운/코드 인식 청킹
기술 문서나 코드가 포함된 경우 구조를 인식하는 스플리터가 효과적입니다.
from langchain.text_splitter import (
MarkdownHeaderTextSplitter,
Language,
RecursiveCharacterTextSplitter
)
# 마크다운 헤더 기반 분할
md_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[
("#", "제목"),
("##", "섹션"),
("###", "하위섹션"),
]
)
md_chunks = md_splitter.split_text(markdown_text)
# 프로그래밍 언어 인식 분할
code_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON,
chunk_size=1000,
chunk_overlap=100
)
code_chunks = code_splitter.split_text(python_code)
청킹 전략 선택 가이드
문서 유형별 권장 전략:
- 일반 텍스트 → RecursiveCharacterTextSplitter (500~1000자)
- 기술 문서 (마크다운) → MarkdownHeaderTextSplitter
- 소스 코드 → Language-aware Splitter
- 법률/학술 문서 → SemanticChunker
- FAQ → 질문-답변 쌍 단위로 분할
LangChain으로 RAG 파이프라인 구축하기
LangChain은 LLM 애플리케이션 개발을 위한 프레임워크로, RAG 파이프라인 구축에 필요한 모든 구성 요소를 제공합니다.
문서 로드부터 인덱싱까지
from langchain_community.document_loaders import (
PyPDFLoader,
TextLoader,
WebBaseLoader,
DirectoryLoader
)
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 1. 다양한 소스에서 문서 로드
pdf_loader = PyPDFLoader("./docs/architecture.pdf")
web_loader = WebBaseLoader("https://docs.example.com/guide")
dir_loader = DirectoryLoader(
"./docs/", glob="**/*.md", loader_cls=TextLoader
)
documents = []
documents.extend(pdf_loader.load())
documents.extend(web_loader.load())
documents.extend(dir_loader.load())
print(f"로드된 문서 수: {len(documents)}")
# 2. 청킹
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=100,
separators=["\n\n", "\n", ".", " "]
)
chunks = text_splitter.split_documents(documents)
print(f"생성된 청크 수: {len(chunks)}")
# 3. 벡터 DB에 인덱싱
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
api_key="YOUR_OPENAI_API_KEY"
)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db",
collection_name="knowledge_base"
)
print("인덱싱 완료!")
LCEL(LangChain Expression Language)로 고급 RAG 체인 구성
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# 프롬프트 템플릿
prompt = ChatPromptTemplate.from_template("""
당신은 기술 문서 전문 어시스턴트입니다.
아래 제공된 컨텍스트를 기반으로 질문에 답변하세요.
컨텍스트에 없는 내용은 "해당 정보를 찾을 수 없습니다"라고 답하세요.
## 컨텍스트
{context}
## 질문
{question}
## 답변 (한국어로, 코드 예시 포함)
""")
# LLM
llm = ChatOpenAI(
model="gpt-4o",
temperature=0.1,
api_key="YOUR_OPENAI_API_KEY"
)
# 리트리버
retriever = vectorstore.as_retriever(
search_type="mmr", # Maximal Marginal Relevance (다양성 확보)
search_kwargs={
"k": 5,
"fetch_k": 20,
"lambda_mult": 0.7
}
)
# 문서를 텍스트로 포맷팅하는 함수
def format_docs(docs):
return "\n\n---\n\n".join(
f"[출처: {doc.metadata.get('source', '알 수 없음')}]\n{doc.page_content}"
for doc in docs
)
# LCEL 체인 구성
rag_chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough()
}
| prompt
| llm
| StrOutputParser()
)
# 실행
answer = rag_chain.invoke("Spring Boot에서 @Transactional의 전파 속성을 설명해주세요")
print(answer)
대화형 RAG (Conversational RAG)
이전 대화 맥락을 유지하면서 RAG를 수행하는 패턴입니다.
from langchain_core.prompts import MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage
from langchain.chains.history_aware_retriever import (
create_history_aware_retriever
)
from langchain.chains.retrieval import create_retrieval_chain
from langchain.chains.combine_documents import (
create_stuff_documents_chain
)
# 대화 히스토리를 반영한 질문 재작성 프롬프트
contextualize_prompt = ChatPromptTemplate.from_messages([
("system", "대화 기록을 참고하여, 사용자의 최신 질문을 "
"독립적으로 이해 가능한 형태로 재작성하세요."),
MessagesPlaceholder("chat_history"),
("human", "{input}")
])
# 히스토리 인식 리트리버
history_aware_retriever = create_history_aware_retriever(
llm, retriever, contextualize_prompt
)
# 답변 생성 프롬프트
answer_prompt = ChatPromptTemplate.from_messages([
("system", "당신은 기술 문서 전문 어시스턴트입니다. "
"컨텍스트를 기반으로 정확하게 답변하세요.\n\n{context}"),
MessagesPlaceholder("chat_history"),
("human", "{input}")
])
# 체인 구성
question_answer_chain = create_stuff_documents_chain(llm, answer_prompt)
conversational_rag = create_retrieval_chain(
history_aware_retriever, question_answer_chain
)
# 대화 실행
chat_history = []
# 첫 번째 질문
response1 = conversational_rag.invoke({
"input": "Redis의 캐시 전략에 대해 알려줘",
"chat_history": chat_history
})
chat_history.extend([
HumanMessage(content="Redis의 캐시 전략에 대해 알려줘"),
AIMessage(content=response1["answer"])
])
# 후속 질문 (이전 맥락 참조)
response2 = conversational_rag.invoke({
"input": "그중에서 Write-Through는 어떤 경우에 적합해?",
"chat_history": chat_history
})
print(response2["answer"])
프롬프트 엔지니어링: RAG 답변 품질 높이기
RAG 파이프라인에서 프롬프트 설계는 답변 품질을 결정짓는 마지막 퍼즐 조각입니다.
Few-shot 프롬프트
few_shot_prompt = ChatPromptTemplate.from_template("""
당신은 기술 블로그 작성 어시스턴트입니다.
제공된 컨텍스트를 기반으로 질문에 답변하되,
아래 예시와 같은 형식으로 작성하세요.
## 답변 형식 예시
### 질문: Spring Bean의 스코프란?
### 답변:
Spring Bean 스코프는 빈의 생명주기를 결정합니다.
**주요 스코프:**
- `singleton`: 컨테이너당 하나 (기본값)
- `prototype`: 요청마다 새로 생성
- `request`: HTTP 요청마다 하나
```java
@Scope("prototype")
@Component
public class MyBean
컨텍스트
{context}
질문
{question}
답변
”””)
### 프롬프트 엔지니어링 핵심 원칙
1. **역할 부여**: "당신은 ~입니다"로 모델의 전문성 설정
2. **출력 형식 지정**: 마크다운, JSON 등 원하는 포맷 명시
3. **제약 조건 명시**: "컨텍스트에 없는 내용은 답하지 마세요"
4. **Few-shot 예시**: 원하는 답변 스타일을 예시로 제시
5. **Chain-of-Thought**: 복잡한 질문에 "단계별로 생각하세요" 추가
```python
# 구조화된 출력을 위한 프롬프트
structured_prompt = ChatPromptTemplate.from_template("""
질문에 대해 다음 JSON 형식으로 답변하세요:
answer
컨텍스트: {context}
질문: {question}
""")
RAG 파이프라인 평가와 최적화
검색 품질 평가
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall
)
# 테스트 데이터셋
eval_dataset = {
"question": [
"Spring Boot에서 트랜잭션 전파란?",
"Redis 캐시 전략의 종류는?"
],
"answer": [generated_answer_1, generated_answer_2],
"contexts": [retrieved_contexts_1, retrieved_contexts_2],
"ground_truth": [
"트랜잭션 전파는 기존 트랜잭션이 있을 때...",
"Cache-Aside, Write-Through, Write-Behind..."
]
}
# RAGAS 평가
result = evaluate(
dataset=eval_dataset,
metrics=[
faithfulness, # 답변이 컨텍스트에 충실한지
answer_relevancy, # 답변이 질문에 관련되는지
context_precision, # 검색된 컨텍스트의 정밀도
context_recall # 필요한 정보가 검색되었는지
]
)
print(result)
성능 최적화 팁
| 최적화 영역 | 방법 | 효과 |
|---|---|---|
| 청크 크기 | 도메인에 맞게 실험 (300~1500자) | 검색 정확도 향상 |
| 리랭킹 | Cohere Reranker, Cross-encoder | 상위 결과 정밀도 향상 |
| 하이브리드 검색 | 벡터 + BM25 키워드 검색 | 재현율 향상 |
| 메타데이터 필터링 | 카테고리, 날짜 등으로 사전 필터 | 노이즈 감소 |
| 캐싱 | 동일 질문 결과 캐시 | 응답 속도 + 비용 절감 |
# 하이브리드 검색 예시 (벡터 + BM25)
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
# BM25 키워드 검색
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5
# 벡터 유사도 검색
vector_retriever = vectorstore.as_retriever(
search_kwargs={"k": 5}
)
# 앙상블 (가중 평균)
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.3, 0.7] # 벡터 검색에 더 높은 가중치
)
results = ensemble_retriever.invoke("Spring 트랜잭션 관리")
마무리: RAG 파이프라인 구축 체크리스트
RAG 기반 AI 서비스를 구축할 때 다음 사항들을 점검하세요:
- 문서 전처리: 소스에 맞는 로더와 청킹 전략 선택
- 임베딩 모델: 한국어 지원 여부와 비용/성능 트레이드오프 고려
- 벡터 DB: 프로토타입은 ChromaDB, 프로덕션은 Pinecone/Weaviate
- 검색 전략: MMR로 다양성 확보, 하이브리드 검색으로 재현율 향상
- 프롬프트 설계: 역할 부여, 출력 형식, 제약 조건, Few-shot 예시
- 평가 체계: RAGAS 등으로 정기적 품질 모니터링
- 비용 관리: 임베딩 캐싱, 응답 캐싱으로 API 호출 최소화
LLM과 RAG는 빠르게 진화하고 있습니다. 중요한 것은 완벽한 시스템을 한 번에 구축하려는 것이 아니라, 작게 시작하고 반복적으로 개선하는 것입니다. 오늘 소개한 패턴들을 기반으로 여러분만의 AI 서비스를 만들어 보세요.
참고 자료
- LangChain 공식 문서
- OpenAI Embeddings API
- ChromaDB 공식 문서
- Pinecone 공식 문서
- RAGAS - RAG 평가 프레임워크
- RAG 논문 (Lewis et al., 2020)