ChatGPT가 세상에 나온 뒤 LangChain, RAG, 그리고 LangGraph 에대해 많이 들어봤을겁니다
셋은 서로 경쟁 관계가 아니라 층(layer)이 다른 도구입니다. 이 글에서는 각각이 무엇인지, 왜 필요한지, 그리고 어떻게 한 파이프라인으로 엮이는지 차근차근 풀어보겠습니다.
GPT든 Claude든, 순수한 LLM은 본질적으로 텍스트 입력 → 텍스트 출력 함수입니다. 하지만 실제 서비스를 만들려면 다음과 같은 문제가 따라옵니다.
이 간극을 메우는 것이 바로 아래 도구들입니다.
LangChain은 LLM을 중심으로 한 컴포넌트 조립 프레임워크입니다. "프롬프트 → 모델 → 파서"처럼 작은 블록들을 표준 인터페이스로 만들고, 이를 체인(Chain) 으로 연결해서 하나의 애플리케이션을 구성합니다.
| 컴포넌트 | 역할 |
|---|---|
| Models | OpenAI, Anthropic, HuggingFace 등 LLM/Embedding 모델 추상화 |
| Prompts | 템플릿화된 프롬프트, 변수 주입, Few-shot 예제 관리 |
| Output Parsers | LLM 출력을 JSON, Pydantic 객체 등 구조화된 형태로 파싱 |
| Retrievers | 벡터 DB 등에서 관련 문서를 가져오는 추상화 |
| Memory | 대화 히스토리, 요약된 맥락 저장 |
| Tools | 계산기, 검색, API 호출 등 LLM이 사용할 수 있는 외부 기능 |
| Agents | LLM이 스스로 Tool을 선택·실행하도록 하는 구조 |
최근 LangChain은 LCEL(LangChain Expression Language) 을 표준으로 밀고 있습니다. 유닉스 파이프처럼 | 연산자로 컴포넌트를 연결합니다.
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
prompt = ChatPromptTemplate.from_template("{topic}에 대해 한 문장으로 설명해줘.")
model = ChatOpenAI(model="gpt-4o-mini")
parser = StrOutputParser()
chain = prompt | model | parser
result = chain.invoke({"topic": "양자역학"})
prompt | model | parser — 이 한 줄이 LangChain 철학을 집약합니다. 각 블록은 invoke, stream, batch 같은 표준 인터페이스를 제공하므로, 어떤 모델을 쓰든 어떤 파서를 쓰든 코드 구조는 그대로 유지됩니다.
한계도 명확합니다. 체인은 기본적으로 선형적(DAG) 이라서, "조건에 따라 되돌아가기"나 "반복 루프"처럼 복잡한 흐름을 표현하기엔 답답합니다. 이 지점에서 LangGraph가 등장합니다.
RAG(Retrieval-Augmented Generation, 검색 증강 생성) 는 이름 그대로 "검색으로 보강한 생성"입니다.
질문이 들어오면 → 관련 문서를 찾아서 → 그 문서를 프롬프트에 함께 넣고 → LLM이 답하게 한다.
파인튜닝 없이도 LLM이 사내 위키, 최신 논문, 제품 매뉴얼 같은 외부 지식을 "아는 것처럼" 답할 수 있게 만드는 가장 실용적인 기법입니다.
RAG는 크게 인덱싱(오프라인) 과 검색·생성(온라인) 두 단계로 나뉩니다.
[오프라인 — 사전 인덱싱]
원본 문서
↓ Load
Document 객체
↓ Split (Chunking)
Chunks (조각)
↓ Embed (임베딩 모델)
Vector (숫자 배열)
↓ Store
Vector DB (Pinecone, Chroma, Weaviate 등)
[온라인 — 질의 시점]
사용자 질문
↓ Embed (동일 임베딩 모델)
질문 벡터
↓ Similarity Search
관련 Chunks Top-K
↓ Prompt 구성 (Context + Question)
LLM
↓
최종 답변
① Load (문서 로드)
PDF, HTML, Notion, Confluence, DB 등에서 원본을 읽어옵니다. LangChain의 DocumentLoader가 수십 종의 소스를 지원합니다.
② Split (청킹)
LLM 컨텍스트 한계, 그리고 검색 정확도를 위해 긴 문서를 잘게 자릅니다. 보통 500~1,500 토큰 단위에 오버랩 10~20% 를 두어 경계에서 잘린 문맥을 보정합니다. 단순 문자 단위보다 재귀적 분할(RecursiveCharacterTextSplitter) 이나 의미 기반 분할(Semantic Chunking) 이 품질이 좋습니다.
③ Embed (임베딩)
각 청크를 임베딩 모델(text-embedding-3-small, bge-m3 등)로 수백~수천 차원의 벡터로 변환합니다. 의미가 비슷한 문장은 벡터 공간에서도 가까이 위치합니다.
④ Store (저장)
벡터 DB에 (벡터, 원문, 메타데이터)를 저장합니다. 메타데이터(출처, 날짜, 태그 등)는 나중에 필터링에 필수입니다.
⑤ Retrieve (검색)
질문도 같은 임베딩 모델로 벡터화한 뒤, 코사인 유사도/내적으로 Top-K 청크를 가져옵니다.
⑥ Generate (생성)
검색된 청크를 컨텍스트로 삼아 프롬프트를 구성합니다.
다음 컨텍스트를 참고해 질문에 답하세요.
모르면 "모릅니다"라고 답하세요.
[컨텍스트]
{retrieved_chunks}
[질문]
{user_question}
기본 RAG를 구현해보면 금방 한계가 드러납니다.
이를 개선하는 기법들을 통틀어 Advanced RAG라고 부릅니다.
이쯤 되면 흐름이 "Retrieve → Generate"의 일직선이 아니라 "검색 → 평가 → 재질의 → 재검색 → 생성" 처럼 분기와 루프가 생깁니다. 체인으로는 표현이 어려워지죠. 이제 LangGraph 차례입니다.
LangChain의 체인이 "파이프라인" 이라면, LangGraph는 "상태 기계(State Machine)" 혹은 "워크플로우 엔진" 입니다. 복잡한 LLM 애플리케이션은 대개 이런 특징을 갖습니다.
이 모든 것을 자연스럽게 표현하려면 방향 그래프, 그것도 사이클을 허용하는 그래프 가 필요합니다. LangGraph는 바로 이걸 위해 만들어졌습니다.
① State (상태)
그래프 전체에서 공유되는 데이터 구조. 보통 TypedDict로 정의합니다.
from typing import TypedDict, List
class AgentState(TypedDict):
question: str
documents: List[str]
answer: str
retry_count: int
② Node (노드)
상태를 입력받아 상태의 일부를 업데이트해 반환하는 함수. 하나의 작업 단위입니다.
def retrieve_node(state: AgentState):
docs = vectorstore.similarity_search(state["question"], k=5)
return {"documents": [d.page_content for d in docs]}
③ Edge (엣지)
노드 간 연결. 두 종류가 있습니다.
from langgraph.graph import StateGraph, END
graph = StateGraph(AgentState)
graph.add_node("retrieve", retrieve_node)
graph.add_node("grade", grade_node)
graph.add_node("generate", generate_node)
graph.add_node("rewrite", rewrite_node)
graph.set_entry_point("retrieve")
graph.add_edge("retrieve", "grade")
# 검색 품질이 낮으면 질문을 재작성해 다시 검색
graph.add_conditional_edges(
"grade",
lambda s: "generate" if s["is_relevant"] else "rewrite",
{"generate": "generate", "rewrite": "rewrite"}
)
graph.add_edge("rewrite", "retrieve") # ← 사이클
graph.add_edge("generate", END)
app = graph.compile()
세 기술이 실제로 어떻게 협연하는지 구체적인 시나리오로 보겠습니다.
시나리오: 사내 기술 문서 Q&A 봇. 검색 품질이 낮으면 스스로 질문을 바꿔 재검색하고, 답변 후에는 환각 여부를 자가 검증한다.
[LangChain 담당]
- 임베딩 모델, 벡터스토어, 프롬프트 템플릿, 출력 파서
- 각 단계의 "블록" 자체는 LangChain으로 구성
[RAG 담당]
- 사내 문서 인덱싱(오프라인)
- 검색 → 컨텍스트 주입 → 생성의 핵심 로직
[LangGraph 담당]
- 전체 흐름의 오케스트레이션
- 조건부 분기, 재시도 루프, 상태 관리
흐름을 그림으로 그리면:
┌──────────┐
│ START │
└────┬─────┘
▼
┌───────────────┐
│ retrieve │ ← LangChain Retriever
└───────┬───────┘
▼
┌───────────────┐
│ grade_docs │ LLM이 문서 관련성 평가
└───┬───────┬───┘
relevant│ │not relevant
▼ ▼
┌───────────┐ ┌──────────────┐
│ generate │ │ rewrite_query│
└─────┬─────┘ └──────┬───────┘
▼ │
┌───────────┐ │ (retry_count < 3)
│ hallucin- │ └──────┐
│ ation? │ ▼
└───┬───┬───┘ ┌───────────┐
no │ │ yes │ retrieve │ (loop)
▼ └──────┐ └───────────┘
┌──────┐ │
│ END │ └──→ regenerate
└──────┘
노드 각각은 LangChain 체인이고, 검색 로직은 RAG이며, 노드들을 잇고 루프를 돌리는 건 LangGraph입니다. 역할이 깔끔하게 분리됩니다.
| 도구 | 한 줄 정의 | 주 용도 |
|---|---|---|
| LangChain | LLM 컴포넌트 조립 프레임워크 | 프롬프트·모델·파서·툴을 표준화된 블록으로 연결 |
| RAG | 검색으로 LLM 지식을 보강하는 기법 | 외부·최신·사내 문서 기반 Q&A |
| LangGraph | 상태·분기·루프가 있는 워크플로우 엔진 | 복잡한 에이전트, Agentic RAG, 멀티 에이전트 |
한 문장으로 요약하면 — LangChain으로 블록을 만들고, RAG로 지식을 연결하고, LangGraph로 흐름을 지휘합니다.
LLM 애플리케이션은 결국 "좋은 맥락을 모아서(Context) → 잘 묻고(Prompt) → 올바른 흐름으로 흘려보내는(Flow)" 싸움입니다. 이 세 가지 도구는 그 각 축을 담당하고 있고, 어느 하나가 다른 하나를 대체하지 않습니다. 상황에 맞는 추상화 수준을 고르는 안목이 실력의 차이를 만듭니다.