이 챕터의 흐름
앞서 설계한 AI Agent의 개념과 워크플로우를 실제 코드로 어떻게 구현하는가를 다룬다. LangChain의 역사적 진화를 통해 왜 LangGraph가 등장했는지 이해하고, LangGraph의 핵심 구성 요소(State, Node, Edge)를 이해한 뒤, 실제 시스템을 설계하는 패턴(Loop, Memory, Human-in-the-Loop)과 병렬 처리 기법을 학습한다. 구현의 핵심은 "사고 구조(Reasoning)"와 "운영 정책(Governance)"을 분리하는 것이다.
초기 LangChain은 LCEL(LangChain Expression Language) 기반으로 다양한 응용 프로그램을 만드는 프레임워크였다.
기본 구조:

LCEL 주요 기능:
AS-IS (문제점):
한계점 — 운영 서비스에서 필요한 것들이 Chain 내부에 포함되면 설계가 복잡해진다:
핵심 문제: "사고 구조"와 "운영 정책"이 뒤섞여 있음
LangChain 1.0은 이 문제를 해결하기 위해 Agent(사고 구조)와 Runtime Governance(운영 정책)를 분리한 아키텍처로 재정렬되었다.
아키텍처 계층:

Pseudo Code 비교:
# LangChain Pre: 사고 구조와 운영 로직이 섞여 있음
def handle_request(user_input):
prompt = build_prompt(user_input)
response = llm.invoke(prompt)
if violates_policy(response): # 운영 로직
response = regenerate(response)
log_trace(response) # 운영 로직
track_cost(response) # 운영 로직
return output_parser(response)
# LangChain 1.0: 사고 구조와 운영 정책이 분리됨
agent = create_agent(graph=design) # 사고 구조
runtime = wrap_with_middleware( # 운영 정책
agent,
governance=[policy_check],
observability=[tracing],
operational=[retry, timeout],
transformation=[schema_validation],
)
def handle_request(user_input):
return runtime.invoke(user_input) # 호출 흐름은 동일
LangGraph는 LLM을 활용한 Stateful Multi-Agent Application 구축을 위한 Orchestration Engine이다.
복잡한 문제는 한 번의 시도(Zero-shot)가 아닌, 반복적 사고와 수정(Loop)을 통해서만 해결 가능하다.
Linear Chain을 넘어, 순환하는 아키텍처가 필요하다.
LangGraph의 핵심 특징:
LangChain 진화 비교:
| 항목 | LangChain (Pre~1.0) | LangChain 1.0 | LangGraph |
|---|---|---|---|
| 설계 철학 | Chain (선형 연결), 단순 조립형 파이프라인 | Standard Agents (표준화), 표준화된 에이전트 규격 | Flow Engineering (흐름 설계), State 기반의 정밀 제어 |
| 흐름 제어 | Linear (단방향), 순차적 처리만 가능 | Managed Loop (관리형 루프), 내장된 루프가 자동 동작 | Cyclic (순환/분기), 개발자가 루프와 조건을 직접 통제 |
| STATE | Stateless, 단발성 실행 후 종료 | Implicit State, 에이전트 내부에서 자동 관리 | Explicit State, Schema로 정의하여 보존 |
| 조건 분기 | Custom 코드 필요, 구현 난이도 높음 | Built-in Logic, 표준 패턴 사용 | Conditional Edge, 흐름 정의로 처리 |
| 디버깅 | Callbacks, 제한적 | LangSmith 연동, 추적 및 모니터링 | Time-Travel, 과거 시점으로 되돌리기 및 수정 |
Agent Architecture = LangChain + LangGraph:
| 역할 | 프레임워크 | 핵심 |
|---|---|---|
| Agent를 정의하는 프레임워크 (설계) | LangChain | Agent = Prompt + Model + Tool |
| Agent를 실행하는 Runtime Engine (실행) | LangGraph | Agent = State + Runtime + Middleware |
LangGraph의 핵심은 3가지 구성 요소다.
| 구성 요소 | 목적 | 역할 |
|---|---|---|
| Node | 어떤 작업(task)을 수행할지 정의 | 특정 로직 수행 또는 상태 업데이트 (입력으로 현재 상태를 받고, 업데이트된 상태 반환) |
| Edge | 다음으로 실행할 동작 정의 | 워크플로우의 흐름 제어 |
| Conditional Edge | 조건에 따른 분기 처리 | 조건에 따른 흐름 분기, 다음에 실행할 노드 결정 |
| State | 현재의 상태값을 저장 및 전달 | 전체 워크플로우의 흐름 정보 유지, 노드 간 정보 공유 |
State는 LangGraph 전체 흐름의 기억 저장소이며, 노드 간 연결성과 판단을 위한 핵심 데이터 구조다.
State의 두 가지 핵심 역할:
State 코드 예시:
from typing import Annotated, TypedDict
from langgraph.graph.message import add_messages
class GraphState(TypedDict):
question: Annotated[list, add_messages] # 질문 (누적)
context: Annotated[str, "Context"] # 문서 검색 결과
answer: Annotated[str, "Answer"] # 답변
messages: Annotated[list, add_messages] # 메시지 (누적)
relevance: Annotated[str, "Relevance"] # 관련성
핵심 개념:
add_messages): 자동으로 리스트에 메시지를 추가해주는 기능. 여러 데이터를 하나로 합치거나 누적 처리할 때 사용State Update 동작 방식 (RAG 기반 예시):
NODE 1 (질문 입력) → NODE 2 (문서 검색) → NODE 3 (답변 확인) → NODE 4 (답변 관련성 평가)
context: (없음) context: 문서1 context: 문서1 context: 문서1
question: 질문1 question: 질문1 question: 질문1 question: 질문1
answer: (없음) answer: (없음) answer: 답변1 answer: 답변1
score: (없음) score: (없음) score: (없음) score: BAD
NODE 4에서 score가 BAD일 경우 선택 가능한 다음 행동:
이것이 바로 Agentic Workflow의 순환 구조가 State를 통해 구현되는 방식이다. State의 값을 보고 다음에 어떤 단계로 이동할지 결정할 수 있다.
State 설계 고려사항 (4가지 원칙):
State는 데이터 컨테이너가 아니라 Read/Write Contract
State는 서비스 관점에서도 고려되어야 함
State는 실패를 고려해야 함 (Error-aware)
State는 확장 가능해야 함
Production State 설계 예시 (Pseudo Code):
# Nested Structures
class RetrievalResult(TypedDict):
query: str
documents: List[str]
source_ids: List[str]
retrieval_time_ms: float
class GenerationResult(TypedDict, total=False):
draft: str
revised: str
final: str
class EvaluationResult(TypedDict):
relevance: float
groundedness: float
overall: Literal["GOOD", "BAD"]
# Production-level GraphState
class GraphState(TypedDict, total=False):
# --- User Input ---
user_input: str
# --- Conversation (append-only trace) ---
messages: Annotated[List[BaseMessage], add_messages]
# --- Retrieval Layer ---
retrieval: RetrievalResult
# --- Generation Layer ---
generation: GenerationResult
# --- Evaluation Layer ---
evaluation: EvaluationResult
# --- Execution Control ---
status: Literal["RUNNING", "FAILED", "SUCCESS"]
current_node: str
step_count: int # 무한루프 방지
# --- Error Handling ---
error: ErrorInfo
Node는 LangGraph 내에서 실제로 어떤 행동(작업)을 수행하는 단위로, 각 노드는 하나의 함수로 정의된다.
Node 코드 예시:
def retriever_document(state: GraphState) -> GraphState:
# Question에 대한 문서 검색을 retriever로 수행
retrieved_docs = pdf_retriever.invoke(state["question"])
# 검색된 문서를 context 키에 저장
return GraphState(context=format_docs(retrieved_docs))
Pitfalls:
Edge: A 다음에 B와 같이 순서를 연결하는 연결선
Conditional Edge: 상황을 보고 다음 행동을 판단/실행하는 분기 로직
Edge 코드 예시:
# 시작점 정의
workflow.set_entry_point("retrieve")
# Node 연결 (Edge)
workflow.add_edges("retrieve", "llm_answer")
workflow.add_edges("llm_answer", "relevance_check")
# Conditional Edge
workflow.add_conditional_edges(
"relevance_check",
is_relevant,
{
"grounded": END, # 관련성이 있으면 종료
"notGrounded": "llm_answer", # 관련성 없으면 다시 답변 생성
"notSure": "llm_answer", # 모호하면 다시 답변 생성
},
)
Pitfalls:
| 구분 | Node (어떻게 수행할지) | Agent (무엇을, 언제 수행할지) |
|---|---|---|
| 정의 | 에이전트 시스템 내에서 특정 기능을 수행하는 가장 작은 독립적인 작업 단위 (레고 블록의 개별 조각) | 특정 목표를 달성하기 위해 상황을 이해하고 계획을 수립하고 행동을 실행 (레고 블럭으로 조립된 로봇) |
| 역할 | 단일 기능 수행: 문서 검색, 텍스트 요약, DB 조회, 답변 생성 등. 함수 형태로 정의 | 목표 설정 및 달성, Node Orchestration, 의사결정 |
| 특징 | 주어진 입력에 따라 정의된 작업을 기계적으로 수행 | 동적이고 유연한 동작: 상황과 입력에 따라 실행할 노드의 종류와 순서 변경 |
Node와 Agent의 관계 — 반드시 1:1 맵핑이 아니다:
| 구분 | Tool | Node | Agent |
|---|---|---|---|
| 목적 | 특정 작업을 수행하는 독립적인 기능 단위 | 워크플로우 내 특정 단계나 처리 과정 담당 | 목표 달성을 위해 동적으로 계획을 수립하고 실행 |
| 구성 | Agent → Tool 호출 (순수 함수, State 모름) | Workflow 내에서 Node 실행 (State를 입력받고 반환) | Agent = LLM + Tools + Memory (Node 내부에 위치하며 실질적 지능 역할) |
| 자율성 | 없음 — 호출 시에만 실행 | 낮음 — 정해진 규칙에 따라 실행 | 높음 — LLM 기반 계획 수립 및 실행 |
| 장점 | 명확하고 예측 가능, 디버깅 용이, 성능 빠름, 비용 효율적 | 워크플로우 재사용성, 체계적 흐름 관리, 병렬 처리 가능 | 유연한 문제 해결, 동적 의사결정 및 실행 |
| 단점 | 단순 작업만 처리, 의사결정 불가 | 모든 경로를 사전에 정의, 예외 상황 처리 제한적 | 예측 불가능성(LLM), 디버깅 어려움, Latency/Cost 높음 |
공장 라인 비유:
LangGraph 설계는 단계적으로 복잡성을 추가하는 방식으로 진행된다.
Vanilla 구조:

