logo
홈블로그소개
4,256

Built with Next.js, Bun, Tailwind CSS and Shadcn/UI

·개인정보처리방침
AIPython

LangGraph Agent: 챗봇에서 Tool·Memory·HITL·Time Travel까지

Toma
2026년 7월 23일
약 44분
목차
🤖 LangGraph Chatbot — 그래프에 LLM 붙이기
1️⃣ AI 모델 연결
2️⃣ state에 messages 공간 만들기 (챗봇의 메모리)
3️⃣ node(작업 단위) 생성
🛠️ Tool Nodes — 챗봇을 agent로 발전시키기
🔧 툴 생성
🧠 Memory — Checkpointer로 대화 기억하기
🙋 Human-in-the-loop — 사람이 중간에 개입하기
▶️ 피드백 제공 후 재개 — Command
⏮️ Time Travel — 대화를 여러 방향으로 분기하기
1️⃣ state 기록 가져오기
2️⃣ fork할 지점 선택 → 메시지 수정
3️⃣ 수정된 지점에서 그래프 재개
🔍 DevTools — LangGraph Studio로 에이전트 시각화하기
이전 포스트LangGraph 기초: State·Node·Edge로 에이전트를 그래프로 설계하기
다음 포스트AI Agent Workflow Architectures — LangGraph로 구현하는 4가지 설계

목차

🤖 LangGraph Chatbot — 그래프에 LLM 붙이기
1️⃣ AI 모델 연결
2️⃣ state에 messages 공간 만들기 (챗봇의 메모리)
3️⃣ node(작업 단위) 생성
🛠️ Tool Nodes — 챗봇을 agent로 발전시키기
🔧 툴 생성
🧠 Memory — Checkpointer로 대화 기억하기
🙋 Human-in-the-loop — 사람이 중간에 개입하기
▶️ 피드백 제공 후 재개 — Command
⏮️ Time Travel — 대화를 여러 방향으로 분기하기
1️⃣ state 기록 가져오기
2️⃣ fork할 지점 선택 → 메시지 수정
3️⃣ 수정된 지점에서 그래프 재개
🔍 DevTools — LangGraph Studio로 에이전트 시각화하기

💬 LangGraph의 기초(State·Node·Edge)를 익혔다면, 이제 여기에 LLM을 붙여 챗봇을 만들고 → tool을 붙여 agent로 발전시키고 → 메모리·사람 개입·타임트래블·개발 도구까지 얹는 과정을 따라간다. 이 글은 그 여정을 실습 코드로 정리한 것이다.

🤖 LangGraph Chatbot — 그래프에 LLM 붙이기

LangGraph에 LLM을 적용할 때는 LangChain의 init_chat_model을 import한다.

💡 LangChain vs. LangGraph — LangChain은 _AI 모델과 쉽게 대화_할 수 있게 해 주는 라이브러리로, 에이전트보다는 AI 앱을 만들 때 활용한다. LangGraph는 그 위에서 그래프(=에이전트) 를 조립한다.

1️⃣ AI 모델 연결

python
from langchain.chat_models import init_chat_model

llm = init_chat_model("openai:gpt-4o-mini")

llm.invoke([{"role": "user", "content": "Hello!"}])
# → AIMessage(content='Hello! How can I assist you today?', ...)

2️⃣ state에 messages 공간 만들기 (챗봇의 메모리)

챗봇은 대화를 기억해야 하므로 state 안에 messages 공간이 필요하다. 이를 위해 langchain_core에서 AnyMessage를 import한다.

💡 LangChain은 AI 모델과 대화하기 위한 라이브러리라, AI 모델에 보내는 메시지를 표현하는 클래스가 여러 종류(Human/AI/System/Tool 메시지 등) 있다. 그래서 messages는 어떤 타입의 메시지든 담을 수 있는 list가 된다.

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2026-07-12_14.54.04.png

python
from langchain_core.messages import AnyMessage
import operator
from typing import Annotated


class State(TypedDict):
    messages: Annotated[list[AnyMessage], operator.add]

⚠️ 그러나 operator.add는 새 메시지를 이전 메시지에 단순히 이어 붙이기만 한다. 특정 메시지를 _수정·삭제_하는 고급 기능까지 필요하다면 operator.add로는 부족하다.

이럴 때는 add_messages를 쓴다. add_messages는 이전 메시지와 새 메시지를 합쳐 줄 뿐 아니라, 메모리 안의 메시지를 수정·삭제할 수 있게 해 준다.

python
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

3️⃣ node(작업 단위) 생성

