4. LangGraph - LangGraph ReAct Agent와 MemorySaver 학습 매뉴얼
- 본 문서는 LangGraph의 핵심 에이전트 패턴인 ReAct(Reasoning and Acting)와 대화 지속성을 위한 MemorySaver를 체계적으로 학습하기 위한 실습 매뉴얼이다.
- LangGraph의 ToolNode를 활용하여 AI 모델이 외부 도구를 호출하고, 그 결과를 기반으로 추론하는 에이전트 시스템을 구축하는 전체 과정을 다룬다.
- 사용자 정의 도구와 내장 도구를 결합하여 레스토랑 메뉴 검색과 웹 검색 기능을 갖춘 실무 수준의 ReAct 에이전트를 완성하며, MemorySaver를 통해 다중 턴 대화를 지원하는 상태 유지형 챗봇까지 구현한다.
1. 사전 작업
- 이 섹션에서는 프로젝트 실행에 필요한 환경 변수 로드와 기본 라이브러리를 임포트한다.
1.1. 환경변수 로드
from dotenv import load_dotenv # [외부 라이브러리] python-dotenv 패키지
load_dotenv(override=True) # .env 파일의 환경 변수를 시스템에 로드 (override=True: 기존 환경 변수 덮어쓰기)
1.2. 기본 라이브러리
import re # [Python 표준 라이브러리] 정규 표현식 처리
import os, json # [Python 표준 라이브러리] OS 환경 변수 접근, JSON 직렬화/역직렬화
from textwrap import dedent # [Python 표준 라이브러리] 멀티라인 문자열의 공통 들여쓰기 제거
from pprint import pprint # [Python 표준 라이브러리] 데이터 구조를 보기 좋게 출력
import uuid # [Python 표준 라이브러리] 고유 식별자(UUID) 생성 - 섹션 6에서 thread_id 생성에 사용
import warnings # [Python 표준 라이브러리] 경고 메시지 제어
warnings.filterwarnings("ignore")
2. Tool 정의
- 이 섹션은 전체 그래프 흐름에서 가장 먼저 수행되는 준비 단계에 해당한다.
- LangGraph 에이전트가 사용할 도구(Tool)를 정의하고, LLM 모델에 바인딩하는 과정을 다룬다.
- 도구란 LLM이 직접 실행할 수 없는 외부 작업(데이터베이스 검색, 웹 검색, API 호출 등)을 대신 수행하는 함수이다.
- LangGraph에서 도구는 반드시 `@tool` 데코레이터를 사용하여 정의해야 하며, 이를 통해 LLM이 도구의 이름, 설명, 인자 정보를 인식할 수 있게 된다.
- 이 프로젝트에서는 두 가지 도구를 정의한다: (1) 레스토랑 메뉴를 Chroma 벡터 저장소에서 검색하는 사용자 정의 도구, (2) `langchain_tavily` 패키지의 `TavilySearch`를 활용하여 인터넷에서 최신 정보를 검색하는 내장 도구이다.
2.1. 사용자 정의 - @tool 데코레이터
- `@tool` 데코레이터는 일반 Python 함수를 LangChain/LangGraph에서 사용 가능한 도구 객체로 변환하는 기능을 제공한다.
- 데코레이터를 적용하면 함수의 이름, docstring(설명), 타입 힌트(인자 정보)가 자동으로 도구 메타데이터로 등록된다.
- LLM은 이 메타데이터를 참조하여, 사용자 질문에 적합한 도구를 선택하고 적절한 인자를 생성하여 호출을 요청한다.
- 아래 코드에서는 Chroma 벡터 저장소와 OllamaEmbeddings를 사용하여 레스토랑 메뉴 데이터를 유사도 검색하는 도구를 정의한다.
- Chroma는 로컬 디스크에 저장된 기존 인덱스를 로드하여 사용하며, `similarity_search` 메서드를 통해 쿼리와 가장 유사한 문서 2개를 반환한다.
from langchain_chroma import Chroma # [LangChain 내장] Chroma 벡터 저장소 래퍼
from langchain_ollama import OllamaEmbeddings # [LangChain 내장] Ollama 임베딩 모델 래퍼
from langchain_core.tools import tool # [LangChain 내장] @tool 데코레이터 - 함수를 도구 객체로 변환
from typing import List # [Python 표준 라이브러리] 타입 힌트
embeddings_model = OllamaEmbeddings(model="bge-m3") # [사용자 정의] 임베딩 모델 인스턴스
# [사용자 정의] Chroma 벡터 저장소 인스턴스 - 로컬 디스크의 기존 인덱스 로드
vector_db = Chroma(
embedding_function=embeddings_model,
collection_name="restaurant_menu",
persist_directory="./chroma_db",
)
# [사용자 정의] 레스토랑 메뉴 검색 도구
@tool # [LangChain 내장] 함수를 LangChain 도구 객체로 변환하는 데코레이터
def search_menu(query: str) -> List[str]:
"""레스토랑 메뉴에서 정보를 검색합니다."""
docs = vector_db.similarity_search(query, k=2) # [LangChain 내장] 유사도 검색 - 쿼리와 가장 유사한 문서 k개 반환
formatted_docs = "\n\n---\n\n".join(
[
f'<Document source="{doc.metadata["source"]}"/>\n{doc.page_content}\n</Document>'
for doc in docs
]
)
if len(docs) > 0:
return formatted_docs
return "관련 메뉴 정보를 찾을 수 없습니다."
- `search_menu` 함수는 `query` 문자열을 입력받아 Chroma 벡터 저장소에서 유사도 검색을 수행한다.
- docstring `"레스토랑 메뉴에서 정보를 검색합니다."`는 LLM이 이 도구의 용도를 판단하는 데 핵심적인 역할을 한다.
- 검색 결과는 XML 형식의 문서 태그로 포맷팅되어 반환되며, 이 형식은 LLM이 출처 정보를 파싱하기 쉽게 설계되었다.
2.2. LangChain 내장 도구
- LangChain은 다양한 외부 서비스와 연동할 수 있는 내장 도구를 제공한다.
- `TavilySearch`는 `langchain_tavily` 패키지에서 제공하는 Tavily 검색 엔진 API 기반의 웹 검색 도구이다.
- 기존에 사용되던 `langchain_community.tools.TavilySearchResults`는 deprecated 되었으며, 최신 API에서는 `langchain_tavily.TavilySearch`를 사용해야 한다.
- 아래 코드에서는 `TavilySearch`를 래핑하는 사용자 정의 도구 `search_web`을 정의하여, 벡터 저장소에 없는 일반적인 질문이나 최신 정보에 대응할 수 있도록 한다.
- LLM은 도구의 docstring을 기반으로 메뉴 관련 질문은 `search_menu`로, 일반 정보 질문은 `search_web`으로 라우팅한다.
from langchain_tavily import TavilySearch # [LangChain 내장] Tavily 웹 검색 도구 (langchain_tavily 패키지)
# 참고: 기존 langchain_community.tools.TavilySearchResults는 deprecated
# [사용자 정의] 웹 검색 도구 - TavilySearch를 래핑하여 포맷팅된 결과 반환
@tool # [LangChain 내장] 함수를 LangChain 도구 객체로 변환하는 데코레이터
def search_web(query: str) -> List[str]:
"""데이터베이스에 존재하지 않는 정보 또는 최신 정보를 인터넷에서 검색합니다."""
tavily_search = TavilySearch(max_results=3) # [LangChain 내장] TavilySearch 인스턴스 생성 (최대 3개 결과)
docs = tavily_search.invoke(query) # [LangChain 내장] invoke 메서드로 검색 실행
formatted_docs = "\n\n---\n\n".join(
[
f'<Document href="{doc["url"]}"/>\n{doc["content"]}\n</Document>'
for doc in docs
]
)
if len(docs) > 0:
return formatted_docs
return "관련 정보를 찾을 수 없습니다."
- `search_web` 도구의 docstring인 `"데이터베이스에 존재하지 않는 정보 또는 최신 정보를 인터넷에서 검색합니다."`는 LLM에게 이 도구를 언제 사용해야 하는지 명확하게 알려주는 역할을 한다.
- `max_results=3`으로 설정하여 상위 3개의 검색 결과만 반환하도록 제한한다.
- `TavilySearch`는 `langchain_tavily` 패키지를 설치해야 사용할 수 있다 (`pip install langchain-tavily`).
2.3. LLM 모델 및 도구 바인딩
- 도구를 정의한 후에는 LLM 모델에 바인딩해야 한다.
- `bind_tools` 메서드는 LLM에게 사용 가능한 도구 목록을 알려주어, LLM이 응답 시 필요한 도구를 호출할 수 있게 해준다.
- 도구가 바인딩된 LLM은 사용자 질문을 분석하여 적절한 도구를 선택하고, 도구 호출에 필요한 인자를 자동으로 생성한다.
- 도구 호출이 필요 없는 질문(예: 간단한 계산)에는 도구를 호출하지 않고 직접 답변한다.
- 참고: 이 코드는 섹션 2.1에서 정의한 `search_menu`과 섹션 2.2에서 정의한 `search_web`을 사용합니다.
from langchain_openai import ChatOpenAI # [LangChain 내장] OpenAI 채팅 모델 래퍼
# [사용자 정의] LLM 모델 인스턴스
llm = ChatOpenAI(model="gpt-4o-mini", streaming=True)
# [사용자 정의] 도구 목록 - search_menu(섹션 2.1)과 search_web(섹션 2.2)을 포함
tools = [search_menu, search_web]
# [사용자 정의] 도구가 바인딩된 LLM 인스턴스
llm_with_tools = llm.bind_tools(tools=tools) # [LangChain 내장] bind_tools - LLM에 사용 가능한 도구 목록을 등록
- 도구가 바인딩된 LLM에 다양한 유형의 질문을 전달하여, LLM이 어떻게 도구를 선택하는지 확인한다.
- 참고: 이 코드는 섹션 2.3에서 정의한 `llm_with_tools`를 사용합니다.
from langchain_core.messages import HumanMessage # [LangChain 내장] 사용자 메시지 클래스
# 메뉴 관련 질문 → search_menu 도구 호출
tool_call = llm_with_tools.invoke([HumanMessage(content="스테이크 메뉴의 가격은 얼마인가요?")]) # [LangChain 내장] invoke - LLM 호출
print(tool_call.additional_kwargs) # [LangChain 내장] additional_kwargs - LLM 응답의 추가 메타데이터 (tool_calls 포함)
{'tool_calls': [{'index': 0, 'id': 'call_AWLbUKHM47Wj83M80r4xCKCD', 'function': {'arguments': '{"query":"스테이크"}', 'name': 'search_menu'}, 'type': 'function'}]}
- LLM이 "스테이크"라는 키워드를 추출하여 `search_menu` 도구를 호출하도록 결정했음을 확인할 수 있다.
- `invoke`의 반환값은 `AIMessage` 객체이며, `additional_kwargs` 딕셔너리에 `tool_calls` 리스트가 포함된다. 각 항목은 `index`, `id`(호출 식별자), `function`(도구 이름과 인자), `type` 필드를 갖는다.
# 일반 정보 질문 → search_web 도구 호출
tool_call = llm_with_tools.invoke([HumanMessage(content="LangGraph는 무엇인가요?")]) # [LangChain 내장] invoke
print(tool_call.additional_kwargs) # [LangChain 내장] additional_kwargs
{'tool_calls': [{'index': 0, 'id': 'call_IXXCOFXlgfFiwZj3YHD3K6Af', 'function': {'arguments': '{"query":"LangGraph"}', 'name': 'search_web'}, 'type': 'function'}]}
- 메뉴와 관련 없는 질문에 대해서는 `search_web` 도구를 선택한 것을 확인할 수 있다.
# 도구 호출이 필요 없는 질문 → 직접 답변
tool_call = llm_with_tools.invoke([HumanMessage(content="3+3은 얼마인가요?")]) # [LangChain 내장] invoke
print(tool_call.additional_kwargs) # [LangChain 내장] additional_kwargs - 도구 호출이 없으면 빈 딕셔너리 {}
print(tool_call.content) # [LangChain 내장] content - AIMessage의 텍스트 응답 내용
{}
3+3은 6입니다.
- `additional_kwargs`가 비어 있으므로, LLM이 도구를 호출하지 않고 직접 답변을 생성했음을 알 수 있다.
- 이처럼 LLM은 도구의 docstring과 사용자 질문을 비교하여, 도구 호출 여부와 어떤 도구를 호출할지를 자율적으로 결정한다.
3. 도구 노드(Tool Node)
- 이 섹션은 전체 그래프 흐름에서 LLM 노드 다음에 위치하며, LLM이 요청한 도구 호출을 실제로 실행하는 단계에 해당한다.
- 도구 노드(ToolNode)는 AI 모델이 요청한 도구(tool) 호출을 실행하는 역할을 처리하는 LangGraph의 핵심 컴포넌트이다.
- ToolNode는 LangGraph의 `langgraph.prebuilt` 모듈에서 제공하는 사전 구축된(prebuilt) 노드로, 도구 실행에 필요한 모든 로직을 캡슐화하고 있다.
- ToolNode의 내부 작동 방식은 다음과 같다:
- 1단계 - 메시지 추출: 입력으로 받은 상태(state)의 `messages` 리스트에서 가장 최근의 AIMessage를 찾는다.
- 2단계 - tool_calls 파싱: AIMessage 객체의 `tool_calls` 속성에서 도구 호출 요청 목록을 추출한다. 각 tool_call에는 호출할 도구의 이름(`name`), 전달할 인자(`args`), 고유 식별자(`id`)가 포함되어 있다.
- 3단계 - 도구 매칭: 추출된 도구 이름을 ToolNode 생성 시 등록한 도구 리스트에서 찾아 매칭한다.
- 4단계 - 병렬 실행: 여러 도구 호출 요청이 있는 경우, 각 도구를 병렬로 실행하여 응답 시간을 최소화한다. 이는 LangGraph가 도구 실행의 효율성을 극대화하기 위해 내장한 기능이다.
- 5단계 - ToolMessage 생성: 각 도구 실행 결과를 `ToolMessage` 객체로 변환하여 반환한다. ToolMessage에는 도구의 출력 내용(`content`)과 어떤 tool_call에 대한 응답인지를 식별하는 `tool_call_id`가 포함된다.
- 중요한 제약 사항: ToolNode에 전달되는 AIMessage에는 반드시 `tool_calls` 속성이 채워져 있어야 한다. `tool_calls`가 비어 있는 AIMessage를 전달하면 ToolNode는 실행할 도구를 찾지 못해 오류가 발생한다.
- ToolNode 도구 실행 처리 흐름

