본문으로 건너뛰기

LangChain vs LlamaIndex 실전 아키텍처

두 프레임워크의 설계 철학 차이부터 실제 RAG·에이전트 아키텍처, 흔한 함정과 프로덕션 베스트 프랙티스까지

중급AI 개발··33
목차10개 섹션

LLM 앱을 만들다 보면 거의 반드시 마주치는 갈림길이 있다. "LangChain을 쓸까, LlamaIndex를 쓸까?" 검색해 보면 "둘 다 좋아요", "섞어 쓰세요" 같은 맥 빠지는 답이 대부분이다. 하지만 실제로 두 프레임워크는 태어난 목적이 다르고, 그 차이가 코드 구조·디버깅 난이도·확장 방향에 그대로 드러난다. 잘못 고르면 "되긴 되는데 왜 되는지 모르겠고, 고치려면 라이브러리 내부를 뜯어야 하는" 상태에 빠진다.

핵심을 한 줄로 정리하면 이렇다. LangChain은 "LLM을 중심으로 한 범용 오케스트레이션 프레임워크"이고, LlamaIndex는 "데이터를 LLM에 연결하는 것(특히 RAG)에 최적화된 데이터 프레임워크"다. LangChain은 체인·에이전트·도구·메모리·그래프(LangGraph)까지 폭이 넓고, LlamaIndex는 문서 적재→인덱싱→검색→합성으로 이어지는 RAG 파이프라인의 깊이가 강점이다.

이 가이드는 다음을 다룬다.

다루는 내용왜 중요한가
설계 철학·추상화 비교어느 쪽이 내 문제에 자연스러운지 판단 기준
최소 RAG를 양쪽으로 구현같은 문제를 두 코드로 보면 차이가 체감됨
검색/인덱싱 심화 (LlamaIndex 강점)RAG 품질의 80%는 retrieval에서 갈림
에이전트·도구·그래프 (LangChain/LangGraph 강점)멀티스텝·분기·상태 관리가 필요한 워크플로
관측성·평가·프로덕션"데모는 되는데 운영이 안 되는" 구간
흔한 함정 모음실제로 시간을 가장 많이 잡아먹는 곳

전제: Python 3.9+ 환경, OpenAI 혹은 Anthropic 같은 상용 LLM API를 쓴다고 가정한다. 코드는 2024~2025 기준의 모듈 분리 구조(langchain-core, langchain-openai, llama-index-core 등)를 따른다. 버전은 빠르게 바뀌므로, 정확한 핀(pin) 버전은 본인 환경의 pip show로 확인하고 마이그레이션 노트를 항상 같이 보길 권한다.

1. 설계 철학: 오케스트레이터 vs 데이터 프레임워크

두 프레임워크의 차이를 이해하려면 "이게 무엇의 추상화인가"를 봐야 한다.

LangChain — LLM 호출을 합성(compose)하는 오케스트레이터

LangChain의 1차 추상화는 Runnable이다. 프롬프트, 모델, 파서, 리트리버, 함수까지 전부 Runnable로 통일되어 있고, 파이프 연산자(|)로 합성한다. 이걸 LCEL(LangChain Expression Language)이라고 부른다.

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

prompt = ChatPromptTemplate.from_template("{topic}를 한 문장으로 설명해줘")
model = ChatOpenAI(model="gpt-4o-mini")
chain = prompt | model | StrOutputParser()   # 전부 Runnable, | 로 합성

chain.invoke({"topic": "벡터 데이터베이스"})

중요한 건, LangChain은 RAG를 "여러 기능 중 하나"로 본다는 점이다. 에이전트, 도구 호출, 메모리, 스트리밍, 병렬 실행, 분기, 라우팅이 모두 1급 시민이다. 폭이 넓은 대신, RAG에 특화된 편의 기능은 직접 조립해야 한다.

LlamaIndex — 데이터를 LLM에 연결하는 데이터 프레임워크

