нейропоток
АвтоматизацияПродвинутый

LangGraph: строим AI-агента с памятью на Python

LangGraph — фреймворк для агентов с состоянием и памятью. Показываю как построить агента, который помнит контекст диалога и принимает решения по шагам.

Павел·6 мин чтения
LangGraph: строим AI-агента с памятью на Python
Поделиться:TelegramVK

Простой AI-агент — это вызов LLM с промптом. Он ответил, сессия закончилась, всё забыто. Реальные агенты устроены иначе: они помнят, что было сказано раньше, могут вызывать инструменты в несколько шагов и принимать решения на основе промежуточных результатов.

LangGraph — это фреймворк для таких агентов. Не просто обёртка над LLM, а система построения агентов как графа состояний. Я разберу, как это работает, и соберу рабочего агента с нуля.

Статья для разработчиков, которые уже работали с LLM через API и хотят построить агента сложнее, чем «промпт → ответ». Базовый Python нужен. Если вы только начинаете — сначала прочитайте про ReAct-паттерн, это фундамент.

Почему именно LangGraph, а не просто LangChain

LangChain — популярная библиотека для работы с LLM. Цепочки (chains) отлично работают для линейных сценариев: загрузить документ → разбить на чанки → поместить в векторную БД → найти похожие → дать контекст модели.

Но когда агент должен:

  • Принять решение на основе ответа инструмента
  • Повторить шаг, если результат неудовлетворительный
  • Перейти к другой ветке логики в зависимости от ситуации

— линейная цепочка ломается. Нужен граф.

LangGraph строит агентов как ориентированный граф (directed graph): узлы — это действия, рёбра — переходы между ними. Можно делать циклы, условные переходы, разветвления.

Инфо

Граф состояний (state graph) — архитектура, где программа движется между состояниями по определённым правилам. Банкомат: ожидание → ввод PIN → выбор операции → выдача денег → завершение. LangGraph использует ту же идею для AI-агентов.

Установка

bash
pip install langgraph langchain-anthropic python-dotenv

Создаём .env:

ANTHROPIC_API_KEY=sk-ant-...

Базовая концепция: State + Nodes + Edges

Три ключевых понятия:

State — словарь с текущим состоянием агента. Всё, что он помнит между шагами. Например, история сообщений, результат поиска, счётчик попыток.

Nodes — функции-обработчики. Принимают State на вход, возвращают обновлённый State. Каждый узел делает одно действие: вызывает LLM, выполняет инструмент, обрабатывает результат.

Edges — правила переходов между узлами. Обычное ребро — всегда идти туда. Условное ребро — выбрать путь на основе состояния.

python
from langgraph.graph import StateGraph, END from langchain_anthropic import ChatAnthropic from typing import TypedDict, List from langchain_core.messages import HumanMessage, AIMessage # Определяем State class AgentState(TypedDict): messages: List[dict] tool_calls: int # Инициализируем модель model = ChatAnthropic(model="claude-sonnet-4-6")

Собираем агента с памятью: пошагово

Построим агента-ассистента, который:

  1. Принимает вопрос пользователя
  2. Решает, нужно ли использовать инструменты
  3. Формирует ответ с учётом контекста

Шаг 1: Определяем инструменты

python
from langchain_core.tools import tool import ast import operator SAFE_OPERATORS = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, } def safe_calculate(node): """Безопасное вычисление простых арифметических выражений.""" if isinstance(node, ast.Constant): return node.value if isinstance(node, ast.BinOp) and type(node.op) in SAFE_OPERATORS: return SAFE_OPERATORS[type(node.op)]( safe_calculate(node.left), safe_calculate(node.right) ) raise ValueError("Unsupported operation") @tool def search_web(query: str) -> str: """Поиск информации в интернете. Используй когда нужны актуальные данные.""" # В реальном агенте здесь был бы Tavily или другой поиск return f"Результаты поиска по запросу '{query}': [здесь данные из поиска]" @tool def calculate(expression: str) -> str: """Вычисляет простое арифметическое выражение (+, -, *, /).""" try: tree = ast.parse(expression, mode='eval') result = safe_calculate(tree.body) return f"Результат: {result}" except Exception as e: return f"Ошибка: {e}" tools = [search_web, calculate] model_with_tools = model.bind_tools(tools)

Шаг 2: Узлы графа

python
from langchain_core.messages import ToolMessage def call_model(state: AgentState) -> AgentState: """Основной узел: вызываем LLM с историей сообщений.""" response = model_with_tools.invoke(state["messages"]) return { "messages": state["messages"] + [response], "tool_calls": state["tool_calls"] } def call_tools(state: AgentState) -> AgentState: """Узел выполнения инструментов.""" last_message = state["messages"][-1] tool_results = [] tool_map = {t.name: t for t in tools} for tool_call in last_message.tool_calls: tool_fn = tool_map[tool_call["name"]] result = tool_fn.invoke(tool_call["args"]) tool_results.append( ToolMessage(content=str(result), tool_call_id=tool_call["id"]) ) return { "messages": state["messages"] + tool_results, "tool_calls": state["tool_calls"] + 1 }