3.1. 도구 노드(Tool Node) 정의
- ToolNode를 생성할 때는 사용 가능한 도구 리스트를 인자로 전달한다.
- ToolNode는 전달받은 도구 리스트를 내부적으로 딕셔너리로 관리하며, 도구 이름을 키(key)로 사용하여 빠르게 매칭할 수 있도록 한다.
- 도구 리스트에는 `@tool` 데코레이터로 정의한 사용자 정의 도구와 LangChain 내장 도구를 모두 포함할 수 있다.
- 참고: 이 코드는 섹션 2.3에서 정의한 `tools` 리스트(`[search_menu, search_web]`)와 `llm_with_tools`를 사용합니다.
from langgraph.prebuilt import ToolNode # [LangGraph 내장] ToolNode - 도구 호출을 실행하는 사전 구축 노드
# [사용자 정의] 도구 노드 인스턴스 - tools는 섹션 2.3에서 정의한 도구 리스트
tool_node = ToolNode(tools=tools) # [사용자 정의 - 섹션 2.3 참조] tools = [search_menu, search_web]
- 먼저 LLM에게 질문을 전달하여 tool_calls가 포함된 AIMessage를 생성한다.
# [사용자 정의 - 섹션 2.3 참조] llm_with_tools로 도구 호출 요청 생성
tool_call = llm_with_tools.invoke([HumanMessage(content="스테이크 메뉴의 가격은 얼마인가요?")]) # [LangChain 내장] invoke
tool_call
AIMessage(content='',
additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_245qcUGi3qCuGls4MhAaRirj',
'function': {'arguments': '{"query":"스테이크"}', 'name': 'search_menu'},
'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls',
'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_bd4be55b21'},
id='run--de509927-f7c0-47e4-a587-7e41a2eedb0a-0', tool_calls=[{'name': 'search_menu',
'args': {'query': '스테이크'},
'id': 'call_245qcUGi3qCuGls4MhAaRirj', 'type': 'tool_call'}])
- `content`가 빈 문자열이고 `tool_calls`에 `search_menu` 도구 호출 정보가 담겨 있다.
- `finish_reason`이 `'tool_calls'`로 설정되어, LLM이 직접 답변을 생성하지 않고 도구 호출을 요청했음을 나타낸다.
- `tool_calls` 리스트의 각 항목에는 `name`(도구 이름), `args`(전달할 인자), `id`(호출 식별자)가 포함된다.
- AIMessage 반환값 구조: `content`(텍스트 응답, 도구 호출 시 빈 문자열), `additional_kwargs`(원시 tool_calls 정보), `response_metadata`(모델명, 종료 사유 등), `tool_calls`(파싱된 도구 호출 리스트)
3.2. 도구 노드(Tool Node) 실행
- ToolNode를 실행하려면 `invoke` 메서드에 `messages` 키를 가진 딕셔너리를 전달한다.
- 메시지 리스트의 마지막 항목이 `tool_calls`가 포함된 AIMessage여야 ToolNode가 정상적으로 작동한다.
- ToolNode는 AIMessage에서 tool_calls를 추출하고, 해당 도구를 실행한 뒤, 결과를 ToolMessage 형태로 반환한다.
- 참고: 이 코드는 섹션 3.1에서 정의한 `tool_node`와 바로 위에서 생성한 `tool_call`(AIMessage)을 사용합니다.
# [사용자 정의 - 섹션 3.1 참조] tool_node로 도구 실행
results = tool_node.invoke({"messages": [tool_call]}) # [LangGraph 내장] ToolNode.invoke - 도구 호출 실행
# 실행 결과 출력하여 확인
for result in results['messages']:
print(result.content) # [LangChain 내장] content - ToolMessage의 도구 실행 결과 텍스트
print()
<Document source="./data/restaurant_menu.txt"/>
1. 시그니처 스테이크
• 가격: ₩35,000
• 주요 식재료: 최상급 한우 등심, 로즈메리 감자, 그릴드 아스파라거스
• 설명: 셰프의 특제 시그니처 메뉴로, 21일간 건조 숙성한 최상급 한우 등심을 사용합니다. 미디엄 레어로 조리하여 육즙을 최대한 보존하며, 로즈메리 향의 감자와 아삭한 그릴드 아스파라거스가 곁들여집니다. 레드와인 소스와 함께 제공되어 풍부한 맛을 더합니다.
</Document>
---
<Document source="./data/restaurant_menu.txt"/>
1. 시그니처 스테이크
• 가격: ₩35,000
• 주요 식재료: 최상급 한우 등심, 로즈메리 감자, 그릴드 아스파라거스
• 설명: 셰프의 특제 시그니처 메뉴로, 21일간 건조 숙성한 최상급 한우 등심을 사용합니다. 미디엄 레어로 조리하여 육즙을 최대한 보존하며, 로즈메리 향의 감자와 아삭한 그릴드 아스파라거스가 곁들여집니다. 레드와인 소스와 함께 제공되어 풍부한 맛을 더합니다.
</Document>
- ToolNode가 `search_menu` 도구를 실행하여 Chroma 벡터 저장소에서 스테이크 관련 메뉴 정보를 검색한 결과를 ToolMessage로 반환하였다.
- 웹 검색 도구를 사용하는 경우에도 동일한 패턴으로 ToolNode를 실행할 수 있다.
- 참고: 이 코드는 섹션 3.1에서 정의한 `tool_node`와 섹션 2.3에서 정의한 `llm_with_tools`를 사용합니다.
# [사용자 정의 - 섹션 3.1, 2.3 참조] LLM으로 도구 호출 생성 후 tool_node로 실행
results = tool_node.invoke({"messages": [llm_with_tools.invoke("LangGraph는 무엇인가요?")]}) # [LangGraph 내장] ToolNode.invoke
# 실행 결과 출력하여 확인
for result in results['messages']:
print(result.content) # [LangChain 내장] content - ToolMessage의 도구 실행 결과 텍스트
print()
<Document href="https://www.ibm.com/think/topics/langgraph"/>
# What is LangGraph?
LangGraph, created by LangChain, is an open source AI agent framework designed to build, deploy and manage complex generative AI agent workflows. ...
</Document>
---
<Document href="https://docs.langchain.com/oss/python/langgraph/overview"/>
Trusted by companies shaping the future of agents— including Klarna, Replit, Elastic, and more— LangGraph is a low-level orchestration framework and runtime for building, managing, and deploying long-running, stateful agents. ...
</Document>
---
<Document href="https://www.geeksforgeeks.org/machine-learning/what-is-langgraph/"/>
LangGraph is an open-source framework built by LangChain that streamlines the creation and management of AI agent workflows. ...
</Document>
- `search_web` 도구가 Tavily API를 통해 인터넷에서 LangGraph 관련 최신 정보를 검색하여 반환하였다.
- ToolNode는 LLM이 선택한 도구가 무엇이든 동일한 인터페이스로 실행한다는 점이 핵심이다.
- LLM과 ToolNode 간 도구 호출 시퀀스