LlamaIndex의 1차 추상화는 DocumentNodeIndexRetrieverQueryEngine으로 이어지는 데이터 파이프라인이다. "문서를 어떻게 자르고, 어떻게 인덱싱하고, 어떻게 검색해서, 어떻게 답을 합성하는가"가 핵심 관심사다.

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader

documents = SimpleDirectoryReader("./data").load_data()   # 적재
index = VectorStoreIndex.from_documents(documents)        # 청킹+임베딩+인덱싱
query_engine = index.as_query_engine()                    # 검색+합성 엔진

response = query_engine.query("이 문서의 핵심 주장은?")

위 5줄에 LangChain이라면 수동으로 조립해야 할 "로더 → 스플리터 → 임베딩 → 벡터스토어 → 리트리버 → 프롬프트 → 합성"이 전부 합리적 기본값으로 들어 있다. RAG를 빨리, 잘 만드는 데에는 LlamaIndex의 추상화가 더 자연스럽다.

판단 기준 한 줄

  • 문제의 중심이 "내 데이터에 대한 질의응답/검색"이면 → LlamaIndex가 자연스럽다
  • 문제의 중심이 "여러 LLM 단계·도구·분기·상태를 조율"이면 → LangChain(특히 LangGraph)이 자연스럽다
  • 둘 다면? → LlamaIndex로 retrieval 레이어를 만들고, LangGraph로 오케스트레이션을 감싸는 조합이 실전에서 흔하다 (8장 참고)

2. 설치와 패키지 구조: 모놀리식의 종말을 이해하라

두 프레임워크 모두 과거의 거대한 단일 패키지에서 코어 + 통합(integration) 분리 구조로 전환했다. 이 구조를 모르면 설치부터 ImportError로 시간을 날린다.

LangChain의 패키지 분리

pip install langchain-core        # Runnable, 프롬프트, 기본 추상화 (의존성 가벼움)
pip install langchain-openai      # OpenAI 통합 (ChatOpenAI, OpenAIEmbeddings)
pip install langchain-anthropic   # Anthropic 통합 (ChatAnthropic)
pip install langchain-community   # 서드파티 로더/벡터스토어 다수
pip install langgraph             # 상태 그래프 (에이전트/워크플로)
  • langchain-core: 안정적 추상화. 직접 import 대상.
  • langchain (메타 패키지): 체인/에이전트 헬퍼. 점점 LangGraph로 이동 중.
  • langchain-community: 통합이 많지만 품질 편차가 크다. 핵심 통합은 전용 패키지(langchain-openai 등)로 빠져나가는 추세.
  • 핵심 함정: 같은 클래스가 여러 경로에서 import 가능해 보이지만, deprecated 경로를 쓰면 경고/동작 차이가 난다. 항상 전용 통합 패키지에서 import하라 (from langchain_openai import ChatOpenAI, NOT from langchain.chat_models import ...).

LlamaIndex의 패키지 분리

pip install llama-index-core                          # 코어
pip install llama-index-llms-openai                    # OpenAI LLM
pip install llama-index-embeddings-openai              # OpenAI 임베딩
pip install llama-index-vector-stores-chroma           # Chroma 벡터스토어
pip install llama-index-readers-file                   # 파일 로더
# 또는 한 번에 (배치 묶음): pip install llama-index  (편의 메타패키지)
  • import 경로가 패키지 구조와 1:1로 맞는다: from llama_index.llms.openai import OpenAI, from llama_index.vector_stores.chroma import ChromaVectorStore.
  • llama-index 메타패키지는 자주 쓰는 통합을 묶어 주지만, 프로덕션에서는 필요한 것만 골라 설치해 의존성을 줄이는 편이 낫다.

베스트 프랙티스

1. 버전을 반드시 핀하라. requirements.txt 또는 pyproject에 정확한 버전 명시.
   예: langchain-core==<확인한 버전>  (런타임에 pip show 로 확인)
2. 코어와 통합 패키지의 버전 호환을 맞춰라. 코어만 올리면 통합이 깨질 수 있다.
3. CI에서 import smoke test를 둬라. (아래)
# tests/test_imports.py — 패키지 분리 깨짐을 조기 발견
def test_core_imports():
    from langchain_openai import ChatOpenAI       # noqa
    from llama_index.core import VectorStoreIndex # noqa