Шаг 3: Условные рёбра

python
def should_use_tools(state: AgentState) -> str: """Решаем, нужно ли вызывать инструменты или завершить.""" last_message = state["messages"][-1] if hasattr(last_message, "tool_calls") and last_message.tool_calls: return "call_tools" if state["tool_calls"] >= 5: return END return END

Шаг 4: Собираем граф

python
workflow = StateGraph(AgentState) workflow.add_node("call_model", call_model) workflow.add_node("call_tools", call_tools) workflow.set_entry_point("call_model") workflow.add_conditional_edges( "call_model", should_use_tools, { "call_tools": "call_tools", END: END } ) # После инструментов — снова к модели workflow.add_edge("call_tools", "call_model") app = workflow.compile()

Шаг 5: Запускаем с памятью

python
initial_state = { "messages": [ HumanMessage(content="Сколько будет 1337 * 42? И найди информацию о LangGraph.") ], "tool_calls": 0 } result = app.invoke(initial_state) print(result["messages"][-1].content)

Совет

Для отладки используйте app.get_graph().print_ascii() — выводит граф в текстовом виде. Видно все узлы и рёбра. Удобно, чтобы убедиться, что структура именно такая, как задумывали.

Добавляем персистентную память

Пока агент помнит только в рамках одного запуска. Чтобы память сохранялась между сессиями — нужен checkpointer:

python
from langgraph.checkpoint.memory import MemorySaver memory = MemorySaver() app = workflow.compile(checkpointer=memory) # thread_id привязывает историю к конкретной сессии config = {"configurable": {"thread_id": "user_123"}} result = app.invoke( {"messages": [HumanMessage(content="Привет, как тебя зовут?")], "tool_calls": 0}, config=config ) # Второй запрос — агент помнит первый result2 = app.invoke( {"messages": [HumanMessage(content="Что я спросил тебя только что?")], "tool_calls": 0}, config=config ) print(result2["messages"][-1].content) # Агент ответит: "Вы спросили, как меня зовут"

Для продакшна используйте SqliteSaver вместо MemorySaver:

python
from langgraph.checkpoint.sqlite import SqliteSaver memory = SqliteSaver.from_conn_string("agent_memory.db") app = workflow.compile(checkpointer=memory)

Внимание

MemorySaver хранит данные в оперативной памяти — при перезапуске всё теряется. Для реального использования нужен внешний storage: SQLite, PostgreSQL, Redis.

Реальный кейс: агент для планирования задач

Вот сценарий, где LangGraph реально незаменим: агент получает описание задачи, разбивает её на шаги и выполняет каждый по очереди.

python
class TaskState(TypedDict): task: str steps: List[str] completed_steps: List[str] current_step: int def plan_steps(state: TaskState) -> TaskState: """Разбиваем задачу на шаги.""" prompt = f"Разбей задачу на 3-5 конкретных шагов: {state['task']}" response = model.invoke(prompt) steps = [line.strip() for line in response.content.split('\n') if line.strip()] return {**state, "steps": steps, "current_step": 0} def execute_step(state: TaskState) -> TaskState: """Выполняем текущий шаг.""" step = state["steps"][state["current_step"]] prompt = f"Выполни шаг: {step}. Контекст задачи: {state['task']}" response = model.invoke(prompt) completed = state["completed_steps"] + [f"✓ {step}: {response.content[:100]}"] return {**state, "completed_steps": completed, "current_step": state["current_step"] + 1} def is_done(state: TaskState) -> str: if state["current_step"] >= len(state["steps"]): return "finish" return "execute_step"

LangGraph vs просто цикл на Python

Честный вопрос: зачем граф, если можно написать while True и if/else?

Можно. Для простого агента на 2-3 шага — цикл проще. LangGraph даёт ценность когда:

  1. Параллельные ветки — несколько узлов работают одновременно (встроенная поддержка)
  2. Наблюдаемость — каждый шаг логируется автоматически, можно смотреть в LangSmith
  3. Персистентность — встроенная поддержка checkpointing
  4. Сложные условия — визуально видно структуру агента

Для продакшн-агентов с 5+ узлами и памятью — LangGraph значительно упрощает жизнь. Для скрипта на 50 строк — оверхед.

Следующий шаг

Если хотите пойти дальше — изучите multi-agent системы: несколько специализированных агентов, которые передают задачи друг другу. LangGraph поддерживает это через subgraphs. Это следующий уровень после одиночного агента.

Про паттерны агентов на более высоком уровне — читайте про ReAct, там объясняю базовую архитектуру «думать → действовать → наблюдать», на которой строится любой агент.