4. ReAct Agent
- 이 섹션은 전체 그래프 흐름의 핵심에 해당하며, 도구 정의와 ToolNode를 결합하여 자율적으로 동작하는 에이전트를 구성하는 단계이다.
- ReAct(Reasoning and Acting)는 가장 일반적이고 널리 사용되는 에이전트 패턴으로, Yao et al.이 2022년 논문(https://arxiv.org/abs/2210.03629)에서 제안하였다.
- ReAct 패턴은 LLM이 단순히 답변을 생성하는 것이 아니라, 외부 도구를 활용하여 정보를 수집하고, 그 결과를 기반으로 추론하여 최종 답변을 생성하는 반복적인 사이클을 수행한다.
- ReAct의 3단계 순환 구조:
- 행동(Act): 모델이 현재 상태를 분석하여 특정 도구를 호출한다. 어떤 도구를 호출할지, 어떤 인자를 전달할지를 스스로 결정한다.
- 관찰(Observe): 도구의 실행 결과(ToolMessage)를 모델에 다시 전달한다. 모델은 이 결과를 메시지 히스토리에 추가하여 컨텍스트를 확장한다.
- 추론(Reason): 모델이 도구 출력을 분석하여 다음 행동을 결정한다. 추가 정보가 필요하면 다른 도구를 호출하고, 충분한 정보가 모이면 최종 답변을 생성한다.
- ReAct 패턴의 종료 조건: LLM이 더 이상 도구를 호출하지 않고 직접 텍스트 응답을 생성할 때 사이클이 종료된다. 즉, AIMessage의 `tool_calls`가 비어 있으면 그래프 실행이 끝난다.
- ReAct 패턴이 강력한 이유:
- 단일 도구 호출로 부족한 정보를 여러 번의 도구 호출을 통해 보완할 수 있다.
- 도구 실행 결과가 예상과 다른 경우, 다른 도구를 시도하거나 다른 쿼리로 재검색할 수 있다.
- 모든 의사결정이 메시지 히스토리에 기록되어 추적이 가능하다.
- ReAct 패턴 실행 흐름

4.1. 랭그래프 내장 ReAct 에이전트 (create_react_agent)
- `create_react_agent`는 LangGraph에서 제공하는 사전 구축된(prebuilt) 함수로, ReAct 패턴의 에이전트를 한 줄의 코드로 생성할 수 있게 해준다.
- 이 함수는 내부적으로 다음과 같은 그래프 구조를 자동으로 구성한다:
- `agent` 노드: LLM을 호출하여 사용자 질문에 대한 응답 또는 도구 호출을 생성하는 노드
- `tools` 노드: ToolNode를 사용하여 LLM이 요청한 도구를 실행하는 노드
- 조건부 엣지: `agent` 노드의 출력에 `tool_calls`가 있으면 `tools` 노드로, 없으면 `END`로 라우팅하는 엣지
- 순환 엣지: `tools` 노드의 출력을 다시 `agent` 노드로 전달하는 엣지
- `prompt` 파라미터를 통해 시스템 프롬프트를 지정할 수 있으며, 이를 통해 에이전트의 행동 양식을 세밀하게 제어할 수 있다.
- 참고: 이전 버전에서는 `state_modifier` 파라미터를 사용했으나, 최신 LangGraph API에서는 `prompt` 파라미터로 변경되었다.
- 시스템 프롬프트 없이 가장 기본적인 ReAct 에이전트를 생성하고 실행한다.
- 참고: 이 코드는 섹션 2.3에서 정의한 `llm`과 `tools`를 사용합니다.
from langgraph.prebuilt import create_react_agent # [LangGraph 내장] ReAct 에이전트 자동 생성 함수
from IPython.display import Image, display # [외부 라이브러리] IPython - Jupyter 환경에서 이미지 표시
# [사용자 정의] ReAct 에이전트 그래프 - llm(섹션 2.3)과 tools(섹션 2.3) 사용
graph = create_react_agent(
llm, # [사용자 정의 - 섹션 2.3 참조] ChatOpenAI 인스턴스
tools=tools, # [사용자 정의 - 섹션 2.3 참조] [search_menu, search_web]
)
# 그래프 구조 시각화
display(Image(graph.get_graph().draw_mermaid_png())) # [LangGraph 내장] get_graph/draw_mermaid_png - 그래프 구조를 이미지로 렌더링
# 그래프 실행
from langchain_core.messages import HumanMessage # [LangChain 내장] 사용자 메시지 클래스
inputs = {"messages": [HumanMessage(content="스테이크 메뉴의 가격은 얼마인가요?")]}
messages = graph.invoke(inputs) # [LangGraph 내장] invoke - 그래프 실행
for m in messages['messages']:
m.pretty_print() # [LangChain 내장] pretty_print - 메시지를 사람이 읽기 쉬운 형식으로 출력
================================ Human Message =================================
스테이크 메뉴의 가격은 얼마인가요?
================================== Ai Message ==================================
Tool Calls:
search_menu (call_VmybnRudRIlcOAFCqxXiNG9G)
Call ID: call_VmybnRudRIlcOAFCqxXiNG9G
Args:
query: 스테이크
================================= Tool Message =================================
Name: search_menu
<Document source="./data/restaurant_menu.txt"/>
1. 시그니처 스테이크
• 가격: ₩35,000
• 주요 식재료: 최상급 한우 등심, 로즈메리 감자, 그릴드 아스파라거스
• 설명: 셰프의 특제 시그니처 메뉴로, 21일간 건조 숙성한 최상급 한우 등심을 사용합니다. ...
</Document>
================================== Ai Message ==================================
스테이크 메뉴의 가격은 ₩35,000입니다. 이 메뉴는 최상급 한우 등심을 사용하며, 로즈메리 감자와 그릴드 아스파라거스가 곁들여집니다.
- ReAct 에이전트의 동작 흐름을 단계별로 분석하면 다음과 같다:
- 1단계 (Act): LLM이 사용자 질문을 분석하여 `search_menu` 도구를 호출한다.
- 2단계 (Observe): ToolNode가 `search_menu`를 실행하고, 결과를 ToolMessage로 반환한다.
- 3단계 (Reason): LLM이 ToolMessage의 내용을 분석하여 최종 답변을 생성한다. 더 이상 도구 호출이 필요 없으므로 사이클이 종료된다.
- 시스템 프롬프트를 추가하여 에이전트의 행동 양식을 제어한다.
- 이 프롬프트는 에이전트가 도구 사용 결과에 대한 출처를 반드시 명시하도록 지시한다.
- 참고: 이 코드는 섹션 2.3에서 정의한 `llm`과 `tools`를 사용합니다.
from langgraph.prebuilt import create_react_agent # [LangGraph 내장] ReAct 에이전트 자동 생성 함수
from IPython.display import Image, display # [외부 라이브러리] IPython
from textwrap import dedent # [Python 표준 라이브러리] 멀티라인 문자열 들여쓰기 제거
# [사용자 정의] 시스템 프롬프트 - 에이전트의 행동 양식과 출처 표기 규칙을 지시
system_prompt = dedent("""
You are an AI assistant designed to answer human questions.
You can use the provided tools to help generate your responses.
Follow these steps to answer questions:
1. Carefully read and understand the question.
2. Use the provided tools to obtain necessary information.
3. Immediately after using a tool, cite the source using the format below.
4. Construct an accurate and helpful answer using the tool outputs and citations.
5. Provide the final answer when you determine it's complete.
When using tools, follow this format:
Action: tool_name
Action Input: input for the tool
Immediately after receiving tool output, cite the source as follows:
[Source: tool_name | document_title/item_name | url/file_path]
For example:
Action: search_menu
Action Input: 스테이크
(After receiving tool output)
[Source: search_menu | 스테이크 | ./data/data.txt]
스테이크에 대한 정보는 다음과 같습니다...
Action: search_web
Action Input: History of AI
(After receiving tool output)
[Source: search_web | AI History | https://en.wikipedia.org/wiki/History_of_artificial_intelligence]
AI의 역사는 다음과 같이 요약됩니다...
If tool use is not necessary, answer directly.
Your final answer should be clear, concise, and directly related to the user's question.
Ensure that every piece of factual information in your response is accompanied by a citation.
Remember: ALWAYS include these citations for all factual information, tool outputs, and referenced documents in your response.
Do not provide any information without a corresponding citation.
""")
# [사용자 정의] 시스템 프롬프트가 적용된 ReAct 에이전트 그래프
graph = create_react_agent(
llm, # [사용자 정의 - 섹션 2.3 참조] ChatOpenAI 인스턴스
tools=tools, # [사용자 정의 - 섹션 2.3 참조] [search_menu, search_web]
prompt=system_prompt, # [사용자 정의] 시스템 프롬프트를 prompt 파라미터로 전달
# 참고: 이전 버전의 state_modifier는 deprecated, 최신 API는 prompt 사용
)
# 그래프 구조 시각화
display(Image(graph.get_graph().draw_mermaid_png())) # [LangGraph 내장] 그래프 시각화
# 그래프 실행
inputs = {"messages": [HumanMessage(content="스테이크 메뉴의 가격은 얼마인가요?")]}
messages = graph.invoke(inputs) # [LangGraph 내장] invoke - 그래프 실행
for m in messages['messages']:
m.pretty_print() # [LangChain 내장] pretty_print
```
```text
================================ Human Message =================================
스테이크 메뉴의 가격은 얼마인가요?
================================== Ai Message ==================================
Tool Calls:
search_menu (call_FFGRVeVIMByVYIH4BAj9n5ph)
Call ID: call_FFGRVeVIMByVYIH4BAj9n5ph
Args:
query: 스테이크
================================= Tool Message =================================
Name: search_menu
<Document source="./data/restaurant_menu.txt"/>
1. 시그니처 스테이크
• 가격: ₩35,000
...
</Document>
================================== Ai Message ==================================
스테이크 메뉴 중 "시그니처 스테이크"의 가격은 ₩35,000입니다. 이 스테이크는 최상급 한우 등심을
사용하여 미디엄 레어로 조리되며, 로즈메리 감자와 그릴드 아스파라거스가 곁들여집니다.
[Source: search_menu | 스테이크 | ./data/restaurant_menu.txt]
- 시스템 프롬프트를 적용한 결과, 에이전트가 답변 말미에 `[Source: ...]` 형식으로 출처를 명시하고 있음을 확인할 수 있다.
4.2. 조건부 엣지 함수를 사용자 정의 (should_continue)
- `create_react_agent`는 편리하지만, 실무에서는 그래프의 흐름을 더 세밀하게 제어해야 하는 경우가 있다.
- 이때 `StateGraph`를 사용하여 그래프를 직접 구성하고, 조건부 엣지 함수를 사용자 정의할 수 있다.
- `should_continue` 함수는 LLM 노드의 출력을 분석하여 다음 단계를 결정하는 라우팅 함수이다.
- 이 함수는 상태(state)의 마지막 메시지를 검사하여 `tool_calls`가 있으면 도구 실행 노드로, 없으면 `END`로 라우팅한다.
- should_continue 함수의 동작 원리:
- LLM이 도구 호출이 필요하다고 판단하면, AIMessage의 `tool_calls` 속성에 호출 정보를 채워서 반환한다.
- `should_continue`는 이 `tool_calls` 속성의 존재 여부를 확인하여 그래프의 다음 경로를 결정한다.
- `tool_calls`가 있으면 `"execute_tools"` 문자열을 반환하여 도구 실행 노드로 이동하고, 없으면 `END`를 반환하여 그래프 실행을 종료한다.
- 이 패턴은 ReAct의 순환 구조를 구현하는 핵심 메커니즘이다.
- 참고: 이 코드는 섹션 2.3에서 정의한 `llm_with_tools`, `tools`와 섹션 4.1 실무 코드에서 정의한 `system_prompt`를 사용합니다.
from langgraph.graph import MessagesState, StateGraph, START, END # [LangGraph 내장] 그래프 구성 핵심 클래스/상수
from langchain_core.messages import HumanMessage, SystemMessage # [LangChain 내장] 메시지 클래스
from langgraph.prebuilt import ToolNode # [LangGraph 내장] 도구 실행 노드
from IPython.display import Image, display # [외부 라이브러리] IPython
# [LangGraph 내장] MessagesState - 메시지 리스트를 상태로 관리하는 LangGraph 기본 상태 클래스
# MessagesState는 'messages' 키에 메시지 리스트를 저장하며, 새 메시지가 추가될 때 기존 리스트에 append하는 리듀서가 내장
# 추가 상태 필드가 필요 없으면 MessagesState를 직접 사용해도 된다 (예: StateGraph(MessagesState))
# 여기서는 확장 가능성을 위해 GraphState로 상속하여 사용
class GraphState(MessagesState):
pass
# [사용자 정의] LLM 호출 노드 함수
def call_model(state: GraphState):
system_message = SystemMessage(content=system_prompt) # [사용자 정의 - 섹션 4.1 참조] system_prompt 사용
messages = [system_message] + state['messages']
response = llm_with_tools.invoke(messages) # [사용자 정의 - 섹션 2.3 참조] llm_with_tools로 LLM 호출
return {"messages": [response]}
# [사용자 정의] 조건부 엣지 함수 - tool_calls 유무에 따라 다음 노드 결정
def should_continue(state: GraphState):
last_message = state["messages"][-1]
# [LangChain 내장] tool_calls - AIMessage의 도구 호출 요청 리스트 속성
if last_message.tool_calls:
return "execute_tools"
# 도구 호출이 없으면 답변 생성하고 종료
return END # [LangGraph 내장] END - 그래프 종료 상수
# [사용자 정의] 그래프 구성
builder = StateGraph(GraphState) # [LangGraph 내장] StateGraph - 상태 기반 그래프 빌더
builder.add_node("call_model", call_model) # [LangGraph 내장] add_node - 노드 등록
builder.add_node("execute_tools", ToolNode(tools)) # [사용자 정의 - 섹션 2.3 참조] tools 리스트로 ToolNode 생성
builder.add_edge(START, "call_model") # [LangGraph 내장] add_edge - 시작점에서 call_model로 연결
builder.add_conditional_edges( # [LangGraph 내장] add_conditional_edges - 조건부 분기 엣지 추가
"call_model",
should_continue, # [사용자 정의] 라우팅 함수
{
"execute_tools": "execute_tools", # 반환값 "execute_tools" → execute_tools 노드로 이동
END: END # 반환값 END → 그래프 종료
}
)
builder.add_edge("execute_tools", "call_model") # [LangGraph 내장] add_edge - 도구 실행 후 다시 LLM 노드로 순환
graph = builder.compile() # [LangGraph 내장] compile - 그래프를 실행 가능한 형태로 컴파일
# 그래프 구조 시각화
display(Image(graph.get_graph().draw_mermaid_png())) # [LangGraph 내장] 그래프 시각화
- 위 코드의 그래프 구성을 상세히 분석하면 다음과 같다:
- `GraphState(MessagesState)`: LangGraph의 MessagesState를 상속하여 메시지 리스트를 상태로 관리한다. MessagesState는 `messages` 키에 메시지 리스트를 저장하며, 새로운 메시지가 추가될 때 기존 리스트에 append하는 리듀서(reducer)가 내장되어 있다.
- `call_model` 노드: 시스템 프롬프트와 사용자 메시지를 결합하여 LLM을 호출하고, 응답을 상태에 추가한다.
- `should_continue` 조건부 엣지: LLM 응답의 `tool_calls` 유무에 따라 분기한다.
- `add_conditional_edges`의 세 번째 인자는 경로 매핑 딕셔너리로, 조건부 함수의 반환값을 실제 노드 이름에 매핑한다.
- `add_edge("execute_tools", "call_model")`: 도구 실행 후 다시 LLM 노드로 돌아가는 순환 엣지를 구성한다.
# 그래프 실행
inputs = {"messages": [HumanMessage(content="스테이크 메뉴의 가격은 얼마인가요?")]}
messages = graph.invoke(inputs) # [LangGraph 내장] invoke - 그래프 실행
for m in messages['messages']:
m.pretty_print() # [LangChain 내장] pretty_print
================================ Human Message =================================
스테이크 메뉴의 가격은 얼마인가요?
================================== Ai Message ==================================
Tool Calls:
search_menu (call_mY4HJCbDgSXSeGaMNKtB3VhV)
Call ID: call_mY4HJCbDgSXSeGaMNKtB3VhV
Args:
query: 스테이크
================================= Tool Message =================================
Name: search_menu
<Document source="./data/restaurant_menu.txt"/>
1. 시그니처 스테이크
• 가격: ₩35,000
...
</Document>
================================== Ai Message ==================================
스테이크 메뉴의 가격은 ₩35,000입니다. 이 메뉴는 최상급 한우 등심을 사용하며,
로즈메리 감자와 그릴드 아스파라거스가 곁들여져 있습니다. 셰프의 특제 시그니처 메뉴로,
21일간 건조 숙성하여 미디엄 레어로 조리됩니다. [Source: search_menu | 스테이크 메뉴 |
./data/restaurant_menu.txt]
- 사용자 정의 그래프와 `create_react_agent`가 동일한 결과를 생성하는 것을 확인할 수 있다.
- 차이점은 사용자 정의 그래프에서는 노드 이름, 조건부 로직, 시스템 프롬프트 적용 방식 등을 자유롭게 커스터마이징할 수 있다는 점이다.
- 사용자 정의 그래프 실행 흐름

4.3. tools_condition 활용
- LangGraph는 도구 호출 여부에 따른 분기 로직을 간편하게 구현할 수 있도록 `tools_condition`이라는 사전 구축된 조건부 엣지 함수를 제공한다.
- `tools_condition`은 앞서 직접 구현한 `should_continue` 함수와 동일한 기능을 수행한다.
- 내부적으로 최신 메시지의 `tool_calls` 속성을 확인하여, 도구 호출이 있으면 `"tools"` 문자열을 반환하고, 없으면 `END`를 반환한다.
- `tools_condition`과 `should_continue`의 차이점:
- `tools_condition`은 반환값이 `"tools"`로 고정되어 있으므로, 도구 실행 노드의 이름을 반드시 `"tools"`로 지정해야 한다.
- `should_continue`는 사용자가 반환값을 자유롭게 정의할 수 있으므로, 노드 이름을 자유롭게 지정할 수 있다.
- `tools_condition`은 코드가 더 간결하고, LangGraph의 관례를 따르기 때문에 가독성이 좋다.
- 실무에서는 특별한 커스터마이징이 필요 없는 경우 `tools_condition`을 사용하는 것이 권장된다.
- 참고: 이 코드는 섹션 2.3에서 정의한 `llm_with_tools`, `tools`와 섹션 4.1 실무 코드에서 정의한 `system_prompt`, 섹션 4.2에서 정의한 `GraphState`를 사용합니다.
from langgraph.prebuilt import tools_condition # [LangGraph 내장] 도구 호출 여부 분기를 위한 사전 구축 조건부 함수
# [사용자 정의] 노드 함수 정의 - 섹션 4.2의 call_model과 동일한 로직
def call_model(state: GraphState): # [사용자 정의 - 섹션 4.2 참조] GraphState 사용
system_message = SystemMessage(content=system_prompt) # [사용자 정의 - 섹션 4.1 참조] system_prompt 사용
messages = [system_message] + state['messages']
response = llm_with_tools.invoke(messages) # [사용자 정의 - 섹션 2.3 참조] llm_with_tools 사용
return {"messages": [response]}
# [사용자 정의] 그래프 구성 - tools_condition 사용 버전
builder = StateGraph(GraphState) # [LangGraph 내장] StateGraph
builder.add_node("agent", call_model) # [LangGraph 내장] add_node - "agent" 이름으로 LLM 노드 등록
builder.add_node("tools", ToolNode(tools)) # [사용자 정의 - 섹션 2.3 참조] tools 리스트로 ToolNode 생성, 노드 이름은 반드시 "tools"
builder.add_edge(START, "agent") # [LangGraph 내장] add_edge
# [LangGraph 내장] tools_condition 사용 - 경로 매핑 딕셔너리 생략 가능
builder.add_conditional_edges(
"agent",
tools_condition, # [LangGraph 내장] tool_calls 있으면 "tools", 없으면 END 반환
)
builder.add_edge("tools", "agent") # [LangGraph 내장] add_edge - 도구 실행 후 다시 agent 노드로 순환
graph = builder.compile() # [LangGraph 내장] compile
# 그래프 구조 시각화
display(Image(graph.get_graph().draw_mermaid_png())) # [LangGraph 내장] 그래프 시각화
- `tools_condition`을 사용할 때 주의할 점: `add_conditional_edges`에서 세 번째 인자(경로 매핑 딕셔너리)를 생략할 수 있다. `tools_condition`이 반환하는 `"tools"` 문자열이 자동으로 `"tools"` 노드에 매핑되고, `END`는 자동으로 종료로 매핑되기 때문이다.
# 그래프 실행
inputs = {"messages": [HumanMessage(content="파스타에 어울리는 음료는 무엇인가요?")]}
messages = graph.invoke(inputs) # [LangGraph 내장] invoke - 그래프 실행
for m in messages['messages']:
m.pretty_print() # [LangChain 내장] pretty_print
================================ Human Message =================================
파스타에 어울리는 음료는 무엇인가요?
================================== Ai Message ==================================
Tool Calls:
search_menu (call_Zy9duSqmaf3kXVHQxk9HepZb)
Call ID: call_Zy9duSqmaf3kXVHQxk9HepZb
Args:
query: 파스타 음료
================================= Tool Message =================================
Name: search_menu
<Document source="./data/restaurant_menu.txt"/>
6. 해산물 파스타
• 가격: ₩24,000
• 주요 식재료: 링귀네 파스타, 새우, 홍합, 오징어, 토마토 소스
• 설명: 알 덴테로 삶은 링귀네 파스타에 신선한 해산물을 듬뿍 올린 메뉴입니다. ...
</Document>
================================== Ai Message ==================================
파스타에 어울리는 음료는 여러 가지가 있지만, 일반적으로 다음과 같은 옵션들이 추천됩니다:
1. **화이트 와인**: 파스타의 크림 소스나 해산물 소스와 잘 어울립니다.
2. **레드 와인**: 토마토 소스가 사용된 파스타에 잘 어울리며, 특히 미트 소스 파스타와 조화를 이룹니다.
3. **스파클링 워터**: 상쾌한 맛으로 파스타의 기름진 맛을 중화해 줍니다.
4. **과일 주스**: 특히 레몬이나 라임 주스는 파스타의 맛을 더욱 상큼하게 해 줄 수 있습니다.
- 이 실행 결과에서 흥미로운 점은, LLM이 `search_menu` 도구로 파스타 관련 정보를 검색한 뒤, 도구 결과에 음료 정보가 충분하지 않다고 판단하여 자체 지식을 활용해 음료를 추천했다는 것이다.
- 이는 ReAct 패턴의 '추론(Reason)' 단계에서 LLM이 도구 결과의 충분성을 판단하고, 필요 시 자체 지식을 보충적으로 활용할 수 있음을 보여준다.
- create_react_agent 실행 흐름

5. MemorySaver
- 이 섹션은 전체 그래프 흐름에서 그래프 컴파일 단계에 해당하며, 그래프에 상태 지속성(persistence)을 부여하는 과정을 다룬다.
- 기본적으로 LangGraph 그래프는 실행 시 상태가 일시적(stateless)이다. 즉, 한 번의 `invoke` 호출이 완료되면 그래프 내부의 모든 상태(메시지 히스토리 포함)가 사라진다.
- 이로 인해 다음과 같은 문제가 발생한다:
- 이전 대화 내용을 기억하지 못하여 다중 턴 대화가 불가능하다.
- 사용자가 "이 중에 하나만 추천해주세요"와 같이 이전 컨텍스트를 참조하는 질문을 하면, 에이전트가 맥락을 파악하지 못한다.
- 대화 중 중단이 발생하면 처음부터 다시 시작해야 한다.
- MemorySaver는 이러한 상태 일시성 문제를 해결하기 위한 LangGraph의 체크포인터(checkpointer) 중 가장 기본적인 구현체이다.
- MemorySaver의 핵심 개념:
- 체크포인터(Checkpointer): 그래프 실행의 각 단계(step)에서 전체 상태를 자동으로 저장하는 메커니즘이다. 그래프가 한 노드에서 다음 노드로 이동할 때마다 현재 상태의 스냅샷을 저장한다.
- 인메모리 키-값 저장소: MemorySaver는 파이썬 딕셔너리를 기반으로 한 인메모리 저장소를 사용한다. 빠르고 간편하지만, 프로세스가 종료되면 데이터가 사라진다. 프로덕션 환경에서는 SqliteSaver나 PostgresSaver 같은 영속적 저장소를 사용해야 한다.
- 스레드(Thread): 대화의 논리적 단위를 구분하는 개념이다. 각 스레드는 고유한 `thread_id`로 식별되며, 독립적인 메시지 히스토리와 상태를 유지한다. 서로 다른 `thread_id`를 사용하면 완전히 별개의 대화 세션이 생성된다.
- thread_id: 스레드를 식별하는 고유 문자열이다. 같은 `thread_id`로 여러 번 `invoke`를 호출하면, 체크포인터가 이전 상태를 복원하여 대화가 이어진다. 다른 `thread_id`로 호출하면 새로운 대화가 시작된다.
- MemorySaver의 작동 방식:
- 1단계: 그래프 컴파일 시 `checkpointer=memory` 인자로 MemorySaver를 지정한다.
- 2단계: 그래프 실행 시 `config` 딕셔너리에 `thread_id`를 지정하여 전달한다.
- 3단계: 그래프가 각 노드를 실행할 때마다, 체크포인터가 현재 상태(전체 메시지 히스토리 포함)를 `thread_id`를 키로 하여 저장한다.
- 4단계: 같은 `thread_id`로 다시 그래프를 실행하면, 체크포인터가 이전에 저장한 상태를 복원하여 대화를 이어간다.
- Stateless(비상태) 실행과 Stateful(상태 유지) 실행의 차이:
- Stateless: 매 호출마다 빈 상태에서 시작한다. 이전 대화를 알 수 없다. 단일 질의-응답에 적합하다.
- Stateful: 체크포인터가 이전 상태를 복원하여 이어서 실행한다. 다중 턴 대화, 맥락 참조 질문에 필수적이다. 사용자별, 세션별로 독립적인 대화 관리가 가능하다.
- Stateless 실행 (MemorySaver 없음)

- Stateful 실행 (MemorySaver 있음)

5.1. 사용자 정의 그래프
- 앞서 4.3절에서 구성한 사용자 정의 그래프에 MemorySaver를 적용하는 과정을 다룬다.
- 먼저 MemorySaver 없이 실행하여 상태가 유지되지 않는 문제를 확인한다.
- 참고: 이 코드는 섹션 4.3에서 컴파일한 `graph`를 사용합니다. MemorySaver가 적용되지 않은 상태이므로 이전 대화 컨텍스트가 유지되지 않습니다.
# 그래프 실행 - 이전 대화 내용을 기억하지 못하는 문제
inputs = {"messages": [HumanMessage(content="이 중에 하나만 추천해주세요.")]}
messages = graph.invoke(inputs) # [LangGraph 내장] invoke - MemorySaver 없이 실행
for m in messages['messages']:
m.pretty_print() # [LangChain 내장] pretty_print
================================ Human Message =================================
이 중에 하나만 추천해주세요.
================================== Ai Message ==================================
어떤 것 중에서 추천을 원하는지 구체적으로 말씀해 주시면, 최선의 선택을 도와드릴 수 있습니다! 예를 들어 음식, 여행지, 책 등 어떤 주제인지 알려주세요.
- 이전 대화에서 파스타에 어울리는 음료를 질문했음에도 불구하고, 에이전트가 "어떤 것 중에서 추천을 원하는지" 되묻고 있다.
- 이는 그래프 실행이 stateless하기 때문에 이전 대화 컨텍스트가 완전히 사라졌기 때문이다.
5.1.1. 체크포인터 지정
- 그래프를 컴파일할 때 `checkpointer` 인자에 MemorySaver 인스턴스를 전달하면, 그래프의 모든 실행에 대해 상태가 자동으로 저장된다.
- MemorySaver 인스턴스는 하나만 생성하여 여러 그래프에 공유할 수 있지만, 일반적으로는 그래프별로 별도의 인스턴스를 생성하는 것이 권장된다.
- `builder.compile(checkpointer=memory)`를 호출하면, 기존 그래프 구조(노드, 엣지)는 그대로 유지되면서 체크포인팅 기능만 추가된다.
- 참고: 이 코드는 섹션 4.3에서 구성한 `builder`(StateGraph 인스턴스)를 사용합니다. `builder`에는 "agent" 노드, "tools" 노드, 조건부 엣지가 이미 등록되어 있습니다.
from langgraph.checkpoint.memory import MemorySaver # [LangGraph 내장] 인메모리 체크포인터
# [사용자 정의] 메모리 인스턴스 - 인메모리 키-값 저장소 기반
memory = MemorySaver()
# [사용자 정의] 체크포인터가 지정된 그래프 - builder는 섹션 4.3에서 구성한 StateGraph 인스턴스
graph_memory = builder.compile(checkpointer=memory) # [LangGraph 내장] compile - checkpointer 인자로 MemorySaver 전달
5.1.2. 체크포인터 사용
- 체크포인터가 지정된 그래프를 실행할 때는 반드시 `config` 딕셔너리에 `thread_id`를 지정해야 한다.
- `thread_id`는 문자열 타입이며, 같은 `thread_id`로 여러 번 호출하면 이전 대화 기록이 자동으로 유지된다.
- `config` 딕셔너리 구조: `{"configurable": {"thread_id": "고유_문자열"}}` - 최상위에 `"configurable"` 키가 있고, 그 안에 `"thread_id"` 키로 스레드 식별자를 지정한다. LangGraph는 이 구조를 통해 체크포인터에 전달할 설정 정보를 관리한다.
- 참고: 이 코드는 섹션 5.1.1에서 컴파일한 `graph_memory`를 사용합니다.
# [사용자 정의] config - thread_id로 대화 세션을 식별하는 설정 딕셔너리
config = {"configurable": {"thread_id": "1"}} # "configurable" > "thread_id" 구조로 스레드 식별
messages = [HumanMessage(content="스테이크 메뉴의 가격은 얼마인가요?")]
messages = graph_memory.invoke({"messages": messages}, config) # [LangGraph 내장] invoke - config와 함께 그래프 실행
for m in messages['messages']:
m.pretty_print() # [LangChain 내장] pretty_print
================================ Human Message =================================
스테이크 메뉴의 가격은 얼마인가요?
================================== Ai Message ==================================
Tool Calls:
search_menu (call_eal4FSkV5mW8Sr7Rri2HFI9O)
Call ID: call_eal4FSkV5mW8Sr7Rri2HFI9O
Args:
query: 스테이크
================================= Tool Message =================================
Name: search_menu
<Document source="./data/restaurant_menu.txt"/>
1. 시그니처 스테이크
• 가격: ₩35,000
...
</Document>
================================== Ai Message ==================================
스테이크 메뉴의 가격은 ₩35,000입니다. 이 메뉴는 최상급 한우 등심을 사용하며,
로즈메리 감자와 그릴드 아스파라거스가 곁들여집니다.
셰프의 특제 레드와인 소스와 함께 제공됩니다 [Source: search_menu | 스테이크 |
./data/restaurant_menu.txt].
- 이제 같은 `thread_id`로 후속 질문을 전달하여 대화 기록이 유지되는지 확인한다.
# 같은 thread_id("1")로 후속 질문 - MemorySaver가 이전 대화 컨텍스트를 자동 복원
config = {"configurable": {"thread_id": "1"}} # [사용자 정의] 동일한 thread_id 사용
messages = [HumanMessage(content="둘 중에 더 저렴한 메뉴는 무엇인가요?")]
messages = graph_memory.invoke({"messages": messages}, config) # [LangGraph 내장] invoke
for m in messages['messages']:
m.pretty_print() # [LangChain 내장] pretty_print
================================ Human Message =================================
스테이크 메뉴의 가격은 얼마인가요?
================================== Ai Message ==================================
Tool Calls:
search_menu (call_eal4FSkV5mW8Sr7Rri2HFI9O)
Call ID: call_eal4FSkV5mW8Sr7Rri2HFI9O
Args:
query: 스테이크
================================= Tool Message =================================
Name: search_menu
<Document source="./data/restaurant_menu.txt"/>
1. 시그니처 스테이크
• 가격: ₩35,000
...
</Document>
================================== Ai Message ==================================
스테이크 메뉴의 가격은 ₩35,000입니다. ...
================================ Human Message =================================
둘 중에 더 저렴한 메뉴는 무엇인가요?
================================== Ai Message ==================================
Tool Calls:
search_menu (call_2wZeg6azCDHlU4mrZ6c6AyOO)
Call ID: call_2wZeg6azCDHlU4mrZ6c6AyOO
Args:
query: 메뉴
================================= Tool Message =================================
Name: search_menu
<Document source="./data/restaurant_menu.txt"/>
9. 치킨 콘피
• 가격: ₩23,000
...
</Document>
================================== Ai Message ==================================
두 메뉴의 가격은 다음과 같습니다:
- 스테이크: ₩35,000
- 치킨 콘피: ₩23,000
따라서, 더 저렴한 메뉴는 치킨 콘피로, 가격은 ₩23,000입니다
[Source: search_menu | 메뉴 | ./data/restaurant_menu.txt].
- 출력 결과를 보면, 두 번째 호출에서 이전 대화(스테이크 가격 질문과 응답)가 전체 메시지 히스토리에 포함되어 있음을 확인할 수 있다.
- "둘 중에 더 저렴한 메뉴"라는 맥락 참조형 질문에 대해, 에이전트가 이전 대화에서 언급된 스테이크와 새로 검색한 치킨 콘피를 비교하여 답변을 생성하였다.
- 이것이 MemorySaver의 핵심 가치이다: 같은 `thread_id`로 호출하면 이전 대화의 모든 컨텍스트가 자동으로 복원된다.
- MemorySaver 대화 상태 복원 시퀀스

5.2. 내장 ReAct + MemorySaver
- `create_react_agent` 함수에도 `checkpointer` 인자를 지정하여 MemorySaver를 적용할 수 있다.
- 이 방식은 그래프를 직접 구성하지 않고도 상태 유지형 ReAct 에이전트를 간편하게 생성할 수 있다는 장점이 있다.
- 참고: 이 코드는 섹션 2.3에서 정의한 `llm`, `tools`와 섹션 4.1 실무 코드에서 정의한 `system_prompt`를 사용합니다.
from langgraph.prebuilt import create_react_agent # [LangGraph 내장] ReAct 에이전트 자동 생성 함수
from langgraph.checkpoint.memory import MemorySaver # [LangGraph 내장] 인메모리 체크포인터
from IPython.display import Image, display # [외부 라이브러리] IPython
# [사용자 정의] 메모리 인스턴스 - 새로운 MemorySaver 생성
memory = MemorySaver()
# [사용자 정의] MemorySaver가 적용된 ReAct 에이전트 그래프
graph = create_react_agent(
llm, # [사용자 정의 - 섹션 2.3 참조] ChatOpenAI 인스턴스
tools=tools, # [사용자 정의 - 섹션 2.3 참조] [search_menu, search_web]
prompt=system_prompt, # [사용자 정의 - 섹션 4.1 참조] 시스템 프롬프트 (최신 API: prompt 파라미터)
checkpointer=memory, # [LangGraph 내장] 체크포인터 지정으로 상태 유지 활성화
)
# 그래프 구조 시각화
display(Image(graph.get_graph().draw_mermaid_png())) # [LangGraph 내장] 그래프 시각화
- 새로운 `thread_id`를 사용하여 독립적인 대화 세션을 시작한다.
# [사용자 정의] config - thread_id="2"로 새로운 독립 대화 세션 생성
config = {"configurable": {"thread_id": "2"}}
messages = [HumanMessage(content="채식주의자를 위한 메뉴가 있나요?")]
messages = graph.invoke({"messages": messages}, config) # [LangGraph 내장] invoke
for m in messages['messages']:
m.pretty_print() # [LangChain 내장] pretty_print
================================ Human Message =================================
채식주의자를 위한 메뉴가 있나요?
================================== Ai Message ==================================
Tool Calls:
search_menu (call_PPe594T2dFClAuqGf2VcfkI4)
Call ID: call_PPe594T2dFClAuqGf2VcfkI4
Args:
query: 채식주의자 메뉴
================================= Tool Message =================================
Name: search_menu
<Document source="./data/restaurant_menu.txt"/>
5. 가든 샐러드
• 가격: ₩12,000
• 주요 식재료: 유기농 믹스 그린, 체리 토마토, 오이, 당근, 발사믹 드레싱
• 설명: 신선한 유기농 채소들로 구성된 건강한 샐러드입니다. ...
</Document>
================================== Ai Message ==================================
채식주의자를 위한 메뉴로는 "가든 샐러드"가 있습니다. 다음은 이 메뉴의 상세 정보입니다:
- **가격**: ₩12,000
- **주요 식재료**: 유기농 믹스 그린, 체리 토마토, 오이, 당근, 발사믹 드레싱
- **설명**: 신선한 유기농 채소들로 구성된 건강한 샐러드로, 아삭한 식감의 믹스 그린에 달콤한 체리 토마토, 오이, 당근을 더해 다양한 맛과 식감을 제공합니다. 특제 발사믹 드레싱이 채소 본연의 맛을 살려줍니다.
이 메뉴는 채식주의자에게 적합한 선택입니다.
- 같은 `thread_id`로 후속 질문을 전달하여 대화 기록 유지를 확인한다.
# 같은 thread_id("2")로 후속 질문 - 이전 대화 컨텍스트 자동 복원
config = {"configurable": {"thread_id": "2"}} # [사용자 정의] 동일한 thread_id 사용
messages = [HumanMessage(content="방금 답변에 대한 출처가 있나요?")]
messages = graph.invoke({"messages": messages}, config) # [LangGraph 내장] invoke
for m in messages['messages']:
m.pretty_print() # [LangChain 내장] pretty_print
================================ Human Message =================================
채식주의자를 위한 메뉴가 있나요?
================================== Ai Message ==================================
Tool Calls:
search_menu (call_PPe594T2dFClAuqGf2VcfkI4)
...
================================== Ai Message ==================================
채식주의자를 위한 메뉴로는 "가든 샐러드"가 있습니다. ...
================================ Human Message =================================
방금 답변에 대한 출처가 있나요?
================================== Ai Message ==================================
네, 방금 제공한 정보의 출처는 다음과 같습니다:
[Source: search_menu | 메뉴 정보 | ./data/restaurant_menu.txt]
이 파일에서 채식주의자를 위한 "가든 샐러드"에 대한 상세 정보를 확인할 수 있습니다.
- "방금 답변에 대한 출처가 있나요?"라는 질문은 이전 대화 컨텍스트 없이는 답변이 불가능한 질문이다.
- MemorySaver 덕분에 에이전트가 이전 대화에서 `search_menu` 도구를 사용했다는 사실과 출처 파일 경로를 정확하게 기억하고 답변하였다.
- `thread_id="2"`는 `thread_id="1"`과 완전히 독립적인 대화 세션이므로, 서로 다른 스레드의 대화 내용이 혼합되지 않는다.
6. Gradio 챗봇
- 이 섹션은 전체 그래프 흐름의 최종 단계로, 지금까지 구축한 ReAct 에이전트와 MemorySaver를 Gradio UI와 결합하여 웹 기반 챗봇 서비스를 구현한다.
- Gradio는 머신러닝 모델을 위한 웹 인터페이스를 간편하게 생성할 수 있는 파이썬 라이브러리이다.
- `ChatInterface`를 사용하면 채팅 UI를 자동으로 생성하며, 메시지 히스토리 관리도 내장되어 있다.
- 각 ChatBot 인스턴스는 고유한 `thread_id`(UUID)를 가지므로, 서비스 재시작 시 새로운 대화 세션이 생성된다.
- 참고: 이 코드는 섹션 4.3에서 구성한 `builder`(StateGraph 인스턴스)를 사용하여 MemorySaver가 적용된 그래프를 새로 컴파일합니다. 또한 섹션 1.2에서 임포트한 `uuid` 모듈을 `ChatBot` 클래스에서 고유 `thread_id` 생성에 사용합니다.
import gradio as gr # [외부 라이브러리] Gradio - ML 웹 인터페이스 프레임워크
from typing import List, Dict # [Python 표준 라이브러리] 타입 힌트
from langchain_core.messages import HumanMessage, AIMessage # [LangChain 내장] 메시지 클래스
from langgraph.checkpoint.memory import MemorySaver # [LangGraph 내장] 인메모리 체크포인터
# [사용자 정의] 메모리 인스턴스 - Gradio 챗봇 전용
memory = MemorySaver()
# [사용자 정의 - 섹션 4.3 참조] builder는 섹션 4.3에서 구성한 StateGraph 인스턴스
graph_memory = builder.compile(checkpointer=memory) # [LangGraph 내장] compile - MemorySaver 적용
# [사용자 정의] 예시 질문들 - Gradio UI에 클릭 가능한 예시 버튼으로 표시
example_questions = [
"채식주의자를 위한 메뉴를 추천해주세요.",
"오늘의 스페셜 메뉴는 무엇인가요?",
"파스타에 어울리는 음료는 무엇인가요?"
]
# [사용자 정의] 답변 메시지 처리 함수 - graph_memory를 실행하고 최종 AIMessage의 content를 반환
def process_message(message: str, history: List[Dict[str, str]], thread_id: str) -> str:
try:
config = {"configurable": {"thread_id": thread_id}} # config 딕셔너리 구조: {"configurable": {"thread_id": 문자열}}
inputs = {"messages": [HumanMessage(content=message)]}
result = graph_memory.invoke(inputs, config=config) # [LangGraph 내장] invoke - config와 함께 실행
if "messages" in result:
# 메시지 로깅 (선택사항)
print(f"스레드 ID: {thread_id}")
for msg in result["messages"]:
msg.pretty_print() # [LangChain 내장] pretty_print
last_message = result["messages"][-1]
if isinstance(last_message, AIMessage): # [LangChain 내장] AIMessage - AI 응답 메시지 클래스
return last_message.content # [LangChain 내장] content - 메시지의 텍스트 내용
return "응답을 생성하지 못했습니다."
except Exception as e:
print(f"Error occurred: {str(e)}")
return "죄송합니다. 응답을 생성하는 동안 오류가 발생했습니다. 다시 시도해 주세요."
# [사용자 정의] 챗봇 클래스 - 인스턴스별 고유 thread_id로 대화 세션 관리
class ChatBot:
def __init__(self):
self.thread_id = str(uuid.uuid4()) # [Python 표준 라이브러리 - 섹션 1.2 참조] uuid.uuid4()로 고유 식별자 생성
def chat(self, message: str, history: List[Dict[str, str]]) -> str:
print(f"Thread ID: {self.thread_id}")
response = process_message(message, history, self.thread_id) # [사용자 정의] process_message 호출
return response
# [사용자 정의] ChatBot 인스턴스 생성 - 고유 thread_id가 자동 할당됨
chatbot = ChatBot()
# [사용자 정의] Gradio ChatInterface 생성
demo = gr.ChatInterface( # [외부 라이브러리] gr.ChatInterface - 채팅 UI 자동 생성
fn=chatbot.chat, # [사용자 정의] 콜백 함수 - 사용자 입력 시 자동 호출
title="레스토랑 메뉴 AI 어시스턴트",
description="메뉴 정보, 추천, 음식 관련 질문에 답변해 드립니다. 정보의 출처를 함께 제공합니다.",
examples=example_questions, # [사용자 정의] 예시 질문 리스트
)
# [외부 라이브러리] Gradio 앱 실행 - 로컬 웹 서버 시작
demo.launch()
* Running on local URL: http://127.0.0.1:7860
To create a public link, set `share=True` in `launch()`.
- 위 코드의 주요 구성 요소를 분석하면 다음과 같다:
- `process_message` 함수: 사용자 메시지를 받아 MemorySaver가 적용된 그래프를 실행하고, 최종 AIMessage의 content를 반환한다. 에러 처리를 포함하여 안정적인 서비스를 보장한다.
- `ChatBot` 클래스: 인스턴스 생성 시 UUID를 사용하여 고유한 `thread_id`를 할당한다. 동일한 ChatBot 인스턴스에서 발생하는 모든 대화는 같은 `thread_id`를 공유하므로, 대화의 연속성이 보장된다.
- `gr.ChatInterface`: Gradio의 채팅 인터페이스로, `fn` 인자에 콜백 함수를 전달하면 사용자 입력 시 자동으로 호출된다. `examples`에 예시 질문을 지정하면 UI에 클릭 가능한 예시 버튼이 표시된다.
# [외부 라이브러리] 데모 종료
demo.close()
Closing server running on port: 7860
- Gradio 챗봇 UI 처리 흐름

- 본 문서에서는 LangGraph의 ReAct 에이전트 패턴과 MemorySaver를 활용하여 상태 유지형 챗봇을 구축하는 전체 과정을 학습하였다.
- 핵심 개념을 요약하면 다음과 같다:
- ToolNode: AIMessage에서 tool_calls를 추출하여 등록된 도구를 병렬 실행하고, 결과를 ToolMessage로 반환하는 LangGraph의 사전 구축 노드이다.
- ReAct 패턴: Act(도구 호출) -> Observe(결과 관찰) -> Reason(추론 및 판단)의 순환 구조로, tool_calls가 없을 때까지 반복된다.
- create_react_agent: ReAct 패턴을 한 줄로 구현하는 편의 함수로, 내부적으로 agent 노드 + tools 노드 + 조건부 엣지를 자동 구성한다.
- should_continue / tools_condition: 그래프 종료 여부를 결정하는 조건부 엣지 함수로, AIMessage의 tool_calls 유무를 기준으로 분기한다.
- MemorySaver: 인메모리 체크포인터로 각 단계의 상태를 thread_id별로 저장하여, 다중 턴 대화와 맥락 참조를 가능하게 한다.
'Study > LangChain' 카테고리의 다른 글
| 6. LangGraph - LangGraph 법률 PDF QA 종합 프로젝트 (0) | 2026.05.10 |
|---|---|
| 5. LangGraph - LangGraph Agentic RAG 학습 매뉴얼 (0) | 2026.05.06 |
| 3. LangGraph - LangGraph State Reducer & 메시지 상태 관리 학습 매뉴얼 (0) | 2026.05.04 |
| 2. LangGraph - StateGraph 상태 기반 그래프 (0) | 2026.05.03 |
| 1. LangGraph - LangChain Tool Calling 학습 매뉴얼 (0) | 2026.03.24 |
댓글