버전 마이그레이션은 두 프레임워크 공통으로 가장 흔한 운영 비용이다. 업그레이드 전에는 반드시 각 프로젝트의 마이그레이션 가이드를 읽고, 작은 통합 테스트로 회귀를 잡아라.

3. 같은 RAG를 양쪽으로 구현해 비교하기

동일한 문제 — "./data 폴더의 문서로 질의응답" — 를 두 프레임워크로 짜 보면 추상화 수준 차이가 손에 잡힌다.

LlamaIndex 버전 (영속 저장 + 재로드까지)

from llama_index.core import (
    VectorStoreIndex, SimpleDirectoryReader,
    StorageContext, load_index_from_storage, Settings,
)
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding

# 전역 기본값 설정 (LlamaIndex 0.10+ 패턴: ServiceContext 폐기, Settings 사용)
Settings.llm = OpenAI(model="gpt-4o-mini")
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")

import os
if not os.path.exists("./storage"):
    docs = SimpleDirectoryReader("./data").load_data()
    index = VectorStoreIndex.from_documents(docs)
    index.storage_context.persist("./storage")        # 디스크 영속
else:
    sc = StorageContext.from_defaults(persist_dir="./storage")
    index = load_index_from_storage(sc)               # 재빌드 없이 로드

qe = index.as_query_engine(similarity_top_k=4)
print(qe.query("환불 정책 요약해줘"))

주목: 청킹·임베딩·합성 프롬프트·출처(node) 추적이 전부 기본 제공된다. response.source_nodes로 어떤 청크가 근거였는지 바로 볼 수 있다.

LangChain 버전 (LCEL로 명시 조립)

from langchain_community.document_loaders import DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

docs = DirectoryLoader("./data", glob="**/*.txt").load()
splits = RecursiveCharacterTextSplitter(
    chunk_size=1000, chunk_overlap=150
).split_documents(docs)

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

prompt = ChatPromptTemplate.from_template(
    "아래 맥락만 근거로 답하라. 모르면 모른다고 하라.\n\n맥락:\n{context}\n\n질문: {question}"
)

def format_docs(docs):
    return "\n\n".join(d.page_content for d in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt | ChatOpenAI(model="gpt-4o-mini") | StrOutputParser()
)
print(rag_chain.invoke("환불 정책 요약해줘"))

무엇이 다른가

항목LlamaIndexLangChain
코드 길이짧다 (기본값 풍부)길다 (명시 조립)
제어 granularity옵션으로 조정단계마다 손댈 수 있음
출처 추적source_nodes 기본직접 통과시켜야 함
프롬프트 가시성숨어 있음(커스터마이즈 가능)코드에 노출
학습 곡선(RAG)완만다소 가파름

언제 어느 쪽? 표준 RAG를 빠르게 세우고 retrieval 튜닝에 집중하려면 LlamaIndex. 검색 결과를 다른 LLM 단계와 비표준적으로 엮거나, 프롬프트/파서를 완전 통제하려면 LangChain의 명시성이 유리하다.

4. 인덱싱·검색 심화 (LlamaIndex가 빛나는 곳)

RAG 품질은 대부분 retrieval 단계에서 갈린다. LlamaIndex는 이 영역의 도구가 깊다.

노드 파서/청킹 전략

from llama_index.core.node_parser import (
    SentenceSplitter,          # 문장 경계 보존 청킹
    SentenceWindowNodeParser,  # 문장 단위 인덱싱 + 주변 윈도우 보강
)

# 문장 윈도우: 작은 단위로 검색하되, 합성 시엔 앞뒤 맥락을 붙여 줌
parser = SentenceWindowNodeParser.from_defaults(
    window_size=3,
    window_metadata_key="window",
    original_text_metadata_key="original_text",
)

