RAG Framework
RAG (Retrieval Augmented Generation) 은 LLM 이 정보를 검색하고 이를 활용하여 결과 생성할 수 있도록 하는 방법이다.

유명한 프레임워크로 LangChain 과 LlamaIndex 가 있다. 이 포스트에서는 두 프레임워크에 어떤 차이점이 있는지를 살펴보려고 한다. RAG 에는 4가지 컴포넌트가 있다. Loaders, Splitters, Indexing, Chains 이 그것이다. 각각 LangChain과 LlamaIndex 가 이를 어떻게 구현하고 있는지 살펴보자.
1. Loaders
Loader 는 API, document, DB 와 같은 다양한 소스들을 로드하는 객체이다. LangChain, LlamaIndex 모두 흔하게 사용되는 소스들을 로드할 수 있는 built-in 함수들을 제공한다. 로더를 통해 파일 위치를 입력해주면 Directory 객체가 반환되며, 이를 다시 한 번 더 로드해주면 텍스트를 로드할 수 있다.
LangChain: Text 파일 로드하기
from langchain.document_loaders import TextLoader
# Load a text document
loader = TextLoader("sample.txt")
documents = loader.load()
print(documents[0].page_content)
LlamaIndex: 특정 디렉토리안에 있는 텍스트 파일 로드하기
from llama_index.core import SimpleDirectoryReader
# Load a text document from a directory
loader = SimpleDirectoryReader('path/to/docs')
documents = loader.load_data()
print(documents[0].text)
2. Splitters
Splitter 는 도큐먼트를 작은 Chunk 단위로 분해해서 GPT 나 BERT 의 token limit 을 넘기지 않도록 만들어주는 역할을 한다.
LangChain
LangChain 의 `TextSplitter` 는 텍스트를 Character, Word, Sentence 중 어떤 단위로 분할할지를 선택할 수 있다. 아래 코드는 Character 단위로 텍스트를 분할하며, chunk size 는 1000이고, chunk 간에 200개의 중복 두는 방식으로 텍스트를 분할한다.
from langchain.text_splitter import CharacterTextSplitter
# Define a character splitter
splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = splitter.split_documents(documents)
print(chunks[0].page_content)
LlamaIndex
LlamaIndex 에서는 아래와 같이 똑같은 작업을 수행할 수 있다.
from llama_index import TokenTextSplitter
# Define a token splitter
splitter = TokenTextSplitter(chunk_size=1000)
chunks = splitter.split(documents)
print(chunks[0].text)
3. Indexing
Indexing 은 RAG system 에서 가장 핵심 부분이라고 할 수 있다. 유저 쿼리와 가장 관련있는 청크를 빠르고 효율적으로 검색할 수 있어야한다. VectorStoreIndex 라는 말이 자주 등장하는데, 이는 VectorStore 를 빠르게 검색해서 유저 쿼리와 연관된 청크를 반환해주는 색인 시스템을 의미한다고 이해하면 된다.
LangChain
LangChain 의 VectorStoreIndex 는 Splitter 를 통해 만들어진 청크들을 통해 인덱스를 생성한다. 그리고 Similarity search 를 기반으로 동작하게 된다.
from langchain.vectorstores import FAISS
from langchain.embeddings.openai import OpenAIEmbeddings
# Create embeddings
embedding_model = OpenAIEmbeddings()
# Create FAISS index
index = FAISS.from_documents(chunks, embedding_model)
LlamaIndex
LlamaIndex 에서는 아래와 같이 Document 로부터 바로 VectorStoreIndex 를 생성할 수 있고, Splitter 를 바로 인자로 주어 생성할 수 있다.
from llama_index.core import VectorStoreIndex
embed_model = OpenAIEmbedding(model="text-embedding-3-small")
index = VectorStoreIndex.from_documents(
documents,
embed_model=embed_model,
transformations=[SentenceSplitter(chunk_size=16000)]
)
4. Chain
RAG 에서 Chain 이란 Retrieval 과 Generation 의 요소들이 결합된 일련의 작업들이라고 보면 된다. LangChain 과 LlamaIndex 모두 이러한 RAG 의 컴포넌트를 엮어서 하나의 체인으로 만드는 기능을 제공한다.
LangChain
LangChain 에서는 아래와 같이 `RetrievalQA` 를 활용해서 LLM 과 Retrieval 가 결합된 체인을 만들 수 있다. 이를 통해 RAG workflow 를 좀 더 간결하게 표현할 수 있다.
from langchain.chains import RetrievalQA
from langchain.llms import OpenAI
# Combine FAISS index and OpenAI for RAG
llm = OpenAI()
qa_chain = RetrievalQA(llm=llm, retriever=index.as_retriever())
result = qa_chain.run("What is the content of the document?")
print(result)
LlamaIndex
LlamaIndex 에서는 VectorStoreIndex 에 as_query_engine 또는 as_chat_engine 을 이용해서 쉽게 Index 와 LLM 을 연결할 수 있다. 해당 Index 를 활용하는 LLM 을 만들라는 의미이다. query engine 과 chat engine 의 차이점은 chat engine 은 컨텍스트를 기억한다. as_chat_engine 을 보면, memory 인자를 전달함으로써, 과거 기억을 얼마나 전달할지를 정할 수 있다. token_limit 이 작을수록 과거 컨텍스트를 모델에 더 적게 전달한다.
# Chat Engine 생성하기
memory = ChatMemoryBuffer.from_defaults(token_limit=16000)
chat_engine = index.as_chat_engine(
chat_mode="context",
memory=memory,
system_prompt=system_prompt
)
response = chat_engine.query("What is the content of the document?")
print(response)
LangChain 과 LlamaIndex 중 무엇을 선택해야할까?
1. Customization
LangChain 의 장점은 다양한 소스를 기반으로 RAG 를 구현해야할 때 더욱 유연하다는 것이다. 예를 들어, Multi-modal RAG 시스템을 구축한다고 해보자. PDF, API 문서, Web 문서 등을 각각 Load 해서 VectorStoreIndex 를 구축한다고 해보자. LangChain 에서는 `+` operation 을 통해 쉽게 로더를 결합할 수 있다.
from langchain.chains import SequentialChain
from langchain.llms import OpenAI
from langchain.document_loaders import WebPageLoader, TextLoader
from langchain.vectorstores import FAISS
# Step 1: Load data from multiple sources
pdf_loader = TextLoader("docs/sample.pdf")
web_loader = WebPageLoader(url="https://example.com")
documents = pdf_loader.load() + web_loader.load()
# Step 2: Create embeddings and index
embedding_model = OpenAIEmbeddings()
index = FAISS.from_documents(documents, embedding_model)
# Step 3: Build a chain that retrieves and generates responses
llm = OpenAI()
qa_chain = RetrievalQA(llm=llm, retriever=index.as_retriever())
# Run the chain
response = qa_chain.run("What are the key points in the PDF and website?")
print(response)
LlamaIndex 의 장점은 더욱 직관적이며, 심플하게 RAG 를 구현할 수 있다는 것이다. 예를 들어 LlamaIndex 의 경우, 법 문서나 medical report 등을 기반으로 RAG 를 만들 때, 매우 빠르게 최소한의 코드로 구축해볼 수 있다.
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
# Load documents from a directory
loader = SimpleDirectoryReader('docs/')
documents = loader.load_data()
# Build a tree index and query
index = VectorStoreIndex(documents)
query_engine = index.as_query_engine()
# Query the system
response = query_engine.query("Summarize the legal document.")
print(response)
2. Ecosystem 통합
LangChain 은 여러 AI Ecosystem 들을 통합할 때 더욱 유용하다. LangChain 은 Multi-LLM, Multi-Retrieval 를 지원한다고 볼 수 있다.
- Vector Database : FAISS, Pinecone, Chroma
- Language Model : OpenAI, GPT-4, Anthropic's Claude
- API : Hugging Face, Cohere
위와 같은 요소들을 자유롭게 결합할 수 있다는 것이 LangChain 의 장점이라고 볼 수 있다. 예를 들어, OpenAI 의 GPT-4 와 HuggingFace 의 BERT 를 태스크에 따라 자유롭게 스위치 할 수 있다.
from langchain.chains import LLMChain
from langchain.chains import SimpleSequentialChain
from langchain.llms import OpenAI, HuggingFaceHub
from langchain_core.prompts import PromptTemplate
prompt_template = "Tell me a {adjective} joke"
prompt = PromptTemplate(
input_variables=["adjective"], template=prompt_template
)
gpt_chain = LLMChain(llm=OpenAI(), prompt=prompt)
gpt_chain.invoke("scary")
bert_chain = LLMChain(llm=HuggingFaceHub(model="bert-large-uncased"), prompt=prompt)
bert_chain.invoke("scary")
반면 LlamaIndex 의 경우 LangChain 과 같은 유연성은 떨어지지만, 앞서 언급한 것처럼 빠르게 document 기반의 RAG 를 만들 때 유용하다고 할 수 있다.
3. Multi-retrieval
LangChain 의 경우 Multi-retrieval 를 구현할 수 있다. 예를 들어, 유저 쿼리를 받아, 법과 관련된 쿼리라면, 법 문서에 적용된 key-based retrieval 를 호출하고, 과학 관련 쿼리라면, 과학 논문에 적용된 embedding-based retrieval 를 호출하는 multi-retrieval 를 구현해보자.
from langchain.retrievers.multi_retriever import MultiRetriever
from langchain.retrievers import FAISSRetriever, KeywordRetriever
# Define keyword and embedding-based retrieval systems
keyword_retriever = KeywordRetriever(documents=legal_documents)
embedding_retriever = FAISSRetriever(index=scientific_index)
# Combine them in a MultiRetriever
retriever = MultiRetriever(retrievers={
'legal': keyword_retriever,
'science': embedding_retriever
})
# Query the retriever with a legal question
response = retriever.retrieve("What are the recent changes in contract law?")
print(response)
4. Community Support 측면
LangChain 은 위와 같은 advanced 된 기능들 때문에 최근 빠르게 관련 커뮤니티가 성장하고 있으며, Llama Index 와 비교하여 좀 더 성장된 ecosystem 을 갖고 있다. LlamaIndex 의 경우 비교적 최근에 등장했지만 마찬가지로 빠르게 관심을 얻고 있으며, 앞서 언급한 것처럼 좀 더 간단한 작업에 적합하다.
결로: 결론적으로 어떤 상황에서 어떤 프레임워크를 선택해야할까?
LangChain 을 선택하면 좋은 상황
- 다양한 구성 요소들을 자유롭게 커스텀하고 싶을 때 (Retrieval, LLM, OpenAI, HuggingFace, FAISS, Chroma...)
- VectorStore 를 다양한 타입의 소스로 구성하고 싶을 때 (Text, PDF, API 등)
- 다양한 Retrieval 전략을 사용하고 싶을 때 (Keyworkd search, post-retrieval ranking 등)
LlamaIndex 를 선택하면 좋은 상황
- 간단한 RAG System 을 구축하고 싶을 때 (예를 들어, document 를 요약해주는 AI 를 구현)
- 특별한 커스텀이 필요없을 때
- 구조적인 문서 (법 관련 문서 등) 을 기반으로 RAG 를 구축할 때 잘 작동함
출처
'Data science > AI' 카테고리의 다른 글
Openai API 기본적인 호출방식 (0) | 2025.02.18 |
---|---|
chroma db 설치시 sqlite3 에러 해결 (0) | 2025.02.07 |
기본적인 RAG 를 좀 더 개선해보기 (w/ langchain) (0) | 2024.10.30 |