RAG,Langgraph,LangChain

한상우·2026년 4월 21일

AI

목록 보기
3/11
post-thumbnail

LangChain, RAG, LangGraph — LLM 애플리케이션의 뼈대를 이해하기

ChatGPT가 세상에 나온 뒤 LangChain, RAG, 그리고 LangGraph 에대해 많이 들어봤을겁니다

셋은 서로 경쟁 관계가 아니라 층(layer)이 다른 도구입니다. 이 글에서는 각각이 무엇인지, 왜 필요한지, 그리고 어떻게 한 파이프라인으로 엮이는지 차근차근 풀어보겠습니다.


1. 왜 LLM만으로는 부족한가

GPT든 Claude든, 순수한 LLM은 본질적으로 텍스트 입력 → 텍스트 출력 함수입니다. 하지만 실제 서비스를 만들려면 다음과 같은 문제가 따라옵니다.

  • 환각(Hallucination): 모르는 걸 모른다고 하지 않고 그럴듯하게 지어냅니다.
  • 지식 한계: 학습 시점 이후의 정보, 사내 문서, 최신 뉴스를 모릅니다.
  • 상태 없음(Stateless): 대화 맥락을 스스로 기억하지 못합니다.
  • 외부 세계와의 단절: DB 조회, API 호출, 파일 읽기 같은 행동을 할 수 없습니다.
  • 복잡한 워크플로우: 여러 단계의 추론·결정이 필요한 작업을 구조화하기 어렵습니다.

이 간극을 메우는 것이 바로 아래 도구들입니다.


2. LangChain — LLM 애플리케이션의 레고 블록

2.1 핵심 아이디어

LangChain은 LLM을 중심으로 한 컴포넌트 조립 프레임워크입니다. "프롬프트 → 모델 → 파서"처럼 작은 블록들을 표준 인터페이스로 만들고, 이를 체인(Chain) 으로 연결해서 하나의 애플리케이션을 구성합니다.

2.2 주요 컴포넌트

컴포넌트역할
ModelsOpenAI, Anthropic, HuggingFace 등 LLM/Embedding 모델 추상화
Prompts템플릿화된 프롬프트, 변수 주입, Few-shot 예제 관리
Output ParsersLLM 출력을 JSON, Pydantic 객체 등 구조화된 형태로 파싱
Retrievers벡터 DB 등에서 관련 문서를 가져오는 추상화
Memory대화 히스토리, 요약된 맥락 저장
Tools계산기, 검색, API 호출 등 LLM이 사용할 수 있는 외부 기능
AgentsLLM이 스스로 Tool을 선택·실행하도록 하는 구조

2.3 LCEL — 파이프라인 문법

최근 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 같은 표준 인터페이스를 제공하므로, 어떤 모델을 쓰든 어떤 파서를 쓰든 코드 구조는 그대로 유지됩니다.

2.4 언제 LangChain이 유용한가

  • LLM 기반 기능을 빠르게 프로토타이핑할 때
  • 여러 모델/벡터DB/툴을 교체 가능하게 만들고 싶을 때
  • RAG, 요약, 분류 같은 정형화된 패턴을 구현할 때

한계도 명확합니다. 체인은 기본적으로 선형적(DAG) 이라서, "조건에 따라 되돌아가기"나 "반복 루프"처럼 복잡한 흐름을 표현하기엔 답답합니다. 이 지점에서 LangGraph가 등장합니다.


3. RAG — LLM에 외부 지식을 주입하는 방법

3.1 RAG란 무엇인가

RAG(Retrieval-Augmented Generation, 검색 증강 생성) 는 이름 그대로 "검색으로 보강한 생성"입니다.

질문이 들어오면 → 관련 문서를 찾아서 → 그 문서를 프롬프트에 함께 넣고 → LLM이 답하게 한다.

파인튜닝 없이도 LLM이 사내 위키, 최신 논문, 제품 매뉴얼 같은 외부 지식을 "아는 것처럼" 답할 수 있게 만드는 가장 실용적인 기법입니다.

3.2 RAG 파이프라인 전체 그림

RAG는 크게 인덱싱(오프라인)검색·생성(온라인) 두 단계로 나뉩니다.

[오프라인 — 사전 인덱싱]
원본 문서
   ↓ Load
Document 객체
   ↓ Split (Chunking)
Chunks (조각)
   ↓ Embed (임베딩 모델)
Vector (숫자 배열)
   ↓ Store
Vector DB (Pinecone, Chroma, Weaviate 등)

[온라인 — 질의 시점]
사용자 질문
   ↓ Embed (동일 임베딩 모델)
질문 벡터
   ↓ Similarity Search
관련 Chunks Top-K
   ↓ Prompt 구성 (Context + Question)
LLM
   ↓
최종 답변

3.3 단계별 상세

① 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}

3.4 Naive RAG의 함정과 Advanced RAG

기본 RAG를 구현해보면 금방 한계가 드러납니다.

  • 질문이 짧거나 모호하면 엉뚱한 청크가 걸려옴
  • 여러 문서를 종합해야 답할 수 있는 질문에 약함
  • 검색된 청크 중 진짜 관련 있는 건 1~2개뿐인데 전부 프롬프트에 들어감