청크가 너무 크면 노이즈가 끼고, 너무 작으면 맥락이 끊긴다. "작게 검색하고 크게 합성한다"(small-to-big)는 LlamaIndex가 sentence-window, auto-merging 같은 패턴으로 잘 지원한다.

하이브리드 검색 + 리랭킹 (실전 품질의 핵심)

from llama_index.core.postprocessor import SentenceTransformerRerank

rerank = SentenceTransformerRerank(
    model="cross-encoder/ms-marco-MiniLM-L-6-v2", top_n=3
)
# 벡터 검색으로 top_k=20을 넓게 가져온 뒤, 리랭커로 top_n=3만 남김
qe = index.as_query_engine(
    similarity_top_k=20,
    node_postprocessors=[rerank],
)

패턴: 넓게 회수(high recall) → 리랭킹으로 정밀화(high precision). 단순 top-k만 키우면 컨텍스트에 노이즈가 늘어 오히려 답이 나빠진다. 리랭커를 붙이는 게 거의 항상 이득이다.

메타데이터 필터링

from llama_index.core.vector_stores import MetadataFilters, MetadataFilter

filters = MetadataFilters(filters=[
    MetadataFilter(key="category", value="billing"),
    MetadataFilter(key="lang", value="ko"),
])
qe = index.as_query_engine(filters=filters)

적재 시 노드 메타데이터(doc.metadata = {...})를 잘 심어 두면, 검색 공간을 미리 좁혀 정확도와 비용을 동시에 잡는다.

LangChain 쪽 동급 기능

LangChain도 가능은 하다. MultiQueryRetriever(질문을 여러 변형으로 확장), ContextualCompressionRetriever(검색 후 압축/리랭킹), EnsembleRetriever(BM25 + 벡터 하이브리드) 등이 있다.

from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever

bm25 = BM25Retriever.from_documents(splits); bm25.k = 5
vect = vectorstore.as_retriever(search_kwargs={"k": 5})
hybrid = EnsembleRetriever(retrievers=[bm25, vect], weights=[0.4, 0.6])

결론: 두 프레임워크 모두 고급 retrieval이 되지만, "기본값의 품질"과 "패턴의 완성도"는 LlamaIndex가 앞선다. RAG 품질이 제품의 생명선이라면 LlamaIndex를 retrieval 엔진으로 두는 선택이 합리적이다.

함정: 리랭커 모델(cross-encoder)은 별도 다운로드/추론 비용이 든다. 서버리스/저메모리 환경에서는 로컬 cross-encoder 대신 호스팅 리랭킹 API를 쓰는 편이 운영이 단순하다.

5. 에이전트·도구·상태 (LangChain/LangGraph가 빛나는 곳)

"검색만"이 아니라 "여러 도구를 부르고, 결과에 따라 분기하고, 상태를 들고 다니는" 워크플로가 되면 무게중심이 LangChain — 특히 LangGraph — 로 넘어간다.

왜 LangGraph인가: 초기 LangChain의 AgentExecutor는 내부 루프가 블랙박스라 디버깅·중단·재개·휴먼인더루프가 어려웠다. LangGraph는 워크플로를 명시적 상태 그래프(노드 + 엣지 + 공유 state)로 모델링해, 각 스텝을 들여다보고 제어할 수 있게 한다.

from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Annotated
import operator

class State(TypedDict):
    question: str
    context: str
    answer: str
    steps: Annotated[list, operator.add]   # 누적 병합 (reducer)

def retrieve(state: State):
    docs = retriever.invoke(state["question"])
    return {"context": format_docs(docs), "steps": ["retrieve"]}

def generate(state: State):
    ans = rag_chain_core.invoke(state)   # context+question -> answer
    return {"answer": ans, "steps": ["generate"]}

def needs_more(state: State):
    return "retrieve" if len(state["context"]) < 200 else "generate"

g = StateGraph(State)
g.add_node("retrieve", retrieve)
g.add_node("generate", generate)
g.add_edge(START, "retrieve")
g.add_conditional_edges("retrieve", needs_more, {"retrieve": "retrieve", "generate": "generate"})
g.add_edge("generate", END)
app = g.compile()