[추가-1] Query Transform:

[추가-2] 추가 검색기를 통한 문맥(context) 보강:

[추가-3] 최종 답변 유효성 검증:

Loop는 단순히 "몇 번 반복할 것인가"가 아니라, 언제 재진입하고 언제 멈출지를 명확히 설계한 구조다.
Loop Design 전체 구조:

각 Loop 종료 조건:
max_retry: 5 — 검색 재시도 최대 5회max_revision: 3 — 답변 수정 최대 3회⚠️ 무한루프 방지는 필수다
Loop 구조에서 종료 조건이 없으면 Agent는 영원히 돌아간다. 반드시 다음을 정의해야 한다:
- Max iteration (최대 반복 횟수)
- 실패 시 fallback 전략
- 비용 초과 시 종료 정책
LangGraph에서는
recursion_limit파라미터로 최대 재귀 횟수를 설정할 수 있다.
Memory를 추가하면 이전 대화와 검증된 지식을 다음 실행에 활용할 수 있다.
Memory 추가 구조:

핵심 포인트: "검증된 답만 저장" — 품질이 확인된 정보만 장기 메모리에 저장하여 다음 실행에서 활용한다.
class GraphState(TypedDict, total=False):
# --- User Input ---
user_query: str
# --- Memory Lifecycle ---
loaded_memory: LoadedMemory
memory_update: MemoryUpdate
# --- Conversation (append-only trace) ---
messages: Annotated[List[dict], add_messages]
# --- Retrieval Flow ---
retrieval: RetrievalResult
search_evaluation: SearchEvaluation
# --- Generation Flow ---
generation: GenerationResult
reflection: ReflectionResult
# --- Execution Control ---
control: ExecutionControl # status, step_count, max_retry, max_revision
Human을 State Machine의 Transition 조건으로 녹여넣는 구조
Human-in-the-Loop는 "사람이 중간에 끼어드는 것"이 아니라, 사람이 State Machine의 일부 Transition 조건에 참여하는 행위자로 설계되는 개념이다.
일반 Agent Loop vs HITL:

