RAG 튜토리얼은 넘쳐나는데, 정작 실전에서 쓰려니 비용 폭탄을 맞는 경험 해보셨나요? 앱을 실행할 때마다 임베딩을 새로 만들어서 API 비용이 계속 나가는 거죠. 이 문제, LlamaIndex의 인덱스 영속화 기능으로 해결할 수 있습니다.

Real Python이 발표한 LlamaIndex 실전 가이드는 단순한 RAG 구현이 아니라, 비용을 줄이고 성능을 높이는 실무 패턴에 집중합니다. 토이 프로젝트와 프로덕션의 차이를 만드는 세 가지 핵심 기술을 다루고 있습니다.
출처: LlamaIndex in Python: A RAG Guide With Examples – Real Python
인덱스 영속화가 비용을 바꾼다
RAG의 핵심은 문서를 벡터 임베딩으로 변환해서 검색 가능하게 만드는 겁니다. 문제는 이 임베딩을 만드는 데 API 비용이 든다는 거죠. 대부분의 튜토리얼에서는 앱을 실행할 때마다 인덱스를 새로 만듭니다. 문서 100개짜리 앱을 하루에 10번 테스트하면? 1,000번의 임베딩 비용을 내는 셈입니다.
LlamaIndex는 .persist() 메서드로 인덱스를 디스크에 저장할 수 있습니다. 한 번 만든 인덱스를 저장해두면, 다음부터는 밀리초 만에 불러와서 쓸 수 있죠. 임베딩 API 호출은 제로, 비용도 제로입니다.
# 인덱스 생성 및 저장
index = VectorStoreIndex.from_documents(documents)
index.storage_context.persist(persist_dir="./storage")
# 다음 실행 시 - 저장된 인덱스 불러오기 (비용 제로!)
storage_context = StorageContext.from_defaults(persist_dir="./storage")
index = load_index_from_storage(storage_context)가이드는 인덱스 영속화의 세 가지 이점을 강조합니다. 첫째, 성능 향상입니다. 재인덱싱을 건너뛰면 앱 시작 시간이 극적으로 줄어들죠. 둘째, 비용 절감입니다. 임베딩 생성 비용은 한 번만 지불합니다. 셋째, 일관성입니다. 같은 임베딩을 재사용하면 세션 간 결과가 일관되게 유지됩니다.
실전에서는 문서가 변경됐을 때만 해당 부분만 다시 인덱싱하는 증분 업데이트도 가능합니다. 전체를 다시 만드는 대신 변경된 문서만 처리하는 거죠.
LLM 갈아타기로 비용과 성능 조절하기
LlamaIndex는 기본적으로 OpenAI의 GPT-3.5 Turbo를 사용하지만, 프로젝트 요구사항에 따라 모델을 바꿀 수 있습니다. 가이드는 OpenAI, Google Gemini, 그리고 로컬 Ollama까지 세 가지 통합 방법을 다룹니다.
API 비용이 부담스럽다면 Ollama로 로컬 모델을 돌릴 수 있습니다. Llama 3.2 같은 소형 모델은 요약 작업에 충분하고, API 호출 비용이 전혀 들지 않죠. 반대로 더 나은 품질이 필요하면 GPT-4나 Gemini 2.0 Flash로 업그레이드할 수도 있습니다.
# OpenAI 모델 선택
from llama_index.llms.openai import OpenAI
llm = OpenAI(model="gpt-4")
query_engine = index.as_query_engine(llm=llm)
# 또는 로컬 Ollama로 전환 (비용 제로!)
from llama_index.llms.ollama import Ollama
llm = Ollama(model="llama3.2")
query_engine = index.as_query_engine(llm=llm)핵심은 같은 RAG 파이프라인을 유지하면서 LLM만 교체할 수 있다는 점입니다. 인덱스는 그대로 두고 쿼리 엔진만 바꾸면 되니까, 여러 모델로 실험하면서 비용 대비 성능의 최적점을 찾을 수 있습니다.
Google의 경우 무료 티어도 제공하니 프로토타입 단계에서는 비용 부담 없이 시작할 수 있습니다. 가이드는 각 제공업체별로 필요한 패키지 설치와 API 키 설정 방법을 구체적으로 안내합니다.
비동기 쿼리로 실전 성능 끌어올리기
대부분의 RAG 예제는 질문 하나씩 동기적으로 처리합니다. 사용자가 여러 질문을 하면 하나씩 순차적으로 답하는 거죠. 하지만 실전 앱에서는 여러 쿼리를 동시에 처리해야 하는 경우가 많습니다.
LlamaIndex는 .aquery() 메서드와 Python의 asyncio를 사용한 비동기 쿼리를 지원합니다. 여러 질문을 리스트로 만들고 asyncio.gather()로 동시에 실행하면, 순차 처리보다 훨씬 빠르게 응답을 받을 수 있습니다.
import asyncio
queries = [
"PEP 8의 주요 목적은?",
"들여쓰기 규칙은?",
"명명 규칙은?"
]
async def run_queries():
tasks = [query_engine.aquery(q) for q in queries]
responses = await asyncio.gather(*tasks)
return responses
# 세 질문을 동시에 처리 - 6초 → 2초로 단축!
responses = asyncio.run(run_queries())예를 들어 세 개의 질문을 순차적으로 처리하면 각각 2초씩 총 6초가 걸립니다. 비동기로 동시에 처리하면 약 2초면 끝나죠. 사용자 경험의 차이가 확실합니다.
RAG를 실전으로 만드는 디테일
가이드는 RAG의 다섯 단계 중 네 가지를 다룹니다. Loading은 SimpleDirectoryReader로 문서를 불러오는 단계입니다. PDF, CSV, Markdown 등 다양한 포맷을 지원하고, LlamaHub에서 추가 커넥터를 찾을 수도 있죠.
Indexing은 VectorStoreIndex로 문서를 벡터 임베딩으로 변환합니다. 작은 데이터셋에는 SummaryIndex, 계층 구조에는 TreeIndex 같은 다른 인덱스 타입도 선택할 수 있습니다.
Persisting은 앞서 말한 대로 인덱스를 저장하는 단계입니다. StorageContext로 저장된 인덱스를 다시 불러올 수 있죠.
Querying은 .as_query_engine()으로 쿼리 엔진을 만들고 실제 질문을 던지는 단계입니다. 자연어 질문이 인덱스를 통해 관련 문서를 찾아내고, 그 컨텍스트와 함께 LLM에 전달됩니다.
토이 프로젝트는 작동만 하면 됩니다. 하지만 실전 앱은 비용 효율적이고, 빠르고, 유연해야 하죠. 인덱스를 재사용하고, LLM을 상황에 맞게 선택하고, 비동기로 성능을 높이는 것. 이 세 가지가 RAG를 실전으로 만드는 차이입니다.
참고자료:
- LlamaIndex Documentation – LlamaIndex 공식 문서
- LlamaHub – 데이터 커넥터 레지스트리

답글 남기기