app.invoke({"question": "환불은 며칠 걸려?", "steps": []})

LangGraph의 실전 강점

  • 명시적 제어 흐름: 분기/루프/재시도가 그래프로 보인다.
  • 체크포인팅 + 휴먼인더루프: checkpointer로 상태를 저장해 중단/재개, 사람 승인 후 재개가 가능하다.
  • 멀티 에이전트: 서브그래프로 "supervisor가 워커를 라우팅"하는 구조를 깔끔히 만든다.
  • 스트리밍: 노드별 중간 결과/토큰 스트리밍이 1급 지원.

도구 호출 (tool calling)

from langchain_core.tools import tool

@tool
def get_order_status(order_id: str) -> str:
    """주문 ID로 배송 상태를 조회한다."""
    return db.lookup(order_id)

llm_with_tools = ChatOpenAI(model="gpt-4o-mini").bind_tools([get_order_status])

@tool 데코레이터가 함수 시그니처/독스트링에서 스키마를 자동 생성한다. LangGraph의 prebuilt(예: ReAct 스타일 에이전트)와 결합하면 도구 루프를 빠르게 세울 수 있다.

LlamaIndex의 에이전트는? LlamaIndex에도 FunctionAgent/워크플로 등 에이전트 기능이 있고, 특히 여러 인덱스를 도구로 노출하는 RAG 에이전트(예: 문서별 query engine을 tool로 묶어 라우팅)에 강하다. 다만 임의의 비-RAG 도구·복잡한 분기·멀티 에이전트 오케스트레이션의 성숙도와 제어력은 LangGraph가 더 깊다.

판단 기준: 워크플로가 "검색 도구들 사이의 라우팅" 수준이면 LlamaIndex로 충분. "외부 시스템 호출 + 조건 분기 + 상태 + 사람 승인 + 멀티 에이전트"면 LangGraph.

6. 프롬프트·합성·출력 제어

RAG에서 검색만큼 중요한 게 합성(synthesis) 단계의 프롬프트와 출력 구조다.

LlamaIndex — response_mode와 프롬프트 템플릿

LlamaIndex는 여러 청크를 어떻게 LLM에 먹일지 response_mode로 제어한다.

qe = index.as_query_engine(
    response_mode="compact",      # 청크를 컨텍스트 한도까지 묶어 호출 (기본·효율적)
    # "refine"     : 청크마다 순차 호출하며 답을 점진 개선 (정확하지만 호출 多)
    # "tree_summarize": 계층적 요약 (긴 문서 요약에 적합)
)

기본 합성 프롬프트를 바꾸려면:

from llama_index.core import PromptTemplate

qa_tmpl = PromptTemplate(
    "맥락 정보:\n{context_str}\n\n"
    "위 맥락만 근거로, 한국어로 간결히 답하라. 추측 금지.\n"
    "질문: {query_str}\n답:"
)
qe.update_prompts({"response_synthesizer:text_qa_template": qa_tmpl})

함정: LlamaIndex의 기본 프롬프트는 보이지 않게 동작하므로, 출력 톤·언어·환각 억제가 마음에 안 들면 "내부 프롬프트가 영어 기본일 수 있다"는 점을 의심하고 위처럼 명시 교체하라.

구조화 출력 (둘 다 지원, 방식이 다름)

LangChain — Pydantic 스키마로 구조화:

from pydantic import BaseModel, Field

class Answer(BaseModel):
    summary: str = Field(description="한 문장 요약")
    confidence: float = Field(description="0~1")
    sources: list[str]

structured_llm = ChatOpenAI(model="gpt-4o-mini").with_structured_output(Answer)
structured_llm.invoke("환불 정책을 요약하고 출처를 들어라")

LlamaIndex — 출력 클래스 지정:

from llama_index.core.program import LLMTextCompletionProgram

program = LLMTextCompletionProgram.from_defaults(
    output_cls=Answer,
    prompt_template_str="질문에 답하라: {query}",
)
result: Answer = program(query="환불 정책 요약")