이를 개선하는 기법들을 통틀어 Advanced RAG라고 부릅니다.

  • Query Rewriting / HyDE: 질문을 LLM이 다시 쓰거나, 가상의 답변을 먼저 만들어 그걸로 검색
  • Hybrid Search: 벡터 검색 + BM25 키워드 검색을 결합
  • Reranking: Cross-Encoder(예: Cohere Rerank, bge-reranker)로 Top-K를 재정렬
  • Multi-Query: 질문을 여러 개로 확장해 각각 검색 후 통합
  • Parent-Child Chunking: 작은 청크로 검색하되, 생성 시에는 부모 청크를 전달
  • Self-RAG / Corrective RAG: LLM이 검색 결과의 품질을 스스로 평가하고 필요하면 재검색

이쯤 되면 흐름이 "Retrieve → Generate"의 일직선이 아니라 "검색 → 평가 → 재질의 → 재검색 → 생성" 처럼 분기와 루프가 생깁니다. 체인으로는 표현이 어려워지죠. 이제 LangGraph 차례입니다.


4. LangGraph — 상태와 분기가 있는 에이전트를 위한 프레임워크

4.1 왜 그래프인가

LangChain의 체인이 "파이프라인" 이라면, LangGraph는 "상태 기계(State Machine)" 혹은 "워크플로우 엔진" 입니다. 복잡한 LLM 애플리케이션은 대개 이런 특징을 갖습니다.

  • 조건에 따라 다른 경로로 분기한다
  • 실패하면 이전 노드로 되돌아가 재시도한다
  • 도구를 쓰고, 결과를 보고, 또 쓸지 말지 반복 판단한다
  • 여러 에이전트가 협업하며 메시지를 주고받는다

이 모든 것을 자연스럽게 표현하려면 방향 그래프, 그것도 사이클을 허용하는 그래프 가 필요합니다. LangGraph는 바로 이걸 위해 만들어졌습니다.

4.2 핵심 개념 세 가지

① 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 (엣지)
노드 간 연결. 두 종류가 있습니다.

  • 일반 엣지: A 다음엔 무조건 B
  • 조건부 엣지(Conditional 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()

4.3 LangGraph가 빛나는 상황

  • Agentic RAG: 검색 결과가 빈약하면 스스로 질문을 다시 쓰는 RAG
  • ReAct 에이전트: "생각 → 도구 사용 → 관찰 → 생각..." 루프
  • Multi-Agent: 기획자·작성자·검토자 에이전트가 협업
  • Human-in-the-loop: 특정 노드에서 사람 승인을 기다림 (LangGraph는 체크포인트/재개를 네이티브 지원)
  • 장기 실행 워크플로우: 상태를 DB에 저장하고 며칠 뒤 이어받기

5. 셋을 엮어보기 — Agentic RAG 파이프라인 예시

세 기술이 실제로 어떻게 협연하는지 구체적인 시나리오로 보겠습니다.

시나리오: 사내 기술 문서 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입니다. 역할이 깔끔하게 분리됩니다.


6. 실무에서 기억해두면 좋은 팁

  • RAG 품질 = 청킹 + 검색. 모델 바꾸기 전에 청킹 전략과 Reranker부터 튜닝하세요.
  • 메타데이터 필터링을 과소평가하지 마세요. "2024년 이후 문서만", "특정 프로젝트만" 같은 조건이 종종 벡터 유사도보다 강력합니다.
  • Naive RAG → Advanced RAG → Agentic RAG 순으로 점진적으로 올라가세요. 처음부터 LangGraph를 꺼낼 필요는 없습니다.
  • 평가 체계를 먼저 만드세요. RAGAS, LangSmith 같은 도구로 "정답률, 충실도(Faithfulness), 관련성"을 정량화하지 않으면 개선했는지 알 길이 없습니다.
  • LangGraph는 체크포인팅이 강력합니다. 프로덕션에서 실패 복구, human-in-the-loop, 긴 워크플로우를 다룰 때 체인으로는 흉내내기 어려운 이점입니다.
  • 프레임워크에 지나치게 종속되지 마세요. 특히 LangChain은 API가 빠르게 변합니다. 핵심 로직(프롬프트, 검색 전략)은 프레임워크 바깥에 두는 구조를 권장합니다.

7. 정리

도구한 줄 정의주 용도
LangChainLLM 컴포넌트 조립 프레임워크프롬프트·모델·파서·툴을 표준화된 블록으로 연결
RAG검색으로 LLM 지식을 보강하는 기법외부·최신·사내 문서 기반 Q&A
LangGraph상태·분기·루프가 있는 워크플로우 엔진복잡한 에이전트, Agentic RAG, 멀티 에이전트

한 문장으로 요약하면 — LangChain으로 블록을 만들고, RAG로 지식을 연결하고, LangGraph로 흐름을 지휘합니다.

LLM 애플리케이션은 결국 "좋은 맥락을 모아서(Context) → 잘 묻고(Prompt) → 올바른 흐름으로 흘려보내는(Flow)" 싸움입니다. 이 세 가지 도구는 그 각 축을 담당하고 있고, 어느 하나가 다른 하나를 대체하지 않습니다. 상황에 맞는 추상화 수준을 고르는 안목이 실력의 차이를 만듭니다.

profile
안녕하세요

0개의 댓글