RAG를 LangChain과 LlamaIndex로 직접 만들어 비교한 후기: 코드량·검색속도·유지보수

같은 RAG를 LangChain과 LlamaIndex로 둘 다 만들어봤다: 코드량·검색속도·유지보수 비교

같은 문서 세트로 RAG(검색 증강 생성) 파이프라인을 LangChain과 LlamaIndex 두 가지로 각각 만들어봤습니다. 인터넷에 떠도는 “둘 다 좋은 프레임워크입니다” 식의 뭉뚱그린 비교 말고, 실제로 같은 마크다운 문서 묶음과 같은 임베딩 모델, 같은 벡터 스토어(Chroma)를 써서 코드량·검색 응답 시간·유지보수 체감 차이를 직접 재본 기록을 남깁니다. LangChain과 LlamaIndex 중 어느 쪽을 선택할지 고민하는 분이라면 이 글에서 구체적인 판단 기준을 얻어가실 수 있습니다.

실험 환경: 같은 문서, 같은 벡터 스토어로 맞췄다

비교가 의미 있으려면 조건을 최대한 맞춰야 했습니다. 사내 위키에서 뽑은 마크다운 문서 180개(총 약 45만 자)를 청크 크기 500자, 중복 50자로 나눠 900여 개 청크를 만들고, 임베딩은 text-embedding-3-small, LLM은 gpt-4o-mini, 벡터 스토어는 Chroma로 통일했습니다. 두 프레임워크 모두 같은 Chroma 컬렉션 위에서 동작하도록 구성했기 때문에, 벡터 검색 자체의 알고리즘 차이보다는 “프레임워크가 그 위에 얼마나 무거운 추상화를 얹었는가”를 비교하는 실험이 됐습니다.

패키지 버전은 langchain 0.3.x, langchain-community, langchain-openai, langchain-chroma, 그리고 llama-index-core 0.11.x, llama-index-embeddings-openai, llama-index-llms-openai를 사용했습니다. 두 프레임워크 모두 빠르게 버전이 올라가는 편이라, 실제로 따라 해보실 때는 검색 증강 생성(RAG)의 기본 개념을 먼저 잡고 각 프레임워크의 공식 문서에서 현재 API를 확인하시는 걸 추천합니다.

코드량 비교: LangChain vs LlamaIndex 실제 라인 수

먼저 LangChain으로 짠 코드입니다. 문서 로딩부터 답변 생성까지 전 과정을 LCEL(LangChain Expression Language) 방식으로 작성했습니다.

from langchain_community.document_loaders import DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_chroma import Chroma
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate

loader = DirectoryLoader("./docs", glob="**/*.md")
docs = loader.load()

splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(docs)

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(chunks, embeddings, persist_directory="./chroma_db")
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt = ChatPromptTemplate.from_template(
    "다음 문맥만 참고해서 답변하세요.\n\n{context}\n\n질문: {input}"
)
combine_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(retriever, combine_chain)

result = rag_chain.invoke({"input": "이 문서의 핵심 요약은?"})
print(result["answer"])

LangChain과 LlamaIndex RAG 파이프라인 벡터 검색 구조 비교 다이어그램

같은 결과를 내는 LlamaIndex 코드는 이렇습니다.

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, Settings
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI

Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)

documents = SimpleDirectoryReader("./docs").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine(similarity_top_k=4)

response = query_engine.query("이 문서의 핵심 요약은?")
print(response)

import문을 빼고 실제 로직 라인만 세면 LangChain 쪽이 17줄, LlamaIndex 쪽이 8줄이었습니다. 이유는 명확합니다. LangChain은 프롬프트 템플릿, 문서 결합 체인(create_stuff_documents_chain), 검색-생성 체인(create_retrieval_chain)을 각각 명시적으로 조립해야 하는 반면, LlamaIndex는 Settings 전역 객체에 임베딩·LLM을 등록해두면 VectorStoreIndexquery_engine이 프롬프트 구성까지 내부적으로 처리합니다. 다만 이 차이는 “LlamaIndex가 더 낫다”는 뜻이 아니라 “LangChain은 각 단계를 눈으로 다 보여주고, LlamaIndex는 기본값을 감춘다”는 설계 철학 차이에 가깝습니다.

검색 속도 비교: 벡터 검색 응답 시간 실측

로컬 macOS(M2, 16GB RAM) 환경에서 같은 질의 10개를 각각 5회씩 반복해 retriever.invoke()(LangChain)와 index.as_retriever().retrieve()(LlamaIndex)만 따로 떼어 검색 단계의 시간만 측정했습니다. LLM 답변 생성 시간은 두 프레임워크 모두 OpenAI API 호출이 병목이라 제외했습니다.

항목 LangChain (Chroma) LlamaIndex (Chroma)
청크 수 912개 912개
검색(top-4) 평균 응답 약 90~130ms 약 100~150ms
인덱스 최초 구축 시간 약 38초 약 41초
검색 단계 코드 라인 2줄 2줄

