Главная / Статьи / Как агенты LangGraph выживают после перезагрузки: чекпоинты, управляющие чекпоинтами, потоки

Как агенты LangGraph выживают после перезагрузки: чекпоинты, управляющие чекпоинтами, потоки

Узнайте, как работает сохранение состояния в LangGraph: почему агенты теряют всё без него, и как состояние, чекпоинты, инструменты для сохранения и идентификаторы потоков позволяют возобновить работу после сбоя.

4200 слов

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

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

Почему агентам нужно состояние, длительное по времени

Представьте, что вы два часа пишете длинный документ, не сохраняя его, а затем выключается электричество. Все ваши усилия пропадают, и вам приходится начинать сначала с первой страницы. Агент без памяти оказывается в точно такой ситуации каждый раз, когда он перезагружается. У него нет никакой информации о том, что происходило несколько секунд назад, не говоря уже о нескольких днях ранее.

Если говорить кратко, настойчивость означает хранение состояния приложения в надежном месте, чтобы его можно было восстановить и продолжить работу после остановки программы. Это похоже на автоматическую кнопку сохранения, которая срабатывает практически после каждого шага, без необходимости её нажатия.

Это особенно важно для агентов, а не для классического кода запроса/ответа, поскольку современные агенты редко представляют собой просто один вопрос и один ответ. Обычно они:

  • продолжают диалоги в течение часов или дней;
  • останавливаются и ждут, пока человек одобрит какое-либо действие;
  • используют серию вызовов инструментов для выполнения одной задачи;
  • проходят через множество шагов в течение длительного времени;
  • должны выживать после перезагрузок и сбоев, не теряя достигнутого прогресса.

ОЗУ работает быстро, но является энергозависимым: его содержимое сбрасывается при завершении процесса. Все, что агенту нужно после окончания жизненного цикла одного процесса, должно быть сохранено в постоянной памяти и затем снова прочитано. В LangGraph именно такая память и механизмы управления ею и означают понятие «персистентности».

Что ломается, когда у агента нет персистентности

Проблему легче понять на примерах конкретных сценариев сбоев. Каждый из перечисленных ниже случаев встречается регулярно в производственной среде.

Потеря питания на середине длительной задачи

Агент подводит итоги 200-страничному отчету и достиг страницы 100, когда устройство теряет питание. Без сохраненного состояния нет никаких данных о том, что половина работы выполнена, поэтому следующая попытка начинается с первой страницы.

Обычная перезагрузка сервера

Агент работает на облачном сервере, и его перезапуск происходит при обычной развертке. Каждый пользователь, находившийся в процессе разговора, теряет свою историю. Чат-бот даже больше не помнит имя пользователя, указанное двумя минутами ранее.

Сбой в процессе выполнения многократных действий

В рамках более крупной задачи агент обращается к внешнему API, которое тайм-аутит, в результате чего процесс на Python погибает из-за неразрешенной исключения. Неудавшееся действие теряется, как и всё, что агент сделал до этого.

Рабочие процессы, работающие часами или днями

Некоторые агенты специально работают медленно. Представьте агента, который отслеживает цены акций и действует только тогда, когда превышается определенный порог. Если вся его информация хранится в памяти, его нельзя приостановить, переразвернуть или переместить на другой компьютер без полного перезапуска.

Утверждение человеком, занимающее часы

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

Многоэтапное обоснование, которое терпит неудачу поздно

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

Почему «просто запустить снова» не является стратегией

