Chain이 아니라 Graph — LangGraph로 AI 에이전트를 만드는 이유
LangChain이 있는데 왜 LangGraph가 따로 필요할까? LLM 에이전트의 핵심 흐름이 일직선(Chain)이 아니라 순환하는 그래프(Graph)이기 때문입니다. 실제 코드를 따라가며, 이 그래프가 어떤 모양이고 왜 이런 구조가 되는지 풀어봅니다.
Chain의 한계 — 직선은 판단을 못 한다
LangChain의 기본 모델은 이름 그대로 Chain입니다. A → B → C 순서로 단계를 밟는 파이프라인이죠.
# Chain 방식: 검색 → LLM → 출력 (순서 고정)
chain = retriever | prompt | llm | output_parser
이 구조는 "검색해서 답변해" 같은 단순 RAG에는 충분합니다. 하지만 이런 상황에서 막힙니다:
- 검색 결과가 불충분하면? → 다시 검색해야 하는데, 체인은 뒤로 못 돌아갑니다
- 웹 검색이 필요한지, RAG가 필요한지? → LLM이 판단해야 하는데, 체인은 분기를 못 합니다
- 도구를 3번 쓸지 1번 쓸지? → 실행 중에 결정해야 하는데, 체인은 고정된 순서만 따릅니다
에이전트에게 필요한 건 "다음에 뭘 할지 스스로 판단하고, 필요하면 되돌아가는" 구조입니다. 이것은 직선이 아니라 순환 그래프입니다.
ReAct Agent의 그래프 구조
LangGraph의 create_react_agent가 만드는 그래프는 아래와 같은 형태입니다. 핵심은 LLM 노드와 Tool 노드 사이의 순환입니다.
그래프의 흐름을 정리하면:
- START → LLM: 사용자 메시지 + 시스템 프롬프트 + 메모리 컨텍스트를 받는다
- LLM → 조건 분기: LLM의 응답에
tool_calls가 있는지 검사한다 - YES → Tools: 지정된 도구를 실행하고, 결과를 메시지에 추가한다
- Tools → LLM: 도구 결과를 포함해서 다시 LLM에게 돌아간다 (이것이 핵심 루프)
- NO → END:
tool_calls가 없으면 최종 텍스트 응답이므로 출력한다
Chain이었다면 3번에서 끝나야 합니다. 하지만 Graph이기 때문에 4번이 가능합니다. LLM이 도구 결과를 보고 "아직 부족하다, 한 번 더 검색하자"고 판단할 수 있다는 뜻입니다.
코드로 보는 그래프 생성
LangGraph의 create_react_agent는 위 그래프를 단 몇 줄로 만들어 줍니다:
# agent/graph.py
from langgraph.prebuilt import create_react_agent
from langchain_core.messages import SystemMessage
graph = create_react_agent(
model=llm, # ChatOpenAI (vLLM 엔드포인트)
tools=tools, # @tool 함수 리스트
prompt=system_prompt, # 시스템 프롬프트
)
내부적으로 이 함수는 StateGraph를 생성하고, 두 개의 노드(agent, tools)와 조건부 엣지를 연결합니다. 직접 만든다면 이런 형태입니다:
# create_react_agent의 내부 동작 (개념)
from langgraph.graph import StateGraph, END
graph = StateGraph(AgentState)
graph.add_node("agent", call_llm) # LLM 호출
graph.add_node("tools", execute_tools) # 도구 실행
graph.set_entry_point("agent")
graph.add_conditional_edges(
"agent",
should_continue, # tool_calls 있으면 "tools", 없으면 END
{"tools": "tools", "end": END},
)
graph.add_edge("tools", "agent") # ← 이것이 순환 엣지
마지막 줄 add_edge("tools", "agent")가 Chain과 Graph의 결정적 차이입니다. 도구 실행 후 다시 LLM으로 돌아가는 이 엣지가 에이전트의 자율적 반복을 가능하게 합니다.
도구(Tool) 등록 — @tool 데코레이터
도구 정의에는 LangChain의 @tool 데코레이터를 사용합니다. LangGraph 자체에 별도의 도구 시스템이 있는 것이 아니라, LangChain이 제공하는 도구 추상화를 그대로 가져다 씁니다. @tool은 Python 함수의 docstring을 LLM에게 전달할 도구 설명으로, 타입 힌트를 파라미터 스키마로 자동 변환합니다.
from langchain_core.tools import tool
@tool
def search_knowledge_vault(
query: str,
top_k: int = 3,
category: Optional[str] = None,
) -> str:
"""개인 지식금고(Obsidian vault)에서 관련 정보를 검색합니다.
업무 기술 노트, 생활 기록, 경영학 수업 노트 등을 포함합니다."""
results = search(query=query, top_k=top_k, ...)
return formatted_results
이 함수 하나를 등록하면, LLM은 자동으로 아래와 같은 JSON 스키마를 전달받습니다:
{
"name": "search_knowledge_vault",
"description": "개인 지식금고에서 관련 정보를 검색합니다...",
"parameters": {
"query": {"type": "string"},
"top_k": {"type": "integer", "default": 3},
"category": {"type": "string", "nullable": true}
}
}
LLM은 이 스키마를 보고 언제, 어떤 파라미터로 호출할지를 스스로 결정합니다. 사람이 "RAG 성능 이슈 정리해줘"라고 하면, LLM이 search_knowledge_vault("RAG 성능 이슈")를 호출하겠다고 응답하는 식입니다.
도구 실패 방어 — _safe_tool 래퍼
에이전트 루프에서 도구 하나가 예외를 던지면 전체 그래프가 멈춥니다. 이를 막기 위해 모든 도구를 래퍼로 감쌉니다:
def _safe_tool(tool_func):
original_func = tool_func.func
def wrapper(*args, **kwargs):
try:
return original_func(*args, **kwargs)
except Exception as e:
return f"[도구 오류] {tool_func.name}: {e}"
tool_func.func = wrapper
return tool_func
# 그래프 생성 시 모든 도구에 적용
tools = [_safe_tool(t) for t in CORE_TOOLS]
예외가 에러 텍스트로 변환되어 LLM에게 돌아갑니다. LLM은 이 에러 메시지를 보고 다른 도구를 시도하거나, 사용자에게 상황을 설명할 수 있습니다. 그래프의 순환 구조 덕분에 가능한 복구 패턴입니다.
실행 예시 — 그래프가 실제로 도는 모습
사용자가 "지난달 논의한 RAG 성능 이슈 정리해줘"라고 하면, 그래프는 아래처럼 순환합니다:
Chain이었다면 Iteration 1에서 검색 결과를 받고 바로 답변해야 합니다. 하지만 Graph 구조이기 때문에 LLM이 "결과가 부족하다"고 판단하면 추가 검색을 할 수 있습니다. 이 자율적 판단이 에이전트의 핵심 가치입니다.
스트리밍 — astream_events로 그래프 내부 엿보기
LangGraph는 그래프 실행 중 각 노드의 이벤트를 스트리밍으로 내보냅니다. 이를 활용하면 사용자에게 에이전트가 지금 무엇을 하고 있는지 실시간으로 보여줄 수 있습니다:
async for event in graph.astream_events(
{"messages": langchain_messages}, version="v2"
):
if event["event"] == "on_tool_start":
# "검색 중..." 상태 표시
send_status(tool_name=event["name"], status="running")
elif event["event"] == "on_chat_model_stream":
# LLM 토큰을 실시간 스트리밍
send_token(event["data"]["chunk"])
on_tool_start, on_tool_end, on_chat_model_stream 같은 이벤트가 그래프의 각 노드 전환 시점에 발생합니다. 그래프의 구조가 명확하기 때문에, 이벤트 타이밍도 예측 가능합니다.
왜 Graph인가 — 정리
LangGraph를 쓰는 이유를 한 문장으로 요약하면: 에이전트의 흐름에는 루프가 있기 때문입니다.
| Chain (LangChain) | Graph (LangGraph) | |
|---|---|---|
| 흐름 | A → B → C (직선) | A → B → A → B → C (순환) |
| 도구 호출 | 고정된 순서, 1회 | LLM이 판단, 0~N회 |
| 분기 | 사전 정의된 조건 | LLM 출력 기반 동적 분기 |
| 상태 관리 | 입력 → 출력 파이프 | 메시지 리스트에 누적 |
| 적합한 용도 | 단순 RAG, 변환 파이프라인 | 에이전트, 멀티턴 도구 사용 |
Chain은 "정해진 절차를 따라 처리"하는 데 강합니다. Graph는 "상황을 보고 다음 행동을 결정"하는 데 강합니다. AI 에이전트는 후자입니다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.