같은 Chroma 컬렉션을 쓰다 보니 실제 벡터 유사도 계산 시간 자체는 거의 같았고, 차이는 대부분 프레임워크가 결과를 문서 객체로 감싸는 오버헤드(메타데이터 매핑, 콜백 훅 실행 등)에서 나왔습니다. 즉 “LangChain이 빠르다/LlamaIndex가 빠르다”고 일반화하기보다, 검색 속도는 결국 벡터 스토어 선택(Chroma, FAISS, pgvector 등)과 임베딩 차원 수가 좌우하고, 프레임워크 자체의 오버헤드는 수십 밀리초 수준의 부가 비용이라는 게 이번 실측의 결론입니다. 문서 수가 수만 건 이상으로 늘어나면 이 오버헤드보다는 벡터 인덱스 알고리즘(HNSW 파라미터 등)의 영향이 훨씬 커질 것으로 보입니다.

유지보수 관점: 커스터마이징과 버전 업그레이드 체감

파이썬 코드 에디터에서 AI 언어 모델 개발하는 화면

코드량과 속도보다 실제로 더 크게 체감한 차이는 유지보수였습니다. LangChain은 체인의 각 단계(로더, 스플리터, 리트리버, 프롬프트, 콤바인 체인)가 분리돼 있어서, 예를 들어 리랭커(reranker)를 중간에 끼워 넣거나 특정 문서 타입만 필터링하는 커스텀 로직을 추가할 때 해당 단계만 갈아끼우면 됐습니다. create_retrieval_chain은 LCEL의 Runnable 인터페이스를 따르기 때문에 파이프(|) 연산자로 커스텀 함수를 끼워 넣기도 수월했습니다.

반면 LlamaIndex는 Settings 전역 설정과 query_engine의 기본 동작이 잘 맞을 때는 압도적으로 코드가 짧지만, 검색 후처리 로직(예: 특정 메타데이터 기준 재정렬)을 세밀하게 바꾸려면 NodePostprocessor, ResponseSynthesizer 같은 하위 클래스를 이해해야 했고, 이 개념들이 LangChain의 체인 구조보다 학습 곡선이 살짝 더 있었습니다. 대신 LlamaIndex는 문서 파싱·인덱싱 관련 유틸(SimpleDirectoryReader의 다양한 파일 포맷 지원, 계층형 인덱스 등)이 RAG 특화로 잘 갖춰져 있어서, “검색 품질 튜닝”에 집중하고 싶을 때는 손이 덜 갔습니다.

버전 업그레이드 측면에서는 LangChain이 지난 1~2년 동안 Chain 기반 구식 API(RetrievalQA 등)에서 LCEL 기반 API로 넘어오면서 마이그레이션 이슈가 종종 있었던 반면, LlamaIndex는 0.10 버전에서 llama-index-core와 통합(integration) 패키지를 분리하는 큰 구조 변경이 있었습니다. 두 프레임워크 모두 활발히 개발 중이라 프로덕션에 쓸 경우 requirements.txt에 버전을 고정해두는 게 안전합니다. 공식 문서는 각각 LangChain 공식 문서LlamaIndex 공식 문서에서 최신 API를 확인할 수 있습니다.

결론: 어떤 상황에 뭘 쓸지

이번 실험을 정리하면, 검색 속도는 벡터 스토어가 같으면 프레임워크 차이가 크지 않았고, 코드량은 기본 파이프라인 기준 LlamaIndex가 절반 수준으로 짧았으며, 유지보수는 세밀한 커스터마이징이 잦다면 LangChain의 체인 분리 구조가, 빠르게 프로토타입을 만들고 검색 품질 자체에 집중하고 싶다면 LlamaIndex의 기본값이 유리했습니다. 사내 챗봇처럼 답변 형식·필터링 로직이 자주 바뀌는 프로젝트는 LangChain으로, 문서 QA처럼 “일단 잘 찾아서 잘 답하면 되는” 프로젝트는 LlamaIndex로 시작해보시길 권합니다. 다음 단계로는 이번 글의 청크 크기(500자)를 200자, 1000자로 바꿔가며 같은 벤치마크를 돌려보시면 프레임워크 차이보다 청크 전략이 검색 품질에 더 크게 영향을 준다는 걸 직접 확인하실 수 있을 겁니다.

자주 묻는 질문(FAQ)

Q1. LangChain과 LlamaIndex를 같이 써도 되나요?
가능합니다. 예를 들어 문서 파싱과 인덱싱은 LlamaIndex의 SimpleDirectoryReader로 하고, 검색 이후 로직은 LangChain의 Runnable 체인으로 감싸는 식의 혼합 구성도 커뮤니티에서 종종 쓰입니다. 다만 두 프레임워크의 의존성이 겹치는 경우가 있어 버전 충돌을 주의해야 합니다.

Q2. 벡터 스토어를 Chroma 대신 FAISS나 pgvector로 바꾸면 결과가 달라지나요?
네, 특히 문서 수가 많아질수록 벡터 스토어의 인덱싱 알고리즘(HNSW, IVF 등) 차이가 검색 속도에 프레임워크 오버헤드보다 훨씬 큰 영향을 줍니다. 이번 글의 결과는 어디까지나 900여 개 청크, Chroma 기준의 상대적 비교로 참고해 주시기 바랍니다.

Q3. 둘 다 처음이라면 어느 쪽부터 배우는 게 좋을까요?
빠르게 동작하는 RAG를 보고 싶다면 LlamaIndex의 기본 VectorStoreIndex부터 시작하고, 이후 검색-생성 흐름을 직접 제어하는 경험이 필요해지면 LangChain의 LCEL 체인 구조로 넘어가는 순서를 추천합니다.

Leave a Comment