Перезапуск с нуля кажется приемлемым, пока вы не учтёте затраты:

  • Деньги. Каждый вызов большой языковой модели потребляет токены, поэтому повторная обработка шести успешных шагов из-за неудачи седьмого означает двойную оплату.
  • Время. Пользователи вынуждены ждать, пока будет повторена работа, за которую они уже ждали.
  • Доверие. Банковский ассистент, который забывает заявку на кредит каждый раз при обновлении страницы, не может считаться настоящим продуктом.
  • Побочные эффекты. Если на более раннем этапе уже был отправлен электронный письмо или произведена оплата картой, повторение этих действий приводит к реальному ущербу. Некоторые действия нельзя выполнять повторно безопасным образом.
  • Последний пункт является самым важным. Цель сохранения состояния заключается не только в экономии усилий; она также позволяет приложению временно остановиться, сбоить, перезагрузиться или подождать, а затем продолжить с того места, где оно фактически остановилось, чтобы выполненная работа оставалась завершенной.

    Точное определение сохранения состояния

    Учитывая эту проблему, полезно дать более точное определение: сохранение состояния — это способность системы записывать свои внутренние данные, свое состояние, в надежное хранилище таким образом, чтобы эти данные пережили текущую работу системы и могли быть снова загружены для продолжения выполнения именно с того места, где они были прерваны.

    В этом определении содержатся три различных возможности:

    1. Сохранение состояния: запись того, что агент в настоящее время знает и что он сделал.
    2. Восстановление предыдущей экспекуции: чтение этих записей обратно, даже после перезагрузки.
    3. Продолжение рабочего процесса: продолжение работы с момента восстановления, а не с самого начала.

    Временная память против постоянного хранилища

    Частой причиной путаницы является разница между хранением данных и их сохранением. Обычный словарь Python, содержащий ход разговора, представляет собой временную память: когда процесс завершается, словарь исчезает. Сохранение данных означает создание копии этих данных и их запись в месте, которое сохраняется после завершения процесса, обычно в базу данных. В этом и заключается суть; остальная часть данного руководства объясняет, как LangGraph реализует это.

    Что дает вам персистентность в производственной среде

    Помимо предотвращения потери работы, персистентность позволяет создавать совершенно другие типы систем.

    • Агенты с длительной работой. Агенты, которые просматривают множество страниц, обрабатывают большие наборы данных или ожидают внешних событий, могут быть приостановлены и возобновлены в любое время на любом устройстве, имеющем доступ к хранилищу.
    • Устойчивость к сбоям. Система, устойчивая к сбоям, продолжает корректно функционировать при сбоях, сетевых проблемах или истечении времени ожидания. Благодаря персистентности сбой влияет только на текущий шаг выполнения, а не на всю задачу целиком.
    • Рабочие процессы с одобрением. Когда кому-то необходимо что-то проверить, агент может останавливаться на месте столько времени, сколько требуется, не теряя ничего. Создание таких процессов становится простым, если состояние системы сохраняется надежно.
  • Восстановление без специального кода. Вам не нужно писать особые рутины восстановления. Достаточно загрузить последний сохранённый чекпоинт и продолжить работу; LangGraph делает это за вас после включения функции сохранения данных.
  • Надёжные продукты. Настоящие пользователи не согласятся на необходимость начинать всё заново после каждой развертки. Функция сохранения данных — ключевое отличие хрупкой демо-версии от готового продукта.
  • Более низкая стоимость. Дорогостоящие операции, которые уже были выполнены с успехом, такие как длительные процессы генерации или медленные вызовы инструментов, не оплачиваются повторно.
  • Лучший пользовательский опыт. Люди ожидают, что чат-ассистент будет помнить ход разговора даже после закрытия вкладки и возвращения обратно. Именно это и является проявлением функции сохранения данных.
  • Краткий тест, чтобы определить необходимость его использования: если процесс сейчас перезагрузится, будет ли пользователь недоволен? Если ответ «да», графу необходима персистентность.

    Состояние: то, что действительно сохраняется

    Понятие персистентности в LangGraph имеет смысл только после понимания состояния, поскольку сохранение графа на самом деле означает сохранение его состояния в подходящие моменты времени.

    Приложение LangGraph представляет собой граф: набор шагов, называемых узлами, соединённых рёбрами, через которые проходит данные. При выполнении между узлами им нужна общая структура для чтения и записи. Эта общая структура — состояние. Удобным примером может служить доска в конференц-зале: каждый узел подходит, читает написанное, добавляет свой вклад и передаёт доску следующему узлу.

    Состояние обычно объявляется как словарь с типизацией. Первым шагом является импорт:

    from typing import TypedDict
    

    Затем в схеме перечисляются ключи, с которыми работает граф, и их типы. Здесь состояние отслеживает список сообщений, имя пользователя и счётчик шагов:

    class State(TypedDict):
        messages: list
        user_name: str
        step_count: int
    

    Поскольку State является объектом типа TypedDict, это просто словарь с фиксированным набором ключей и объявленными типами. Каждый узел получает текущее состояние и возвращает частичную обновлённую версию, после чего LangGraph объединяет это обновление с общим состоянием.

    Почему всё сводится к состоянию

    Состояние является центром приложения LangGraph:

    • узлы читают его, чтобы определить дальнейшие действия;
    • узлы записывают свои результаты обратно в него;
    • рёбра могут направлять поток данных к разным узлам в зависимости от значений, содержащихся в состоянии;
    • механизм сохранения хранит и восстанавливает это состояние.

    Как только это будет сделано, концепция сохранения состояния сводится к одному предложению: после каждого шага делайте фотографию текущего состояния и храните её в безопасном месте.

    Снимки состояния после каждого шага

    Сохраняйте эту модель на протяжении всего руководства. Каждый раз, когда узел завершает свою работу, LangGraph фиксирует текущее состояние и сохраняет его в хранилище. Если процесс останавливается сразу после завершения второго узла, снимок состояния от этого момента всё равно существует, поэтому выполнение может продолжиться оттуда, а не с первого узла. У такого снимка есть название — чекпоинт.

    Чекпоинты: снимки состояния в определённый момент времени

    Чекпоинт — это снимок состояния графа в конкретный момент времени. Этот термин происходит из видеоигр: это безопасная точка, к которой можно вернуться, вместо того чтобы перезапускать всю игру.

    Что позволяют чекпоинты

    Чекпоинт надежно отвечает на один вопрос: как выглядело состояние сразу после завершения определенного шага? Без чекпоинтов вы знаете только текущее состояние, и то только во время выполнения программы. С их помощью можно просмотреть любую более раннюю точку выполнения, а система может восстановиться до последнего сохраненного чекпоинта после сбоя.

    Чекпоинты создаются автоматически

    Многих новичков удивляет тот факт, что сохранять чекпоинты вручную не нужно. Как только к графу привязывается чекпоинтер, LangGraph создает новый чекпоинт после каждого супершага. Супершаг — это один цикл выполнения графа; в простом линейном графе он соответствует завершению одного узла, а в графе с параллельными ветвями все узлы, запланированные на один цикл, относятся к одному супершагу. Вы пишете обычный код графа, а сохранение чекпоинтов происходит одновременно с ним.

    Что содержит чекпоинт

    Обычно чекпоинт записывает:

    • id: уникальный идентификатор, отсортированный таким образом, чтобы более поздние чекпоинты следовали за более ранними;
    • ts: время создания чекпоинта;
    • channel_values: сами данные состояния, такие как сообщения и другие переменные на данный момент;
    • channel_versions: внутренние счетчики версий, которые использует LangGraph для отслеживания тех частей состояния, что изменились;
    • метаданные: информация для учета, такая как узел, создавший чекпоинт, и номер шага.

    Нет необходимости запоминать эту структуру. Достаточно рабочего определения: чекпоинт — это снимок состояния вместе с некоторыми учетными данными, записанный в определенный момент. Если вы хотите узнать, как эти элементы хранятся внутри системы, включая незавершенные записи и блобы, более подробное описание приведено в Inside LangGraph's InMemorySaver.

    Счетчик, шаг за шагом

    Рассмотрим граф с одним узлом, который увеличивает число, причем эта операция выполняется три раза подряд. После первой итерации сохраненное состояние содержит {count: 1}, после второй — {count: 2}, а после третьей — {count: 3}; каждое из этих состояний является отдельной точкой контроля. Если программа выходит из строя сразу после записи второй точки контроля, её можно запустить заново с {count: 2}, не повторяя первые два увеличения.

    Чекпоинтеры: компонент, отвечающий за сохранение и загрузку

    Если точка контроля представляет собой снимок состояния, то чекпоинтер — это компонент, который создает такие снимки, хранит их и восстанавливает. Это слой персистентности LangGraph.

    Хорошей аналогией может служить камера с встроенным шкафом для хранения документов. Каждый раз, когда узел обрабатывается до конца, камера делает снимок его состояния и сохраняет его. Этот шкаф может быть оперативной памятью, локальным файлом или базой данных в зависимости от выбранного указателя состояния.

    Три основные функции указателя состояния

    Указатель состояния отвечает за:

    1. Сохранение состояния: запись нового снимка в хранилище после каждого шага обработки.
    2. Загрузка состояния: восстановление самого свежего снимка при повторном запуске графа для той же нити (нити рассматриваются в следующем разделе).
    3. Восстановление выполнения: передача этого снимка системе выполнения, чтобы граф продолжил работу с того места, где остановился, а не начал заново.

    Подключение указателя состояния на этапе компиляции

    Режим сохранения данных включается при компиляции графа. Вы импортируете класс checkpointer и инструмент для построения графа; в исходном коде этот фрагмент обозначен как JavaScript, но на самом деле это Python:

    from langgraph.checkpoint.memory import InMemorySaver
    from langgraph.graph import StateGraph
    

    Затем вы создаете объект checkpointer и передаёте его в функцию compile(). Обратите внимание, что в приведённом фрагменте комментарий и операция присваивания checkpointer = InMemorySaver() объединены в одну строку, из-за чего операция присваивания становится частью комментария; в реальном коде они должны находиться на разных строках:

    # ... assume `builder` is a StateGraph you've already defined ...checkpointer = InMemorySaver()
    graph = builder.compile(checkpointer=checkpointer)
    

    Аргумент checkpointer=checkpointer включает режим сохранения данных для всего графа. Без него LangGraph ничего не сохраняет, и каждый вызов invoke() начинается с пустого состояния.

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

    Есть один нюанс, который легко упустить. Пункт контроля сохраняет только те структуры, которым он был передан. Если из одного и того же файла скомпилируется вторая структура без пункта контроля, у этой второй структуры совсем нет возможности сохранения.

    Потоки: разделение диалогов

    Значение thread_id встречается повсюду в коде LangGraph, и оно заслуживает точного объяснения. Поток представляет собой один непрерывный диалог или задачу. У каждого потока есть уникальный идентификатор, и все точки контроля, созданные для данного диалога, группируются под ним.

    Почему каждому диалогу нужен собственный поток

    Представьте чат-бота поддержки, обслуживающего тысячи клиентов одновременно. Один клиент спрашивает о возврате денег, а другой — о задержке доставки. Это отдельные диалоги, происходящие параллельно, и смешивание их, например, сообщение первому клиенту о посылке второго, стало бы серьезной ошибкой.

    ID тредов предотвращают это, действуя как метки на папках. Каждый этап обработки сохраняется под именно одним ID треда, поэтому данные из разных диалогов никогда не смешиваются.

    Передача ID треда в коде

    Тред выбирается с помощью словаря конфигурации. Параметр thread_id находится под ключом configurable (этот и следующий фрагменты написаны на Python, а не простым текстом):

    config = {"configurable": {"thread_id": "customer-a-session-101"}}
    

    Эта конфигурация передается вместе с входными данными при каждом вызове:

    result = graph.invoke({"messages": [{"role": "user", "content": "Where's my refund?"}]}, config)
    

    Каждый вызов graph.invoke() или graph.stream() принимает параметр config, содержащий значение thread_id. LangGraph использует его для:

    • поиска существующих точек контроля для данного потока, если есть история данных для загрузки;
    • сохранения новых точек контроля под тем же идентификатором по мере продолжения работы.

    Если использовать другой thread_id, начнется совершенно новый, пустой диалог, словно состояние сброшено, хотя скомпилированный граф и точки контроля остаются абсолютно теми же объектами.

    Как потоки соотносятся с реальными продуктами

    • Интерфейс чата: каждый открытый вами чат фактически является отдельным потоком, а смена чата эквивалентна смене идентификатора потока. То, что вы сказали в одном диалоге, не попадает в другой.
  • Служба поддержки: каждый заявок может соответствовать одному идентификатору потока, что позволяет сохранять историю обращений конкретного клиента в отдельности и легко её восстанавливать позже.
  • Личный ассистент: ассистент, который управляет календарем и списком задач одного человека, может использовать единственный долгоживущий поток, связанный с учётной записью пользователя, чтобы настройки сохранялись в течение многих дней.
  • Практическим следствием этого является необходимость целенаправленного генерирования и хранения идентификаторов потоков, например, на основе номера заявки или идентификатора сессии в собственной базе данных. Если идентификатор потерян, контрольные точки всё равно существуют, но нет ничего, что указывало бы на них.

    Один сгенерированный граф, множество потоков

    Вам не нужна графика на каждого пользователя. Одна сгенерированная графика может одновременно обслуживать любое количество потоков; вам нужен уникальный идентификатор потока для каждого пользователя или каждого диалога. Графика определяет поведение, а поток — состояние, над которым осуществляется это поведение.

    От начала запуска до сбоя и далее

    Совокупность состояний, точек контроля, указателя на точку контроля и потоков позволяет получить полное представление о том, что происходит во время выполнения. Приведенная ниже диаграмма потоков (синтаксис Mermaid, представленный в текстовом виде) отражает один запуск, включая сбой и путь возврата после него:

    flowchart TD
        A[1. Graph starts with invoke] --> B[2. Checkpointer checks thread_id for existing State]
        B --> C{State exists for this thread?}
        C -->|Yes| D[3a. Load last saved State]
        C -->|No| E[3b. Start with fresh empty State]
        D --> F[4. Node executes]
        E --> F
        F --> G[5. State updates in memory]
        G --> H[6. Checkpoint saved to storage]
        H --> I{More nodes to run?}
        I -->|Yes| F
        I -->|No| J[7. Return final result to caller]
        H -.->|💥 Crash happens here| K[Process restarts]
        K --> B
    

    В простой формулировке последовательность выглядит следующим образом:

    1. Начало работы графики. Вы вызываете graph.invoke(input, config) с определенным thread_id.
  • Создаётся или загружается состояние. Механизм проверки контрольных точек определяет, имеются ли у данного потока уже контрольные точки. Если они есть, последняя из них становится начальным состоянием; в противном случае выполнение начинается с пустого состояния.
  • Узел выполняется с использованием переданного ему состояния.
  • Состояние обновляется. Всё, что возвращает узел, объединяется с текущим состоянием.
  • Записывается контрольная точка. Механизм проверки контрольных точек сохраняет новое состояние под идентификатором потока.
  • Запускается следующий узел, и шаги с 3 по 5 повторяются.
  • Происходит сбой, скажем, сразу после записи контрольной точки для узла 2, но до начала работы узла 3.
  • Выполнение возобновляется. Повторный вызов графа в том же потоке загружает последний успешно сохранённый чекпоинт, то есть тот, что после узла 2, и выполнение продолжается с узла 3, а не с узла 1.
  • Не требуется реализация отдельного режима восстановления. Для того чтобы LangGraph определил, с чего продолжить, достаточно повторно вызвать граф в том же потоке.

    Есть один момент, в котором важно быть точным. Чтобы продолжить запуск, который был прерван или завершился неудачей, вызывается граф с параметром None в качестве входных данных и той же конфигурацией; это указывает LangGraph на необходимость продолжения с сохраненного чекпоинта, а не начала нового запуска. Передача новых входных данных на существующем потоке запускает новый процесс, который строится на основе сохраненного состояния: ключи с функцией объединения, такие как список сообщений, накапливаются, в то время как обычные ключи заменяются новыми значениями. Вы можете посмотреть, что было сохранено, с помощью graph.get_state(config) для получения последнего снимка состояния, и graph.get_state_history(config) — для просмотра полной последовательности изменений.

    Именно этот механизм позволяет агенту намеренно остановиться, например, чтобы дождаться решения человека, и возобновить работу спустя несколько часов или дней на другой машине, при условии, что эта машина может обращаться к тому же постоянному хранилищу. Чтобы узнать больше об этом подходе, ознакомьтесь с Статьей о паузировании и возобновлении работы агентов LangGraph с использованием функций interrupt и Command.

    Самый простой бэкенд: InMemorySaver

    LangGraph не привязывает вас к одной системе хранения. Он поддерживает несколько бэкендов, то есть мест, где физически хранятся чекпоинты, и они отличаются в основном устойчивостью к повреждениям и количеством процессов, которые могут ими пользоваться. Самым простым является InMemorySaver.

    Что это такое и как он хранит чекпоинты

    InMemorySaver — это инструмент для хранения точек контроля, который сохраняет каждую из них в ОЗУ текущего процесса Python в виде обычного словаря в памяти, индексируемого по идентификатору потока. Здесь нет ни файлов, ни баз данных — только объект Python.

    В первом примере используется состояние с определенным типом, содержащее один ключ count и узел, который увеличивает его значение на один. Узел соединен от START до END, граф компилируется с использованием InMemorySaver, и он запускается на thread-1 с начальным значением count равным 1, в результате чего получается 2. В исходном тексте указано, что это JavaScript, но на самом деле это Python; также следует отметить, что в примере предполагается, что ранее были импортированы объекты TypedDict, StateGraph, START, END и InMemorySaver:

    class StateInt(TypedDict):
        count: int
    
    def add_one(state: StateInt) -> dict:
        return {"count": state["count"] + 1}
    
    builder = StateGraph(StateInt)
    builder.add_node("add_one", add_one)
    builder.add_edge(START, "add_one")
    builder.add_edge("add_one", END)
    
    memory = InMemorySaver()
    graph = builder.compile(checkpointer=memory)
    
    config = {"configurable": {"thread_id": "thread-1"}}
    result = graph.invoke({"count": 1}, config)
    print(result)  # {'count': 2}
    

    Следующий фрагмент представляет собой простое описание, показывающее разницу между минимальной версией графа и более реалистичной версией выше:

    Two ways to write the same graph — minimal vs. real-world.
    

    Для минимальной версии требуется библиотека asyncio наряду с механизмом контроля состояния и инструментом построения графа:

    import asyncio
    from langgraph.checkpoint.memory import InMemorySaver
    from langgraph.graph import StateGraph
    

    Затем в качестве всего состояния используется обычное целое число int, в качестве узла регистрируется лямбда-функция, этот узел отмечается как точка входа и выхода, после чего граф запускается асинхронно с помощью функции ainvoke. Как показано, несколько операций выполняются одновременно на одной строке (например, вызов set_finish_point и присваивание значения функции InMemorySaver()), поэтому их необходимо разделить перед запуском; фрагмент написан на Python, несмотря на свою метку:

    builder = StateGraph(int)
    builder.add_node("add_one", lambda x: x + 1)
    builder.set_entry_point("add_one")
    builder.set_finish_point("add_one")memory = InMemorySaver()
    graph = builder.compile(checkpointer=memory)config = {"configurable": {"thread_id": "thread-1"}}
    result = asyncio.run(graph.ainvoke(1, config))
    print(result)  # Output: 2
    

    Рассмотрим ключевые строки:

    • InMemorySaver() создает пустой хранилище контрольных точек в памяти.
  • builder.compile(checkpointer=memory) привязывает этот хранилище к графу, что включает режим сохранения данных.
  • config = {"configurable": {"thread_id": "thread-1"}} связывает вызов с определенным диалогом.
  • graph.ainvoke(1, config) — это асинхронная точка входа; здесь начало работы задается целым числом 1 в качестве состояния на потоке "thread-1".
  • Повторный вызов с тем же thread_id использует уже сохраненные для этого потока точки контроля, а не пустую историю. Имейте в виду разницу с предыдущим разделом: в этом небольшом графе выполнение уже завершилось, и состояние представляет собой одно перезаписанное значение, поэтому новый ввод просто заменяет его, тогда как ввод None продолжил бы незавершенное выполнение.

    Преимущества

    • Нет необходимости в настройке: не требуется база данных или внешний сервис.
    • Очень быстро, поскольку отсутствует задержка из-за диска или сети.
    • Идеально подходит для тестирования на единицы.
    • Удобно для обучения и экспериментов в ноутбуках.

    Ограничения

    • Всё исчезает при остановке процесса. Это обычная память RAM, что и является проблемой, описанной в начале этого руководства.
    • Её нельзя делить между процессами или серверами, поскольку у каждого процесса есть собственная память.
    • Она небезопасна для промышленного использования, поскольку обычная перезагрузка удаляет всю информацию.

    Когда использовать

    Подходит для:

    • локальной разработки и отладки;
    • автоматизированных тестов, включая тесты на единицы и CI-пайплайны;
    • быстрых прототипов и ноутбуков, где важность сохранения данных после перезагрузки отсутствует.

    Любой компонент, от которого зависят реальные пользователи, требует использования контрольного механизма с поддержкой базы данных. В документации LangGraph прямо указано, что InMemorySaver предназначен для отладки и тестирования, и рекомендуется использовать более надежную реализацию, такую как PostgresSaver, в производственных условиях. Настраивание такой системы вместе с другими бэкендами, такими как SQLite и Redis, а также пользовательских контрольных механизмов и процедур утверждения, построенных на функциях interrupt() и Command, является следующим логичным шагом; пример работы LangGraph на Postgres и Redis в производственных условиях приведен в статье Самостоятельная настройка сервера агента LangGraph.

    Основные выводы

    • Механизм сохранения данных записывает состояние графа в надежное хранилище, что позволяет продолжить работу после сбоев, перезагрузок и длительного ожидания, вместо того чтобы начинать всё сначала.
  • Начало работы заново — это не только медленно: это приводит к повторным вызовам платных LLM и может вызвать такие побочные эффекты, как отправка электронных писем или проведение платежей.
  • Состояние — это общие данные, проходящие через граф, и именно они сохраняются.
  • Чекпоинт — это снимок этого состояния после выполнения одного шага; чекпоинтер автоматически создает, сохраняет и загружает чекпоинты после передачи в функцию compile().
  • thread_id изолирует отдельное общение или задачу, благодаря чему один скомпилированный граф может безопасно обслуживать множество пользователей.
  • Возобновление прерванной работы означает вызов той же задачи с параметром None; новый ввод в существующую задачу запускает новую работу на основе сохраненного состояния.
  • InMemorySaver идеален для тестов и прототипов, но поскольку он находится в памяти процесса, в производственных системах требуется чекпоинтер, основанный на базе данных.