python
def chatbot(state: State):
    # state 안의 messages를 그대로 모델에 전달
    response = llm.invoke(state["messages"])
    # LLM이 반환한 response를 messages에 넣어 반환 → add_messages가 누적
    return {"messages": [response]}


graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)

graph = graph_builder.compile()

graph.invoke(
    {"messages": [{"role": "user", "content": "how are you?"}]}
)
# → {'messages': [HumanMessage(content='how are you?', ...),
#                 AIMessage(content="I'm just a computer program, ...", ...)]}

💡 참고 — MessagesState LangGraph는 위에서 직접 만든 State와 완전히 동일한 역할을 하는 MessagesState를 기본 제공한다. 그러므로 아래처럼 축약할 수 있다. (messages 필드와 add_messages reducer가 이미 들어 있다.)

python
from langgraph.graph import MessagesState  
  
  
class State(MessagesState):  
    pass  # 나중에 커스텀 속성 추가 가능

🛠️ Tool Nodes — 챗봇을 agent로 발전시키기

챗봇에 function calling을 추가하면 챗봇이 곧 agent가 된다. tool을 추가하려면 tool 역할의 node를 만들어 chatbot node와 conditional edge로 연결한다. 흐름은 이렇다 — 사용자 입력 → chatbot이 LLM에 전달 → LLM 응답에 tool_call이 있으면 대화를 끝내지 않고 tool node로 연결.

💡 LangGraph는 이를 위한 prebuilt 두 가지를 제공한다.

  • ToolNode — tool을 호출하는 node. 모델의 response에서 function 이름을 확인하고, 그 function을 실행한 뒤, function output을 다시 메시지로 돌려보낸다.

  • tools_condition — state의 messages를 꺼내 tool call이 있는지 감지하는 라우팅 함수. 있으면 tools로, 없으면 END로 보낸다.

python
from langgraph.prebuilt import ToolNode, tools_condition

tool_node = ToolNode(
    tools=[],
)

graph_builder.add_node("chatbot", chatbot)
graph_builder.add_node("tools", tool_node)

graph_builder.add_edge(START, "chatbot")
graph_builder.add_conditional_edges("chatbot", tools_condition)
graph_builder.add_edge("tools", "chatbot")

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2026-07-12_15.35.37.png

정리하면: 메시지를 chatbot node에 보내면 chatbot이 AI 모델에 전달 → 간단한 응답이면 END, tool call을 요청하면 tools node로 이동해 해당 tool을 실행 → tool output을 MessagesState에 넣고 다시 chatbot node로 돌아와 업데이트된 state로 AI 모델과 재통신하는 loop가 돈다.

🔧 툴 생성

python
from langchain_core.tools import tool


@tool
def get_weather(city: str):
    """Gets weather in city"""
    return f"The weather in {city} is sunny"


# AI 모델이 이 tool을 알고 있어야 한다 → LLM을 tool과 바인딩(결합)
llm_with_tools = llm.bind_tools(tools=[get_weather])


def chatbot(state: State):
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}


# node가 호출할 tool을 등록
tool_node = ToolNode(
    tools=[get_weather],
)

💡 @tool 데코레이터를 붙이면 일반 함수가 LLM이 호출 가능한 tool이 된다. 함수의 docstring이 tool 설명이 되어 LLM이 언제 이 tool을 쓸지 판단하는 근거가 되므로, docstring을 명확히 쓰는 것이 중요하다.

🧠 Memory — Checkpointer로 대화 기억하기

LangGraph는 Checkpointer로 메모리를 구현한다.

💡 Checkpointer는 단순히 state만 저장하는 게 아니라, 그 state가 어떻게 바뀌어 왔는지(변경 이력) 와 어떤 node가 호출됐는지까지 저장한다. 덕분에 과거로 돌아가 대화를 fork(분기) 할 수 있다 (→ 뒤의 Time Travel).

python
import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver

# SQLite connection
conn = sqlite3.connect("memory.db", check_same_thread=False)

# SqliteSaver를 compile 시 checkpointer로 넘긴다
graph = graph_builder.compile(
    checkpointer=SqliteSaver(conn),
)

graph.invoke(
    {"messages": [{"role": "user", "content": "how are you?"}]},
    config={
        "configurable": {
            "thread_id": "1"
        },
        # "recursion_limit": 2  # 그래프가 몇 step까지 허용되는지
    },
)

