From 5e2079f3f436a399c34e8e2f7666e5276a67755b Mon Sep 17 00:00:00 2001 From: seongyeon Date: Thu, 5 Feb 2026 00:46:51 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20LangGraph=20v1=20=EB=A9=94=EB=AA=A8?= =?UTF-8?q?=EB=A6=AC=20=ED=8A=9C=ED=86=A0=EB=A6=AC=EC=96=BC=20=EC=9E=AC?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 01-LangGraph-Add-Memory.ipynb: LangGraph v1 API에 맞게 전면 재작성 - MemorySaver 체크포인터 사용법 - thread_id를 통한 대화 세션 관리 - get_state(), get_state_history() 메서드 활용 - 도구와 메모리 결합 예제 추가 - 프로덕션 환경 체크포인터 비교 - 03-LangGraph-Short-Term-Memory.ipynb: LangGraph v1 API에 맞게 전면 재작성 - trim_messages 함수의 token_counter 올바른 사용법 (len → model) - RemoveMessage를 사용한 메시지 삭제 - 그래프 내 동적 메시지 관리 패턴 - 대화 요약을 통한 컨텍스트 압축 - langchain_teddynote 패키지 활용 (visualize_graph, stream_graph, logging) - 한국어 마크다운 및 docstring 작성 --- 05-Memory/01-LangGraph-Add-Memory.ipynb | 1423 ++++++------- .../03-LangGraph-Short-Term-Memory.ipynb | 1767 +++++++++++------ 2 files changed, 1697 insertions(+), 1493 deletions(-) diff --git a/05-Memory/01-LangGraph-Add-Memory.ipynb b/05-Memory/01-LangGraph-Add-Memory.ipynb index 8798689..3fc3267 100644 --- a/05-Memory/01-LangGraph-Add-Memory.ipynb +++ b/05-Memory/01-LangGraph-Add-Memory.ipynb @@ -2,1115 +2,812 @@ "cells": [ { "cell_type": "markdown", + "id": "cell-0", "metadata": {}, "source": [ - "# 🧠 LangGraph 메모리 시스템 완벽 가이드\n", + "# LangGraph 메모리 추가\n", "\n", - "## 📚 개요\n", + "LangGraph에서 `메모리(Memory)`는 에이전트가 이전 대화 내용을 기억하고 맥락에 맞는 응답을 생성할 수 있게 해주는 핵심 기능입니다. 메모리가 없으면 에이전트는 매번 새로운 대화를 시작하는 것처럼 동작하여 일관된 다중 턴(multi-turn) 대화가 불가능합니다.\n", "\n", - "AI 애플리케이션이 진정한 가치를 제공하려면 **메모리(Memory)**가 필수적입니다. LangGraph는 두 가지 강력한 메모리 시스템을 제공합니다:\n", + "LangGraph는 **Checkpointer**를 통해 이 문제를 해결합니다. 그래프를 컴파일할 때 checkpointer를 제공하고, 그래프를 호출할 때 `thread_id`를 전달하면 각 실행 단계 후 상태가 자동으로 저장됩니다. 동일한 `thread_id`로 다시 호출하면 저장된 상태를 불러와 이전 대화를 이어서 진행할 수 있습니다.\n", "\n", - "1. **단기 메모리(Short-term Memory)**: 대화 세션 내에서 컨텍스트 유지\n", - "2. **장기 메모리(Long-term Memory)**: 세션을 넘어 사용자별 정보 저장\n", - "\n", - "## 🎯 학습 목표\n", - "\n", - "이 튜토리얼을 완료하면 다음을 마스터하게 됩니다:\n", - "\n", - "1. **단기 메모리** 구현 - Checkpointer를 활용한 대화 지속성\n", - "2. **장기 메모리** 구축 - Store를 활용한 영구 데이터 저장\n", - "3. **메모리 관리** 전략 - 메시지 트리밍, 요약, 삭제\n", - "4. **시맨틱 검색** - 임베딩 기반 메모리 검색\n", - "5. **프로덕션 배포** - PostgreSQL, Redis 등 실제 환경 적용\n", - "\n", - "## 🔑 핵심 개념 미리보기\n", - "\n", - "```\n", - "┌─────────────────────────────────────────────────────────┐\n", - "│ LangGraph Memory System │\n", - "├──────────────────────────┬──────────────────────────────┤\n", - "│ Short-term Memory │ Long-term Memory │\n", - "├──────────────────────────┼──────────────────────────────┤\n", - "│ • Thread-level │ • User-level │\n", - "│ • Checkpointer │ • Store │\n", - "│ • Multi-turn chats │ • Persistent data │\n", - "│ • Session context │ • Cross-session │\n", - "└──────────────────────────┴──────────────────────────────┘\n", - "```\n", - "\n", - "## 💡 중요 원칙\n", - "\n", - "> **\"메모리는 AI 에이전트를 단순한 도구에서 지능적인 파트너로 변화시킵니다\"**\n", - "> \n", - "> _Memory transforms AI agents from simple tools to intelligent partners_" + "> 참고 문서: [LangGraph Persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/)" ] }, { "cell_type": "markdown", + "id": "cell-1", "metadata": {}, "source": [ - "## 환경 설정" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# 필요한 패키지 임포트\n", - "import os\n", - "from dotenv import load_dotenv\n", + "## 학습 목표\n", "\n", - "# 환경 변수 로드\n", - "load_dotenv()\n", + "이 튜토리얼에서는 다음 내용을 학습합니다:\n", "\n", - "# API 키 설정\n", - "os.environ[\"OPENAI_API_KEY\"] = os.getenv(\"OPENAI_API_KEY\", \"your-api-key\")\n", - "\n", - "print(\"✅ 환경 설정 완료!\")" + "- MemorySaver 체크포인터를 사용한 상태 저장\n", + "- thread_id를 통한 대화 세션 관리\n", + "- 저장된 상태(스냅샷) 조회\n", + "- 메시지 트리밍을 통한 컨텍스트 관리\n", + "- 프로덕션 환경에서의 체크포인터 선택" ] }, { "cell_type": "markdown", + "id": "cell-2", "metadata": {}, "source": [ - "---\n", + "## 환경 설정\n", "\n", - "# Part 1: 단기 메모리 (Short-term Memory) 🎯\n", + "LangGraph 튜토리얼을 시작하기 전에 필요한 환경을 설정합니다. `dotenv`를 사용하여 API 키를 로드하고, `langchain_teddynote`의 로깅 기능을 활성화하여 LangSmith에서 실행 추적을 확인할 수 있도록 합니다.\n", "\n", - "## 1.1 단기 메모리란?\n", - "\n", - "단기 메모리는 **대화 세션 내에서** 컨텍스트를 유지하는 메커니즘입니다. 이는 다음과 같은 특징을 가집니다:\n", - "\n", - "- **Thread 기반**: 각 대화는 고유한 thread_id로 식별\n", - "- **Checkpointer 사용**: 상태를 저장하고 복원\n", - "- **Multi-turn 대화**: 이전 대화 내용을 기억\n", - "\n", - "### 핵심 컴포넌트: Checkpointer\n", + "아래 코드는 환경 변수를 로드하고 LangSmith 프로젝트를 설정합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# API 키를 환경변수로 관리하기 위한 설정 파일\n", + "from dotenv import load_dotenv\n", "\n", - "Checkpointer는 그래프의 상태를 저장하고 복원하는 역할을 합니다. 개발 환경에서는 `InMemorySaver`를, 프로덕션에서는 데이터베이스 기반 Checkpointer를 사용합니다." + "# API 키 정보 로드\n", + "load_dotenv(override=True)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, + "id": "cell-4", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LangSmith 추적을 시작합니다.\n", + "[프로젝트명]\n", + "LangGraph-V1-Tutorial\n" + ] + } + ], "source": [ - "from langgraph.checkpoint.memory import InMemorySaver\n", - "from langgraph.graph import StateGraph, MessagesState, START, END\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "# LLM 초기화\n", - "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n", - "\n", - "# Checkpointer 생성 - 메모리에 상태를 저장\n", - "checkpointer = InMemorySaver()\n", - "\n", - "\n", - "# 간단한 챗봇 그래프 생성\n", - "def call_model(state: MessagesState):\n", - " \"\"\"Call the LLM with the current messages\"\"\"\n", - " # 현재 메시지로 LLM 호출\n", - " response = llm.invoke(state[\"messages\"])\n", - " # 응답을 메시지 리스트에 추가\n", - " return {\"messages\": response}\n", + "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n", + "from langchain_teddynote import logging\n", "\n", - "\n", - "# 그래프 구성\n", - "builder = StateGraph(MessagesState)\n", - "builder.add_node(\"call_model\", call_model)\n", - "builder.add_edge(START, \"call_model\")\n", - "builder.add_edge(\"call_model\", END)\n", - "\n", - "# Checkpointer와 함께 컴파일 - 핵심!\n", - "graph = builder.compile(checkpointer=checkpointer)\n", - "\n", - "print(\"✅ 단기 메모리가 활성화된 그래프 생성 완료!\")" + "# 프로젝트 이름을 입력합니다.\n", + "logging.langsmith(\"LangGraph-V1-Tutorial\")" ] }, { "cell_type": "markdown", + "id": "cell-5", "metadata": {}, "source": [ - "## 1.2 단기 메모리 실습: Multi-turn 대화\n", + "---\n", "\n", - "이제 같은 thread에서 여러 번 대화를 나누며 봇이 이전 대화를 기억하는지 확인해봅시다." + "## MemorySaver 체크포인터\n", + "\n", + "체크포인터(Checkpointer)는 그래프의 각 단계에서 상태를 저장하여, 이후 동일한 대화를 이어서 진행할 수 있게 하는 컴포넌트입니다. `MemorySaver`는 인메모리 체크포인터로, 메모리에 상태를 저장하므로 개발 및 테스트 환경에서 사용하기 적합합니다.\n", + "\n", + "프로덕션 환경에서는 서버 재시작 시에도 상태가 유지되어야 하므로 `PostgresSaver`나 `SqliteSaver` 같은 영구 저장소 기반 체크포인터를 사용하는 것이 좋습니다.\n", + "\n", + "아래 코드에서는 `MemorySaver` 체크포인터를 생성합니다." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, + "id": "cell-6", "metadata": {}, "outputs": [], "source": [ - "# Thread ID 설정 - 대화 세션 식별자\n", - "config = {\"configurable\": {\"thread_id\": \"conversation_1\"}} # 고유한 대화 식별자\n", + "from langgraph.checkpoint.memory import MemorySaver\n", "\n", - "# 첫 번째 메시지 - 자기소개\n", - "print(\"👤 User: 안녕! 나는 철수야\")\n", - "result = graph.invoke(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"안녕! 나는 철수야\"}]},\n", - " config, # thread_id 전달\n", - ")\n", - "print(f\"🤖 Bot: {result['messages'][-1].content}\\n\")\n", - "\n", - "# 두 번째 메시지 - 이름 확인\n", - "print(\"👤 User: 내 이름이 뭐라고 했지?\")\n", - "result = graph.invoke(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"내 이름이 뭐라고 했지?\"}]},\n", - " config, # 같은 thread_id 사용\n", - ")\n", - "print(f\"🤖 Bot: {result['messages'][-1].content}\\n\")\n", - "\n", - "# 다른 thread로 테스트 - 메모리 분리 확인\n", - "config_2 = {\"configurable\": {\"thread_id\": \"conversation_2\"}}\n", - "\n", - "print(\"--- 새로운 대화 세션 ---\")\n", - "print(\"👤 User: 내 이름이 뭐야?\")\n", - "result = graph.invoke(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"내 이름이 뭐야?\"}]},\n", - " config_2, # 다른 thread_id\n", - ")\n", - "print(f\"🤖 Bot: {result['messages'][-1].content}\")\n", - "print(\"\\n💡 다른 thread에서는 이전 대화를 기억하지 못합니다!\")" + "# 메모리 저장소 생성\n", + "memory = MemorySaver()" ] }, { "cell_type": "markdown", + "id": "cell-7", "metadata": {}, "source": [ - "## 1.3 프로덕션 환경: PostgreSQL Checkpointer\n", + "---\n", + "\n", + "## 메모리가 있는 챗봇 구축\n", + "\n", + "이제 체크포인터를 사용하여 대화 기록을 저장하는 간단한 챗봇을 구축합니다. 챗봇은 사용자의 메시지를 받아 LLM에 전달하고, 응답을 상태에 추가합니다. 체크포인터가 각 단계의 상태를 저장하므로 동일한 `thread_id`로 호출하면 이전 대화 내용이 유지됩니다.\n", + "\n", + "### State 정의\n", "\n", - "실제 서비스에서는 데이터베이스 기반 Checkpointer를 사용해야 합니다. 여기서는 PostgreSQL 예제를 보여드립니다." + "State는 그래프 전체에서 공유되는 데이터 구조를 정의합니다. `messages` 필드에 `add_messages` 리듀서를 적용하면, 새 메시지가 기존 리스트를 덮어쓰지 않고 추가됩니다. 이는 대화 이력을 유지하는 데 필수적입니다.\n", + "\n", + "아래 코드에서는 State를 정의하고, 챗봇 노드 함수와 그래프를 구성합니다." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 4, + "id": "cell-8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "메모리가 활성화된 그래프 생성 완료!\n" + ] + } + ], "source": [ - "# PostgreSQL Checkpointer 예제 (실제 실행 시 DB 연결 필요)\n", - "from typing import Dict, Any\n", + "from typing import Annotated\n", + "from typing_extensions import TypedDict\n", + "from langchain_openai import ChatOpenAI\n", + "from langgraph.graph import StateGraph, START, END\n", + "from langgraph.graph.message import add_messages\n", "\n", "\n", - "def create_production_graph():\n", - " \"\"\"Create a production-ready graph with PostgreSQL checkpointer\"\"\"\n", + "# State 정의: 메시지 리스트를 관리\n", + "class State(TypedDict):\n", + " \"\"\"챗봇의 상태를 정의하는 타입\n", "\n", - " # 실제 환경에서는 아래 주석을 해제하고 사용\n", - " from langgraph.checkpoint.postgres import PostgresSaver\n", + " messages: 대화 메시지 리스트\n", + " - add_messages 리듀서를 통해 새 메시지가 추가됩니다\n", + " \"\"\"\n", "\n", - " DB_URI = \"postgresql://postgres:postgres@localhost:5432/mydb\"\n", + " messages: Annotated[list, add_messages]\n", "\n", - " with PostgresSaver.from_conn_string(DB_URI) as checkpointer:\n", - " # 첫 실행 시 테이블 생성\n", - " # checkpointer.setup()\n", "\n", - " builder = StateGraph(MessagesState)\n", - " builder.add_node(\"call_model\", call_model)\n", - " builder.add_edge(START, \"call_model\")\n", - " builder.add_edge(\"call_model\", END)\n", + "# LLM 초기화\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n", "\n", - " graph = builder.compile(checkpointer=checkpointer)\n", - " return graph\n", "\n", - " print(\"📝 프로덕션 환경 코드 예제:\")\n", - " print(\n", - " \"\"\"\n", - " DB_URI = \"postgresql://user:pass@host:port/db\"\n", - " \n", - " with PostgresSaver.from_conn_string(DB_URI) as checkpointer:\n", - " graph = builder.compile(checkpointer=checkpointer)\n", + "# 챗봇 노드 함수 정의\n", + "def chatbot(state: State):\n", + " \"\"\"챗봇 노드 함수\n", + "\n", + " 현재 상태의 메시지를 받아 LLM에 전달하고,\n", + " 응답을 새 메시지로 추가하여 반환합니다.\n", " \"\"\"\n", - " )\n", - " return None\n", + " # LLM 호출 및 응답 반환\n", + " response = llm.invoke(state[\"messages\"])\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# StateGraph 생성\n", + "graph_builder = StateGraph(State)\n", + "\n", + "# 노드 추가\n", + "graph_builder.add_node(\"chatbot\", chatbot)\n", + "\n", + "# 엣지 추가\n", + "graph_builder.add_edge(START, \"chatbot\")\n", + "graph_builder.add_edge(\"chatbot\", END)\n", "\n", + "# 체크포인터와 함께 컴파일\n", + "graph = graph_builder.compile(checkpointer=memory)\n", "\n", - "create_production_graph()" + "print(\"메모리가 활성화된 그래프 생성 완료!\")" ] }, { "cell_type": "markdown", + "id": "cell-9", "metadata": {}, "source": [ - "## 1.4 Subgraph에서의 메모리\n", + "### 그래프 시각화\n", "\n", - "서브그래프를 사용할 때는 부모 그래프에만 Checkpointer를 설정하면 자동으로 전파됩니다." + "`langchain_teddynote.graphs` 모듈의 `visualize_graph()` 함수를 사용하여 그래프 구조를 확인합니다. 체크포인터가 추가되어도 그래프의 구조 자체는 동일하며, 차이점은 각 노드 실행 시 상태가 자동으로 저장된다는 것입니다." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, + "id": "cell-10", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "from typing_extensions import TypedDict\n", - "\n", - "\n", - "class SubgraphState(TypedDict):\n", - " \"\"\"State for subgraph example\"\"\"\n", - "\n", - " message: str\n", - " counter: int\n", - "\n", - "\n", - "# 서브그래프 생성\n", - "def create_subgraph():\n", - " \"\"\"Create a subgraph\"\"\"\n", - "\n", - " def subgraph_node(state: SubgraphState):\n", - " # 카운터 증가\n", - " return {\n", - " \"message\": state[\"message\"] + \" (서브그래프 처리)\",\n", - " \"counter\": state[\"counter\"] + 1,\n", - " }\n", - "\n", - " subgraph_builder = StateGraph(SubgraphState)\n", - " subgraph_builder.add_node(\"process\", subgraph_node)\n", - " subgraph_builder.add_edge(START, \"process\")\n", - " subgraph_builder.add_edge(\"process\", END)\n", - "\n", - " # 서브그래프는 checkpointer 없이 컴파일\n", - " return subgraph_builder.compile()\n", - "\n", - "\n", - "# 부모 그래프 생성\n", - "def main_node(state: SubgraphState):\n", - " \"\"\"Main graph node\"\"\"\n", - " return {\n", - " \"message\": state[\"message\"] + \" (메인 처리)\",\n", - " \"counter\": state[\"counter\"] + 10,\n", - " }\n", + "from langchain_teddynote.graphs import visualize_graph\n", "\n", - "\n", - "# 서브그래프 생성\n", - "subgraph = create_subgraph()\n", - "\n", - "# 메인 그래프 구성\n", - "main_builder = StateGraph(SubgraphState)\n", - "main_builder.add_node(\"main\", main_node)\n", - "main_builder.add_node(\"subgraph\", subgraph) # 서브그래프를 노드로 추가\n", - "\n", - "main_builder.add_edge(START, \"main\")\n", - "main_builder.add_edge(\"main\", \"subgraph\")\n", - "main_builder.add_edge(\"subgraph\", END)\n", - "\n", - "# 부모 그래프만 checkpointer와 함께 컴파일\n", - "parent_checkpointer = InMemorySaver()\n", - "parent_graph = main_builder.compile(checkpointer=parent_checkpointer)\n", - "\n", - "# 실행\n", - "result = parent_graph.invoke(\n", - " {\"message\": \"시작\", \"counter\": 0}, {\"configurable\": {\"thread_id\": \"sub_test\"}}\n", - ")\n", - "\n", - "print(f\"✅ 서브그래프 실행 결과:\")\n", - "print(f\" 메시지: {result['message']}\")\n", - "print(f\" 카운터: {result['counter']}\")\n", - "print(\"\\n💡 부모 그래프의 checkpointer가 서브그래프에도 자동 적용됩니다!\")" + "# 그래프 시각화\n", + "visualize_graph(graph)" ] }, { "cell_type": "markdown", + "id": "cell-11", "metadata": {}, "source": [ "---\n", "\n", - "# Part 2: 장기 메모리 (Long-term Memory) 💾\n", - "\n", - "## 2.1 장기 메모리란?\n", - "\n", - "장기 메모리는 **세션을 넘어서** 사용자별 또는 애플리케이션 레벨의 데이터를 저장합니다:\n", + "## 멀티턴 대화 테스트\n", "\n", - "- **사용자 프로필**: 선호도, 설정, 개인 정보\n", - "- **대화 히스토리**: 과거 상호작용 기록\n", - "- **학습된 정보**: 시스템이 시간이 지남에 따라 학습한 내용\n", + "이제 같은 `thread_id`로 여러 번 대화를 나누며 챗봇이 이전 대화를 기억하는지 확인해봅시다. `RunnableConfig`를 사용하여 `thread_id`를 설정하면, 해당 스레드의 대화 기록이 체크포인터에 저장되고 불러와집니다.\n", "\n", - "### 핵심 컴포넌트: Store\n", - "\n", - "Store는 key-value 형식으로 데이터를 영구 저장하는 시스템입니다." + "아래 코드에서는 첫 번째 대화에서 이름을 알려주고, 두 번째 대화에서 이름을 물어봅니다." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, + "id": "cell-12", "metadata": {}, "outputs": [], "source": [ - "from langgraph.store.memory import InMemoryStore\n", "from langchain_core.runnables import RunnableConfig\n", - "from langgraph.store.base import BaseStore\n", - "import uuid\n", - "\n", - "# 메모리 스토어 생성\n", - "store = InMemoryStore()\n", - "\n", - "\n", - "# 사용자 정보를 저장하는 그래프\n", - "class UserState(MessagesState):\n", - " \"\"\"State with user context\"\"\"\n", - "\n", - " user_id: str\n", - "\n", - "\n", - "def chat_with_memory(\n", - " state: UserState, config: RunnableConfig, *, store: BaseStore # store 주입\n", - "):\n", - " \"\"\"Chat function with long-term memory\"\"\"\n", - "\n", - " # 사용자 ID 가져오기\n", - " user_id = config[\"configurable\"].get(\"user_id\", \"default_user\")\n", "\n", - " # 네임스페이스 정의 - 사용자별 메모리 분리\n", - " namespace = (\"users\", user_id)\n", - "\n", - " # 마지막 메시지 확인\n", - " last_message = state[\"messages\"][-1]\n", - "\n", - " # \"기억해\" 키워드가 있으면 저장\n", - " if \"기억해\" in last_message.content:\n", - " # 메모리에 저장할 내용 추출\n", - " memory_content = last_message.content.replace(\"기억해:\", \"\").strip()\n", - " # Store에 저장\n", - " store.put(namespace, str(uuid.uuid4()), {\"memory\": memory_content})\n", - "\n", - " return {\n", - " \"messages\": [\n", - " {\n", - " \"role\": \"assistant\",\n", - " \"content\": f\"알겠습니다. '{memory_content}'를 기억하겠습니다.\",\n", - " }\n", - " ]\n", - " }\n", - "\n", - " # \"뭐 기억하고 있어?\" 키워드가 있으면 조회\n", - " elif \"뭐 기억하고 있어\" in last_message.content:\n", - " # 저장된 메모리 검색\n", - " memories = store.search(namespace, query=\"*\") # 모든 메모리 조회\n", - "\n", - " if memories:\n", - " memory_list = [item.value[\"memory\"] for item in memories]\n", - " response = \"제가 기억하고 있는 내용:\\n\" + \"\\n\".join(\n", - " f\"• {m}\" for m in memory_list\n", - " )\n", - " else:\n", - " response = \"아직 기억하고 있는 내용이 없습니다.\"\n", - "\n", - " return {\"messages\": [{\"role\": \"assistant\", \"content\": response}]}\n", - "\n", - " # 일반 대화\n", - " else:\n", - " # 기존 메모리를 컨텍스트로 사용\n", - " memories = store.search(namespace, query=\"*\")\n", - " context = \"\"\n", - " if memories:\n", - " context = \"\\n\".join([item.value[\"memory\"] for item in memories])\n", - " system_prompt = f\"당신은 도움이 되는 어시스턴트입니다. 사용자에 대해 알고 있는 정보: {context}\"\n", - " else:\n", - " system_prompt = \"당신은 도움이 되는 어시스턴트입니다.\"\n", - "\n", - " # LLM 호출\n", - " messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n", - " response = llm.invoke(messages)\n", - "\n", - " return {\"messages\": response}\n", - "\n", - "\n", - "# 그래프 구성\n", - "memory_builder = StateGraph(UserState)\n", - "memory_builder.add_node(\"chat\", chat_with_memory)\n", - "memory_builder.add_edge(START, \"chat\")\n", - "memory_builder.add_edge(\"chat\", END)\n", - "\n", - "# Store와 Checkpointer 모두 사용\n", - "memory_graph = memory_builder.compile(\n", - " checkpointer=InMemorySaver(), store=store # 단기 메모리 # 장기 메모리\n", - ")\n", - "\n", - "print(\"✅ 장기 메모리가 활성화된 그래프 생성 완료!\")" + "# Config 설정: thread_id로 대화 세션을 구분\n", + "config = RunnableConfig(\n", + " recursion_limit=10, # 최대 방문 노드 수\n", + " configurable={\"thread_id\": \"conversation_1\"}, # 대화 세션 ID\n", + ")" ] }, { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2.2 장기 메모리 실습: 세션 간 정보 유지" + "cell_type": "code", + "execution_count": 7, + "id": "cell-13", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "User: 안녕하세요! 저는 철수입니다.\n", + "Bot: 안녕하세요, 철수님! 어떻게 도와드릴까요?\n" + ] + } + ], + "source": [ + "# 첫 번째 메시지: 자기소개\n", + "print(\"User: 안녕하세요! 저는 철수입니다.\")\n", + "\n", + "result = graph.invoke(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": \"안녕하세요! 저는 철수입니다.\"}]},\n", + " config,\n", + ")\n", + "\n", + "print(f\"Bot: {result['messages'][-1].content}\")" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# 첫 번째 세션 - 정보 저장\n", - "config_session1 = {\"configurable\": {\"thread_id\": \"session_1\", \"user_id\": \"user_123\"}}\n", - "\n", - "print(\"=== 세션 1: 정보 저장 ===\")\n", - "print(\"\\n👤 User: 기억해: 내 이름은 김철수이고 개발자야\")\n", - "result = memory_graph.invoke(\n", - " {\n", - " \"messages\": [\n", - " {\"role\": \"user\", \"content\": \"기억해: 내 이름은 김철수이고 개발자야\"}\n", - " ]\n", - " },\n", - " config_session1,\n", - ")\n", - "print(f\"🤖 Bot: {result['messages'][-1].content}\")\n", + "execution_count": 8, + "id": "cell-14", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "User: 제 이름이 뭐라고 했죠?\n", + "Bot: 당신의 이름은 철수입니다. 다른 질문이나 도움이 필요하신가요?\n" + ] + } + ], + "source": [ + "# 두 번째 메시지: 이름 확인 (이전 대화 기억 확인)\n", + "print(\"User: 제 이름이 뭐라고 했죠?\")\n", "\n", - "print(\"\\n👤 User: 기억해: 나는 파이썬을 좋아해\")\n", - "result = memory_graph.invoke(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"기억해: 나는 파이썬을 좋아해\"}]},\n", - " config_session1,\n", - ")\n", - "print(f\"🤖 Bot: {result['messages'][-1].content}\")\n", - "\n", - "# 두 번째 세션 - 다른 thread_id지만 같은 user_id\n", - "config_session2 = {\n", - " \"configurable\": {\n", - " \"thread_id\": \"session_2\", # 다른 세션\n", - " \"user_id\": \"user_123\", # 같은 사용자\n", - " }\n", - "}\n", - "\n", - "print(\"\\n=== 세션 2: 새로운 대화 (다른 thread_id) ===\")\n", - "print(\"\\n👤 User: 뭐 기억하고 있어?\")\n", - "result = memory_graph.invoke(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"뭐 기억하고 있어?\"}]}, config_session2\n", + "result = graph.invoke(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": \"제 이름이 뭐라고 했죠?\"}]},\n", + " config, # 같은 thread_id 사용\n", ")\n", - "print(f\"🤖 Bot: {result['messages'][-1].content}\")\n", "\n", - "print(\"\\n💡 다른 세션에서도 사용자 정보를 기억합니다!\")" + "print(f\"Bot: {result['messages'][-1].content}\")" ] }, { "cell_type": "markdown", + "id": "cell-15", "metadata": {}, "source": [ - "## 2.3 Tool에서 메모리 접근\n", + "### 다른 thread_id로 테스트\n", + "\n", + "`thread_id`가 다르면 별도의 대화 세션으로 취급되므로, 이전 대화 내용을 기억하지 못합니다. 이를 통해 여러 사용자 또는 여러 대화 세션을 독립적으로 관리할 수 있습니다.\n", "\n", - "에이전트의 도구(Tool)에서도 메모리에 접근할 수 있습니다." + "아래 코드에서는 다른 `thread_id`로 같은 질문을 해봅니다." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Annotated\n", - "from langgraph.prebuilt import InjectedState\n", - "from langchain_core.tools import tool\n", - "\n", - "\n", - "# Tool에서 State 접근 예제\n", - "class AgentState(MessagesState):\n", - " \"\"\"State for agent with tools\"\"\"\n", - "\n", - " user_preference: str\n", - "\n", - "\n", - "@tool\n", - "def get_user_preference(\n", - " state: Annotated[AgentState, InjectedState], # State 주입\n", - ") -> str:\n", - " \"\"\"Get user preference from state\"\"\"\n", - " preference = state.get(\"user_preference\", \"없음\")\n", - " return f\"사용자 선호도: {preference}\"\n", - "\n", - "\n", - "@tool\n", - "def update_user_preference(\n", - " new_preference: str, state: Annotated[AgentState, InjectedState]\n", - ") -> str:\n", - " \"\"\"Update user preference in state\"\"\"\n", - " # Tool에서 state 업데이트는 Command를 통해 수행\n", - " return f\"선호도를 '{new_preference}'로 업데이트했습니다.\"\n", - "\n", - "\n", - "# 도구 사용 예제\n", - "def agent_with_tools(state: AgentState):\n", - " \"\"\"Agent that can use tools\"\"\"\n", - " # 여기서는 간단한 예제만 보여줌\n", - " last_message = state[\"messages\"][-1].content\n", - "\n", - " if \"선호도\" in last_message:\n", - " # Tool 호출 시뮬레이션\n", - " tool_result = get_user_preference.invoke({\"state\": state})\n", - " return {\"messages\": [{\"role\": \"assistant\", \"content\": tool_result}]}\n", + "execution_count": 9, + "id": "cell-16", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- 새로운 대화 세션 (thread_id: conversation_2) ---\n", + "User: 제 이름이 뭐예요?\n", + "Bot: 죄송하지만, 당신의 이름을 알 수 있는 정보가 없습니다. 당신의 이름을 알려주시면 그에 맞춰 대화할 수 있습니다!\n", + "\n", + "다른 thread에서는 이전 대화를 기억하지 못합니다.\n" + ] + } + ], + "source": [ + "# 다른 thread_id로 Config 설정\n", + "config_2 = RunnableConfig(\n", + " recursion_limit=10,\n", + " configurable={\"thread_id\": \"conversation_2\"}, # 다른 세션\n", + ")\n", "\n", - " return {\"messages\": [{\"role\": \"assistant\", \"content\": \"무엇을 도와드릴까요?\"}]}\n", + "print(\"--- 새로운 대화 세션 (thread_id: conversation_2) ---\")\n", + "print(\"User: 제 이름이 뭐예요?\")\n", "\n", + "result = graph.invoke(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": \"제 이름이 뭐예요?\"}]},\n", + " config_2,\n", + ")\n", "\n", - "print(\"✅ Tool에서 메모리 접근 패턴 정의 완료!\")\n", - "print(\"\\n💡 InjectedState를 사용하여 Tool에서 state에 접근할 수 있습니다.\")" + "print(f\"Bot: {result['messages'][-1].content}\")\n", + "print(\"\\n다른 thread에서는 이전 대화를 기억하지 못합니다.\")" ] }, { "cell_type": "markdown", + "id": "cell-17", "metadata": {}, "source": [ "---\n", "\n", - "# Part 3: 메모리 관리 전략 🔧\n", + "## 저장된 상태(스냅샷) 확인\n", "\n", - "## 3.1 메시지 트리밍 (Trimming)\n", + "체크포인터는 그래프 실행의 각 단계에서 상태를 저장합니다. `get_state()` 메서드를 사용하면 특정 `thread_id`의 현재 상태를 조회할 수 있습니다. 스냅샷에는 현재 상태 값(values), 설정 정보(config), 다음 노드(next) 등이 포함됩니다.\n", "\n", - "긴 대화에서 LLM의 컨텍스트 윈도우 제한을 관리하기 위해 메시지를 트리밍합니다." + "아래 코드에서는 저장된 상태를 조회하고 각 속성을 확인합니다." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, + "id": "cell-18", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "저장된 메시지:\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "안녕하세요! 저는 철수입니다.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "안녕하세요, 철수님! 어떻게 도와드릴까요?\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "제 이름이 뭐라고 했죠?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "당신의 이름은 철수입니다. 다른 질문이나 도움이 필요하신가요?\n" + ] + } + ], + "source": [ + "# 첫 번째 thread의 상태 조회\n", + "snapshot = graph.get_state(config)\n", + "\n", + "# 저장된 메시지 확인\n", + "print(\"저장된 메시지:\")\n", + "for msg in snapshot.values[\"messages\"]:\n", + " msg.pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "cell-19", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Config 정보:\n", + "{'configurable': {'thread_id': 'conversation_1', 'checkpoint_ns': '', 'checkpoint_id': '1f101af6-c18b-66a8-8004-52adef3f3c97'}}\n" + ] + } + ], "source": [ - "from langchain_core.messages import trim_messages, HumanMessage, AIMessage\n", - "\n", - "\n", - "# 메시지 트리밍 예제\n", - "def demonstrate_trimming():\n", - " \"\"\"Demonstrate message trimming strategies\"\"\"\n", - "\n", - " # 긴 대화 히스토리 시뮬레이션\n", - " messages = [\n", - " HumanMessage(content=\"안녕하세요\"),\n", - " AIMessage(content=\"안녕하세요! 무엇을 도와드릴까요?\"),\n", - " HumanMessage(content=\"날씨가 어때요?\"),\n", - " AIMessage(content=\"오늘은 맑은 날씨입니다.\"),\n", - " HumanMessage(content=\"추천 음식이 있나요?\"),\n", - " AIMessage(content=\"파스타를 추천합니다.\"),\n", - " HumanMessage(content=\"레시피를 알려주세요\"),\n", - " AIMessage(content=\"토마토 파스타 레시피입니다...\"),\n", - " HumanMessage(content=\"감사합니다\"),\n", - " AIMessage(content=\"천만에요!\"),\n", - " ]\n", - "\n", - " print(f\"원본 메시지 수: {len(messages)}\\n\")\n", - "\n", - " # 전략 1: 최근 N개 메시지만 유지\n", - " trimmed_last = trim_messages(\n", - " messages,\n", - " strategy=\"last\",\n", - " max_tokens=100, # 대략 100 토큰만 유지\n", - " start_on=\"human\", # 사람 메시지로 시작\n", - " end_on=(\"human\", \"ai\"), # 사람 또는 AI 메시지로 끝\n", - " )\n", - "\n", - " print(\"전략 1 - 최근 메시지 유지:\")\n", - " for msg in trimmed_last:\n", - " role = \"User\" if isinstance(msg, HumanMessage) else \"Bot\"\n", - " print(f\" {role}: {msg.content[:30]}...\")\n", - "\n", - " # 전략 2: 첫 메시지와 최근 메시지 유지\n", - " trimmed_mixed = trim_messages(\n", - " messages,\n", - " strategy=\"first\",\n", - " max_tokens=100,\n", - " include_system=False,\n", - " )\n", - "\n", - " print(\"\\n전략 2 - 첫 메시지 유지:\")\n", - " for msg in trimmed_mixed:\n", - " role = \"User\" if isinstance(msg, HumanMessage) else \"Bot\"\n", - " print(f\" {role}: {msg.content[:30]}...\")\n", - "\n", - " return trimmed_last\n", - "\n", - "\n", - "trimmed = demonstrate_trimming()\n", - "print(f\"\\n✅ 트리밍 후 메시지 수: {len(trimmed)}\")" + "# 설정 정보 확인\n", + "print(\"Config 정보:\")\n", + "print(snapshot.config)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, + "id": "cell-20", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "다음 노드: ()\n" + ] + } + ], "source": [ - "# 그래프에서 트리밍 적용\n", - "class TrimmedState(MessagesState):\n", - " \"\"\"State with message trimming\"\"\"\n", - "\n", - " pass\n", - "\n", - "\n", - "def call_model_with_trimming(state: TrimmedState):\n", - " \"\"\"Call model with automatic message trimming\"\"\"\n", - "\n", - " # 메시지 트리밍 - 최대 500 토큰만 유지\n", - " trimmed_messages = trim_messages(\n", - " state[\"messages\"],\n", - " strategy=\"last\",\n", - " max_tokens=500,\n", - " start_on=\"human\",\n", - " end_on=(\"human\", \"ai\"),\n", - " include_system=True, # 시스템 메시지 포함\n", - " )\n", - "\n", - " # 트리밍된 메시지로 LLM 호출\n", - " response = llm.invoke(trimmed_messages)\n", - "\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# 트리밍이 적용된 그래프 생성\n", - "trimming_builder = StateGraph(TrimmedState)\n", - "trimming_builder.add_node(\"chat\", call_model_with_trimming)\n", - "trimming_builder.add_edge(START, \"chat\")\n", - "trimming_builder.add_edge(\"chat\", END)\n", - "\n", - "trimming_graph = trimming_builder.compile(checkpointer=InMemorySaver())\n", - "\n", - "print(\"✅ 자동 트리밍이 적용된 그래프 생성 완료!\")\n", - "print(\"\\n💡 긴 대화에서도 컨텍스트 윈도우 제한을 자동으로 관리합니다.\")" + "# 다음 노드 확인 (실행 완료 시 빈 값)\n", + "print(\"다음 노드:\", snapshot.next)" ] }, { "cell_type": "markdown", + "id": "cell-21", "metadata": {}, "source": [ - "## 3.2 메시지 삭제 (Deletion)\n", + "### 메타데이터 시각화\n", + "\n", + "스냅샷의 메타데이터는 중첩된 구조로 되어 있어 직접 확인하기 어려울 수 있습니다. `langchain_teddynote.messages` 모듈의 `display_message_tree()` 함수를 사용하면 트리 형태로 보기 쉽게 출력할 수 있습니다.\n", "\n", - "특정 메시지를 영구적으로 삭제하여 메모리를 관리할 수 있습니다." + "아래 코드에서는 스냅샷의 메타데이터를 트리 형태로 시각화합니다." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, + "id": "cell-22", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " \u001b[93msource\u001b[0m: \"loop\"\n", + " \u001b[93mstep\u001b[0m: 4\n", + " \u001b[93mparents\u001b[0m: {}\n" + ] + } + ], "source": [ - "from langchain_core.messages import RemoveMessage\n", + "from langchain_teddynote.messages import display_message_tree\n", "\n", + "# 메타데이터를 트리 형태로 출력\n", + "display_message_tree(snapshot.metadata)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-23", + "metadata": {}, + "source": [ + "---\n", "\n", - "class DeletionState(MessagesState):\n", - " \"\"\"State for message deletion example\"\"\"\n", - "\n", - " pass\n", - "\n", - "\n", - "def delete_old_messages(state: DeletionState):\n", - " \"\"\"Delete messages older than threshold\"\"\"\n", - " messages = state[\"messages\"]\n", - "\n", - " # 5개 이상의 메시지가 있으면 오래된 메시지 삭제\n", - " if len(messages) > 5:\n", - " # 처음 2개 메시지 삭제\n", - " messages_to_delete = [RemoveMessage(id=msg.id) for msg in messages[:2]]\n", - " return {\"messages\": messages_to_delete}\n", - "\n", - " return {}\n", - "\n", - "\n", - "def chat_and_cleanup(state: DeletionState):\n", - " \"\"\"Chat with automatic cleanup\"\"\"\n", - " # 먼저 오래된 메시지 정리\n", - " cleanup_result = delete_old_messages(state)\n", - "\n", - " # LLM 호출\n", - " response = llm.invoke(state[\"messages\"])\n", - "\n", - " # 응답과 정리 결과 병합\n", - " messages_update = [response]\n", - " if \"messages\" in cleanup_result:\n", - " messages_update = cleanup_result[\"messages\"] + messages_update\n", - "\n", - " return {\"messages\": messages_update}\n", - "\n", + "## 상태 이력 조회\n", "\n", - "# 삭제 로직이 포함된 그래프\n", - "deletion_builder = StateGraph(DeletionState)\n", - "deletion_builder.add_node(\"chat\", chat_and_cleanup)\n", - "deletion_builder.add_edge(START, \"chat\")\n", - "deletion_builder.add_edge(\"chat\", END)\n", + "체크포인터는 모든 상태 변경을 기록하므로, `get_state_history()` 메서드를 사용하여 과거 상태들을 시간 역순으로 조회할 수 있습니다. 이를 통해 특정 시점의 상태로 롤백하거나 디버깅에 활용할 수 있습니다.\n", "\n", - "deletion_graph = deletion_builder.compile(checkpointer=InMemorySaver())\n", + "> 참고 문서: [LangGraph Persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/)\n", "\n", - "print(\"✅ 자동 메시지 삭제가 적용된 그래프 생성 완료!\")\n", - "print(\"\\n💡 오래된 메시지를 자동으로 삭제하여 메모리를 효율적으로 관리합니다.\")" + "아래 코드에서는 상태 이력을 조회하여 각 체크포인트를 확인합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "cell-24", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "상태 이력:\n", + " [0] checkpoint_id: 1f101af6-c18b-66a8-8... | 메시지 수: 4\n", + " [1] checkpoint_id: 1f101af6-b968-628c-8... | 메시지 수: 3\n", + " [2] checkpoint_id: 1f101af6-b966-6bf8-8... | 메시지 수: 2\n", + " [3] checkpoint_id: 1f101af6-b958-6490-8... | 메시지 수: 2\n", + " [4] checkpoint_id: 1f101af6-af61-64c8-8... | 메시지 수: 1\n", + " [5] checkpoint_id: 1f101af6-af5f-6038-b... | 메시지 수: 0\n" + ] + } + ], + "source": [ + "# 상태 이력 조회\n", + "print(\"상태 이력:\")\n", + "for i, state in enumerate(graph.get_state_history(config)):\n", + " checkpoint_id = state.config[\"configurable\"].get(\"checkpoint_id\", \"N/A\")\n", + " msg_count = len(state.values.get(\"messages\", []))\n", + " print(f\" [{i}] checkpoint_id: {checkpoint_id[:20]}... | 메시지 수: {msg_count}\")" ] }, { "cell_type": "markdown", + "id": "cell-25", "metadata": {}, "source": [ - "## 3.3 메시지 요약 (Summarization)\n", + "---\n", + "\n", + "## 도구와 메모리 결합\n", "\n", - "오래된 메시지를 요약하여 컨텍스트를 압축할 수 있습니다." + "실제 에이전트는 도구를 사용하여 외부 정보를 검색하거나 작업을 수행합니다. 도구를 사용하는 에이전트에도 체크포인터를 적용하면 도구 호출 결과까지 포함한 전체 대화 기록이 저장됩니다.\n", + "\n", + "아래 코드에서는 검색 도구를 사용하는 에이전트에 메모리를 추가합니다." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, + "id": "cell-26", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "도구와 메모리가 통합된 에이전트 생성 완료!\n" + ] + } + ], "source": [ - "from langchain_core.messages import SystemMessage\n", - "\n", - "\n", - "class SummarizationState(MessagesState):\n", - " \"\"\"State with summarization support\"\"\"\n", + "from langchain_teddynote.tools.tavily import TavilySearch\n", + "from langgraph.prebuilt import ToolNode, tools_condition\n", "\n", - " summary: str = \"\" # 대화 요약 저장\n", + "# 새로운 체크포인터 생성\n", + "agent_memory = MemorySaver()\n", "\n", + "# 검색 도구 설정\n", + "search_tool = TavilySearch(max_results=2)\n", + "tools = [search_tool]\n", "\n", - "def summarize_conversation(messages: list) -> str:\n", - " \"\"\"Summarize a list of messages\"\"\"\n", - " # 요약을 위한 프롬프트\n", - " summary_prompt = \"\"\"\n", - " Please summarize the following conversation in 2-3 sentences,\n", - " focusing on key information and context:\n", - " \n", - " {conversation}\n", - " \"\"\"\n", + "# LLM에 도구 바인딩\n", + "llm_with_tools = ChatOpenAI(model=\"gpt-4o-mini\").bind_tools(tools)\n", "\n", - " # 대화 내용 포맷팅\n", - " conversation = \"\\n\".join([f\"{msg.type}: {msg.content}\" for msg in messages])\n", "\n", - " # LLM으로 요약 생성 (실제로는 별도의 요약 모델 사용 권장)\n", - " summary_response = llm.invoke(\n", - " [\n", - " {\n", - " \"role\": \"system\",\n", - " \"content\": summary_prompt.format(conversation=conversation),\n", - " }\n", - " ]\n", - " )\n", + "# 에이전트 노드 함수\n", + "def agent(state: State):\n", + " \"\"\"에이전트 노드 함수\n", "\n", - " return summary_response.content\n", - "\n", - "\n", - "def chat_with_summarization(state: SummarizationState):\n", - " \"\"\"Chat with automatic summarization\"\"\"\n", - " messages = state[\"messages\"]\n", + " 도구가 바인딩된 LLM을 호출하여 응답을 생성합니다.\n", + " \"\"\"\n", + " response = llm_with_tools.invoke(state[\"messages\"])\n", + " return {\"messages\": [response]}\n", "\n", - " # 10개 이상의 메시지가 있으면 요약\n", - " if len(messages) > 10:\n", - " # 처음 5개 메시지 요약\n", - " messages_to_summarize = messages[:5]\n", - " summary = summarize_conversation(messages_to_summarize)\n", "\n", - " # 요약을 시스템 메시지로 추가하고 오래된 메시지 삭제\n", - " new_messages = [\n", - " SystemMessage(content=f\"Previous conversation summary: {summary}\")\n", - " ] + messages[\n", - " 5:\n", - " ] # 요약된 메시지는 제거\n", + "# 그래프 구성\n", + "agent_builder = StateGraph(State)\n", "\n", - " # 상태 업데이트\n", - " return {\"messages\": new_messages, \"summary\": summary}\n", + "# 노드 추가\n", + "agent_builder.add_node(\"agent\", agent)\n", + "agent_builder.add_node(\"tools\", ToolNode(tools=tools))\n", "\n", - " # 일반 응답\n", - " response = llm.invoke(messages)\n", - " return {\"messages\": [response]}\n", + "# 엣지 추가\n", + "agent_builder.add_edge(START, \"agent\")\n", + "agent_builder.add_conditional_edges(\"agent\", tools_condition)\n", + "agent_builder.add_edge(\"tools\", \"agent\")\n", "\n", + "# 체크포인터와 함께 컴파일\n", + "agent_graph = agent_builder.compile(checkpointer=agent_memory)\n", "\n", - "print(\"✅ 대화 요약 기능 구현 완료!\")\n", - "print(\"\\n💡 긴 대화를 자동으로 요약하여 컨텍스트를 효율적으로 관리합니다.\")" + "print(\"도구와 메모리가 통합된 에이전트 생성 완료!\")" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 16, + "id": "cell-27", "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "---\n", - "\n", - "# Part 4: 시맨틱 검색 (Semantic Search) 🔍\n", - "\n", - "## 4.1 임베딩 기반 메모리 검색\n", - "\n", - "Store에 시맨틱 검색을 활성화하면 의미적으로 유사한 메모리를 찾을 수 있습니다." + "# 그래프 시각화\n", + "visualize_graph(agent_graph)" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "# 시맨틱 검색이 활성화된 Store 생성\n", - "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\n", - "\n", - "semantic_store = InMemoryStore(\n", - " index={\n", - " \"embed\": embeddings, # 임베딩 모델\n", - " \"dims\": 1536, # 임베딩 차원\n", - " }\n", + "execution_count": 17, + "id": "cell-28", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "🔄 Node: \u001b[1;36magent\u001b[0m 🔄\n", + "- - - - - - - - - - - - - - - - - - - - - - - - - \n", + "\n", + "==================================================\n", + "🔄 Node: \u001b[1;36mtools\u001b[0m 🔄\n", + "- - - - - - - - - - - - - - - - - - - - - - - - - \n", + "[{\"url\": \"https://pypi.org/project/langgraph/\", \"title\": \"langgraph - PyPI\", \"content\": \"langgraph 1.0.7. pip install langgraph. Copy PIP instructions. Latest version. Released: Jan 22, 2026. Building stateful, multi-actor applications with LLMs\", \"score\": 0.8170061, \"raw_content\": \"![PyPI](/static/images/logo-small.8998e9d1.svg)\\n\\n# langgraph 1.0.7\\n\\npip install langgraph\\n\\n\\nCopy PIP instructions\\n\\nReleased: \\nJan 22, 2026\\n\\nBuilding stateful, multi-actor applications with LLMs\\n\\n### Navigation\\n\\n### Verified details\\n\\n###### Project links\\n\\n###### GitHub Statistics\\n\\n###### Maintainers\\n\\n![Avatar for hwchase17 from gravatar.com](https://pypi-camo.freetls.fastly.net/1cfaf7a4a11345982a82162569a80132773223b2/68747470733a2f2f7365637572652e67726176617461722e636f6d2f6176617461722f34323334343831366538383438623232383732363861366132613264636134323f73697a653d3530 \\\"Avatar for hwchase17 from gravatar.com\\\")\\n![Avatar for langchain from gravatar.com](https://pypi-camo.freetls.fastly.net/10849e96b3129daeb6e3cea4358f26c29391a326/68747470733a2f2f7365637572652e67726176617461722e636f6d2f6176617461722f35333034373437326436306565616461383938393764356464656464653064393f73697a653d3530 \\\"Avatar for langchain from gravatar.com\\\")\\n![Avatar for nfcampos from gravatar.com](https://pypi-camo.freetls.fastly.net/c923ff105e656a4b1cfdd3485b772eacfa1909c6/68747470733a2f2f7365637572652e67726176617461722e636f6d2f6176617461722f61653636636439386133616636333930636265336464643537343330663531623f73697a653d3530 \\\"Avatar for nfcampos from gravatar.com\\\")\\n\\n### Unverified details\\n\\n###### Project links\\n\\n###### Meta\\n\\n###### Classifiers\\n\\n## Project description\\n\\n![LangGraph Logo](https://pypi-camo.freetls.fastly.net/68186a512b8a67dc7e6998fab8e1ad139f9c9570/68747470733a2f2f6c616e67636861696e2d61692e6769746875622e696f2f6c616e6767726170682f7374617469632f776f72646d61726b5f6461726b2e737667)\\n\\n[![Version](https://pypi-camo.freetls.fastly.net/830fecaa9294a9f8038a3a37718272a26d1f6498/68747470733a2f2f696d672e736869656c64732e696f2f707970692f762f6c616e6767726170682e737667)](https://pypi.org/project/langgraph/)\\n[![Downloads](https://pypi-camo.freetls.fastly.net/6f026d474c4f9a1d14616600638c813075032074/68747470733a2f2f7374617469632e706570792e746563682f62616467652f6c616e6767726170682f6d6f6e7468)](https://pepy.tech/project/langgraph)\\n[![Open Issues](https://pypi-camo.freetls.fastly.net/d88db9815c1e34d9449907c2361113c4c92a23ee/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6973737565732d7261772f6c616e67636861696e2d61692f6c616e676772617068)](https://github.com/langchain-ai/langgraph/issues)\\n[![Docs](https://pypi-camo.freetls.fastly.net/9284977fc757d5cf8fb9af0452b3f6b045990371/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f63732d6c61746573742d626c7565)](https://docs.langchain.com/oss/python/langgraph/overview)\\n\\n![Version](https://pypi-camo.freetls.fastly.net/830fecaa9294a9f8038a3a37718272a26d1f6498/68747470733a2f2f696d672e736869656c64732e696f2f707970692f762f6c616e6767726170682e737667)\\n![Downloads](https://pypi-camo.freetls.fastly.net/6f026d474c4f9a1d14616600638c813075032074/68747470733a2f2f7374617469632e706570792e746563682f62616467652f6c616e6767726170682f6d6f6e7468)\\n![Open Issues](https://pypi-camo.freetls.fastly.net/d88db9815c1e34d9449907c2361113c4c92a23ee/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6973737565732d7261772f6c616e67636861696e2d61692f6c616e676772617068)\\n![Docs](https://pypi-camo.freetls.fastly.net/9284977fc757d5cf8fb9af0452b3f6b045990371/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f63732d6c61746573742d626c7565)\\n\\nTrusted by companies shaping the future of agents – including Klarna, Replit, Elastic, and more – LangGraph is a low-level orchestration framework for building, managing, and deploying long-running, stateful agents.\\n\\n## Get started\\n\\nInstall LangGraph:\\n\\n`pip install -U langgraph`\\n\\nCreate a simple workflow:\\n\\nGet started with the [LangGraph Quickstart](https://docs.langchain.com/oss/python/langgraph/quickstart).\\n\\nTo quickly build agents with LangChain's `create_agent` (built on LangGraph), see the [LangChain Agents documentation](https://docs.langchain.com/oss/python/langchain/agents).\\n\\n`create_agent`\\n\\n## Core benefits\\n\\nLangGraph provides low-level supporting infrastructure for *any* long-running, stateful workflow or agent. LangGraph does not abstract prompts or architecture, and provides the following central benefits:\\n\\n## LangGraph’s ecosystem\\n\\nWhile LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:\\n\\n[!NOTE]\\nLooking for the JS version of LangGraph? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://docs.langchain.com/oss/javascript/langgraph/overview).\\n\\n## Additional resources\\n\\n## Acknowledgements\\n\\nLangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.\\n\\n## Project details\\n\\n### Verified details\\n\\n###### Project links\\n\\n###### GitHub Statistics\\n\\n###### Maintainers\\n\\n![Avatar for hwchase17 from gravatar.com](https://pypi-camo.freetls.fastly.net/1cfaf7a4a11345982a82162569a80132773223b2/68747470733a2f2f7365637572652e67726176617461722e636f6d2f6176617461722f34323334343831366538383438623232383732363861366132613264636134323f73697a653d3530 \\\"Avatar for hwchase17 from gravatar.com\\\")\\n![Avatar for langchain from gravatar.com](https://pypi-camo.freetls.fastly.net/10849e96b3129daeb6e3cea4358f26c29391a326/68747470733a2f2f7365637572652e67726176617461722e636f6d2f6176617461722f35333034373437326436306565616461383938393764356464656464653064393f73697a653d3530 \\\"Avatar for langchain from gravatar.com\\\")\\n![Avatar for nfcampos from gravatar.com](https://pypi-camo.freetls.fastly.net/c923ff105e656a4b1cfdd3485b772eacfa1909c6/68747470733a2f2f7365637572652e67726176617461722e636f6d2f6176617461722f61653636636439386133616636333930636265336464643537343330663531623f73697a653d3530 \\\"Avatar for nfcampos from gravatar.com\\\")\\n\\n### Unverified details\\n\\n###### Project links\\n\\n###### Meta\\n\\n###### Classifiers\\n\\n## Release history [Release notifications](/help/#project-release-notifications) | [RSS feed](/rss/project/langgraph/releases.xml)\\n\\n![](https://pypi.org/static/images/blue-cube.572a5bfb.svg)\\n\\n1.0.7\\n\\nJan 22, 2026\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n1.0.6\\n\\nJan 12, 2026\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n1.0.5\\n\\nDec 12, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n1.0.4\\n\\nNov 25, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n1.0.3\\n\\nNov 10, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n1.0.2\\n\\nOct 29, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n1.0.1\\n\\nOct 20, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n1.0.0\\n\\nOct 17, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n1.0.0rc1\\npre-release\\n\\nOct 17, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n1.0.0a4\\npre-release\\n\\nSep 29, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n1.0.0a3\\npre-release\\n\\nSep 7, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n1.0.0a2\\npre-release\\n\\nSep 2, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n1.0.0a1\\npre-release\\n\\nAug 27, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.11\\n\\nOct 21, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.10\\n\\nOct 9, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.9\\nyanked\\n\\nOct 7, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.8\\n\\nSep 29, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.7\\n\\nSep 7, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.6\\n\\nAug 20, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.5\\n\\nAug 13, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.4\\n\\nAug 7, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.3\\n\\nAug 3, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.2\\n\\nJul 30, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.1\\n\\nJul 29, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.0\\n\\nJul 28, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.0a2\\npre-release\\n\\nJul 25, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.6.0a1\\npre-release\\n\\nJul 22, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.5.4\\n\\nJul 21, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.5.3\\n\\nJul 14, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.5.2\\n\\nJul 9, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.5.1\\n\\nJul 2, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.5.0\\n\\nJun 26, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.5.0rc1\\npre-release\\n\\nJun 17, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.5.0rc0\\npre-release\\n\\nJun 16, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.4.10\\n\\nJun 25, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.4.9\\n\\nJun 25, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.4.8\\n\\nJun 2, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.4.7\\n\\nMay 24, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.4.6\\n\\nMay 23, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.4.5\\n\\nMay 15, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.4.4\\nyanked\\n\\nMay 15, 2025\\n\\nReason this release was yanked:\\n\\nIncorrect dependency range\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.4.3\\n\\nMay 8, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.4.2\\n\\nMay 7, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.4.1\\n\\nApr 30, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.4.0\\n\\nApr 29, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.34\\n\\nApr 24, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.33\\n\\nApr 23, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.32\\n\\nApr 23, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.31\\n\\nApr 17, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.30\\n\\nApr 14, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.29\\n\\nApr 11, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.28\\n\\nApr 11, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.27\\n\\nApr 8, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.26\\n\\nApr 8, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.25\\n\\nApr 3, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.24\\n\\nApr 2, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.23\\n\\nApr 2, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.22\\n\\nApr 1, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.21\\n\\nMar 27, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.20\\n\\nMar 25, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.19\\n\\nMar 24, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.18\\n\\nMar 19, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.17\\n\\nMar 19, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.16\\n\\nMar 19, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.15\\n\\nMar 18, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.14\\n\\nMar 18, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.13\\n\\nMar 18, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.12\\n\\nMar 18, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.11\\n\\nMar 14, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.10\\n\\nMar 14, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.9\\n\\nMar 13, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.8\\n\\nMar 12, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.7\\n\\nMar 12, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.6\\n\\nMar 11, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.5\\n\\nMar 5, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.4\\n\\nMar 4, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.3\\n\\nMar 4, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.2\\n\\nFeb 28, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.1\\n\\nFeb 27, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.3.0\\nyanked\\n\\nFeb 26, 2025\\n\\nReason this release was yanked:\\n\\nMissing dependency on langgraph-prebuilt\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.76\\n\\nFeb 26, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.75\\n\\nFeb 26, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.74\\n\\nFeb 19, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.73\\n\\nFeb 15, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.72\\n\\nFeb 13, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.71\\n\\nFeb 11, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.70\\n\\nFeb 6, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.69\\n\\nJan 31, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.68\\n\\nJan 28, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.67\\n\\nJan 23, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.66\\n\\nJan 21, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.65\\n\\nJan 21, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.64\\n\\nJan 17, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.63\\n\\nJan 16, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.62\\n\\nJan 10, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.61\\n\\nJan 5, 2025\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.60\\n\\nDec 18, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.59\\n\\nDec 11, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.58\\n\\nDec 10, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.57\\n\\nDec 10, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.56\\n\\nDec 5, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.55\\n\\nDec 5, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.54\\n\\nDec 3, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.53\\n\\nNov 21, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.52\\n\\nNov 19, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.51\\n\\nNov 19, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.50\\n\\nNov 15, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.49\\n\\nNov 15, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.48\\n\\nNov 14, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.47\\n\\nNov 13, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.46\\n\\nNov 13, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.45\\n\\nNov 4, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.44\\n\\nNov 2, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.43\\n\\nOct 31, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.42\\n\\nOct 31, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.41\\n\\nOct 31, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.40\\n\\nOct 30, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.39\\n\\nOct 18, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.38\\n\\nOct 15, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.37\\n\\nOct 15, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.36\\n\\nOct 14, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.35\\n\\nOct 9, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.34\\n\\nOct 2, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.33\\n\\nOct 2, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.32\\n\\nOct 1, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.31\\nyanked\\n\\nSep 30, 2024\\n\\nReason this release was yanked:\\n\\nUses internal APIs of upstream package.\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.30\\nyanked\\n\\nSep 30, 2024\\n\\nReason this release was yanked:\\n\\nUses internal APIs of upstream package.\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.29\\nyanked\\n\\nSep 30, 2024\\n\\nReason this release was yanked:\\n\\nUses internal APIs of upstream package.\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.28\\n\\nSep 25, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.27\\n\\nSep 24, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.26\\n\\nSep 24, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.25\\n\\nSep 23, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.24\\n\\nSep 23, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.23\\n\\nSep 20, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.22\\n\\nSep 16, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.21\\n\\nSep 13, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.20\\n\\nSep 13, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.19\\n\\nSep 6, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.18\\n\\nSep 6, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.17\\n\\nSep 5, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.16\\n\\nSep 1, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.15\\n\\nAug 30, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.14\\n\\nAug 24, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.13\\n\\nAug 23, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.12\\n\\nAug 22, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.11\\n\\nAug 22, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.10\\n\\nAug 21, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.9\\n\\nAug 21, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.8\\n\\nAug 21, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.7\\n\\nAug 21, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.7a0\\npre-release\\n\\nAug 21, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.6\\n\\nAug 21, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.5\\n\\nAug 21, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.5a0\\npre-release\\n\\nAug 20, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.4\\n\\nAug 15, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.3\\n\\nAug 8, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.2\\n\\nAug 7, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.1\\n\\nAug 7, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.2.0\\n\\nAug 7, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.19\\n\\nAug 1, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.18\\nyanked\\n\\nJul 31, 2024\\n\\nReason this release was yanked:\\n\\nbroken\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.17\\n\\nJul 31, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.16\\n\\nJul 29, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.15\\n\\nJul 26, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.14\\n\\nJul 24, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.13\\n\\nJul 24, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.12\\n\\nJul 24, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.11\\n\\nJul 23, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.10\\n\\nJul 23, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.9\\n\\nJul 18, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.8\\n\\nJul 12, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.7\\n\\nJul 10, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.6\\n\\nJul 9, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.5\\n\\nJul 1, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.4\\n\\nJun 28, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.3\\n\\nJun 28, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.2\\n\\nJun 26, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.1.1\\n\\nJun 22, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.69\\n\\nJun 14, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.68\\n\\nJun 13, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.67\\n\\nJun 13, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.66\\n\\nJun 10, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.65\\n\\nJun 7, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.64\\n\\nJun 6, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.63\\n\\nJun 5, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.62\\n\\nJun 3, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.61\\n\\nJun 2, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.60\\n\\nMay 31, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.59\\n\\nMay 30, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.58\\n\\nMay 30, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.57\\n\\nMay 30, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.56\\n\\nMay 29, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.55\\n\\nMay 22, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.54\\n\\nMay 21, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.53\\n\\nMay 20, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.52\\n\\nMay 20, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.51\\n\\nMay 20, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.50\\n\\nMay 17, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.49\\n\\nMay 14, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.48\\n\\nMay 8, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.47\\n\\nMay 8, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.46\\n\\nMay 7, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.45\\n\\nMay 7, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.44\\n\\nMay 3, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.43\\n\\nMay 3, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.42\\n\\nMay 3, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.41\\n\\nMay 3, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.40\\n\\nApr 30, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.39\\n\\nApr 25, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.38\\n\\nApr 17, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.37\\n\\nApr 12, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.36\\n\\nApr 11, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.35\\n\\nApr 11, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.34\\n\\nApr 10, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.33\\n\\nApr 10, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.32\\n\\nApr 4, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.31\\n\\nApr 2, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.30\\n\\nMar 22, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.29\\n\\nMar 20, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.28\\n\\nMar 13, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.27\\n\\nMar 13, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.26\\n\\nFeb 22, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.25\\n\\nFeb 22, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.24\\n\\nFeb 8, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.23\\n\\nFeb 4, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.22\\n\\nFeb 4, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.21\\n\\nJan 31, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.20\\n\\nJan 27, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.19\\n\\nJan 23, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.18\\n\\nJan 23, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.17\\n\\nJan 23, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.16\\n\\nJan 21, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.15\\n\\nJan 19, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.14\\n\\nJan 18, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.13\\n\\nJan 17, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.12\\n\\nJan 17, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.11\\n\\nJan 16, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.10\\n\\nJan 9, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.9\\n\\nJan 8, 2024\\n\\n![](https://pypi.org/static/images/white-cube.2351a86c.svg)\\n\\n0.0.8\\nyanked\\n\\nJan 8, 2024\\n\\n## Download files\\n\\nDownload the file for your platform. If you're not sure which to choose, learn more about [installing packages](https://packaging.python.org/tutorials/installing-packages/ \\\"External link\\\").\\n\\n### Source Distribution\\n\\nUploaded \\nJan 22, 2026\\n`Source`\\n\\n`Source`\\n\\n### Built Distribution\\n\\nFilter files by name, interpreter, ABI, and platform.\\n\\nIf you're not sure about the file name format, learn more about [wheel file names](https://packaging.python.org/en/latest/specifications/binary-distribution-format/ \\\"External link\\\").\\n\\nThe dropdown lists show the available interpreters, ABIs, and platforms.\\n\\nEnable javascript to be able to filter the list of wheel files.\\n\\nCopy a direct link to the current filters \\n\\nCopy\\n\\nUploaded \\nJan 22, 2026\\n`Python 3`\\n\\n`Python 3`\\n\\n## File details\\n\\nDetails for the file `langgraph-1.0.7.tar.gz`.\\n\\n`langgraph-1.0.7.tar.gz`\\n\\n### File metadata\\n\\n### File hashes\\n\\nHashes for langgraph-1.0.7.tar.gz\\n\\n| Algorithm | Hash digest | |\\n| --- | --- | --- |\\n| SHA256 | `0cfdfee51e6e8cfe503ecc7367c73933437c505b03fa10a85c710975c8182d9a` | Copy |\\n| MD5 | `a350531ec3eddfd0dc07d068b94feb71` | Copy |\\n| BLAKE2b-256 | `725bf72655717c04e33d3b62f21b166dc063d192b53980e9e3be0e2a117f1c9f` | Copy |\\n\\n`0cfdfee51e6e8cfe503ecc7367c73933437c505b03fa10a85c710975c8182d9a`\\n`a350531ec3eddfd0dc07d068b94feb71`\\n`725bf72655717c04e33d3b62f21b166dc063d192b53980e9e3be0e2a117f1c9f`\\n\\n[See more details on using hashes here.](https://pip.pypa.io/en/stable/topics/secure-installs/#hash-checking-mode \\\"External link\\\")\\n\\n## File details\\n\\nDetails for the file `langgraph-1.0.7-py3-none-any.whl`.\\n\\n`langgraph-1.0.7-py3-none-any.whl`\\n\\n### File metadata\\n\\n### File hashes\\n\\nHashes for langgraph-1.0.7-py3-none-any.whl\\n\\n| Algorithm | Hash digest | |\\n| --- | --- | --- |\\n| SHA256 | `9d68e8f8dd8f3de2fec45f9a06de05766d9b075b78fb03171779893b7a52c4d2` | Copy |\\n| MD5 | `5f2e0a25b6e7788ac150e51f4292de21` | Copy |\\n| BLAKE2b-256 | `7e0efe80144e3e4048e5d19ccdb91ac547c1a7dc3da8dbd1443e210048194c14` | Copy |\\n\\n`9d68e8f8dd8f3de2fec45f9a06de05766d9b075b78fb03171779893b7a52c4d2`\\n`5f2e0a25b6e7788ac150e51f4292de21`\\n`7e0efe80144e3e4048e5d19ccdb91ac547c1a7dc3da8dbd1443e210048194c14`\\n\\n[See more details on using hashes here.](https://pip.pypa.io/en/stable/topics/secure-installs/#hash-checking-mode \\\"External link\\\")\\n\\n![](/static/images/white-cube.2351a86c.svg)\\n\\n## Help\\n\\n## About PyPI\\n\\n## Contributing to PyPI\\n\\n## Using PyPI\\n\\nStatus:[all systems operational](https://status.python.org/ \\\"External link\\\")\\n\\nDeveloped and maintained by the Python community, for the Python community. \\n[Donate today!](https://donate.pypi.org)\\n\\n\\\"PyPI\\\", \\\"Python Package Index\\\", and the blocks logos are registered [trademarks](/trademarks/) of the [Python Software Foundation](https://www.python.org/psf-landing).\\n\\n© 2026 [Python Software Foundation](https://www.python.org/psf-landing/ \\\"External link\\\")\\n \\n[Site map](/sitemap/)\\n\\nSupported by\\n\\n![](https://pypi-camo.freetls.fastly.net/ed7074cadad1a06f56bc520ad9bd3e00d0704c5b/68747470733a2f2f73746f726167652e676f6f676c65617069732e636f6d2f707970692d6173736574732f73706f6e736f726c6f676f732f6177732d77686974652d6c6f676f2d7443615473387a432e706e67)\\n![](https://pypi-camo.freetls.fastly.net/8855f7c063a3bdb5b0ce8d91bfc50cf851cc5c51/68747470733a2f2f73746f726167652e676f6f676c65617069732e636f6d2f707970692d6173736574732f73706f6e736f726c6f676f732f64617461646f672d77686974652d6c6f676f2d6668644c4e666c6f2e706e67)\\n![](https://pypi-camo.freetls.fastly.net/60f709d24f3e4d469f9adc77c65e2f5291a3d165/68747470733a2f2f73746f726167652e676f6f676c65617069732e636f6d2f707970692d6173736574732f73706f6e736f726c6f676f732f6465706f742d77686974652d6c6f676f2d7038506f476831302e706e67)\\n![](https://pypi-camo.freetls.fastly.net/df6fe8829cbff2d7f668d98571df1fd011f36192/68747470733a2f2f73746f726167652e676f6f676c65617069732e636f6d2f707970692d6173736574732f73706f6e736f726c6f676f732f666173746c792d77686974652d6c6f676f2d65684d3077735f6f2e706e67)\\n![](https://pypi-camo.freetls.fastly.net/420cc8cf360bac879e24c923b2f50ba7d1314fb0/68747470733a2f2f73746f726167652e676f6f676c65617069732e636f6d2f707970692d6173736574732f73706f6e736f726c6f676f732f676f6f676c652d77686974652d6c6f676f2d616734424e3774332e706e67)\\n![](https://pypi-camo.freetls.fastly.net/d01053c02f3a626b73ffcb06b96367fdbbf9e230/68747470733a2f2f73746f726167652e676f6f676c65617069732e636f6d2f707970692d6173736574732f73706f6e736f726c6f676f732f70696e67646f6d2d77686974652d6c6f676f2d67355831547546362e706e67)\\n![](https://pypi-camo.freetls.fastly.net/67af7117035e2345bacb5a82e9aa8b5b3e70701d/68747470733a2f2f73746f726167652e676f6f676c65617069732e636f6d2f707970692d6173736574732f73706f6e736f726c6f676f732f73656e7472792d77686974652d6c6f676f2d4a2d6b64742d706e2e706e67)\\n![](https://pypi-camo.freetls.fastly.net/b611884ff90435a0575dbab7d9b0d3e60f136466/68747470733a2f2f73746f726167652e676f6f676c65617069732e636f6d2f707970692d6173736574732f73706f6e736f726c6f676f732f737461747573706167652d77686974652d6c6f676f2d5467476c6a4a2d502e706e67)\"}, {\"url\": \"https://wikidocs.net/261587\", \"title\": \"1-2-1. LangGraph 설치 및 환경 구성 - 위키독스\", \"content\": \"**LangGraph 가이드북 - 에이전트 RAG with 랭그래프**. 랭그래프 LangGraph 기초) 1-1. 랭그래프 LangGraph 소개) 1-1-1. LangGraph 첫걸음) 1-1-2. LangGraph 환경 설정 및 기본 사용법) 1-2-1. LangGraph 설치 및 환경 구성) 1-2-2. 상태 (State)) 1-3-2-1. 상태 관리 기초) 1-3-2-2. 노드 구성 고급 패턴 유형) 1-3-4. 그래프 연결(컴파일) 및 실행) 1-3-5-1. 그래프 구성 요소 연결) 1-3-5-2. 1. **LangGraph 가이드북 - 에이전트 RA…**. LangGraph 환경 설정 및 기…. LangGraph 설치 및 환경 구성. ## LangGraph 설치 및 환경 구성. ### Python 설치. * Python 다운로드 및 설치: python.org. 설치 후 터미널에서 다음 명령어로 Python 버전을 확인합니다:. conda create --name langgraph_env python=3.11. 이 명령어는 'langgraph\\\\_env'라는 이름의 새로운 conda 환경을 생성하며, Python 3.11 버전을 사용합니다. 가상 환경 활성화: - Windows, macOS 및 Linux 모두 같은 명령어를 사용합니다. ### LangGraph 설치. pip install -U langgraph # 최신 버전 pip install langgraph==0.0.16 # 특정 버전 (예: 0.0.16). from langgraph.graph import StateGraph print(\\\"LangGraph successfully installed!\\\"). ### 그래프 시각화를 위한 Graphviz 설치 (선택사항). LangGraph 환경 설정 및 기본 사용법).\", \"score\": 0.7942736, \"raw_content\": \"[**LangGraph 가이드북 - 에이전트 RAG with 랭그래프**](/book/16723) \\n\\n[Part 0. 글쓴이 소개](javascript:page(261572)) [Part 1. 랭그래프 LangGraph 기초](javascript:page(261576)) [1-1. 랭그래프 LangGraph 소개](javascript:page(261577)) [1-1-1. LangGraph 첫걸음](javascript:page(261584)) [1-1-2. LangGraph와 LangChain의 차이점](javascript:page(261585)) [1-1-3. LangGraph를 사용해야 하는 이유](javascript:page(261586)) [1-2. LangGraph 환경 설정 및 기본 사용법](javascript:page(261578)) [1-2-1. LangGraph 설치 및 환경 구성](javascript:page(261587)) [1-2-2. 기본적인 LLM 모델 설정 (GPT)](javascript:page(261588)) [1-2-3. 다양한 LLM 모델 활용 방법 (Anthropic Claude, Google Gemini)](javascript:page(261589)) [1-3. StateGraph 이해하기](javascript:page(261579)) [1-3-1. 기본 구성요소 이해](javascript:page(261590)) [1-3-2. 상태 (State)](javascript:page(261591)) [1-3-2-1. 상태 관리 기초](javascript:page(293297)) [1-3-2-2. 상태 스키마 설계](javascript:page(293353)) [1-3-2-3. 리듀서(Reducer) - 상태 업데이트](javascript:page(293355)) [1-3-2-4. MessagesState - 대화형 앱의 상태 관리](javascript:page(319006)) [1-3-2-5. Private State - 노드 간 비공개 데이터](javascript:page(319067)) [1-3-3. 노드 (Node)](javascript:page(261580)) [1-3-3-1. 노드의 기본 개념](javascript:page(261593)) [1-3-3-2. 노드의 역할과 책임](javascript:page(293519)) [1-3-3-3. 노드 타입과 패턴](javascript:page(293520)) [1-3-3-4. 노드와 구성 (Configuration)](javascript:page(293528)) [1-3-3-5. 노드 구성 고급 패턴 유형](javascript:page(293529)) [1-3-4. 엣지 (Edge)](javascript:page(262302)) [1-3-4-1. 엣지의 개념과 종류](javascript:page(261594)) [1-3-4-2. 엣지의 역할과 기능](javascript:page(293533)) [1-3-4-3. 조건부 엣지 (Conditional Edges)](javascript:page(293535)) [1-3-4-4. Command를 활용한 고급 흐름 제어](javascript:page(293558)) [1-3-4-5. Send API - 동적 병렬 실행](javascript:page(319069)) [1-3-5. 그래프 연결(컴파일) 및 실행](javascript:page(262304)) [1-3-5-1. 그래프 구성 요소 연결](javascript:page(293393)) [1-3-5-2. 그래프 실행 방법 (invoke, stream, async)](javascript:page(293826)) [1-4. 메모리 (Memory)](javascript:page(261582)) [1-4-1. 메모리 기능과 기본 예제](javascript:page(261599)) [1-4-2. 체크포인터 종류별 활용법](javascript:page(261600)) [1-4-2-1. InMemorySaver](javascript:page(321315)) [1-4-2-2. SqliteSaver](javascript:page(321316))\\n\\n1. [**LangGraph 가이드북 - 에이전트 RA…**](/book/16723)\\n2. [Part 1. 랭그래프 LangGraph 기초](/261576)\\n3. [1-2. LangGraph 환경 설정 및 기…](/261578)\\n4. [1-2-1. LangGraph 설치 및 환경…](/261587)\\n\\n1. [위키독스](/)\\n\\n# 1-2-1. LangGraph 설치 및 환경 구성\\n\\n광고가 출력될 위치입니다.\\n\\n광고가 출력될 위치입니다.\\n\\n## LangGraph 설치 및 환경 구성\\n\\nLangGraph를 사용하기 위한 환경을 설정하고 설치하는 과정을 확인해보겠습니다.\\n\\n### Python 설치\\n\\nLangGraph는 Python 기반 라이브러리이므로, 먼저 Python이 설치되어 있어야 합니다. Python 3.8 이상의 버전을 권장합니다.\\n\\n* Python 다운로드 및 설치: [python.org](https://www.python.org/downloads/)\\n\\n설치 후 터미널에서 다음 명령어로 Python 버전을 확인합니다:\\n\\n```\\npython --version \\n```\\n\\n### 가상 환경 생성 (선택사항, 권장)\\n\\n프로젝트별로 독립된 환경을 유지하기 위해 가상 환경을 사용하는 것이 좋습니다.\\n\\n```\\nconda create --name langgraph_env python=3.11 \\n```\\n\\n이 명령어는 'langgraph\\\\_env'라는 이름의 새로운 conda 환경을 생성하며, Python 3.11 버전을 사용합니다. Python 버전은 필요에 따라 조정할 수 있습니다.\\n\\n가상 환경 활성화: - Windows, macOS 및 Linux 모두 같은 명령어를 사용합니다.\\n\\n```\\nconda activate langgraph_env \\n```\\n\\nconda를 사용하면 운영 체제에 관계없이 동일한 명령어로 가상 환경을 활성화할 수 있습니다.\\n\\n가상 환경을 비활성화하려면 다음 명령어를 사용합니다.\\n\\n```\\nconda deactivate \\n```\\n\\n### LangGraph 설치\\n\\npip를 사용하여 LangGraph를 설치합니다.\\n\\n```\\npip install langgraph \\n```\\n\\n최신 버전 또는 특정 버전을 설치하려면 다음과 같이 지정할 수 있습니다.\\n\\n```\\npip install -U langgraph # 최신 버전 pip install langgraph==0.0.16 # 특정 버전 (예: 0.0.16) \\n```\\n\\n### 의존성 패키지 설치\\n\\nLangGraph는 일반적으로 다른 라이브러리들과 함께 사용됩니다. 주요 의존성 패키지들을 설치합니다.\\n\\n```\\npip install langchain langchain-openai \\n```\\n\\n### 설치 확인\\n\\n다음 Python 코드로 LangGraph가 제대로 설치되었는지 확인합니다.\\n\\n```\\nfrom langgraph.graph import StateGraph print(\\\"LangGraph successfully installed!\\\") \\n```\\n\\n### 그래프 시각화를 위한 Graphviz 설치 (선택사항)\\n\\nLangGraph의 그래프 시각화 기능을 사용하려면 Graphviz가 필요합니다.\\n\\n* Windows: [Graphviz 다운로드](https://graphviz.org/download/)\\n* macOS: `brew install graphviz`\\n* Linux: `sudo apt-get install graphviz`\\n\\n---\\n\\n마지막 편집일시 : 2024년 10월 6일 4:04 오후\\n\\n[댓글 0](javascript:show_comments();) [피드백](#myModal \\\"피드백을 남겨주세요\\\")\\n\\n[※ 댓글 작성은 로그인이 필요합니다.](/loginForm) [(또는 피드백을 이용해 주세요.)](#myModal)\\n\\n* **이전글** : [1-2. LangGraph 환경 설정 및 기본 사용법](javascript:page(261578))\\n* **다음글** : [1-2-2. 기본적인 LLM 모델 설정 (GPT)](javascript:page(261588))\\n\\n \\n\\n#### 책갈피\\n\\n### 이 페이지에 대한 피드백을 남겨주세요\\n\\n### 댓글을 신고합니다.\"}]\n", + "==================================================\n", + "🔄 Node: \u001b[1;36magent\u001b[0m 🔄\n", + "- - - - - - - - - - - - - - - - - - - - - - - - - \n", + "LangGraph의 최신 버전은 **1.0.7**, 2026년 1월 22일에 출시되었습니다. LangGraph는 LLM(Long Language Model)을 이용하여 상태를 갖는 멀티 액터 응용 프로그램을 구축하고 관리하는 데 중점을 둔 저수준 오케스트레이션 프레임워크입니다.\n", + "\n", + "### 주요 특징\n", + "- LangGraph는 상태를 유지하는 작업 흐름이나 에이전트를 지원하는 인프라를 제공합니다.\n", + "- LangChain 제품과 통합하여 에이전트 구축을 위한 완벽한 도구 세트를 제공합니다.\n", + "- GitHub 및 공식 문서를 통해 다양한 예제와 리소스가 제공됩니다.\n", + "\n", + "### 설치 방법\n", + "LangGraph를 설치하기 위해 사용하는 명령어는 다음과 같습니다:\n", + "```bash\n", + "pip install -U langgraph\n", + "```\n", + "\n", + "자세한 내용은 [PyPI 페이지](https://pypi.org/project/langgraph/)에서 확인할 수 있습니다." + ] + } + ], + "source": [ + "from langchain_teddynote.messages import stream_graph\n", + "\n", + "# 에이전트 config 설정\n", + "agent_config = RunnableConfig(\n", + " recursion_limit=20,\n", + " configurable={\"thread_id\": \"agent_session_1\"},\n", ")\n", "\n", - "# 메모리 저장\n", - "user_namespace = (\"user_456\", \"memories\")\n", - "\n", - "# 다양한 메모리 저장\n", - "memories_to_store = [\n", - " {\"id\": \"1\", \"text\": \"나는 피자를 좋아해\"},\n", - " {\"id\": \"2\", \"text\": \"내 직업은 데이터 사이언티스트야\"},\n", - " {\"id\": \"3\", \"text\": \"파이썬과 머신러닝을 주로 다뤄\"},\n", - " {\"id\": \"4\", \"text\": \"주말에는 등산을 즐겨해\"},\n", - " {\"id\": \"5\", \"text\": \"커피보다 차를 선호해\"},\n", - "]\n", - "\n", - "for memory in memories_to_store:\n", - " semantic_store.put(user_namespace, memory[\"id\"], {\"text\": memory[\"text\"]})\n", - "\n", - "print(\"✅ 시맨틱 메모리 저장 완료!\\n\")\n", - "\n", - "# 시맨틱 검색 테스트\n", - "queries = [\"음식 취향\", \"프로그래밍\", \"여가 활동\"]\n", - "\n", - "print(\"🔍 시맨틱 검색 결과:\\n\")\n", - "for query in queries:\n", - " # 쿼리와 유사한 메모리 검색\n", - " results = semantic_store.search(\n", - " user_namespace, query=query, limit=2 # 상위 2개 결과\n", - " )\n", - "\n", - " print(f\"Query: '{query}'\")\n", - " for item in results:\n", - " print(f\" → {item.value['text']}\")\n", - " print()" + "# 검색 요청\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"LangGraph의 최신 버전에 대해 알려주세요.\"}]}\n", + "\n", + "# 스트리밍 실행\n", + "stream_graph(agent_graph, inputs=inputs, config=agent_config)" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 18, + "id": "cell-29", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "🔄 Node: \u001b[1;36magent\u001b[0m 🔄\n", + "- - - - - - - - - - - - - - - - - - - - - - - - - \n", + "LangGraph의 최신 버전인 1.0.7은 2026년 1월 22일에 출시되었으며, LLM을 이용한 상태 유지 멀티 액터 응용 프로그램 구축을 위한 저수준 오케스트레이션 프레임워크입니다." + ] + } + ], "source": [ - "## 4.2 시맨틱 메모리를 활용한 대화 시스템" + "# 후속 질문 (이전 검색 결과 기반)\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"방금 알려준 내용을 한 문장으로 요약해주세요.\"}]}\n", + "\n", + "stream_graph(agent_graph, inputs=inputs, config=agent_config)" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", + "id": "cell-30", "metadata": {}, - "outputs": [], "source": [ - "def chat_with_semantic_memory(state: MessagesState, *, store: BaseStore):\n", - " \"\"\"Chat function with semantic memory search\"\"\"\n", - "\n", - " # 사용자의 마지막 메시지\n", - " user_message = state[\"messages\"][-1].content\n", - "\n", - " # 관련 메모리 검색\n", - " namespace = (\"user_456\", \"memories\")\n", - " relevant_memories = store.search(namespace, query=user_message, limit=3)\n", - "\n", - " # 컨텍스트 구성\n", - " context = \"\"\n", - " if relevant_memories:\n", - " context = \"사용자에 대한 관련 정보:\\n\"\n", - " context += \"\\n\".join([f\"• {item.value['text']}\" for item in relevant_memories])\n", - "\n", - " # 시스템 프롬프트 구성\n", - " system_prompt = f\"\"\"\n", - " You are a helpful assistant with access to user's personal information.\n", - " Use the following context to provide personalized responses:\n", - " \n", - " {context}\n", - " \n", - " Respond naturally and incorporate relevant information when appropriate.\n", - " \"\"\"\n", - "\n", - " # LLM 호출\n", - " messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n", - "\n", - " response = llm.invoke(messages)\n", - "\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# 시맨틱 메모리 그래프 생성\n", - "semantic_builder = StateGraph(MessagesState)\n", - "semantic_builder.add_node(\"chat\", chat_with_semantic_memory)\n", - "semantic_builder.add_edge(START, \"chat\")\n", - "semantic_builder.add_edge(\"chat\", END)\n", + "---\n", "\n", - "semantic_graph = semantic_builder.compile(store=semantic_store)\n", + "## 프로덕션 환경: 영구 저장소 사용\n", "\n", - "# 테스트\n", - "print(\"💬 시맨틱 메모리 기반 대화 테스트:\\n\")\n", + "`MemorySaver`는 인메모리 저장소이므로 서버 재시작 시 데이터가 사라집니다. 프로덕션 환경에서는 PostgreSQL, SQLite, Redis 등의 영구 저장소를 사용하는 체크포인터를 선택해야 합니다.\n", "\n", - "test_messages = [\n", - " \"오늘 점심 뭐 먹을까?\",\n", - " \"새로운 프로젝트를 시작하려고 하는데 조언 좀 해줘\",\n", - " \"주말 계획이 있어?\",\n", - "]\n", + "### 지원되는 체크포인터\n", "\n", - "for msg in test_messages:\n", - " print(f\"👤 User: {msg}\")\n", - " result = semantic_graph.invoke({\"messages\": [{\"role\": \"user\", \"content\": msg}]})\n", - " print(f\"🤖 Bot: {result['messages'][-1].content}\\n\")" + "| 체크포인터 | 패키지 | 특징 |\n", + "|-----------|--------|------|\n", + "| PostgresSaver | langgraph-checkpoint-postgres | ACID 준수, 엔터프라이즈 표준 |\n", + "| SqliteSaver | langgraph-checkpoint-sqlite | 가벼운 로컬 저장소 |\n", + "| RedisSaver | langgraph-checkpoint-redis | 초고속 메모리 DB, 캐싱 최적화 |" ] }, { "cell_type": "markdown", + "id": "cell-31", "metadata": {}, "source": [ - "---\n", + "### PostgreSQL 사용 예시\n", "\n", - "# Part 5: 프로덕션 배포 🚀\n", + "PostgreSQL 체크포인터를 사용하려면 먼저 패키지를 설치하고 데이터베이스에 연결해야 합니다. `from_conn_string()` 메서드로 연결 문자열을 전달하고, `setup()` 메서드로 필요한 테이블을 생성합니다.\n", "\n", - "## 5.1 데이터베이스 Checkpointer 비교" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# 프로덕션 Checkpointer 옵션\n", - "production_options = {\n", - " \"PostgreSQL\": {\n", - " \"package\": \"langgraph-checkpoint-postgres\",\n", - " \"class\": \"PostgresSaver\",\n", - " \"connection\": \"postgresql://user:pass@host:port/db\",\n", - " \"features\": [\"ACID 준수\", \"복잡한 쿼리 지원\", \"엔터프라이즈 표준\"],\n", - " \"setup\": \"checkpointer.setup() # 첫 실행 시\",\n", - " },\n", - " \"MongoDB\": {\n", - " \"package\": \"langgraph-checkpoint-mongodb\",\n", - " \"class\": \"MongoDBSaver\",\n", - " \"connection\": \"mongodb://localhost:27017\",\n", - " \"features\": [\"NoSQL 유연성\", \"수평 확장성\", \"JSON 네이티브\"],\n", - " \"setup\": \"클러스터 생성 필요\",\n", - " },\n", - " \"Redis\": {\n", - " \"package\": \"langgraph-checkpoint-redis\",\n", - " \"class\": \"RedisSaver\",\n", - " \"connection\": \"redis://localhost:6379\",\n", - " \"features\": [\"초고속 메모리 DB\", \"캐싱 최적화\", \"Pub/Sub 지원\"],\n", - " \"setup\": \"checkpointer.setup() # 첫 실행 시\",\n", - " },\n", - "}\n", - "\n", - "print(\"📊 프로덕션 Checkpointer 비교:\\n\")\n", - "for db, info in production_options.items():\n", - " print(f\"### {db}\")\n", - " print(f\" 패키지: {info['package']}\")\n", - " print(f\" 클래스: {info['class']}\")\n", - " print(f\" 연결: {info['connection']}\")\n", - " print(f\" 특징: {', '.join(info['features'])}\")\n", - " print(f\" 설정: {info['setup']}\\n\")" + "아래는 PostgreSQL 체크포인터 사용 예시 코드입니다. (실제 실행하려면 PostgreSQL 서버가 필요합니다)" ] }, { "cell_type": "markdown", + "id": "cell-33", "metadata": {}, "source": [ - "## 5.2 프로덕션 Store 구현 예제" + "#### Docker를 사용한 PostgreSQL 설정\n", + "\n", + "개발 환경에서 PostgreSQL을 빠르게 설정하려면 Docker를 사용하는 것이 편리합니다.\n", + "\n", + "```bash\n", + "# PostgreSQL 컨테이너 실행\n", + "docker run --name langgraph_db \\\n", + " -e POSTGRES_PASSWORD=postgres \\\n", + " -e POSTGRES_DB=langgraph_db \\\n", + " -p 5432:5432 \\\n", + " -d postgres:15\n", + "\n", + "# 연결 문자열\n", + "DB_URI = \"postgresql://postgres:postgres@localhost:5432/langgraph_db\"\n", + "```" ] }, { "cell_type": "code", "execution_count": null, + "id": "369e7456", "metadata": {}, "outputs": [], "source": [ - "def create_production_setup():\n", - " \"\"\"Production setup example with both checkpointer and store\"\"\"\n", - "\n", - " print(\"🏭 프로덕션 환경 설정 예제:\\n\")\n", - "\n", - " # PostgreSQL 예제\n", - " postgres_example = \"\"\"\n", "from langgraph.checkpoint.postgres import PostgresSaver\n", - "from langgraph.store.postgres import PostgresStore\n", "\n", - "DB_URI = \"postgresql://user:password@localhost:5432/langgraph_db\"\n", + "DB_URI = \"postgresql://postgres:postgres@localhost:5432/langgraph_db\"\n", "\n", - "# Context manager로 자동 리소스 관리\n", - "with (\n", - " PostgresStore.from_conn_string(DB_URI) as store,\n", - " PostgresSaver.from_conn_string(DB_URI) as checkpointer,\n", - "):\n", + "# Context manager로 연결 관리\n", + "with PostgresSaver.from_conn_string(DB_URI) as checkpointer:\n", " # 첫 실행 시 테이블 생성\n", - " store.setup()\n", " checkpointer.setup()\n", " \n", " # 그래프 컴파일\n", - " graph = builder.compile(\n", - " checkpointer=checkpointer, # 단기 메모리\n", - " store=store # 장기 메모리\n", - " )\n", + " graph = graph_builder.compile(checkpointer=checkpointer)\n", " \n", " # 그래프 실행\n", - " result = graph.invoke(input_data, config)\n", - " \"\"\"\n", - "\n", - " print(\"### PostgreSQL 설정:\")\n", - " print(postgres_example)\n", - "\n", - " # Redis 예제\n", - " redis_example = \"\"\"\n", - "from langgraph.checkpoint.redis import RedisSaver\n", - "from langgraph.store.redis import RedisStore\n", - "\n", - "REDIS_URI = \"redis://localhost:6379\"\n", - "\n", - "# Redis 연결\n", - "with (\n", - " RedisStore.from_conn_string(REDIS_URI) as store,\n", - " RedisSaver.from_conn_string(REDIS_URI) as checkpointer,\n", - "):\n", - " store.setup()\n", - " checkpointer.setup()\n", - " \n", - " graph = builder.compile(\n", - " checkpointer=checkpointer,\n", - " store=store\n", - " )\n", - " \"\"\"\n", - "\n", - " print(\"\\n### Redis 설정:\")\n", - " print(redis_example)\n", - "\n", - " return \"프로덕션 설정 예제 완료\"\n", - "\n", - "\n", - "create_production_setup()" + " result = graph.invoke(inputs, config)" ] }, { "cell_type": "markdown", + "id": "cell-34", "metadata": {}, "source": [ - "## 5.3 베스트 프랙티스" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# 메모리 관리 베스트 프랙티스\n", - "best_practices = {\n", - " \"아키텍처 설계\": [\n", - " \"단기/장기 메모리 명확히 구분\",\n", - " \"적절한 네임스페이스 전략 수립\",\n", - " \"메모리 계층 구조 설계\",\n", - " ],\n", - " \"성능 최적화\": [\n", - " \"메시지 트리밍으로 컨텍스트 관리\",\n", - " \"캐싱 적극 활용\",\n", - " \"인덱싱으로 검색 성능 향상\",\n", - " ],\n", - " \"데이터 관리\": [\"정기적인 메모리 정리\", \"백업 전략 수립\", \"민감 정보 암호화\"],\n", - " \"모니터링\": [\"메모리 사용량 추적\", \"응답 시간 모니터링\", \"에러 로깅 및 알림\"],\n", - " \"확장성\": [\"수평 확장 가능한 아키텍처\", \"로드 밸런싱 고려\", \"샤딩 전략 수립\"],\n", - "}\n", - "\n", - "print(\"📋 메모리 관리 베스트 프랙티스:\\n\")\n", - "for category, practices in best_practices.items():\n", - " print(f\"### {category}\")\n", - " for practice in practices:\n", - " print(f\" ✓ {practice}\")\n", - " print()" + "---\n", + "\n", + "## 정리\n", + "\n", + "이번 튜토리얼에서는 LangGraph에서 메모리를 추가하는 방법을 학습했습니다.\n", + "\n", + "### 핵심 내용\n", + "\n", + "- **체크포인터**: 그래프의 상태를 저장하고 복원하는 컴포넌트\n", + "- **thread_id**: 대화 세션을 구분하는 식별자로, 같은 thread_id에서는 대화가 이어짐\n", + "- **MemorySaver**: 개발/테스트용 인메모리 체크포인터\n", + "- **get_state()**: 현재 상태 스냅샷 조회\n", + "- **get_state_history()**: 상태 변경 이력 조회" ] } ], @@ -1121,18 +818,10 @@ "name": "python3" }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", "version": "3.11.0" } }, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 5 } diff --git a/05-Memory/03-LangGraph-Short-Term-Memory.ipynb b/05-Memory/03-LangGraph-Short-Term-Memory.ipynb index 45dda10..c7730e8 100644 --- a/05-Memory/03-LangGraph-Short-Term-Memory.ipynb +++ b/05-Memory/03-LangGraph-Short-Term-Memory.ipynb @@ -1,628 +1,1143 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# LangChain 단기 메모리\n", - "\n", - "메모리는 이전 상호작용에 대한 정보를 기억하는 시스템입니다. AI 에이전트의 경우 메모리는 이전 상호작용을 기억하고, 피드백으로부터 학습하며, 사용자 선호도에 적응할 수 있게 해주므로 매우 중요합니다.\n", - "\n", - "단기 메모리는 애플리케이션이 단일 스레드 또는 대화 내에서 이전 상호작용을 기억할 수 있게 해줍니다.\n", - "\n", - "**스레드**는 이메일이 단일 대화에서 메시지를 그룹화하는 방식과 유사하게 세션에서 여러 상호작용을 구성합니다.\n", - "\n", - "대화 기록은 가장 일반적인 형태의 단기 메모리입니다. 긴 대화는 오늘날의 LLM에 도전 과제를 제시합니다. 전체 기록이 LLM의 컨텍스트 창에 맞지 않아 컨텍스트 손실이나 오류가 발생할 수 있습니다." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 사전 준비\n", - "\n", - "환경 변수를 설정합니다." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from dotenv import load_dotenv\n", - "\n", - "load_dotenv(override=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 기본 사용법\n", - "\n", - "에이전트에 단기 메모리(스레드 수준 지속성)를 추가하려면 에이전트를 생성할 때 `checkpointer`를 지정해야 합니다.\n", - "\n", - "LangChain의 에이전트는 단기 메모리를 에이전트 상태의 일부로 관리합니다. 그래프의 상태에 저장함으로써 에이전트는 서로 다른 스레드 간의 분리를 유지하면서 특정 대화에 대한 전체 컨텍스트에 액세스할 수 있습니다." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.agents import create_agent\n", - "from langchain_openai import ChatOpenAI\n", - "from langgraph.checkpoint.memory import InMemorySaver\n", - "from langchain.tools import tool\n", - "\n", - "# 간단한 도구 정의\n", - "@tool\n", - "def get_user_info(user_id: str) -> str:\n", - " \"\"\"Get user information.\"\"\"\n", - " return f\"User info for {user_id}\"\n", - "\n", - "# 모델 및 에이전트 생성 (체크포인터 포함)\n", - "model = ChatOpenAI(model=\"gpt-4.1-mini\")\n", - "agent = create_agent(\n", - " model=model,\n", - " tools=[get_user_info],\n", - " checkpointer=InMemorySaver(), # 메모리 저장소\n", - ")\n", - "\n", - "# thread_id를 사용하여 대화 추적\n", - "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "\n", - "# 첫 번째 메시지\n", - "result1 = agent.invoke(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"Hi! My name is Bob.\"}]},\n", - " config\n", - ")\n", - "print(\"Response 1:\", result1[\"messages\"][-1].content)\n", - "\n", - "# 두 번째 메시지 (이전 대화 기억)\n", - "result2 = agent.invoke(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"What's my name?\"}]},\n", - " config\n", - ")\n", - "print(\"Response 2:\", result2[\"messages\"][-1].content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 프로덕션 환경\n", - "\n", - "프로덕션 환경에서는 데이터베이스를 기반으로 하는 체크포인터를 사용합니다." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# PostgreSQL 체크포인터 예제 (설치 필요: pip install langgraph-checkpoint-postgres)\n", - "# from langgraph.checkpoint.postgres import PostgresSaver\n", - "\n", - "# DB_URI = \"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable\"\n", - "# with PostgresSaver.from_conn_string(DB_URI) as checkpointer:\n", - "# checkpointer.setup() # PostgreSQL에 테이블 자동 생성\n", - "# agent = create_agent(\n", - "# model=model,\n", - "# tools=[get_user_info],\n", - "# checkpointer=checkpointer,\n", - "# )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 에이전트 메모리 커스터마이징\n", - "\n", - "기본적으로 에이전트는 `AgentState`를 사용하여 단기 메모리를 관리합니다. 특히 `messages` 키를 통한 대화 기록을 관리합니다.\n", - "\n", - "`AgentState`를 확장하여 추가 필드를 추가할 수 있습니다." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.agents import create_agent, AgentState\n", - "from langgraph.checkpoint.memory import InMemorySaver\n", - "\n", - "# 커스텀 상태 정의\n", - "class CustomAgentState(AgentState):\n", - " user_id: str\n", - " preferences: dict\n", - "\n", - "agent = create_agent(\n", - " model=model,\n", - " tools=[get_user_info],\n", - " state_schema=CustomAgentState, # 커스텀 상태 스키마\n", - " checkpointer=InMemorySaver(),\n", - ")\n", - "\n", - "# 커스텀 상태를 invoke에 전달\n", - "result = agent.invoke(\n", - " {\n", - " \"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}],\n", - " \"user_id\": \"user_123\",\n", - " \"preferences\": {\"theme\": \"dark\"}\n", - " },\n", - " {\"configurable\": {\"thread_id\": \"1\"}}\n", - ")\n", - "\n", - "print(result[\"messages\"][-1].content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 일반적인 패턴\n", - "\n", - "단기 메모리가 활성화된 상태에서 긴 대화는 LLM의 컨텍스트 창을 초과할 수 있습니다. 일반적인 해결책은:\n", - "\n", - "1. **메시지 트리밍** - 처음 또는 마지막 N개의 메시지 제거 (LLM 호출 전)\n", - "2. **메시지 삭제** - LangGraph 상태에서 메시지를 영구적으로 삭제\n", - "3. **메시지 요약** - 기록의 이전 메시지를 요약하고 요약으로 대체\n", - "4. **커스텀 전략** - 메시지 필터링 등의 커스텀 전략" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 메시지 트리밍\n", - "\n", - "대부분의 LLM에는 최대 지원 컨텍스트 창(토큰 단위)이 있습니다. 메시지를 트리밍하는 시기를 결정하는 한 가지 방법은 메시지 기록의 토큰을 세고 한계에 접근할 때마다 트리밍하는 것입니다.\n", - "\n", - "에이전트에서 메시지 기록을 트리밍하려면 `@before_model` 미들웨어 데코레이터를 사용합니다." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.messages import RemoveMessage\n", - "from langgraph.graph.message import REMOVE_ALL_MESSAGES\n", - "from langgraph.checkpoint.memory import InMemorySaver\n", - "from langchain.agents import create_agent, AgentState\n", - "from langchain.agents.middleware import before_model\n", - "from langgraph.runtime import Runtime\n", - "from typing import Any\n", - "\n", - "@before_model\n", - "def trim_messages(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:\n", - " \"\"\"컨텍스트 창에 맞도록 최근 몇 개의 메시지만 유지\"\"\"\n", - " messages = state[\"messages\"]\n", - "\n", - " if len(messages) <= 3:\n", - " return None # 변경 필요 없음\n", - "\n", - " # 첫 번째 메시지와 최근 메시지만 유지\n", - " first_msg = messages[0]\n", - " recent_messages = messages[-3:] if len(messages) % 2 == 0 else messages[-4:]\n", - " new_messages = [first_msg] + recent_messages\n", - "\n", - " return {\n", - " \"messages\": [\n", - " RemoveMessage(id=REMOVE_ALL_MESSAGES),\n", - " *new_messages\n", - " ]\n", - " }\n", - "\n", - "agent = create_agent(\n", - " model=model,\n", - " tools=[],\n", - " middleware=[trim_messages],\n", - " checkpointer=InMemorySaver(),\n", - ")\n", - "\n", - "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "\n", - "agent.invoke({\"messages\": [{\"role\": \"user\", \"content\": \"hi, my name is bob\"}]}, config)\n", - "agent.invoke({\"messages\": [{\"role\": \"user\", \"content\": \"write a short poem about cats\"}]}, config)\n", - "agent.invoke({\"messages\": [{\"role\": \"user\", \"content\": \"now do the same but for dogs\"}]}, config)\n", - "final_response = agent.invoke({\"messages\": [{\"role\": \"user\", \"content\": \"what's my name?\"}]}, config)\n", - "\n", - "print(final_response[\"messages\"][-1].content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 메시지 삭제\n", - "\n", - "그래프 상태에서 메시지를 삭제하여 메시지 기록을 관리할 수 있습니다. 특정 메시지를 제거하거나 전체 메시지 기록을 지우려는 경우에 유용합니다.\n", - "\n", - "그래프 상태에서 메시지를 삭제하려면 `RemoveMessage`를 사용할 수 있습니다." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.messages import RemoveMessage\n", - "from langchain.agents import create_agent, AgentState\n", - "from langchain.agents.middleware import after_model\n", - "from langgraph.checkpoint.memory import InMemorySaver\n", - "from langgraph.runtime import Runtime\n", - "\n", - "@after_model\n", - "def delete_old_messages(state: AgentState, runtime: Runtime) -> dict | None:\n", - " \"\"\"대화를 관리 가능하게 유지하기 위해 오래된 메시지 제거\"\"\"\n", - " messages = state[\"messages\"]\n", - " if len(messages) > 2:\n", - " # 가장 오래된 두 개의 메시지 제거\n", - " return {\"messages\": [RemoveMessage(id=m.id) for m in messages[:2]]}\n", - " return None\n", - "\n", - "agent = create_agent(\n", - " model=model,\n", - " tools=[],\n", - " system_prompt=\"Please be concise and to the point.\",\n", - " middleware=[delete_old_messages],\n", - " checkpointer=InMemorySaver(),\n", - ")\n", - "\n", - "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "\n", - "# 첫 번째 메시지\n", - "result1 = agent.invoke(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"hi! I'm bob\"}]},\n", - " config\n", - ")\n", - "print(\"Messages after first invoke:\", len(result1[\"messages\"]))\n", - "\n", - "# 두 번째 메시지\n", - "result2 = agent.invoke(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"what's my name?\"}]},\n", - " config\n", - ")\n", - "print(\"Messages after second invoke:\", len(result2[\"messages\"]))\n", - "print(\"Last message:\", result2[\"messages\"][-1].content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 메시지 요약\n", - "\n", - "메시지를 트리밍하거나 제거하는 문제는 메시지 큐를 제거하여 정보를 잃을 수 있다는 것입니다. 이 때문에 일부 애플리케이션은 채팅 모델을 사용하여 메시지 기록을 요약하는 더 정교한 접근 방식의 이점을 얻습니다.\n", - "\n", - "에이전트에서 메시지 기록을 요약하려면 내장된 `SummarizationMiddleware`를 사용합니다." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.agents import create_agent\n", - "from langchain.agents.middleware import SummarizationMiddleware\n", - "from langgraph.checkpoint.memory import InMemorySaver\n", - "\n", - "checkpointer = InMemorySaver()\n", - "\n", - "agent = create_agent(\n", - " model=\"openai:gpt-4.1-mini\",\n", - " tools=[],\n", - " middleware=[\n", - " SummarizationMiddleware(\n", - " model=\"openai:gpt-4.1-mini\",\n", - " max_tokens_before_summary=4000, # 4000 토큰에서 요약 트리거\n", - " messages_to_keep=20, # 요약 후 최근 20개 메시지 유지\n", - " )\n", - " ],\n", - " checkpointer=checkpointer,\n", - ")\n", - "\n", - "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "\n", - "agent.invoke({\"messages\": [{\"role\": \"user\", \"content\": \"hi, my name is bob\"}]}, config)\n", - "agent.invoke({\"messages\": [{\"role\": \"user\", \"content\": \"write a short poem about cats\"}]}, config)\n", - "agent.invoke({\"messages\": [{\"role\": \"user\", \"content\": \"now do the same but for dogs\"}]}, config)\n", - "final_response = agent.invoke({\"messages\": [{\"role\": \"user\", \"content\": \"what's my name?\"}]}, config)\n", - "\n", - "print(final_response[\"messages\"][-1].content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 메모리 액세스\n", - "\n", - "여러 가지 방법으로 에이전트의 단기 메모리(상태)에 액세스하고 수정할 수 있습니다." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 도구에서 단기 메모리 읽기\n", - "\n", - "`ToolRuntime` 매개변수를 사용하여 도구에서 단기 메모리(상태)에 액세스할 수 있습니다.\n", - "\n", - "`tool_runtime` 매개변수는 도구 시그니처에서 숨겨져 있지만(모델이 볼 수 없음) 도구는 이를 통해 상태에 액세스할 수 있습니다." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.agents import create_agent, AgentState\n", - "from langchain.tools import tool, ToolRuntime\n", - "\n", - "class CustomState(AgentState):\n", - " user_id: str\n", - "\n", - "@tool\n", - "def get_user_info(\n", - " runtime: ToolRuntime\n", - ") -> str:\n", - " \"\"\"Look up user info.\"\"\"\n", - " user_id = runtime.state[\"user_id\"]\n", - " return \"User is John Smith\" if user_id == \"user_123\" else \"Unknown user\"\n", - "\n", - "agent = create_agent(\n", - " model=model,\n", - " tools=[get_user_info],\n", - " state_schema=CustomState,\n", - ")\n", - "\n", - "result = agent.invoke({\n", - " \"messages\": [{\"role\": \"user\", \"content\": \"look up user information\"}],\n", - " \"user_id\": \"user_123\"\n", - "})\n", - "\n", - "print(result[\"messages\"][-1].content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 도구에서 단기 메모리 쓰기\n", - "\n", - "실행 중에 에이전트의 단기 메모리(상태)를 수정하려면 도구에서 직접 상태 업데이트를 반환할 수 있습니다." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.tools import tool, ToolRuntime\n", - "from langchain.messages import ToolMessage\n", - "from langchain.agents import create_agent, AgentState\n", - "from langgraph.types import Command\n", - "from pydantic import BaseModel\n", - "\n", - "class CustomState(AgentState):\n", - " user_name: str\n", - "\n", - "class CustomContext(BaseModel):\n", - " user_id: str\n", - "\n", - "@tool\n", - "def update_user_info(\n", - " runtime: ToolRuntime[CustomContext, CustomState],\n", - ") -> Command:\n", - " \"\"\"Look up and update user info.\"\"\"\n", - " user_id = runtime.context.user_id\n", - " name = \"John Smith\" if user_id == \"user_123\" else \"Unknown user\"\n", - " return Command(update={\n", - " \"user_name\": name,\n", - " \"messages\": [\n", - " ToolMessage(\n", - " \"Successfully looked up user information\",\n", - " tool_call_id=runtime.tool_call_id\n", - " )\n", - " ]\n", - " })\n", - "\n", - "@tool\n", - "def greet(\n", - " runtime: ToolRuntime[CustomContext, CustomState]\n", - ") -> str:\n", - " \"\"\"Use this to greet the user once you found their info.\"\"\"\n", - " user_name = runtime.state[\"user_name\"]\n", - " return f\"Hello {user_name}!\"\n", - "\n", - "agent = create_agent(\n", - " model=model,\n", - " tools=[update_user_info, greet],\n", - " state_schema=CustomState,\n", - " context_schema=CustomContext,\n", - ")\n", - "\n", - "result = agent.invoke(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"greet the user\"}]},\n", - " context=CustomContext(user_id=\"user_123\"),\n", - ")\n", - "\n", - "print(result[\"messages\"][-1].content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 프롬프트에서 메모리 액세스\n", - "\n", - "대화 기록이나 커스텀 상태 필드를 기반으로 동적 프롬프트를 생성하기 위해 미들웨어에서 단기 메모리(상태)에 액세스할 수 있습니다." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.agents import create_agent\n", - "from typing import TypedDict\n", - "from langchain.agents.middleware import dynamic_prompt, ModelRequest\n", - "from langchain.tools import tool\n", - "\n", - "class CustomContext(TypedDict):\n", - " user_name: str\n", - "\n", - "@tool\n", - "def get_weather(city: str) -> str:\n", - " \"\"\"Get the weather in a city.\"\"\"\n", - " return f\"The weather in {city} is always sunny!\"\n", - "\n", - "@dynamic_prompt\n", - "def dynamic_system_prompt(request: ModelRequest) -> str:\n", - " user_name = request.runtime.context[\"user_name\"]\n", - " system_prompt = f\"You are a helpful assistant. Address the user as {user_name}.\"\n", - " return system_prompt\n", - "\n", - "agent = create_agent(\n", - " model=model,\n", - " tools=[get_weather],\n", - " middleware=[dynamic_system_prompt],\n", - " context_schema=CustomContext,\n", - ")\n", - "\n", - "result = agent.invoke(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"What is the weather in SF?\"}]},\n", - " context=CustomContext(user_name=\"John Smith\"),\n", - ")\n", - "\n", - "print(result[\"messages\"][-1].content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 종합 예제\n", - "\n", - "다양한 메모리 관리 기법을 결합한 실용적인 예제입니다." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.agents import create_agent, AgentState\n", - "from langchain.agents.middleware import SummarizationMiddleware, before_model\n", - "from langchain.tools import tool, ToolRuntime\n", - "from langgraph.checkpoint.memory import InMemorySaver\n", - "from langgraph.runtime import Runtime\n", - "from typing import Any\n", - "\n", - "# 커스텀 상태\n", - "class ConversationState(AgentState):\n", - " user_preferences: dict\n", - " conversation_count: int\n", - "\n", - "# 도구 정의\n", - "@tool\n", - "def save_preference(\n", - " preference_key: str,\n", - " preference_value: str,\n", - " runtime: ToolRuntime\n", - ") -> str:\n", - " \"\"\"Save user preference.\"\"\"\n", - " return f\"Saved preference: {preference_key} = {preference_value}\"\n", - "\n", - "# 메시지 트리밍 미들웨어\n", - "@before_model\n", - "def count_and_trim(state: ConversationState, runtime: Runtime) -> dict[str, Any] | None:\n", - " messages = state[\"messages\"]\n", - " count = state.get(\"conversation_count\", 0) + 1\n", - " \n", - " updates = {\"conversation_count\": count}\n", - " \n", - " if len(messages) > 10:\n", - " print(f\"Trimming messages (conversation #{count})\")\n", - " from langchain.messages import RemoveMessage\n", - " from langgraph.graph.message import REMOVE_ALL_MESSAGES\n", - " updates[\"messages\"] = [\n", - " RemoveMessage(id=REMOVE_ALL_MESSAGES),\n", - " messages[0],\n", - " *messages[-5:]\n", - " ]\n", - " \n", - " return updates\n", - "\n", - "# 에이전트 생성\n", - "agent = create_agent(\n", - " model=model,\n", - " tools=[save_preference],\n", - " state_schema=ConversationState,\n", - " middleware=[\n", - " count_and_trim,\n", - " SummarizationMiddleware(\n", - " model=\"openai:gpt-4.1-mini\",\n", - " max_tokens_before_summary=5000,\n", - " messages_to_keep=10,\n", - " )\n", - " ],\n", - " checkpointer=InMemorySaver(),\n", - ")\n", - "\n", - "# 테스트\n", - "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "\n", - "result = agent.invoke(\n", - " {\n", - " \"messages\": [{\"role\": \"user\", \"content\": \"Hi! I prefer dark mode.\"}],\n", - " \"user_preferences\": {},\n", - " \"conversation_count\": 0\n", - " },\n", - " config\n", - ")\n", - "\n", - "print(\"\\nFinal state:\")\n", - "print(f\"Conversation count: {result.get('conversation_count', 0)}\")\n", - "print(f\"Message count: {len(result['messages'])}\")\n", - "print(f\"Last message: {result['messages'][-1].content}\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.0" - } - }, - "nbformat": 4, - "nbformat_minor": 4 + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# LangGraph 단기 메모리 관리\n", + "\n", + "단기 메모리(Short-term Memory)는 하나의 대화 세션(thread) 내에서 이전 상호작용을 기억하는 메커니즘입니다. LangGraph에서는 체크포인터를 통해 대화 기록을 저장하고, 동일한 `thread_id`로 호출하면 이전 대화를 이어서 진행할 수 있습니다.\n", + "\n", + "그러나 대화가 길어지면 LLM의 컨텍스트 윈도우 제한에 도달하거나 토큰 비용이 증가하는 문제가 발생합니다. 이를 해결하기 위해 메시지 트리밍(trimming), 삭제(deletion), 요약(summarization) 등의 전략을 사용합니다.\n", + "\n", + "> 참고 문서: [LangGraph Persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-1", + "metadata": {}, + "source": [ + "## 학습 목표\n", + "\n", + "이 튜토리얼에서는 다음 내용을 학습합니다:\n", + "\n", + "- 메시지 트리밍(trim_messages)을 통한 컨텍스트 관리\n", + "- RemoveMessage를 사용한 메시지 삭제\n", + "- 그래프 내에서 동적으로 메시지 관리하기\n", + "- 대화 요약을 통한 컨텍스트 압축" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "## 환경 설정\n", + "\n", + "LangGraph 튜토리얼을 시작하기 전에 필요한 환경을 설정합니다. `dotenv`를 사용하여 API 키를 로드하고, `langchain_teddynote`의 로깅 기능을 활성화하여 LangSmith에서 실행 추적을 확인할 수 있도록 합니다.\n", + "\n", + "아래 코드는 환경 변수를 로드하고 LangSmith 프로젝트를 설정합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# API 키를 환경변수로 관리하기 위한 설정 파일\n", + "from dotenv import load_dotenv\n", + "\n", + "# API 키 정보 로드\n", + "load_dotenv(override=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LangSmith 추적을 시작합니다.\n", + "[프로젝트명]\n", + "LangGraph-V1-Tutorial\n" + ] + } + ], + "source": [ + "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n", + "from langchain_teddynote import logging\n", + "\n", + "# 프로젝트 이름을 입력합니다.\n", + "logging.langsmith(\"LangGraph-V1-Tutorial\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-5", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 기본 에이전트 구축\n", + "\n", + "먼저 메시지 관리 기법을 테스트할 기본 에이전트를 구축합니다. 이 에이전트는 검색 도구를 사용하며, `MemorySaver` 체크포인터를 통해 대화 기록을 저장합니다. 메시지가 누적되면서 다양한 관리 기법을 적용해볼 수 있습니다.\n", + "\n", + "아래 코드에서는 검색 도구, LLM 모델, 그리고 StateGraph를 정의합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "기본 에이전트 생성 완료!\n" + ] + } + ], + "source": [ + "from typing import Literal\n", + "\n", + "from langchain_core.tools import tool\n", + "from langchain_openai import ChatOpenAI\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.graph import MessagesState, StateGraph, START, END\n", + "from langgraph.prebuilt import ToolNode, tools_condition\n", + "\n", + "# 체크포인트 저장을 위한 메모리 객체 초기화\n", + "memory = MemorySaver()\n", + "\n", + "\n", + "# 웹 검색 기능을 모방하는 도구 함수 정의\n", + "@tool\n", + "def search(query: str):\n", + " \"\"\"웹 검색을 수행합니다.\"\"\"\n", + " return f\"검색 결과: '{query}'에 대한 정보를 찾았습니다. LangGraph는 상태 기반 워크플로우를 구축하는 프레임워크입니다.\"\n", + "\n", + "\n", + "# 도구 목록 생성 및 도구 노드 초기화\n", + "tools = [search]\n", + "tool_node = ToolNode(tools)\n", + "\n", + "# 모델 초기화 및 도구 바인딩\n", + "model = ChatOpenAI(model=\"gpt-4o-mini\")\n", + "model_with_tools = model.bind_tools(tools)\n", + "\n", + "\n", + "# LLM 모델 호출 및 응답 처리 함수\n", + "def call_model(state: MessagesState):\n", + " \"\"\"에이전트 노드 함수\n", + " \n", + " 현재 메시지를 LLM에 전달하고 응답을 반환합니다.\n", + " \"\"\"\n", + " response = model_with_tools.invoke(state[\"messages\"])\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# 상태 기반 워크플로우 그래프 초기화\n", + "workflow = StateGraph(MessagesState)\n", + "\n", + "# 에이전트와 도구 노드 추가\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"tools\", tool_node)\n", + "\n", + "# 시작점을 에이전트 노드로 설정\n", + "workflow.add_edge(START, \"agent\")\n", + "\n", + "# 조건부 엣지 설정: 도구 호출 여부에 따라 분기\n", + "workflow.add_conditional_edges(\"agent\", tools_condition)\n", + "\n", + "# 도구 실행 후 에이전트로 돌아가는 엣지 추가\n", + "workflow.add_edge(\"tools\", \"agent\")\n", + "\n", + "# 체크포인터가 포함된 최종 워크플로우 컴파일\n", + "app = workflow.compile(checkpointer=memory)\n", + "\n", + "print(\"기본 에이전트 생성 완료!\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-7", + "metadata": {}, + "source": [ + "### 그래프 시각화\n", + "\n", + "컴파일된 그래프의 구조를 시각화합니다. `agent` 노드에서 도구 호출 여부에 따라 `tools` 노드로 분기하거나 종료되는 흐름을 확인할 수 있습니다.\n", + "\n", + "아래 코드는 그래프를 시각화합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-8", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from langchain_teddynote.graphs import visualize_graph\n", + "\n", + "# 그래프 시각화\n", + "visualize_graph(app)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-9", + "metadata": {}, + "source": [ + "### 에이전트 테스트\n", + "\n", + "기본 에이전트가 대화를 기억하는지 테스트합니다. 첫 번째 메시지에서 이름을 알려주고, 두 번째 메시지에서 이름을 물어보면 체크포인터에 저장된 대화 기록을 바탕으로 답변합니다.\n", + "\n", + "아래 코드에서는 연속으로 두 번 대화를 수행합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-10", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "안녕하세요! 제 이름은 Teddy입니다.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "안녕하세요, Teddy! 만나서 반가워요. 어떻게 도와드릴까요?\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "# 스레드 ID가 1인 설정 객체 초기화\n", + "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", + "\n", + "# 첫 번째 질문\n", + "input_message = HumanMessage(content=\"안녕하세요! 제 이름은 Teddy입니다.\")\n", + "\n", + "# 스트림 모드로 메시지 처리 및 응답 출력\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-11", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "제 이름이 뭐라고 했죠?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "당신의 이름은 Teddy입니다. 맞나요?\n" + ] + } + ], + "source": [ + "# 후속 질문: 이름 기억 확인\n", + "input_message = HumanMessage(content=\"제 이름이 뭐라고 했죠?\")\n", + "\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cell-12", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "현재 저장된 메시지 수: 4개\n", + "\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "안녕하세요! 제 이름은 Teddy입니다.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "안녕하세요, Teddy! 만나서 반가워요. 어떻게 도와드릴까요?\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "제 이름이 뭐라고 했죠?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "당신의 이름은 Teddy입니다. 맞나요?\n" + ] + } + ], + "source": [ + "# 현재 저장된 메시지 확인\n", + "messages = app.get_state(config).values[\"messages\"]\n", + "print(f\"현재 저장된 메시지 수: {len(messages)}개\\n\")\n", + "\n", + "for message in messages:\n", + " message.pretty_print()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-13", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 메시지 트리밍(Trimming)\n", + "\n", + "대부분의 LLM에는 최대 지원 컨텍스트 윈도우(토큰 단위)가 있습니다. 대화가 길어지면 이 한계에 도달하여 오류가 발생하거나 컨텍스트가 손실될 수 있습니다. `trim_messages` 함수를 사용하면 메시지 기록의 토큰 수를 계산하고, 한계에 도달했을 때 오래된 메시지를 트리밍할 수 있습니다.\n", + "\n", + "### trim_messages 함수\n", + "\n", + "`langchain_core.messages`의 `trim_messages` 함수는 다음과 같은 옵션을 제공합니다:\n", + "\n", + "- `strategy`: \"last\"(최근 메시지 유지) 또는 \"first\"(첫 메시지 유지)\n", + "- `max_tokens`: 유지할 최대 토큰 수\n", + "- `token_counter`: 토큰 수를 계산하는 함수 또는 LLM 모델\n", + "- `start_on`: 시작할 메시지 타입 (\"human\", \"ai\" 등)\n", + "- `include_system`: 시스템 메시지 포함 여부 (strategy가 \"last\"일 경우에만 사용)\n", + "\n", + "### token_counter 주의사항\n", + "\n", + "`token_counter` 파라미터에는 **LLM 모델 객체**를 전달하는 것이 권장됩니다. `token_counter=len`처럼 `len` 함수를 전달하면 메시지 **개수**를 토큰 수로 잘못 계산하여 트리밍이 제대로 작동하지 않습니다.\n", + "\n", + "```python\n", + "# 잘못된 사용 (메시지 개수를 토큰으로 계산)\n", + "trim_messages(messages, max_tokens=100, token_counter=len)\n", + "\n", + "# 올바른 사용 (LLM 모델로 정확한 토큰 계산)\n", + "trim_messages(messages, max_tokens=100, token_counter=model)\n", + "```\n", + "\n", + "아래 코드에서는 LLM 모델을 token_counter로 사용하여 트리밍 전략을 테스트합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cell-14", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "원본 메시지 수: 11개\n" + ] + } + ], + "source": [ + "from langchain_core.messages import trim_messages, HumanMessage, AIMessage, SystemMessage\n", + "\n", + "# 긴 대화 히스토리 시뮬레이션\n", + "sample_messages = [\n", + " SystemMessage(content=\"당신은 친절한 어시스턴트입니다.\"),\n", + " HumanMessage(content=\"안녕하세요\"),\n", + " AIMessage(content=\"안녕하세요! 무엇을 도와드릴까요?\"),\n", + " HumanMessage(content=\"날씨가 어때요?\"),\n", + " AIMessage(content=\"오늘은 맑은 날씨입니다.\"),\n", + " HumanMessage(content=\"추천 음식이 있나요?\"),\n", + " AIMessage(content=\"파스타를 추천합니다.\"),\n", + " HumanMessage(content=\"레시피를 알려주세요\"),\n", + " AIMessage(content=\"토마토 파스타 레시피입니다. 먼저 토마토를 준비하고...\"),\n", + " HumanMessage(content=\"감사합니다\"),\n", + " AIMessage(content=\"천만에요! 다른 질문이 있으시면 말씀해주세요.\"),\n", + "]\n", + "\n", + "print(f\"원본 메시지 수: {len(sample_messages)}개\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cell-15", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "전략 1 - 최근 메시지 유지 (strategy='last'):\n", + "트리밍 후 메시지 수: 7개\n", + "\n", + " [system] 당신은 친절한 어시스턴트입니다.\n", + " [human] 추천 음식이 있나요?\n", + " [ai] 파스타를 추천합니다.\n", + " [human] 레시피를 알려주세요\n", + " [ai] 토마토 파스타 레시피입니다. 먼저 토마토를 준비하고...\n", + " [human] 감사합니다\n", + " [ai] 천만에요! 다른 질문이 있으시면 말씀해주세요.\n" + ] + } + ], + "source": [ + "# 전략 1: 최근 메시지만 유지 (strategy=\"last\")\n", + "# token_counter에 LLM 모델을 전달하면 정확한 토큰 수를 계산합니다.\n", + "trimmed_last = trim_messages(\n", + " sample_messages,\n", + " strategy=\"last\",\n", + " max_tokens=100, # 최대 100 토큰만 유지\n", + " token_counter=model, # LLM 모델을 전달하여 정확한 토큰 계산\n", + " start_on=\"human\", # 사람 메시지로 시작\n", + " include_system=True, # 시스템 메시지 포함\n", + ")\n", + "\n", + "print(\"전략 1 - 최근 메시지 유지 (strategy='last'):\")\n", + "print(f\"트리밍 후 메시지 수: {len(trimmed_last)}개\\n\")\n", + "for msg in trimmed_last:\n", + " print(f\" [{msg.type}] {msg.content[:50]}...\" if len(msg.content) > 50 else f\" [{msg.type}] {msg.content}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "cell-16", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "전략 2 - 첫 메시지 유지 (strategy='first'):\n", + "트리밍 후 메시지 수: 8개\n", + "\n", + " [system] 당신은 친절한 어시스턴트입니다.\n", + " [human] 안녕하세요\n", + " [ai] 안녕하세요! 무엇을 도와드릴까요?\n", + " [human] 날씨가 어때요?\n", + " [ai] 오늘은 맑은 날씨입니다.\n", + " [human] 추천 음식이 있나요?\n", + " [ai] 파스타를 추천합니다.\n", + " [human] 레시피를 알려주세요\n" + ] + } + ], + "source": [ + "# 전략 2: 첫 메시지 유지 (strategy=\"first\")\n", + "trimmed_first = trim_messages(\n", + " sample_messages,\n", + " strategy=\"first\",\n", + " max_tokens=100,\n", + " token_counter=model, # LLM 모델을 전달하여 정확한 토큰 계산\n", + ")\n", + "\n", + "print(\"전략 2 - 첫 메시지 유지 (strategy='first'):\")\n", + "print(f\"트리밍 후 메시지 수: {len(trimmed_first)}개\\n\")\n", + "for msg in trimmed_first:\n", + " print(f\" [{msg.type}] {msg.content[:50]}...\" if len(msg.content) > 50 else f\" [{msg.type}] {msg.content}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-17", + "metadata": {}, + "source": [ + "### 그래프에서 트리밍 적용\n", + "\n", + "LLM을 호출하기 전에 메시지를 트리밍하여 컨텍스트 윈도우 제한을 관리할 수 있습니다. 아래 예제에서는 에이전트 노드에서 LLM을 호출하기 전에 자동으로 메시지를 트리밍합니다.\n", + "\n", + "아래 코드에서는 트리밍이 적용된 에이전트를 구축합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "cell-18", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "트리밍이 적용된 에이전트 생성 완료!\n" + ] + } + ], + "source": [ + "# 새로운 체크포인터 생성\n", + "memory_trimmed = MemorySaver()\n", + "\n", + "\n", + "def call_model_with_trimming(state: MessagesState):\n", + " \"\"\"트리밍이 적용된 에이전트 노드\n", + " \n", + " LLM 호출 전에 메시지를 트리밍하여 컨텍스트 관리를 수행합니다.\n", + " \"\"\"\n", + " # 메시지 트리밍 - 최대 500 토큰만 유지\n", + " # token_counter에 LLM 모델을 전달하여 정확한 토큰 계산\n", + " trimmed_messages = trim_messages(\n", + " state[\"messages\"],\n", + " strategy=\"last\",\n", + " max_tokens=500,\n", + " token_counter=model, # LLM 모델 전달\n", + " start_on=\"human\",\n", + " include_system=True,\n", + " )\n", + " \n", + " # 트리밍된 메시지로 LLM 호출\n", + " response = model_with_tools.invoke(trimmed_messages)\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# 트리밍이 적용된 워크플로우 생성\n", + "workflow_trimmed = StateGraph(MessagesState)\n", + "workflow_trimmed.add_node(\"agent\", call_model_with_trimming)\n", + "workflow_trimmed.add_node(\"tools\", tool_node)\n", + "workflow_trimmed.add_edge(START, \"agent\")\n", + "workflow_trimmed.add_conditional_edges(\"agent\", tools_condition)\n", + "workflow_trimmed.add_edge(\"tools\", \"agent\")\n", + "\n", + "app_trimmed = workflow_trimmed.compile(checkpointer=memory_trimmed)\n", + "\n", + "print(\"트리밍이 적용된 에이전트 생성 완료!\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-19", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## RemoveMessage를 사용한 메시지 삭제\n", + "\n", + "`RemoveMessage`는 LangGraph에서 제공하는 특수 수정자로, 메시지 목록에서 특정 메시지를 영구적으로 삭제할 때 사용합니다. 트리밍과 달리 메시지가 실제로 상태에서 제거되므로, 저장된 데이터 양을 줄이거나 민감한 정보를 삭제하는 데 유용합니다.\n", + "\n", + "### 수동으로 메시지 삭제\n", + "\n", + "`update_state()` 메서드와 `RemoveMessage`를 함께 사용하면 그래프 상태에서 원하는 메시지를 선택적으로 제거할 수 있습니다. 각 메시지는 고유한 `id` 속성을 가지고 있어 정확하게 삭제 대상을 지정할 수 있습니다.\n", + "\n", + "아래 코드에서는 저장된 메시지 중 첫 번째 메시지를 삭제합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "cell-20", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "삭제 전 메시지 수: 4개\n", + "\n", + " [0] human: 안녕하세요! 제 이름은 Teddy입니다.\n", + " [1] ai: 안녕하세요, Teddy! 만나서 반가워요. 어떻게 도와드릴까요?\n", + " [2] human: 제 이름이 뭐라고 했죠?\n", + " [3] ai: 당신의 이름은 Teddy입니다. 맞나요?\n" + ] + } + ], + "source": [ + "from langchain_core.messages import RemoveMessage\n", + "\n", + "# 현재 저장된 메시지 확인\n", + "messages = app.get_state(config).values[\"messages\"]\n", + "print(f\"삭제 전 메시지 수: {len(messages)}개\\n\")\n", + "\n", + "for i, msg in enumerate(messages):\n", + " print(f\" [{i}] {msg.type}: {msg.content[:40]}...\" if len(msg.content) > 40 else f\" [{i}] {msg.type}: {msg.content}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "cell-21", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "삭제 후 메시지 수: 3개\n", + "\n", + " [0] ai: 안녕하세요, Teddy! 만나서 반가워요. 어떻게 도와드릴까요?\n", + " [1] human: 제 이름이 뭐라고 했죠?\n", + " [2] ai: 당신의 이름은 Teddy입니다. 맞나요?\n" + ] + } + ], + "source": [ + "# 첫 번째 메시지 삭제\n", + "app.update_state(config, {\"messages\": RemoveMessage(id=messages[0].id)})\n", + "\n", + "# 삭제 결과 확인\n", + "messages_after = app.get_state(config).values[\"messages\"]\n", + "print(f\"삭제 후 메시지 수: {len(messages_after)}개\\n\")\n", + "\n", + "for i, msg in enumerate(messages_after):\n", + " print(f\" [{i}] {msg.type}: {msg.content[:40]}...\" if len(msg.content) > 40 else f\" [{i}] {msg.type}: {msg.content}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-22", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 그래프 내부에서 동적으로 메시지 삭제\n", + "\n", + "수동 삭제 외에도 그래프 실행 중에 자동으로 오래된 메시지를 삭제하는 로직을 구현할 수 있습니다. 이는 대화가 길어질 때 컨텍스트 윈도우를 관리하거나 토큰 비용을 절약하는 데 유용합니다.\n", + "\n", + "아래 예시에서는 그래프 실행이 종료될 때 최근 3개 메시지만 유지하고 나머지는 자동으로 삭제하는 `delete_messages` 노드를 추가합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "cell-23", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "동적 삭제가 적용된 에이전트 생성 완료!\n" + ] + } + ], + "source": [ + "from typing import Literal\n", + "from langchain_core.messages import RemoveMessage\n", + "\n", + "# 새로운 체크포인터 생성\n", + "memory_auto_delete = MemorySaver()\n", + "\n", + "\n", + "def delete_messages(state: MessagesState):\n", + " \"\"\"오래된 메시지 삭제 노드\n", + " \n", + " 메시지가 3개를 초과하면 최신 3개만 유지하고 나머지를 삭제합니다.\n", + " \"\"\"\n", + " messages = state[\"messages\"]\n", + " if len(messages) > 3:\n", + " # 오래된 메시지 삭제 (최신 3개만 유지)\n", + " return {\"messages\": [RemoveMessage(id=m.id) for m in messages[:-3]]}\n", + " return {}\n", + "\n", + "\n", + "def should_continue(state: MessagesState) -> Literal[\"tools\", \"delete_messages\"]:\n", + " \"\"\"조건부 라우팅 함수\n", + " \n", + " 도구 호출이 있으면 tools 노드로, 없으면 delete_messages 노드로 이동합니다.\n", + " \"\"\"\n", + " last_message = state[\"messages\"][-1]\n", + " # 도구 호출이 없으면 메시지 삭제 노드로 이동\n", + " if not last_message.tool_calls:\n", + " return \"delete_messages\"\n", + " return \"tools\"\n", + "\n", + "\n", + "# 동적 삭제가 포함된 워크플로우 생성\n", + "workflow_auto = StateGraph(MessagesState)\n", + "\n", + "# 노드 추가\n", + "workflow_auto.add_node(\"agent\", call_model)\n", + "workflow_auto.add_node(\"tools\", tool_node)\n", + "workflow_auto.add_node(\"delete_messages\", delete_messages)\n", + "\n", + "# 엣지 추가\n", + "workflow_auto.add_edge(START, \"agent\")\n", + "workflow_auto.add_conditional_edges(\"agent\", should_continue)\n", + "workflow_auto.add_edge(\"tools\", \"agent\")\n", + "workflow_auto.add_edge(\"delete_messages\", END)\n", + "\n", + "# 컴파일\n", + "app_auto = workflow_auto.compile(checkpointer=memory_auto_delete)\n", + "\n", + "print(\"동적 삭제가 적용된 에이전트 생성 완료!\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-24", + "metadata": {}, + "source": [ + "### 수정된 그래프 시각화\n", + "\n", + "수정된 그래프에는 `delete_messages` 노드가 추가되었습니다. 에이전트가 도구 호출 없이 응답을 완료하면 `delete_messages` 노드를 거쳐 종료됩니다.\n", + "\n", + "아래 코드는 수정된 그래프를 시각화합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "cell-25", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# 그래프 시각화\n", + "visualize_graph(app_auto)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-26", + "metadata": {}, + "source": [ + "### 동적 삭제 테스트\n", + "\n", + "이제 그래프를 여러 번 호출하여 메시지가 누적되는지, 그리고 오래된 메시지가 자동으로 삭제되는지 확인해보겠습니다. 각 호출 후 상태에는 최신 3개의 메시지만 유지됩니다.\n", + "\n", + "아래 코드에서는 연속으로 여러 번 대화를 수행합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "cell-27", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "안녕하세요! 저는 철수입니다.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "안녕하세요, 철수님! 어떻게 도와드릴까요?\n", + "\n", + "현재 메시지 수: 2개\n" + ] + } + ], + "source": [ + "# 새로운 스레드 ID로 설정\n", + "config_auto = {\"configurable\": {\"thread_id\": \"auto_delete_test\"}}\n", + "\n", + "# 첫 번째 대화\n", + "input1 = HumanMessage(content=\"안녕하세요! 저는 철수입니다.\")\n", + "for event in app_auto.stream({\"messages\": [input1]}, config_auto, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()\n", + "\n", + "print(f\"\\n현재 메시지 수: {len(app_auto.get_state(config_auto).values['messages'])}개\")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "cell-28", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "제 이름이 뭐라고 했죠?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "당신의 이름은 철수입니다.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "당신의 이름은 철수입니다.\n", + "\n", + "현재 메시지 수: 3개\n" + ] + } + ], + "source": [ + "# 두 번째 대화\n", + "input2 = HumanMessage(content=\"제 이름이 뭐라고 했죠?\")\n", + "for event in app_auto.stream({\"messages\": [input2]}, config_auto, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()\n", + "\n", + "print(f\"\\n현재 메시지 수: {len(app_auto.get_state(config_auto).values['messages'])}개\")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "cell-29", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "오늘 날씨는 어때요?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "Tool Calls:\n", + " search (call_vdi67oi2oSSLOoQUQ946BHPW)\n", + " Call ID: call_vdi67oi2oSSLOoQUQ946BHPW\n", + " Args:\n", + " query: 오늘 서울 날씨\n", + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: search\n", + "\n", + "검색 결과: '오늘 서울 날씨'에 대한 정보를 찾았습니다. LangGraph는 상태 기반 워크플로우를 구축하는 프레임워크입니다.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "오늘 서울의 날씨에 대한 정보를 찾을 수 없었습니다. 다른 방법으로 확인해 보시거나 다른 질문이 있으시면 말씀해 주세요!\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "오늘 서울의 날씨에 대한 정보를 찾을 수 없었습니다. 다른 방법으로 확인해 보시거나 다른 질문이 있으시면 말씀해 주세요!\n", + "\n", + "현재 메시지 수: 3개\n" + ] + } + ], + "source": [ + "# 세 번째 대화 (이 시점에서 오래된 메시지 삭제 예상)\n", + "input3 = HumanMessage(content=\"오늘 날씨는 어때요?\")\n", + "for event in app_auto.stream({\"messages\": [input3]}, config_auto, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()\n", + "\n", + "print(f\"\\n현재 메시지 수: {len(app_auto.get_state(config_auto).values['messages'])}개\")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "cell-30", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "최종 메시지 수: 3개\n", + "\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "Tool Calls:\n", + " search (call_vdi67oi2oSSLOoQUQ946BHPW)\n", + " Call ID: call_vdi67oi2oSSLOoQUQ946BHPW\n", + " Args:\n", + " query: 오늘 서울 날씨\n", + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: search\n", + "\n", + "검색 결과: '오늘 서울 날씨'에 대한 정보를 찾았습니다. LangGraph는 상태 기반 워크플로우를 구축하는 프레임워크입니다.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "오늘 서울의 날씨에 대한 정보를 찾을 수 없었습니다. 다른 방법으로 확인해 보시거나 다른 질문이 있으시면 말씀해 주세요!\n" + ] + } + ], + "source": [ + "# 최종 상태 확인\n", + "final_messages = app_auto.get_state(config_auto).values[\"messages\"]\n", + "print(f\"최종 메시지 수: {len(final_messages)}개\\n\")\n", + "\n", + "for msg in final_messages:\n", + " msg.pretty_print()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-31", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 대화 요약을 통한 컨텍스트 압축\n", + "\n", + "메시지를 트리밍하거나 삭제하면 정보가 손실될 수 있습니다. 대화 요약(summarization)은 오래된 메시지들의 핵심 내용을 압축하여 컨텍스트를 유지하면서도 토큰 사용량을 줄이는 방법입니다.\n", + "\n", + "### 요약 전략\n", + "\n", + "1. 오래된 메시지들을 LLM을 사용하여 요약\n", + "2. 요약 내용을 시스템 메시지로 추가\n", + "3. 원본 메시지는 삭제\n", + "\n", + "아래 코드에서는 대화 요약 기능을 구현합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "cell-32", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "대화 요약 함수 정의 완료!\n" + ] + } + ], + "source": [ + "from typing import Annotated\n", + "from typing_extensions import TypedDict\n", + "from langgraph.graph.message import add_messages\n", + "\n", + "\n", + "# 요약을 저장할 수 있는 확장된 State\n", + "class SummaryState(TypedDict):\n", + " \"\"\"요약 기능이 포함된 State\n", + " \n", + " messages: 대화 메시지 리스트\n", + " summary: 이전 대화의 요약 (선택적)\n", + " \"\"\"\n", + " messages: Annotated[list, add_messages]\n", + " summary: str\n", + "\n", + "\n", + "def summarize_conversation(messages: list) -> str:\n", + " \"\"\"대화 내용을 요약하는 함수\n", + " \n", + " 주어진 메시지 리스트를 LLM을 사용하여 2-3문장으로 요약합니다.\n", + " \"\"\"\n", + " # 요약을 위한 프롬프트\n", + " summary_prompt = f\"\"\"\n", + " 다음 대화 내용을 2-3문장으로 요약해주세요.\n", + " 핵심 정보와 컨텍스트를 유지하면서 간결하게 작성해주세요.\n", + " \n", + " 대화 내용:\n", + " {chr(10).join([f\"{msg.type}: {msg.content}\" for msg in messages])}\n", + " \"\"\"\n", + " \n", + " # LLM으로 요약 생성\n", + " summary_response = model.invoke([HumanMessage(content=summary_prompt)])\n", + " return summary_response.content\n", + "\n", + "\n", + "print(\"대화 요약 함수 정의 완료!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "cell-33", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "대화 요약 결과:\n", + "사람이 AI와의 대화에서 날씨와 음식 추천을 물어보았고, AI는 오늘의 날씨가 맑음을 알리고 파스타를 추천했습니다. 그 후, AI는 토마토 파스타 레시피를 제공하며 대화가 끝났습니다.\n" + ] + } + ], + "source": [ + "# 요약 테스트\n", + "test_summary = summarize_conversation(sample_messages[1:]) # 시스템 메시지 제외\n", + "print(\"대화 요약 결과:\")\n", + "print(test_summary)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-34", + "metadata": {}, + "source": [ + "### 요약이 포함된 에이전트 구축\n", + "\n", + "이제 대화가 길어지면 자동으로 요약을 생성하고, 요약된 내용을 컨텍스트로 사용하는 에이전트를 구축합니다. 이 방식은 정보 손실을 최소화하면서 토큰 효율성을 높입니다.\n", + "\n", + "아래 코드에서는 요약 기능이 포함된 에이전트를 구축합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "cell-35", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "요약 기능이 포함된 에이전트 생성 완료!\n" + ] + } + ], + "source": [ + "# 새로운 체크포인터 생성\n", + "memory_summary = MemorySaver()\n", + "\n", + "\n", + "def call_model_with_summary(state: SummaryState):\n", + " \"\"\"요약을 활용하는 에이전트 노드\n", + " \n", + " 이전 대화 요약이 있으면 시스템 메시지로 추가하여 컨텍스트를 유지합니다.\n", + " \"\"\"\n", + " # 요약이 있으면 시스템 메시지로 추가\n", + " messages = state[\"messages\"]\n", + " if state.get(\"summary\"):\n", + " system_msg = SystemMessage(content=f\"이전 대화 요약: {state['summary']}\")\n", + " messages = [system_msg] + list(messages)\n", + " \n", + " response = model_with_tools.invoke(messages)\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "def maybe_summarize(state: SummaryState):\n", + " \"\"\"조건부 요약 노드\n", + " \n", + " 메시지가 6개를 초과하면 처음 4개를 요약하고 삭제합니다.\n", + " \"\"\"\n", + " messages = state[\"messages\"]\n", + " \n", + " if len(messages) > 6:\n", + " # 처음 4개 메시지 요약\n", + " messages_to_summarize = messages[:4]\n", + " summary = summarize_conversation(messages_to_summarize)\n", + " \n", + " # 요약된 메시지 삭제\n", + " delete_messages = [RemoveMessage(id=m.id) for m in messages_to_summarize]\n", + " \n", + " return {\n", + " \"messages\": delete_messages,\n", + " \"summary\": summary\n", + " }\n", + " \n", + " return {}\n", + "\n", + "\n", + "def should_continue_summary(state: SummaryState) -> Literal[\"tools\", \"summarize\"]:\n", + " \"\"\"조건부 라우팅 함수\"\"\"\n", + " last_message = state[\"messages\"][-1]\n", + " if not last_message.tool_calls:\n", + " return \"summarize\"\n", + " return \"tools\"\n", + "\n", + "\n", + "# 요약 기능이 포함된 워크플로우 생성\n", + "workflow_summary = StateGraph(SummaryState)\n", + "\n", + "workflow_summary.add_node(\"agent\", call_model_with_summary)\n", + "workflow_summary.add_node(\"tools\", tool_node)\n", + "workflow_summary.add_node(\"summarize\", maybe_summarize)\n", + "\n", + "workflow_summary.add_edge(START, \"agent\")\n", + "workflow_summary.add_conditional_edges(\"agent\", should_continue_summary)\n", + "workflow_summary.add_edge(\"tools\", \"agent\")\n", + "workflow_summary.add_edge(\"summarize\", END)\n", + "\n", + "app_summary = workflow_summary.compile(checkpointer=memory_summary)\n", + "\n", + "print(\"요약 기능이 포함된 에이전트 생성 완료!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "cell-36", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# 그래프 시각화\n", + "visualize_graph(app_summary)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-37", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 정리\n", + "\n", + "이번 튜토리얼에서는 LangGraph의 단기 메모리를 효과적으로 관리하는 방법을 학습했습니다.\n", + "\n", + "### 핵심 내용\n", + "\n", + "| 기법 | 특징 | 사용 사례 |\n", + "|------|------|----------|\n", + "| 트리밍 | LLM 호출 전 메시지 필터링 | 컨텍스트 윈도우 제한 관리 |\n", + "| 삭제 | 상태에서 메시지 영구 제거 | 민감 정보 삭제, 저장 공간 관리 |\n", + "| 요약 | 오래된 메시지를 압축 | 정보 손실 최소화하면서 토큰 절약 |\n", + "\n", + "### 전략 선택 가이드\n", + "\n", + "- **트리밍**: 간단하고 빠름, 정보 손실 가능\n", + "- **삭제**: 저장 공간 절약, 정보 손실 가능\n", + "- **요약**: 정보 보존, 추가 LLM 호출 비용 발생" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 }