베스트 프랙티스

  • 환각을 줄이려면 합성 프롬프트에 "맥락에 없으면 모른다고 답하라"를 명시하고, 가능한 한 출처(node/문서 id)를 함께 반환시켜라.
  • 구조화 출력은 가능하면 모델의 네이티브 함수/JSON 모드(with_structured_output, tool calling)를 쓰는 게 정규식 파싱보다 안정적이다.
  • 한국어 서비스라면 기본 프롬프트 언어를 반드시 점검하라. 영어 기본 프롬프트 + 한국어 질의 조합에서 답변 톤이 어색해지는 경우가 잦다.

7. 관측성·평가·디버깅

데모와 프로덕션을 가르는 건 "왜 이 답이 나왔는지 추적 가능한가"다.

트레이싱

  • LangChain/LangGraph → LangSmith: 환경변수만으로 모든 체인/그래프 실행이 트레이스로 캡처된다.
export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=<key>
export LANGCHAIN_PROJECT=my-rag

이후 별도 코드 없이 invoke만 해도 단계별 입력/출력/토큰/지연이 기록된다. 분기·도구 호출이 많은 LangGraph 디버깅에 특히 강력하다.

  • LlamaIndex → 콜백/인스트루먼테이션 + 서드파티: LlamaIndex는 instrumentation 이벤트를 노출하고, 여러 관측 도구(예: Arize Phoenix 등 OpenTelemetry 기반)와 연동된다. LangSmith로도 LlamaIndex 실행을 트레이스할 수 있다(콜백 핸들러 연결). 벤더 종속을 피하려면 OpenTelemetry 기반 도구를 권장.

평가(eval) — 추측 말고 측정하라

RAG의 두 축을 분리해 측정해야 한다.

무엇을 보나대표 지표
Retrieval관련 청크를 가져왔나hit rate, MRR, context recall
Generation가져온 맥락에 충실히 답했나faithfulness(환각), answer relevancy
# LlamaIndex 내장 평가기 예시
from llama_index.core.evaluation import FaithfulnessEvaluator

ev = FaithfulnessEvaluator(llm=Settings.llm)
resp = qe.query("환불은 며칠?")
result = ev.evaluate_response(response=resp)   # 맥락 대비 환각 여부
print(result.passing, result.score)

LangChain은 LangSmith의 데이터셋 + 평가기로 회귀 테스트를 돌린다. 프레임워크 무관하게 RAGAS 같은 전용 평가 라이브러리를 붙여 faithfulness/context-recall을 정량화하는 것도 표준 관행이다.

디버깅 베스트 프랙티스

1. 항상 source_nodes / retrieved docs를 로깅하라. 답이 틀리면
   대개 "검색이 틀렸나" vs "합성이 틀렸나"부터 갈라야 한다.
2. 검색만 따로 떼어 평가하라 (LLM 없이 retriever만 호출).
3. 골든 질문셋 20~50개를 CI에 박아 회귀를 자동 검출하라.
4. 토큰/지연/비용을 트레이스로 모니터링 — top_k를 키울 때
   비용이 선형 이상으로 뛰는 지점을 본다.

핵심: "왜 틀렸지"를 retrieval/generation 둘로 분해하지 않으면 끝없이 프롬프트만 만지게 된다.

8. 둘을 함께 쓰는 실전 하이브리드 아키텍처

현장에서 가장 실용적인 답은 종종 "둘 중 하나"가 아니라 "각자의 강점 레이어를 합치는 것"이다.

패턴 A — LlamaIndex로 retrieval, LangGraph로 오케스트레이션

RAG 품질은 LlamaIndex의 인덱싱/리랭킹으로 끌어올리고, 멀티스텝 워크플로·도구·분기·휴먼인더루프는 LangGraph로 감싼다. LlamaIndex의 query engine을 LangChain 도구로 감싸면 된다.

from langchain_core.tools import tool