💡 config / configurable / thread_id 란? (공식문서 기준)

  • config — 그래프를 실행할 때 넘기는 런타임 설정. state(데이터)와 별개로, "어떻게 실행할지"에 대한 값을 담는다.

  • configurable — config 안에서 LangGraph가 읽는 예약 키들의 네임스페이스. 여기에 thread_id, checkpoint_id 같은 값을 넣는다.

  • thread_id — checkpointer의 primary key. 하나의 대화(thread)를 식별한다. 챗봇처럼 여러 사용자가 동시에 접근하는 앱에서는 _어느 세션과 대화 중인지_를 알아야 하는데, 바로 이 thread_id로 구분한다. 같은 thread_id로 다시 invoke하면 그 대화의 상태를 이어서 재개한다.

python
# state 이력 탐색 — 어느 대화의 history인지 알아야 하므로 config를 넘긴다
for state in graph.get_state_history(
    {"configurable": {"thread_id": "1"}}
):
    print(state.next)

# 결과 (아래에서 위로 진행된 순서)
# ()
# ('chatbot',)
# ('__start__',)

💡 추가로, 그래프를 invoke 대신 astream****(async stream) 으로 호출하면 응답의 delta(증분) 를 실시간으로 받아볼 수 있다. 여러 stream mode를 골라 무엇을 스트리밍할지 선택할 수도 있다.

🙋 Human-in-the-loop — 사람이 중간에 개입하기

Human-in-the-loop(HITL) 은 인간(유저)이 중간에 승인/거절/피드백으로 개입하는 방식이다. 즉, 그래프를 중단하고 피드백을 제공할 수 있는 기능이다.

원리는 익숙한 파이썬 input() 함수와 같다. 아래 코드에서 input() 앞의 코드까지만 실행되고, 사용자가 무언가 입력하기 전까지 실행이 막힌다(blocking).

python
print("hello")
print("world")
input()      # ← 여기서 대기, 입력 전까지 blocking
print("bye")

💡 LangGraph에서 이 input() 역할을 하는 것이 interrupt 함수다. 흐름은 그래프 중단 → 피드백 제공 → 그래프 재개. interrupt가 실행되면 그래프가 멈추고 state를 저장한다. 재개할 때 넘긴 값이 interrupt****의 반환값으로 들어간다. (from langgraph.types import interrupt)

python
from langgraph.types import interrupt


@tool
def get_human_feedback(poem: str):
    """
    Asks the user for feedback on the poem.
    Use this before returning the final response.
    """
    # interrupt 실행 → 그래프 중단. 재개 시 응답이 feedback으로 들어온다
    feedback = interrupt(f"Here is the poem, tell me what you think\n{poem}")
    return feedback


# interrupt 발생 여부 확인
result = graph.invoke(
    {"messages": [{"role": "user", "content": "Please make a poem about Python Code."}]},
    config=config,
)

for message in result["messages"]:
    # pretty_print — 메시지를 사람이 보기 좋게 변환해 주는 langchain 메서드
    message.pretty_print()

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2026-07-14_21.46.25.png

▶️ 피드백 제공 후 재개 — Command

그러면 피드백은 어떻게 제공할까? Command 를 사용한다.

✅ Command는 (앞서 본) 다른 노드로 점프·state 업데이트 기능 외에, 중단된 그래프를 다시 이어가는(resume) 데에도 쓰인다. 즉, interrupt에 응답을 제공하는 도구다.

python
from langgraph.types import Command

# interrupt 질문에 대한 응답을 Command로 제공
response = Command(
    # resume 값이 곧 interrupt의 반환값이 된다.
    # 문자열·딕셔너리 등 모든 형태 가능. 단, interrupt 쪽에서 받을 준비가 되어 있어야 한다.
    resume="It looks good!!"
)

# 이 응답으로 그래프를 다시 호출 → 재개
# state 대신 Command를 전달하면 그래프가 재개된다.
# config가 있어야 어떤 thread_id로 재개할지 알 수 있다.
result = graph.invoke(response, config=config)

⏮️ Time Travel — 대화를 여러 방향으로 분기하기

Time Travel은 대화를 여러 방향으로 이어 가는 conversation forking이다. 앞서 Memory에서 checkpointer가 _state + 실행된 노드 + 다음에 실행될 노드_를 저장하기 때문에, 이전 대화 지점에서 분기가 가능하다고 했다.

예를 들어 아래처럼 대화를 나눴는데, "내가 사는 나라"를 수정해서 다시 물어보고 싶다고 하자.

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2026-07-14_22.03.56.png

1️⃣ state 기록 가져오기

python
# config를 넘겨야 thread_id로 어떤 대화를 가리키는지 알 수 있다
state_history = graph.get_state_history(config)

for state_snapshot in list(state_history):
    print(state_snapshot.next)              # 각 단계에서 실행할 노드 이름
    print(state_snapshot.values["messages"])  # 메시지만 출력
    print("=========\n")

