LangGraph 기초: 순환하고 분기하는 LLM 에이전트 만들기
한 방향으로 흐르는 체인만으로는 표현하기 어려운 순환과 분기를, 상태와 노드와 엣지로 그래프처럼 짜는 LangGraph의 원리를 정리한 노트입니다. 상태 개념에서 출발해 도구를 반복 호출하는 에이전트 루프를 파이썬으로 직접 만듭니다.
LLM 에이전트는 도구를 부르고, 결과를 보고, 더 부를지 답할지 스스로 정하는 순환 구조로 움직입니다. 한 방향으로 흐르는 체인으로는 이 순환을 표현하기가 번거롭습니다. LangGraph는 작업을 상태와 노드와 엣지로 이뤄진 그래프로 짜서, 순환과 분기를 자연스럽게 다루게 해줍니다. 이 글은 그 구성 요소를 정리하고 도구를 반복 호출하는 에이전트를 직접 만듭니다. LangChain 글의 도구 호출을 먼저 보고 오면 이어집니다.
체인으로는 부족한 순간
LangChain 글에서 본 체인은 prompt | llm | parser처럼 한 방향으로 흐릅니다. 입력이 들어가면 정해진 순서를 거쳐 출력이 나옵니다. 많은 작업이 이 모양으로 충분합니다.
하지만 에이전트는 다릅니다. 에이전트가 하는 일은 대략 이렇습니다.
- 사용자의 요청을 받는다.
- 도구를 부를지, 바로 답할지 정한다.
- 도구를 불렀으면 결과를 받아 다시 2번으로 돌아간다.
- 더 부를 게 없으면 최종 답을 낸다.
여기서 3번이 2번으로 돌아가는 순환과, 2번에서 “도구냐 답이냐”를 고르는 분기가 생깁니다. 한 방향 체인으로는 “결과를 보고 처음으로 돌아가기”를 표현하기가 어렵습니다. LangGraph는 이 순환과 분기를 그래프로 그리게 해줍니다.
세 가지 구성 요소
LangGraph는 작업을 세 가지로 나눠 표현합니다.
- 상태(State) 그래프가 도는 동안 계속 들고 다니며 갱신하는 데이터입니다. 보통 지금까지 오간 메시지 목록을 담습니다.
- 노드(Node) 실제 일을 하는 함수입니다. 상태를 입력받아, 바뀐 부분을 돌려줍니다. LLM을 부르는 노드, 도구를 실행하는 노드 같은 식입니다.
- 엣지(Edge) 노드에서 노드로 이어지는 화살표입니다. 어느 노드 다음에 어느 노드로 갈지를 정합니다. 조건에 따라 갈림길을 만드는 조건부 엣지도 있습니다.
정리하면, 상태라는 데이터를 노드들이 번갈아 갱신하고, 엣지가 그 순서와 갈림길을 정합니다. 이 셋으로 순환하는 흐름을 그립니다.
상태 정의하기
먼저 그래프가 들고 다닐 상태를 정합니다. 에이전트의 상태는 대개 메시지 목록 하나면 됩니다. 노드가 새 메시지를 내놓을 때 기존 목록을 덮어쓰는 게 아니라 뒤에 이어 붙여야 하므로, 그 방식을 add_messages로 지정합니다.
1
2
3
4
5
6
7
8
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
class State(TypedDict):
# add_messages: 새 메시지를 덮어쓰지 않고 목록에 이어 붙인다
messages: Annotated[list, add_messages]
Annotated[list, add_messages]가 핵심입니다. 노드가 메시지 하나를 돌려주면, LangGraph는 그것을 기존 messages 목록 끝에 추가합니다. 대화가 순환하며 쌓여도 이력이 유지되는 이유가 이것입니다.
노드 만들기
노드는 두 개면 됩니다. 하나는 모델을 부르는 노드, 하나는 모델이 요청한 도구를 실행하는 노드입니다. 도구 실행 노드는 LangGraph가 만들어 둔 ToolNode를 그대로 씁니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langgraph.prebuilt import ToolNode
@tool
def get_weather(city: str) -> str:
"""도시의 현재 날씨를 알려준다."""
return f"{city}의 날씨는 맑고 22도입니다."
tools = [get_weather]
llm = ChatAnthropic(model="claude-sonnet-5").bind_tools(tools)
# 노드 1: 모델을 부른다
def call_model(state: State):
response = llm.invoke(state["messages"])
return {"messages": [response]}
# 노드 2: 모델이 요청한 도구를 실행한다
tool_node = ToolNode(tools)
call_model은 지금까지의 메시지를 모델에 넣고, 모델의 응답을 돌려줍니다. 응답에는 최종 답이 담길 수도 있고, “도구를 불러달라”는 요청이 담길 수도 있습니다. 어느 쪽인지에 따라 다음 행선지가 갈립니다.
분기: 조건부 엣지
이제 갈림길을 정합니다. 모델의 마지막 응답에 도구 호출 요청이 있으면 도구 노드로 가고, 없으면 끝냅니다. 이 판단을 함수로 씁니다.
1
2
3
4
5
def should_continue(state: State):
last = state["messages"][-1]
if last.tool_calls: # 모델이 도구를 부르려 하면
return "tools" # 도구 노드로
return END # 아니면 종료
이 함수가 돌려주는 값에 따라 다음 노드가 결정됩니다. 도구 호출이 있으면 "tools"로, 없으면 END로 갑니다.
그래프 조립하기
노드와 엣지를 이어 그래프를 완성합니다.
1
2
3
4
5
6
7
8
9
10
graph = StateGraph(State)
graph.add_node("model", call_model)
graph.add_node("tools", tool_node)
graph.add_edge(START, "model") # 시작하면 모델부터
graph.add_conditional_edges("model", should_continue) # 모델 뒤엔 갈림길
graph.add_edge("tools", "model") # 도구를 썼으면 다시 모델로
app = graph.compile()
가장 중요한 줄은 graph.add_edge("tools", "model")입니다. 도구를 실행한 뒤 다시 모델로 돌아가는 이 화살표가 순환을 만듭니다. 모델은 도구 결과를 보고 또 도구를 부를지, 이제 답할지를 다시 판단합니다. 이 고리가 should_continue가 END를 돌려줄 때까지 돕니다.
흐름을 그림으로 보면 이렇습니다.
1
2
3
START → model → (도구 호출 있음?) → tools → model → ...
│
└ (없음) → END
실행해보기
1
2
3
4
5
6
result = app.invoke({
"messages": [{"role": "user", "content": "서울 날씨 어때?"}]
})
for m in result["messages"]:
m.pretty_print()
실행하면 이렇게 흐릅니다. 모델이 get_weather(city="서울")을 부르기로 하고(첫 model), 도구 노드가 날씨 문자열을 만들고(tools), 그 결과를 받은 모델이 이번엔 도구 없이 최종 답을 냅니다(둘째 model). should_continue가 END를 돌려주며 그래프가 멈춥니다. 우리가 반복문을 직접 쓰지 않았는데도 순환이 돈 것입니다.
이미 만들어진 에이전트
방금 만든 “모델과 도구를 오가는 에이전트”는 매우 흔한 패턴이라, LangGraph가 아예 함수 하나로 제공합니다.
1
2
3
4
5
6
7
8
9
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(
ChatAnthropic(model="claude-sonnet-5"),
tools=[get_weather],
)
result = agent.invoke({
"messages": [{"role": "user", "content": "서울 날씨 어때?"}]
})
이 한 줄이 앞에서 손으로 짠 그래프와 사실상 같은 것을 만듭니다. 그렇다면 왜 원리를 보았을까요. 실제 작업에서는 이 기본 루프에 조건을 더 붙이거나, 사람의 승인을 받는 단계를 끼우거나, 갈래를 늘려야 하기 때문입니다. 그때는 create_react_agent로는 부족하고, 상태와 노드와 엣지를 직접 그려야 합니다. 그래서 그래프의 구조를 이해하는 것이 먼저입니다.
정리
| 구성 요소 | 하는 일 |
|---|---|
| 상태(State) | 그래프가 도는 동안 들고 다니는 데이터, 보통 메시지 목록 |
| 노드(Node) | 상태를 받아 일부를 갱신하는 함수, 모델 호출이나 도구 실행 |
| 엣지(Edge) | 노드에서 노드로 가는 화살표, 순서를 정한다 |
| 조건부 엣지 | 상태를 보고 다음 행선지를 고르는 갈림길 |
| 순환 | 도구 노드에서 모델로 되돌아가는 엣지가 만드는 반복 |
LangGraph가 LangChain의 체인과 다른 점은 하나로 요약됩니다. 체인은 한 방향으로 흐르고, 그래프는 되돌아올 수 있습니다. 이 되돌아옴 덕분에 “판단하고, 도구를 쓰고, 결과를 보고 다시 판단하는” 에이전트의 본질적인 순환을 자연스럽게 표현할 수 있습니다. 이 시리즈에서 다룬 프롬프트, RAG, LangChain, 그리고 LangGraph는 각각 LLM 애플리케이션을 짓는 서로 다른 층입니다. 프롬프트로 모델에게 일을 시키고, RAG로 근거를 대고, LangChain으로 부품을 잇고, LangGraph로 순환하는 에이전트를 만듭니다.