# LlamaIndex로 만든 고품질 query engine을 LangChain 도구로 노출
@tool
def search_docs(query: str) -> str:
    """사내 문서에서 관련 내용을 검색해 답한다."""
    resp = qe.query(query)                      # qe = LlamaIndex query engine
    cites = [n.node.node_id for n in resp.source_nodes]
    return f"{resp}\n\n[출처: {', '.join(cites)}]"

# 이 도구를 LangGraph 에이전트/노드에서 호출
llm_with_tools = ChatOpenAI(model="gpt-4o-mini").bind_tools([search_docs])

이러면 "검색 정밀도"와 "흐름 제어"를 각각 가장 잘하는 도구로 분담한다.

패턴 B — LangChain 리트리버 인터페이스로 통합

LlamaIndex 인덱스를 LangChain의 BaseRetriever로 어댑트해, 기존 LCEL 체인에 그대로 끼우는 방식도 가능하다(커뮤니티 통합/얇은 래퍼 사용). 기존 LangChain 코드베이스에 LlamaIndex 검색만 이식할 때 유용하다.

경계를 명확히 하라 (아키텍처 규칙)

┌─────────────────────────────────────────────┐
│ Orchestration Layer  (LangGraph)             │  ← 분기/도구/상태/HITL
│   - 사용자 의도 라우팅                        │
│   - 도구 호출 / 외부 시스템                    │
├─────────────────────────────────────────────┤
│ Retrieval Layer  (LlamaIndex)                │  ← 인덱싱/검색/리랭킹/합성
│   - 청킹 전략, 하이브리드 검색, 리랭커          │
├─────────────────────────────────────────────┤
│ Storage  (Vector DB + Metadata + Cache)      │
└─────────────────────────────────────────────┘

함정 / 주의

  • 두 프레임워크의 Document·Node 객체 모델이 다르다. 경계에서 명시적 변환을 두고, 양쪽 객체를 섞어 흘리지 마라.
  • 의존성 충돌(특히 pydantic 메이저 버전, 공통 하위 라이브러리)에 주의. 가상환경에서 두 패키지군 버전 호환을 먼저 검증하고 핀하라.
  • 하이브리드는 강력하지만 표면적이 늘어난다. "정말 둘 다 필요한가"를 먼저 자문하라. 단순 RAG라면 한쪽으로 충분하다.

9. 흔한 함정 모음 (실전 디버깅 체크리스트)

실제로 시간을 가장 많이 잡아먹는 지점들이다. 증상 → 원인 → 해결 형태로 정리한다.

[공통] 버전·import 경로 함정

  • 증상: ImportError, DeprecationWarning, 같은 클래스가 여러 경로.
  • 원인: 모놀리식 → 분리 패키지 전환 과도기. deprecated 경로 잔존.
  • 해결: 전용 통합 패키지에서만 import. 버전 핀. CI에 import smoke test.

[공통] 인덱스 재빌드 비용 폭발

  • 증상: 매 요청/재시작마다 임베딩을 다시 만들어 비용·지연 급증.
  • 원인: 영속 저장 미사용. 메모리 인덱스를 매번 재생성.
  • 해결: LlamaIndex는 persist/load_index_from_storage, LangChain은 벡터스토어 persist_directory로 디스크 영속. 운영은 관리형 벡터 DB.

[LlamaIndex] 숨은 기본 프롬프트가 영어/원하지 않는 톤

  • 증상: 한국어 질의인데 답 구조·톤이 어색, 환각 억제 안 됨.
  • 원인: 내부 합성 프롬프트가 보이지 않게 적용됨.
  • 해결: query_engine.get_prompts()로 확인 후 update_prompts로 교체. "맥락에 없으면 모른다" 명시.

[LlamaIndex] ServiceContext 코드 따라 했는데 안 됨

  • 증상: 오래된 튜토리얼의 ServiceContext가 동작 안 함.
  • 원인: 0.10+에서 ServiceContext 폐기, 전역 Settings로 대체.
  • 해결: Settings.llm, Settings.embed_model 사용. 항상 최신 문서 기준 코드 확인.

