기본 RAG 구현: 단계별 튜토리얼
이 입문자용 튜토리얼에서는 기본 RAG 구현의 작동 방식과 모범 사례를 설명하고, Python, ChromaDB, Perplexity의 임베딩 모델 및 언어 모델을 사용해 실제로 작동하는 명령줄 RAG 시스템을 구축하는 과정을 안내합니다.

RAG는 최신 LLM 기반 인프라의 핵심 구성 요소입니다. AI 모델이 외부 데이터에 접근할 수 있게 해 할루시네이션이나 불완전한 학습 데이터 같은 문제를 해결하는 데 도움이 됩니다. 또한 기업은 민감한 정보나 독점 정보가 담긴 내부 지식 베이스를 활용할 수 있습니다.
RAG 구현은 매우 복잡할 수 있습니다. 하지만 기본적인 RAG 시스템은 의외로 쉽게 구축할 수 있습니다. 입문자라면 간단한 아키텍처를 직접 만들어 보면서 RAG가 실제로 어떻게 작동하는지 이해하고, 이를 발판 삼아 더 다양한 실험을 이어 갈 수 있습니다.
이 글에서는 기본적인 RAG 구현 과정을 살펴보고 모범 사례를 소개한 뒤, Python, ChromaDB, 그리고 임베딩과 답변 생성을 위한 두 가지 Perplexity 모델을 사용해 실제로 작동하는 시스템을 구축해 보겠습니다.
기본 RAG 구현 프로세스: 전체 개요
기본 RAG 시스템은 오케스트레이터, 정보 소스, LLM이라는 세 가지 주요 구성 요소로 이루어집니다. 일반적인 워크플로우에서는 사용자가 프롬프트를 입력하면 오케스트레이터가 이를 수정해 연결된 정보 소스를 검색합니다. 그런 다음 검색된 정보를 원래 입력 컨텍스트에 추가해 LLM으로 보내면 답변이 생성됩니다.
RAG를 구현할 때는 다음 단계를 고려해야 합니다:
- 인덱싱 및 임베딩: 이 단계에서는 문서를 오케스트레이터가 검색할 수 있는 정보 소스로 변환합니다. 가장 일반적인 방법 중 하나는 검색 가능한 벡터 인덱스를 구축하는 것입니다. 여기서는 Perplexity의 전용 모델을 사용해 문서 청크를 임베딩으로 변환합니다. 그런 다음 널리 쓰이는 오픈 소스 벡터 데이터베이스인 ChromaDB에 임베딩을 저장합니다.
- 오케스트레이션: 외부 정보 소스(이 글에서는 벡터 데이터베이스)와 LLM 사이에 위치하는 애플리케이션입니다. 간단한 Python 스크립트를 만들어 오케스트레이터 역할을 하게 하겠습니다.
- 검색 및 생성: 사용자가 채팅 인터페이스에서 질문하면 오케스트레이터가 이를 받아 벡터 검색을 실행하고, 확장된 입력을 LLM에 전달합니다. 별도의 프런트엔드를 구축하는 대신 명령줄에서 프롬프트를 입력해 Python 오케스트레이터와 통신하겠습니다.
모범 사례: 효과적인 RAG 구현의 조건은 무엇일까요?
RAG 아키텍처에는 여러 유형이 있으며, 사용 사례마다 필요한 접근 방식이 다릅니다. 그렇지만 모든 경우에 적용되는 여러 모범 사례가 있으며, 이를 따르면 정확하고 비용 효율적이며 확장 가능하고 유지 관리하기 쉬운 시스템을 만들 수 있습니다.
다음 모범 사례는 모든 유형의 RAG 구현에 적용됩니다:
- 깨끗한 수집 파이프라인 확보: 소스 데이터는 인덱싱 전에 최대한 깨끗하게 정리해야 합니다. 또한 콘텐츠에 적합한 임베딩 모델을 사용해야 합니다.
- 하이브리드 검색 사용: 벡터 데이터베이스 검색, API 기반 웹 검색, 지식 그래프 탐색, 키워드 검색 등 가장 적합한 검색 방법은 사용 사례에 따라 다릅니다. 비용을 최소화하려면 적절한 조합을 선택하는 것이 중요합니다.
- 시스템 평가: 구현을 마친 후에는 정확도, 관련성, 지연 시간이라는 세 가지 핵심 지표로 시스템을 평가해야 합니다. 정확도는 사실적 정확성을, 관련성은 검색된 정보가 사용자 쿼리에 얼마나 부합하는지를, 지연 시간은 정보를 검색하는 속도를 의미합니다.
- 정보를 안전하게 관리: RAG 시스템이 비공개 내부 정보를 활용한다면 해당 정보를 안전하게 저장하고 시스템 인터페이스에 대한 접근을 관리해야 합니다. API 키는 절대 공유해서는 안 됩니다.
기본 RAG 구현: 7단계 튜토리얼
이 튜토리얼에서는 단일 문서에 관한 질문에 답하는 명령줄 애플리케이션을 만들어 보겠습니다. Python, ChromaDB, Perplexity API를 사용합니다.
목표는 프로덕션 수준의 RAG 시스템을 구축하는 것이 아닙니다. 기초적인 RAG 아키텍처가 실제로 어떻게 작동하는지 보여 주기 위한 입문자용 튜토리얼입니다.
1단계: 소프트웨어 설치
컴퓨터에 다음 항목이 설치되어 있고 Perplexity API 계정이 있는지 확인하세요:
- Python: 임베딩 모델, ChromaDB, Perplexity 간의 데이터 흐름을 조율하는 오케스트레이터를 만드는 데 사용할 프로그래밍 언어입니다.
- Visual Studio: 코드 편집기로 Visual Studio를 사용합니다. 설치가 끝나면 Extensions 패널을 열고 Python 확장 프로그램을 다운로드하세요.
- Perplexity API 계정: Perplexity는 데이터를 벡터 임베딩으로 변환하는 임베딩 모델과 확장된 프롬프트를 처리할 LLM을 모두 제공합니다. 유료 계정이 없어도 되지만, 소량의 크레딧이 소모됩니다.
2단계: 프로젝트 폴더 만들기
먼저 rag-project라는 폴더를 만들고 File > Open Folder를 선택해 VS Code에서 엽니다. 그런 다음 Terminal > New Terminal을 선택해 통합 터미널을 엽니다.
다음 명령을 입력해 Python 가상 환경을 만드세요:
py -3.12 -m venv .venv이제 perplexityai, chromadb, python-dotenv, numpy라는 네 가지 Python 라이브러리를 설치해야 합니다. Perplexity SDK를 사용하면 API를 통해 Perplexity의 모델에 연결할 수 있습니다. ChromaDB는 벡터 데이터베이스를 구축하며 로컬에서 실행됩니다. Python-dotenv는 로컬 파일에서 API 키를 안전하게 불러오고, NumPy는 수집 과정에서 필요합니다.
다음 명령으로 설치하세요:
.\.venv\Scripts\python.exe -m pip install perplexityai chromadb python-dotenv numpy3단계: 파일 설정
rag-project(프로젝트 루트)에 다음 파일을 추가하세요. 탐색기 사이드바에서 New File…을 선택하면 됩니다:
- index.py: 문서를 인덱싱할 애플리케이션입니다. 다시 실행해 텍스트를 추가할 수 있습니다.
- rag.py: 오케스트레이터 역할을 할 Python 애플리케이션입니다.
- document.txt: 벡터 데이터베이스로 변환할 문서 내용이 담길 파일입니다.
- .env: Perplexity API 키를 보관할 파일입니다.
.env에 다음을 입력하세요:
PERPLEXITY_API_KEY=your_api_key_hereyour_api_key_here를 실제 키로 바꾸세요. 키를 발급하려면 Perplexity API Console 홈 대시보드로 이동해 Create an API key를 선택해야 합니다. 이 API 키는 비공개로 유지하세요.
여기서는 두 가지 모델을 사용합니다. ChromaDB에 업로드하기 전에 문서를 임베딩하는 Perplexity의 pplx-embed-v1-0.6b와, 검색된 정보를 처리하는 GPT-5.6-Luna(Perplexity API를 통해 접근)입니다.
document.txt에 RAG 파이프라인이 검색할 텍스트를 추가하세요. 마땅한 문서가 없다면 이 튜토리얼에서는 Project Gutenberg에 있는 윌리엄 블레이크의 순수와 경험의 노래(Songs of Innocence and of Experience)를 사용하세요. 퍼블릭 도메인에 속하는 짧고 이해하기 쉬운 텍스트입니다.
4단계: 인덱싱 프로그램 작성
아래 코드를 복사해 index.py에 붙여 넣으세요. 각 소제목에는 뒤따르는 코드 구간이 하는 일에 대한 간단한 설명이 있습니다. 이 프로그램은 Perplexity의 임베딩 모델인 pplx-embed-v1-0.6b와 NumPy 라이브러리를 사용해 소스 자료를 벡터 임베딩으로 변환하고 ChromaDB 데이터베이스에 저장합니다.
이 프로그램이 하는 일은 다음과 같습니다:
- document.txt를 엽니다.
- 텍스트를 서로 겹치는 청크로 분할합니다.
- 분할한 청크를 Perplexity의 pplx-embed-v1-0.6b 임베딩 모델로 보냅니다.
- 연산 함수를 제공하는 Python 라이브러리인 NumPy를 사용해 반환된 벡터를 디코딩하고 정규화합니다.
- 벡터와 원본 청크를 ChromaDB 데이터베이스에 저장합니다(프로젝트 디렉터리에 데이터베이스가 생성된 것을 확인할 수 있습니다).
실행할 때마다 문서를 다시 인덱싱하지 않도록 설정되어 있습니다. document.txt에 정보를 추가하면 새 텍스트만 임베딩합니다.
from pathlib import Path
from collections import defaultdict, deque
from hashlib import sha256
import base64
import numpy as np
import chromadb
from dotenv import load_dotenv
from perplexity import Perplexity
# 1. Connect to Perplexity and ChromaDB
load_dotenv()
ai = Perplexity()
db = chromadb.PersistentClient(path="./rag_database_perplexity")
collection = db.get_or_create_collection(
name="documents",
metadata={"hnsw:space": "cosine"}
)
# Convert Perplexity's encoded embeddings into numerical vectors
def decode_embedding(encoded):
vector = np.frombuffer(base64.b64decode(encoded), dtype=np.int8).astype(np.float32)
norm = np.linalg.norm(vector)
if norm == 0:
raise ValueError("Received a zero-length embedding vector.")
return (vector / norm).tolist()
# 2. Read the document and split it into chunks
text = Path("document.txt").read_text(encoding="utf-8")
words = text.split()
chunk_size = 180
overlap = 30
step = chunk_size - overlap
chunks = [" ".join(words[i:i + chunk_size]) for i in range(0, len(words), step)]
if not chunks:
raise ValueError("document.txt is empty.")
# 3. Retrieve the chunks already stored in ChromaDB
existing = collection.get(include=["documents"])
stored_chunks = defaultdict(deque)
existing_ids = set(existing["ids"])
for chunk_id, document in zip(existing["ids"], existing["documents"]):
stored_chunks[document].append(chunk_id)
# 4. Identify unchanged and new chunks
new_chunks = []
new_ids = []
kept_ids = set()
unchanged = 0
for chunk in chunks:
if stored_chunks[chunk]:
chunk_id = stored_chunks[chunk].popleft()
kept_ids.add(chunk_id)
unchanged += 1
else:
chunk_hash = sha256(chunk.encode("utf-8")).hexdigest()
chunk_id = f"chunk-{chunk_hash}"
suffix = 1
while chunk_id in existing_ids or chunk_id in kept_ids or chunk_id in new_ids:
chunk_id = f"chunk-{chunk_hash}-{suffix}"
suffix += 1
new_chunks.append(chunk)
new_ids.append(chunk_id)
kept_ids.add(chunk_id)
# 5. Generate embeddings in batches of up to 100 chunks
batch_size = 100
for i in range(0, len(new_chunks), batch_size):
batch_chunks = new_chunks[i:i + batch_size]
batch_ids = new_ids[i:i + batch_size]
embeddings = ai.embeddings.create(
model="pplx-embed-v1-0.6b",
input=batch_chunks,
encoding_format="base64_int8"
)
vectors = [decode_embedding(item.embedding) for item in embeddings.data]
# 6. Store each batch in ChromaDB
collection.upsert(
ids=batch_ids,
documents=batch_chunks,
embeddings=vectors
)
print(f"Indexed batch {(i // batch_size) + 1}")
# 7. Remove outdated chunks
old_ids = [chunk_id for chunk_id in existing["ids"] if chunk_id not in kept_ids]
if old_ids:
collection.delete(ids=old_ids)
# 8. Display indexing results
print("Indexing complete!")
print("Unchanged chunks:", unchanged)
print("New or modified chunks:", len(new_chunks))
print("Removed chunks:", len(old_ids))
print("Total stored chunks:", collection.count())5단계: RAG 시스템의 오케스트레이터 작성
아래 코드를 복사해 rag.py에 붙여 넣으세요. Perplexity SDK와 ChromaDB의 로컬 벡터 데이터베이스 기능을 사용하는, 완전하지만 최소한으로 구성된 RAG 구현입니다.
프로그램을 실행하면 다음과 같은 과정이 진행됩니다:
- Perplexity와 새로 만든 ChromaDB 데이터베이스에 연결합니다.
- 데이터베이스에 인덱싱된 문서가 있는지 확인합니다.
- 사용자의 질문을 받습니다.
- 질문을 Perplexity의 임베딩 모델로 보냅니다.
- NumPy를 사용해 생성된 임베딩을 디코딩하고 정규화합니다.
- ChromaDB에서 가장 유사한 문서 청크 세 개를 검색합니다.
- 검색된 청크와 사용자의 질문을 담은 프롬프트를 구성합니다.
- Perplexity의 Agent API를 통해 프롬프트를 GPT-5.6 Luna로 보냅니다.
- 답변을 표시하고 다음 질문을 기다립니다.
import base64
import numpy as np
import chromadb
from dotenv import load_dotenv
from perplexity import Perplexity
# 1. Connect to Perplexity and the existing ChromaDB database
load_dotenv()
ai = Perplexity()
db = chromadb.PersistentClient(path="./rag_database_perplexity")
collection = db.get_collection("documents")
# Convert Perplexity's encoded embeddings into numerical vectors
def decode_embedding(encoded):
vector = np.frombuffer(base64.b64decode(encoded), dtype=np.int8).astype(np.float32)
norm = np.linalg.norm(vector)
if norm == 0:
raise ValueError("Received a zero-length embedding vector.")
return (vector / norm).tolist()
# 2. Check that the database contains indexed documents
count = collection.count()
if count == 0:
raise ValueError("The database is empty. Run index.py first.")
print("Connected to existing ChromaDB database.")
print("Stored chunks:", count)
# 3. Accept questions from the user
while True:
question = input("\nAsk a question (or type exit): ")
if question.lower().strip() == "exit":
break
if not question.strip():
continue
# 4. Convert the question into a Perplexity embedding
question_response = ai.embeddings.create(
model="pplx-embed-v1-0.6b",
input=[question],
encoding_format="base64_int8"
)
question_vector = decode_embedding(question_response.data[0].embedding)
# 5. Retrieve the most similar document chunks from ChromaDB
results = collection.query(
query_embeddings=[question_vector],
n_results=min(3, count)
)
context = "\n\n".join(results["documents"][0])
# 6. Construct a prompt and send it to the fixed LLM
response = ai.responses.create(
model="openai/gpt-5.6-luna",
instructions=(
"Answer using only the supplied document excerpts. "
"Do not use external information. "
"If the answer is not present, say you don't know."
),
input=f"Document excerpts:\n{context}\n\nQuestion: {question}"
)
# 7. Display the answer
print("\nAnswer:\n", response.output_text)6단계: 애플리케이션 실행
VS Code PowerShell 터미널에 다음을 입력해 document.txt의 콘텐츠를 임베딩하세요:
.\.venv\Scripts\python.exe index.py문서가 성공적으로 인덱싱되면 다음과 비슷한 출력이 표시됩니다:
Indexing complete!
Unchanged chunks: 0
New or modified chunks: 58
Removed chunks: 0
Total stored chunks: 58이제 메인 오케스트레이터 애플리케이션을 실행할 차례입니다. 다음 명령을 입력하세요:
.\.venv\Scripts\python.exe rag.py성공적으로 실행되면 다음과 같은 화면이 표시됩니다:
Connected to existing ChromaDB database.
Stored chunks: 58
Ask a question (or type exit): 이제 직접 만든 벡터 데이터베이스를 바탕으로 다음과 같은 질문을 할 수 있습니다:
Ask a question (or type exit): What animals does William Blake talk about?
Answer: The excerpts mention sheep, lambs, birds, beasts, wolves, tigers, and a lion.7단계: 구축한 시스템 이해하기
여러분이 구축한 시스템에는 모든 RAG 아키텍처에 포함되는 두 가지 핵심 파이프라인, 즉 인덱싱(또는 ‘수집’) 파이프라인과 검색 및 생성 파이프라인이 들어 있습니다.
이제 기본 RAG 구현은 다음 기능을 모두 수행합니다:
- 벡터 저장: 지식 파일을 기반으로 생성된 임베딩이 로컬 환경의 ChromaDB 데이터베이스에 저장됩니다.
- 증분 인덱싱: 변경되지 않은 문서 청크는 다시 임베딩하지 않으며, 원할 때 언제든 문서를 업데이트할 수 있습니다.
- 시맨틱 검색: ChromaDB가 벡터 유사도를 기준으로 청크를 검색합니다(검색은 오케스트레이터가 시작합니다).
- 근거 기반 답변 생성: LLM이 검색된 구절을 컨텍스트로 받아 답변에 반영합니다.
- 인덱싱과 쿼리의 분리: 지식 파일에 포함된 텍스트 전체를 다시 처리하지 않고도 질문할 수 있습니다.
- API 기반 모델 연결: 모든 답변은 로컬 컴퓨터에서 실행되는 모델이 아니라 Perplexity를 통해 GPT-5.6 Luna로 생성됩니다.
다음 단계: 이 구현을 더 발전시키는 방법
이 튜토리얼의 목표는 간단하고 기본적인 구현을 만드는 것입니다. 최신 시스템에 있는 핵심 RAG 아키텍처 구성 요소 중 일부를 다룹니다. 더 나아가고 싶다면 이를 바탕으로 다양한 방식으로 확장할 수 있습니다.
구현 범위를 넓히기 위한 몇 가지 아이디어를 소개합니다:
- LangChain 활용해 보기: LangChain은 가장 인기 있는 오픈 소스 오케스트레이션 프레임워크 중 하나입니다. 복잡한 RAG 파이프라인을 실행할 수 있는 앱을 구축하는 데 필요한 사전 구축된 구성 요소가 포함되어 있습니다.
- 검색 기능 확장: 밀집 벡터 검색을 BM25 같은 어휘(키워드) 검색과 결합해 보세요. Perplexity Search API로 웹 검색을 추가하면 최신 데이터에 근거한 답변도 생성할 수 있습니다.
- 리랭킹 모델 추가: 검색된 구절을 초기 프롬프트와 비교해 순위를 매기고, LLM에 제공되는 입력 컨텍스트에 포함할 가장 관련성 높은 구절을 선택하는 리랭킹 모델을 통합하세요.
- 적응형 RAG 시스템 구축: 적응형 RAG는 LLM 또는 분류기(오케스트레이터 내의 구성 요소)를 사용해 모듈형 RAG 시스템에서 어떤 검색 방법을 사용할지 결정합니다. 적응형 RAG 시스템을 직접 구축해 보면 더 정교한 RAG 구현을 탐구하는 데 도움이 됩니다.
이 모든 것이 복잡하게 들릴 수 있지만, Perplexity 같은 주요 AI 플랫폼이 고급 코딩 지원을 제공한다는 점을 기억하세요. 개인 프로젝트나 실험에 매우 유용하며, 비용도 비교적 낮게 유지할 수 있습니다.
참고: 이 튜토리얼은 GPT-5.6 Luna를 사용합니다. 모델 제공 여부는 변경될 수 있으므로 API 호출을 업데이트해야 할 수도 있습니다.