HITL Pseudo Code:
agent = create_agent(
model="gpt-4.1",
tools=[write_file_tool, execute_sql_tool, read_data_tool],
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"write_file": True, # 모든 결정 허용 (approve, edit, reject)
"execute_sql": {"allowed_decisions": ["approve", "reject"]}, # edit 불가
"read_data": False, # 안전한 작업이므로 승인 불필요
},
description_prefix="Tool execution pending approval",
),
],
checkpointer=InMemorySaver(), # HITL에는 Checkpointing 필수
)
중요한 설계 원칙:
interrupt_on: 개입 지점 정의 — HITL은 전역 멈춤 기능이 아님. 특정 Tool/행동에 선택적으로 적용됨allowed_decisions: Human 권한 설계 → UX 제어가 아니라 아키텍처 설계💡 HITL 설계 시 핵심 질문들
- 어느 단계에서 사람이 개입해야 하는가? — 고위험 작업(파일 쓰기, SQL 실행 등)에만 선택적 적용
- 어떤 결정 권한을 줄 것인가? — APPROVE만? EDIT도? REJECT도? → 각각의 결과가 다르므로 UX가 아닌 아키텍처 관점에서 설계
- Checkpoint는 어디에 저장할 것인가? — 개발 시에는 InMemorySaver, 프로덕션에서는 AsyncPostgresSaver 등 영속적 저장소 사용
단일 에이전트가 여러 작업을 순차적으로 처리하면 응답이 느리고 비효율적이다. 특히 LLM 기반 시스템에서는 Latency가 누적되어 전체 응답 시간이 길어진다.
병렬 처리: 여러 작업(또는 여러 에이전트)을 동시에 실행하여 전체 처리 시간을 단축하고 시스템의 처리 효율을 높이는 방식 → 품질보다 효율 개선이 목적
주요 유형:
Fan-out, Fan-in 패턴