[LangChain] AgentExecutor 무한 루프/블랙박스

  • 증상: 에이전트가 같은 도구를 반복 호출, 왜 그러는지 안 보임.
  • 원인: 구형 AgentExecutor의 불투명 루프.
  • 해결: LangGraph로 마이그레이션해 분기·재시도·최대 스텝을 명시 제어. recursion_limit 설정.

[LangChain] 출처가 사라짐

  • 증상: 답은 나오는데 어떤 문서가 근거였는지 모름.
  • 원인: LCEL 체인에서 retrieved docs를 통과시키지 않음.
  • 해결: RunnableParallel{answer, context}를 함께 반환해 출처 보존.

[공통] top_k만 키워서 답이 더 나빠짐

  • 증상: 컨텍스트 늘렸는데 환각·산만함 증가.
  • 원인: recall은 올랐으나 precision 하락, 노이즈 유입.
  • 해결: 넓게 회수 후 리랭커로 정밀화. 컨텍스트는 양보다 질.

[공통] 비동기/스트리밍 혼선

  • 증상: ainvoke/astream 안에서 동기 블로킹 호출로 이벤트 루프 정지.
  • 원인: 동기/비동기 API 혼용.
  • 해결: 경로를 async로 일관. LangGraph/LlamaIndex 모두 async 인터페이스를 끝까지 사용.

[공통] 평가 없이 프롬프트만 튜닝

  • 증상: 고쳐도 나아졌는지 모르고 회귀 발생.
  • 원인: 골든셋·지표 부재.
  • 해결: retrieval/generation 분리 평가 + CI 회귀 테스트.

10. 의사결정 가이드와 마이그레이션 전략

정리. "무엇을 언제"를 한 화면에 모은다.

빠른 결정 표

상황1순위이유
내 문서 Q&A / 챗봇 빠르게LlamaIndexRAG 기본값·출처추적이 풍부
검색 품질이 제품 생명선LlamaIndex청킹/하이브리드/리랭킹 성숙
멀티스텝·도구·분기·상태LangGraph명시적 그래프·체크포인팅·HITL
멀티 에이전트 오케스트레이션LangGraphsupervisor/서브그래프 패턴
둘 다 (검색 + 복잡 워크플로)하이브리드LlamaIndex 검색 + LangGraph 흐름
기존 LangChain 코드에 검색만 강화LangChain + LlamaIndex 리트리버점진적 이식
프로토타입 최단 시간둘 다 가능문제 형태에 따라

팀 관점 추가 고려

  • 관측성을 LangSmith로 표준화할 거면 LangChain 생태계가 매끄럽다. 벤더 중립을 원하면 OpenTelemetry 기반(예: Phoenix) + LlamaIndex 조합.
  • 두 프레임워크 모두 추상화를 끝까지 신뢰하지 말고, 핵심 경로(검색·합성·도구 호출)는 직접 들여다볼 수 있게 로깅/트레이싱을 깔아라.

마이그레이션·교체 전략

1. retrieval 레이어를 인터페이스로 격리하라.
   - search(query) -> [{text, score, source}] 같은 자체 인터페이스를 두고
     내부 구현(LangChain/LlamaIndex)을 갈아끼울 수 있게 한다.
2. 프롬프트·합성 로직을 프레임워크 밖 함수로 빼라.
   - 프레임워크 교체 시 재사용 가능.
3. 골든 질문셋 + 평가 지표를 먼저 만들어라.
   - 교체 전후 품질 회귀를 정량 비교. "느낌"으로 바꾸지 마라.
4. 한 번에 통째로 갈지 말고, 레이어 단위로 점진 교체.
   - 예: 검색만 LlamaIndex로, 오케스트레이션은 그대로.

마지막 원칙: 프레임워크는 수단이다. 두 라이브러리 모두 빠르게 진화하므로, 버전을 핀하고, 마이그레이션 노트를 읽고, 자체 평가셋으로 회귀를 막는 운영 습관이 "LangChain이냐 LlamaIndex냐"보다 제품 수명에 더 큰 영향을 준다. 작게 시작해 측정 가능한 채로 키워라.