LlamaIndex의 데이터 커넥터, 인덱스 유형, 쿼리 엔진, 그리고 이벤트 드리븐 Workflows 1.0을 실전 예제와 함께 분석합니다.
LangChain이 "체인"을 중심으로 설계되었다면, LlamaIndex는 "데이터"를 중심으로 설계되었습니다. "당신의 데이터를 LLM에 연결하라"는 슬로건 그대로, 다양한 데이터 소스에서 정보를 수집하고, 구조화하고, LLM이 효과적으로 활용할 수 있게 만드는 것이 핵심 미션입니다.
이 접근 방식의 장점은 명확합니다. 데이터 수집부터 인덱싱, 검색, 응답 생성까지의 전체 파이프라인을 하나의 프레임워크 안에서 일관되게 관리할 수 있습니다.
데이터 커넥터는 외부 데이터 소스에서 Document 객체를 생성하는 역할을 합니다. LlamaIndex는 LlamaHub을 통해 수백 개의 커넥터를 제공합니다.
from llama_index.core import SimpleDirectoryReader
from llama_index.readers.web import SimpleWebPageReader
from llama_index.readers.database import DatabaseReader
# 로컬 파일 (PDF, DOCX, TXT 등 자동 감지)
documents = SimpleDirectoryReader(
input_dir="./data",
recursive=True,
required_exts=[".pdf", ".md", ".txt"],
).load_data()
# 웹 페이지
web_docs = SimpleWebPageReader(html_to_text=True).load_data(
urls=["https://docs.example.com/guide"]
)
# 데이터베이스
db_docs = DatabaseReader(
sql_database=sql_db,
).load_data(
query="SELECT title, content FROM articles WHERE published = true"
)SimpleDirectoryReader는 파일 확장자를 자동으로 감지하여 적절한 파서를 선택합니다. PDF에는 PyPDF, DOCX에는 python-docx를 사용하며, 커스텀 파서를 등록할 수도 있습니다.
Document는 보통 긴 텍스트를 담고 있습니다. 노드 파서는 이를 LLM의 컨텍스트 윈도우에 맞는 크기의 노드(Node)로 분할합니다.
from llama_index.core.node_parser import (
SentenceSplitter,
SemanticSplitterNodeParser,
HierarchicalNodeParser,
)
from llama_index.core.ingestion import IngestionPipeline
# 문장 기반 분할 (가장 기본적)
sentence_splitter = SentenceSplitter(
chunk_size=1024,
chunk_overlap=200,
)
# 의미 기반 분할 (임베딩 유사도로 경계 결정)
semantic_splitter = SemanticSplitterNodeParser(
buffer_size=1,
breakpoint_percentile_threshold=95,
embed_model=embed_model,
)
# 계층적 분할 (부모-자식 관계 유지)
hierarchical_splitter = HierarchicalNodeParser.from_defaults(
chunk_sizes=[2048, 512, 128],
)
# 인제스천 파이프라인으로 조합
pipeline = IngestionPipeline(
transformations=[
sentence_splitter,
embed_model,
]
)
nodes = pipeline.run(documents=documents)| 전략 | 장점 | 단점 | 적합한 경우 |
|---|---|---|---|
| SentenceSplitter | 빠르고 예측 가능 | 의미 경계 무시 | 일반 문서 |
| SemanticSplitter | 의미 단위 보존 | 임베딩 비용 발생 | 기술 문서, 논문 |
| HierarchicalNodeParser | 다중 해상도 검색 | 복잡한 구조 | 긴 문서, 보고서 |
LlamaIndex는 다양한 인덱스 유형을 제공하며, 각각 다른 검색 패턴에 최적화되어 있습니다.
가장 널리 사용되는 인덱스 유형입니다. 노드를 벡터로 변환하여 유사도 검색을 수행합니다.
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb
# Chroma 벡터 스토어 연결
chroma_client = chromadb.PersistentClient(path="./chroma_db")
chroma_collection = chroma_client.get_or_create_collection("docs")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(
vector_store=vector_store
)
# 인덱스 생성
index = VectorStoreIndex.from_documents(
documents,
storage_context=storage_context,
show_progress=True,
)문서를 트리 구조로 요약하여 인덱싱합니다. 전체 문서에 대한 요약 질문에 효과적입니다.
from llama_index.core import TreeIndex
tree_index = TreeIndex.from_documents(
documents,
num_children=10, # 각 노드의 자식 수
build_tree_from_text=True,
)키워드 기반 검색에 특화된 인덱스입니다. 각 노드에서 키워드를 추출하여 역인덱스를 구축합니다.
from llama_index.core import KeywordTableIndex
keyword_index = KeywordTableIndex.from_documents(
documents,
max_keywords_per_node=10,
)엔티티와 관계를 추출하여 지식 그래프를 구축합니다.
from llama_index.core import KnowledgeGraphIndex
kg_index = KnowledgeGraphIndex.from_documents(
documents,
max_triplets_per_chunk=10,
include_embeddings=True,
)인덱스 유형 선택은 질문의 특성에 따라 달라집니다. "X에 대해 설명해줘"처럼 특정 정보를 찾는 질문에는 VectorStoreIndex가, "이 문서의 핵심 내용은?"처럼 전체 맥락이 필요한 질문에는 TreeIndex가 적합합니다.
인덱스 위에 쿼리 엔진을 구성하여 사용자 질문에 응답합니다.
# 기본 쿼리 엔진
query_engine = index.as_query_engine(
similarity_top_k=5,
response_mode="compact",
)
response = query_engine.query("LlamaIndex의 핵심 개념은 무엇인가요?")
print(response.response)
print(f"소스 노드: {len(response.source_nodes)}")| 모드 | 동작 | 적합한 경우 |
|---|---|---|
refine | 각 노드로 순차적 응답 개선 | 정확한 응답이 필요할 때 |
compact | 노드를 압축 후 한 번에 처리 | 기본 추천 |
tree_summarize | 트리 구조로 요약 | 긴 컨텍스트 요약 |
simple_summarize | 모든 노드를 한 번에 요약 | 빠른 응답 |
여러 인덱스를 조합하여 질문 유형에 따라 적절한 인덱스를 선택합니다.
from llama_index.core.query_engine import RouterQueryEngine
from llama_index.core.selectors import LLMSingleSelector
from llama_index.core.tools import QueryEngineTool
# 각 인덱스를 도구로 래핑
vector_tool = QueryEngineTool.from_defaults(
query_engine=vector_index.as_query_engine(),
description="특정 기술 세부사항을 찾을 때 사용",
)
summary_tool = QueryEngineTool.from_defaults(
query_engine=tree_index.as_query_engine(),
description="전체적인 내용 요약이 필요할 때 사용",
)
# 라우터로 조합
router_engine = RouterQueryEngine(
selector=LLMSingleSelector.from_defaults(),
query_engine_tools=[vector_tool, summary_tool],
)
response = router_engine.query("이 프로젝트의 전반적인 아키텍처를 설명해줘")LlamaIndex의 Workflows는 이벤트 드리븐, 비동기 우선의 오케스트레이션 시스템입니다. 데이터 인제스천, 쿼리 처리, 에이전트 워크플로우를 유연하게 구성할 수 있습니다.
Workflows는 이벤트(Event) 를 중심으로 동작합니다. 각 스텝(Step)은 특정 이벤트를 수신하고, 처리 후 새로운 이벤트를 발행합니다.
from llama_index.core.workflow import (
Workflow,
Event,
StartEvent,
StopEvent,
step,
)
# 커스텀 이벤트 정의
class RetrievalEvent(Event):
nodes: list
query: str
class SynthesisEvent(Event):
response: str
# 워크플로우 정의
class RAGWorkflow(Workflow):
@step
async def retrieve(self, ev: StartEvent) -> RetrievalEvent:
"""문서 검색 단계"""
query = ev.query
nodes = await self.index.aretrieve(query)
return RetrievalEvent(nodes=nodes, query=query)
@step
async def synthesize(self, ev: RetrievalEvent) -> StopEvent:
"""응답 합성 단계"""
context = "\n".join(n.text for n in ev.nodes)
response = await self.llm.acomplete(
f"컨텍스트: {context}\n\n질문: {ev.query}"
)
return StopEvent(result=str(response))
# 실행
workflow = RAGWorkflow()
result = await workflow.run(query="LlamaIndex Workflows란?")복잡한 워크플로우에서 여러 경로를 병렬로 실행하고 결과를 합치는 패턴입니다.
class ParallelRAGWorkflow(Workflow):
@step
async def route(self, ev: StartEvent) -> RetrievalEvent:
"""질문을 여러 검색 경로로 분기"""
# 동시에 두 이벤트 발행
self.send_event(
RetrievalEvent(source="vector", query=ev.query)
)
self.send_event(
RetrievalEvent(source="keyword", query=ev.query)
)
return None # 이 스텝은 StopEvent를 반환하지 않음
@step
async def retrieve(self, ev: RetrievalEvent) -> ResultEvent:
"""각 소스에서 검색"""
if ev.source == "vector":
nodes = await self.vector_index.aretrieve(ev.query)
else:
nodes = await self.keyword_index.aretrieve(ev.query)
return ResultEvent(nodes=nodes, source=ev.source)
@step(num_workers=2) # 2개의 결과를 모두 수집
async def combine(self, ev: ResultEvent) -> StopEvent:
"""결과 합성"""
results = self.collect_events(ev, [ResultEvent, ResultEvent])
if results is None:
return None # 아직 모든 결과가 도착하지 않음
all_nodes = []
for r in results:
all_nodes.extend(r.nodes)
response = await self.synthesize(all_nodes)
return StopEvent(result=response)collect_events를 사용할 때 기대하는 이벤트 수가 정확해야 합니다. 하나의 이벤트가 누락되면 워크플로우가 영구적으로 대기 상태에 빠질 수 있으므로, 타임아웃 설정을 함께 적용하세요.
Workflows 위에 구축된 에이전트 시스템으로, 도구 호출과 추론을 이벤트 기반으로 처리합니다.
from llama_index.core.agent.workflow import AgentWorkflow
from llama_index.core.tools import FunctionTool
# 도구 정의
def search_web(query: str) -> str:
"""웹 검색을 수행합니다."""
return f"검색 결과: {query}에 대한 정보..."
def calculate(expression: str) -> str:
"""수학 계산을 수행합니다."""
return str(eval(expression))
# 에이전트 워크플로우 생성
agent = AgentWorkflow.from_tools_or_functions(
tools_or_functions=[
FunctionTool.from_defaults(fn=search_web),
FunctionTool.from_defaults(fn=calculate),
],
llm=llm,
system_prompt="당신은 유능한 연구 어시스턴트입니다.",
)
# 실행
response = await agent.run("서울의 인구와 면적을 검색하고, 인구밀도를 계산해줘")LlamaIndex는 프레임워크 오버헤드 측면에서 효율적인 성능을 보입니다.
LlamaIndex의 강점은 데이터 처리에 있습니다. 복잡한 에이전트 오케스트레이션이 필요하다면 LangGraph와 조합하는 하이브리드 접근을 고려하세요. LlamaIndex가 데이터 레이어를, LangGraph가 오케스트레이션 레이어를 담당하는 구조입니다. 이 패턴은 11장에서 자세히 다룹니다.
5장에서는 Microsoft의 Semantic Kernel을 분석합니다. C#, Python, Java를 지원하는 멀티 언어 아키텍처, 플러그인 시스템과 플래너, Azure 통합, 그리고 엔터프라이즈 환경에서의 보안과 거버넌스 기능을 살펴보겠습니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
Microsoft Semantic Kernel의 멀티 언어 아키텍처, 플러그인 시스템, 플래너, Azure 통합, 엔터프라이즈 보안과 거버넌스를 분석합니다.
LangGraph 1.0/1.1의 StateGraph, 듀러블 상태, 조건부 엣지, 휴먼인더루프, type-safe 스트리밍을 실전 예제와 함께 분석합니다.
deepset Haystack 2.x의 컴포넌트와 파이프라인 개념, 방향성 멀티그래프, AsyncPipeline, 라우터, 문서 스토어를 분석합니다.