Map-Reduce 패턴


Fan-out/Fan-in 고려사항:
Map-Reduce Reduce 전략:
# LangGraph에서의 Fan-out 구현 핵심
class ParallelState(TypedDict, total=False):
input: str
result_a: str # 부분 업데이트를 허용하는 타입 정의
result_b: str
result_c: str
final_result: str
# 동일한 upstream 노드를 가진 downstream 노드를 자동 병렬 실행
builder.add_edge("fan_out_start", "task_a")
builder.add_edge("fan_out_start", "task_b")
builder.add_edge("fan_out_start", "task_c")
# 모든 upstream 노드가 완료되어야 fan_in 실행
builder.add_edge(["task_a", "task_b", "task_c"], "fan_in")
LangChain에서 LangGraph로의 진화는 "사고 구조"와 "운영 정책"을 분리하고, 추론 흐름을 State Machine으로 표현하면서 순환과 분기를 가능하게 만든 과정이다. State는 노드 간 데이터 공유 계약이고, Node는 작업 단위, Edge는 흐름 결정이다. 실제 시스템은 단순 Q&A에서 Loop → Memory → HITL 순으로 복잡성을 쌓으며, HITL은 사람이 State Machine의 전환 조건 자체로 녹아드는 구조라 Checkpoint 설계가 필수다.