기록은 맨 위가 가장 최신 state이고, 아래로 갈수록 과거다. 각 snapshot의 .next는 그 시점에서 다음에 실행될 노드(('chatbot',), ('__start__',), 혹은 완료 시 ())를 보여 준다.

2️⃣ fork할 지점 선택 → 메시지 수정

python
# 포크할 위치 선택 (기록의 -5번째라고 가정)
# a = [1, 2, 3, 4, 5] → a[-1] = 5
state_history = graph.get_state_history(config)
to_fork = list(state_history)[-5]
to_fork.values["messages"]

from langchain_core.messages import HumanMessage

# 그래프 상태 업데이트 (fork)
graph.update_state(
    # 기본 config가 아니라, "포크할 위치"를 가리키는 config (checkpoint_id 포함).
    # 이걸 넘기면 LangGraph는 이 시점의 state를 업데이트하려는 것으로 인식한다.
    to_fork.config,
    # 무엇을 — 기존 메시지 ID를 그대로 넘기면 그 메시지를 수정한다
    {
        "messages": HumanMessage(
            content="I live in Japan",
            id="8eac45a4-688c-4c2c-870d-29673e964d5f",
        )
    },
)

💡 MessagesState가 제공하는 add_messages reducer는 새 메시지를 _추가_할 뿐 아니라, 같은 메시지 ID를 전달하면 그 메시지를 수정할 수 있게 해 준다. 그래서 위처럼 기존 HumanMessage의 id를 그대로 주고 content만 바꾸면 해당 메시지가 교체된다.

3️⃣ 수정된 지점에서 그래프 재개

python
# update_state 후 받은 checkpoint_id로 변경 확인
forked_state = graph.get_state_history(
    {"configurable": {
        "thread_id": "1",
        "checkpoint_ns": "",
        "checkpoint_id": "1f17f873-caef-612e-8004-a02fab4321a6",
    }}
)
list(forked_state)

# 그 지점에서 그래프를 다시 호출 (state 대신 None → 저장된 state로 재개)
result = graph.invoke(
    None,
    {"configurable": {
        "thread_id": "1",
        "checkpoint_ns": "",
        "checkpoint_id": "1f17f873-caef-612e-8004-a02fab4321a6",
    }},
)

for message in result["messages"]:
    message.pretty_print()

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2026-07-14_22.27.48.png

🔍 DevTools — LangGraph Studio로 에이전트 시각화하기

LangSmith는 에이전트를 모니터링·관찰하는 도구로, 그중 에이전트를 검사하고 시각화하는 UI(LangGraph Studio)를 제공한다.

이를 활성화하려면 프로젝트 루트에 langgraph.json 파일을 만든다.

json
{
  "dependencies": ["langchain_openai", "./main.py"],
  "env": "./.env",
  "graphs": {
    "mr_poet": "./main.py:graph"
  }
}

💡 각 필드의 의미는 다음과 같다.

  • dependencies — 그래프를 실행하는 데 필요한 의존성.

  • env — 환경 변수 파일 위치. UI에게 가상 환경/설정이 어디 있는지 알려준다.

  • graphs — 노출할 그래프. "mr_poet": "./main.py:graph" 는 "__main.py 안의 _graph_라는 변수를 _mr_poet_이라는 이름으로 노출한다" 는 뜻이다.

langgraph dev 명령어를 실행하면 LangGraph Studio(에이전트를 검사·시각화하는 UI)가 열린다.

bash
langgraph dev

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2026-07-14_22.40.07.png

💡 API_KEY를 요구하는데, LangSmith는 일부 무료이므로 API 키를 발급받아 .env 파일에 LANGSMITH_API_KEY 로 추가하면 된다.

LangGraph 서버를 실행한 채로 https://agentchat.vercel.app 에 접속하면 내가 만든 에이전트와 채팅할 수 있다.

💡 접속하면 LangSmith API 키를 입력하라고 하지만 아무것도 입력할 필요 없다. 다음 두 가지만 넣으면 된다.

  • 서버 URL — LangGraph 서버 실행 시 나오는 주소 http://127.0.0.1:2024

  • 그래프 이름 — compile 시 지정한 이름. 예: graph = graph_builder.compile(name="mr_poet")

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2026-07-14_22.49.21.png


🧭 한눈에 정리 — Chatbot(LLM 연결 + MessagesState) → Tool Nodes(ToolNode + tools_condition로 agent화) → Memory(Checkpointer + thread_id) → Human-in-the-loop(interrupt + Command(resume=…)) → Time Travel(get_state_history + update_state fork) → DevTools(langgraph.json + langgraph dev). 이 순서대로 얹으면 단순 챗봇이 관찰 가능한 완성형 에이전트가 된다.