LlamaIndex를 실무 흐름으로 이해하기
LlamaIndex로 고급 RAG 파이프라인, ReAct Agent, 서브쿼리 분해, 하이브리드 검색을 구축합니다. ChromaDB·Pinecone 연동, RAGAS 평가, 프로덕션 배포까지 데이터 중심 AI Agent의 전 과정을 다룹니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
LlamaIndex로 고급 RAG 파이프라인, ReAct Agent, 서브쿼리 분해, 하이브리드 검색을 구축합니다. ChromaDB·Pinecone 연동, RAGAS 평가, 프로덕션 배포까지 데이터 중심 AI Agent의 전 과정을 다룹니다.
LlamaIndex로 고급 RAG 파이프라인, ReAct Agent, 서브쿼리 분해, 하이브리드 검색을 구축합니다. ChromaDB·Pinecone 연동, RAGAS 평가, 프로덕션 배포까지 데이터 중심 AI Agent의 전 과정을 다룹니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
모델과 프롬프트만 보지 않고, 데이터 흐름, 평가, 배포 이후의 운영 지표까지 한 번에 연결해서 봅니다.
글로 읽은 내용을 머릿속에 오래 남기려면 먼저 흐름을 그림으로 잡는 편이 좋습니다. 아래 두 그림은 LlamaIndex를 학습할 때 계속 되돌아볼 수 있는 기준 지도입니다.
LlamaIndex를 처음 펼칠 때는 세부 명령보다 큰 그림이 먼저입니다. 이 섹션에서는 앞으로 배울 개념들이 어떤 문제를 풀기 위해 등장했는지부터 잡아봅니다.
from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
# 전역 설정 — 모든 모듈이 자동으로 참조
Settings.llm = OpenAI(model="gpt-4o", temperature=0.1)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.chunk_size = 512 # 노드 최대 토큰 수
Settings.chunk_overlap = 50 # 청크 간 겹침 (문맥 연속성 유지)| 인덱스 유형 | 특징 | 적합한 용도 |
|---|---|---|
| VectorStoreIndex | 임베딩 유사도 기반 검색 | 의미 검색, FAQ, 지식베이스 |
| SummaryIndex | 전체 노드 순차 처리 | 문서 전체 요약, 긴 리포트 |
| KeywordTableIndex | BM25 키워드 매칭 | 정확한 용어 검색, 법률/의학 |
| KnowledgeGraphIndex | 그래프 관계 탐색 | 엔티티 연결, 다중 홉 질문 |
여기서는 고급 인덱싱 전략을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from llama_index.core.node_parser import (
SentenceWindowNodeParser,
HierarchicalNodeParser,
get_leaf_nodes,
)
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.core.indices.postprocessor import (
MetadataReplacementPostProcessor,
SentenceTransformerRerank,
)
# --- Sentence Window 인덱싱 ---
window_parser = SentenceWindowNodeParser.from_defaults(
window_size=3, # 앞뒤 3문장을 윈도우로
window_metadata_key="window",
original_text_metadata_key="original_text",
)
nodes = window_parser.get_nodes_from_documents(documents)
window_index = VectorStoreIndex(nodes)
# 검색 시 윈도우로 텍스트를 교체하는 후처리기
window_query_engine = window_index.as_query_engine(
node_postprocessors=[
MetadataReplacementPostProcessor(target_metadata_key="window"),
SentenceTransformerRerank(top_n=3, model="BAAI/bge-reranker-base"),
]
)여기서는 하이브리드 검색 & 리랭킹을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.postprocessor.cohere_rerank import CohereRerank
# 벡터 검색기와 BM25 검색기 생성
vector_retriever = index.as_retriever(similarity_top_k=10)
bm25_retriever = BM25Retriever.from_defaults(nodes=nodes, similarity_top_k=10)
# RRF로 두 검색 결과 융합 + 4개의 쿼리 변형으로 재현율 향상
hybrid_retriever = QueryFusionRetriever(
[vector_retriever, bm25_retriever],
similarity_top_k=10,
num_queries=4, # 쿼리를 4가지로 변형해 검색
mode="reciprocal_rerank",
)
# Cohere cross-encoder로 최종 top-3 선택
reranker = CohereRerank(api_key="co_...", top_n=3)
from llama_index.core.query_engine import RetrieverQueryEngine
query_engine = RetrieverQueryEngine(
retriever=hybrid_retriever,
node_postprocessors=[reranker],
)여기서는 ReAct Agent을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from llama_index.core.agent import ReActAgent
from llama_index.core.tools import QueryEngineTool, ToolMetadata
# 지식 소스를 도구로 래핑
knowledge_tool = QueryEngineTool(
query_engine=query_engine,
metadata=ToolMetadata(
name="knowledge_base",
description="회사 내부 문서, 정책, 절차를 검색합니다. 구체적인 질문을 입력하세요.",
),
)
agent = ReActAgent.from_tools(
[knowledge_tool],
verbose=True, # 추론 과정 출력 (디버깅에 필수)
max_iterations=10, # 무한 루프 방지
)
# 에이전트가 스스로 검색 → 분석 → 답변 생성
response = agent.chat("분기별 매출 트렌드를 분석하고 이상치를 찾아줘")
print(response)여기서는 이벤트 기반 워크플로우을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from llama_index.core.workflow import (
Workflow, StartEvent, StopEvent, step, Event
)
from llama_index.core import VectorStoreIndex
class RetrievalEvent(Event):
"""검색 결과를 다음 스텝으로 전달하는 이벤트"""
nodes: list
query: str
class RAGWorkflow(Workflow):
@step
async def retrieve(self, ev: StartEvent) -> RetrievalEvent:
"""1단계: 쿼리로 관련 문서 검색"""
retriever = index.as_retriever(similarity_top_k=5)
nodes = await retriever.aretrieve(ev.query)
return RetrievalEvent(nodes=nodes, query=ev.query)
@step
async def generate(self, ev: RetrievalEvent) -> StopEvent:
"""2단계: 검색된 문서를 바탕으로 LLM 답변 생성"""
context = "\n".join([n.get_content() for n in ev.nodes])
response = await Settings.llm.acomplete(
f"컨텍스트:\n{context}\n\n질문: {ev.query}"
)
return StopEvent(result=str(response))
# 워크플로우 실행
wf = RAGWorkflow(timeout=60)
result = await wf.run(query="핵심 내용을 요약해줘")여기서는 Sub-question 분해을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from llama_index.core.query_engine import SubQuestionQueryEngine
from llama_index.core.tools import QueryEngineTool, ToolMetadata
# 각 데이터 소스를 도구로 등록
tools = [
QueryEngineTool(
query_engine=sales_engine,
metadata=ToolMetadata(
name="sales_2024",
description="2024년 월별·분기별 매출 데이터. 금액, 수량, 지역 포함.",
),
),
QueryEngineTool(
query_engine=hr_engine,
metadata=ToolMetadata(
name="hr_data",
description="인사 데이터. 입사일, 부서, 직급, 성과 평가 포함.",
),
),
]
# LLM이 자동으로 서브 질문을 생성하고 병렬 질의
engine = SubQuestionQueryEngine.from_defaults(
query_engine_tools=tools,
verbose=True, # 생성된 서브 질문 확인
)
# 복합 질문 → 서브 질문 분해 → 통합 답변
response = engine.query("2024년 신규 직원의 성과와 매출 기여도는?")
print(response)여기서는 벡터 DB 통합을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import chromadb
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core import StorageContext, VectorStoreIndex
# ChromaDB 영구 클라이언트 생성
chroma_client = chromadb.PersistentClient(path="./chroma_db")
collection = chroma_client.get_or_create_collection(
name="company_docs",
metadata={"hnsw:space": "cosine"}, # 코사인 유사도 사용
)
# LlamaIndex 벡터 스토어로 래핑
vector_store = ChromaVectorStore(chroma_collection=collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
# 최초 인덱싱 (이후 재시작 시에는 from_vector_store로 로드)
index = VectorStoreIndex.from_documents(
documents,
storage_context=storage_context,
show_progress=True,
)
# 기존 컬렉션 재사용 (재인덱싱 불필요)
index = VectorStoreIndex.from_vector_store(vector_store)여기서는 RAGAS 평가을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_recall
from datasets import Dataset
# 평가 데이터셋 구성 — 최소 50개 이상의 질문-정답 쌍 권장
eval_dataset = Dataset.from_dict({
"question": questions, # 테스트 질문 목록
"answer": answers, # RAG 시스템의 답변
"contexts": contexts, # 검색된 컨텍스트 (리스트의 리스트)
"ground_truth": ground_truths, # 참조 정답
})
result = evaluate(
eval_dataset,
metrics=[
faithfulness, # 답변이 컨텍스트에 근거하는가 (환각 탐지)
answer_relevancy, # 답변이 질문과 관련 있는가
context_recall, # 정답에 필요한 정보가 컨텍스트에 있는가
],
)
df = result.to_pandas()
print(df[["question", "faithfulness", "answer_relevancy", "context_recall"]])
# faithfulness > 0.9, answer_relevancy > 0.85 를 목표로 하세요LlamaIndex 실무 설계은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 결정 지점 | 확인 질문 | 실무 기준 |
|---|---|---|
| 경계 | LlamaIndex 코드에서 바뀌기 쉬운 부분은 어디인가? | 입출력, 설정, 외부 연동, 핵심 규칙을 분리합니다. |
| 상태 | 상태가 어디서 생성되고 어디서 사라지는가? | 상태 소유자와 수명 주기를 코드로 드러냅니다. |
| 장애 | 실패했을 때 호출자는 무엇을 받는가? | timeout, fallback, error contract를 먼저 정합니다. |
이 섹션은 LlamaIndex 운영 기준을 실무 관점에서 정리합니다. 개념을 외우기보다, 어떤 상황에서 이 기준을 꺼내 쓸지에 초점을 맞춰보세요.
LlamaIndex 검증 전략은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 품질 축 | 검증 방법 | 완료 기준 |
|---|---|---|
| 정확성 | 정상/실패 케이스를 자동화합니다. | 핵심 시나리오가 재현 가능하게 통과합니다. |
| 회귀 방지 | 버그 수정 시 동일 케이스를 테스트로 남깁니다. | 같은 장애가 다시 배포되지 않습니다. |
| 운영성 | 로그, 메트릭, 알림을 확인합니다. | 문제가 생겼을 때 원인 추적 경로가 있습니다. |