Як агенти LangGraph виживають після перезапуску: чекпоїнти, керувачі чекпоїнтів, потоки
Дізнайтеся, як працює збереження даних у LangGraph: чому агенти втрачають усе без нього, та як стани, чекпоїнти, інструменти для їх створення та ідентифікатори потоків дозволяють продовжити виконання після збою.
Агент, який зберігає все у пам’яті процесу, забуває все в момент зупинки процесу: запуск, збій, некерований таймаут чи навіть завершення однієї запиту достатньо, щоб стерти весь діалог та всі проміжні результати. LangGraph вирішує цю проблему за допомогою механізму постійності, який фіксує стан графа після кожного кроку, щоб можна було призупинити виконання, відновити його та продовжити замість повторення. У цьому посібнику будується модель розуміння з нуля: що йде не так без механізму постійності, як стан LangGraph пов’язаний із точками перевірки, що робить точка перевірки, як ідентифікатори потоків допомагають розділяти діалоги та яке місце займає точка перевірки у пам’яті. До кінця ви зможете приєднати механізм постійності до графа, зрозуміти, що саме та коли зберігається, та дізнатися, чому варіант у пам’яті є лише інструментом для розробки.
Стійкість є також основою для кількох функцій, які зазвичай обговорюються окремо: кроки людського схвалення, виклик інструментів протягом кількох етапів та агенти, що працюють довго, — усі вони залежать від можливості зупинити роботу графа та продовжити її пізніше. Спочатку зрозумівши це, ці функції стають значно менш загадковими.
Чому агентам потрібен стан, який існує довше за процес
Одним словом, постійне зберігання означає зберігання стану додатку в якомусь надійному місці, щоб його можна було відновити та продовжити роботу після зупинки програми. Це працює як автоматична кнопка збереження, яка активується майже після кожного кроку, без необхідності її натискання.
Це має більше значення для агентів, ніж для класичного коду запит/відповідь, оскільки сучасні агенти рідко є просто одним запитанням та одною відповіддю. Вони зазвичай:
- продовжують розмови протягом годин чи днів;
- зупиняються та чекають, поки людина схвалить дію;
- послідовно використовують кілька інструментів для виконання одного завдання;
- проходять багато кроків протягом тривалого періоду;
- муслять продовжувати роботу після перезапуску чи збоїв, не втрачаючи прогресу.
Оперативна пам’ять працює швидко, але є тимчасовою: вона очищується при завершенні процесу. Усе, що агенту потрібно поза терміном життя одного процесу, має бути збережено у постійному сховищі та зчитано пізніше. У LangGraph саме це сховище та механізми його керування і є тим, на що посилається поняття „перзистентності“.
Що ламається, коли у агента немає перзистентності
Цю проблему найлегше зрозуміти через конкретні сценарії збоїв. Кожен із наведених нижче є звичайним явищем у продакшені.
Втрата електроенергії посеред довгої роботи
Агент підсумовує 200-сторінковий звіт та досяг сторінки 100, коли комп’ютер втрачає живлення. Без збереженого стану немає жодних даних про те, що половина роботи вже виконана, тому наступна спроба починається зі сторінки 1.
Звичайна перезавантаження сервера
Агент працює на хмарному сервері, і його перезапускають під час звичайної розгортки. Кожен користувач, який був у процесі розмови, втрачає свою історію. Чат-бот навіть більше не пам’ятає ім’я користувача, яке було вказане дві хвилини тому.
Збій під час виконання багатокрокового завдання
У рамках більшого завдання агент викликає зовнішній API, який досягає таймауту, і процес Python завершується через некеровану виняток. Неудавшийся крок втрачається, як і все, що агент виконав до цього.
Робочі потоки, які працюють годинами чи днями
Деякі агенти навмисно працюють повільно. Уявіть собі такого, який стежить за цінами акцій та діє лише тоді, коли досягнуто певного порогу. Якщо весь його стан зберігається в пам’яті, його неможливо призупинити, перерозгорнути чи перемістити на іншу машину без повного початку роботи.
Схвалення людиною, яке триває годинами
Агент складає юридичний документ, який мусить схвалити юрист перед надсиланням. Юрист не може переглядати чергу протягом шести годин. Утримання процесу у замороженому стані та займання ресурсів протягом такого часу є марнотратством, а якщо сервер перезавантажиться під час очікування, завдання зникне.
Багатоетапне міркування, яке зазнає невдачі на пізньому етапі
Складні агенти часто ділять роботу на цикл планування, пошуку, перевірки та узагальнення. Якщо 7-й з 10 кроків зазнає невдачі, повторне виконання кроків з 1 по 6 є марною тратою часу, API-запитів та грошей.
Чому „просто запустити ще раз“ не є стратегією
Перезапуск з нуля здається прийнятним, поки ви не порахуєте витрати:
- Гроші. Кожен запит до LLM споживає токени, тому повторне виконання шести успішних кроків через невдачу сьомого означає оплату їх двічі.
- Час. Користувачам доводиться чекати, поки буде повторена робота, на яку вони вже чекали.
Останній пункт є найважливішим. Метою стійкості є не лише економія зусиль; це також можливість для додатку призупинитися, зазнати невдачі, перезапуститися чи почекати, а потім продовжити роботу з того місця, де він фактично зупинився, щоб завершена робота залишалася завершеною.
Точне визначення стійкості
З огляду на цю проблему корисним є більш ретельне визначення: стійкість — це здатність системи записувати свої внутрішні дані, свій стан, у надійне сховище так, щоб ці дані переживали поточну роботу та могли бути знову завантажені для продовження виконання саме з того місця, де вони були залишені.
Це визначення включає три окремі можливості:
- Збереження стану: фіксація того, що агент наразі знає та що він зробив.
- Відновлення попередньої роботи: читання цих записів знову, навіть після перезапуску.
- Продовження роботи: продовження з відновленої точки замість початку.
Тимчасова пам’ять проти стійкого зберігання
Частою причиною плутанини є різниця між зберіганням даних та їхньою постійною фіксацією. Звичайний словник Python, який містить хід розмови, є тимчасовою пам’яттю: коли процес закінчується, словник зникає. Постійна фіксація означає створення копії цих даних та їх збереження в місці, яке залишається активним після закінчення процесу, зазвичай у базі даних. Саме в цьому полягає суть; решта цього посібника пояснює, як 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 для відстеження тих частин стану, що змінилися;- metadata: інформація для обліку, наприклад, який вузол створив чекпоїнт та номер кроку.
Немає потреби запам’ятовувати цю структуру. Достатньо робочого визначення: пункт перевірки — це знімок стану разом із певними записами, які фіксуються у конкретний момент часу. Якщо ви хочете дізнатися, як ці елементи зберігаються всередині системи, включаючи поточні записи та блоки даних, у статті Inside LangGraph's InMemorySaver наведено більш детальний огляд.
Лічильник, крок за кроком
Розгляньмо граф із одним вузлом, який збільшує число, що виконується тричі поспіль. Після першого виконання збережений стан містить {count: 1}, після другого — {count: 2}, а після третього — {count: 3}; кожен з цих станів є окремою точкою контролю. Якщо програма зупиняється відразу після запису другої точки контролю, її можна запустити знову від {count: 2}, не повторюючи перші два збільшення.
Чекпоїнтери: компонент, який зберігає та завантажує
Якщо точка контролю — це знімок стану, то чекпоїнтер — це компонент, який створює такі знімки, зберігає їх та відновлює. Це шар персистентності LangGraph.
Хорошою аналогією є камера з вбудованою шухлядою для зберігання. Кожного разу, коли вузол завершує свою роботу, камера робить знімок його стану та зберігає його. Ця шухляда може бути оперативною пам’яттю, локальним файлом чи базою даних — залежно від того, який чекпойнтер ви оберете.
Три функції чекпойнтера
Чекпойнтер відповідає за:
- Збереження стану: записування нового знімка у сховище після кожного кроку.
- Завантаження стану: читання найновішого знімка під час повторного запуску графа для тієї самої нитки (нитки розглядаються у наступному розділі).
- Відновлення виконання: передача цього знімка системі виконання, щоб граф продовжив роботу з того місця, де зупинився, а не починав з нуля.
Приєднання чекпойнтера під час компіляції
Функція збереження даних увімкнена під час компіляції графа. Ви імпортуєте клас 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() починається з порожнього стану.
Отриманий цикл складається з завантаження, виконання та збереження даних, які повторюються після кожного кроку. Саме тому процес зберігання здається автоматичним: ви ніколи не викликаєте функції save чи load особисто, оскільки скомпільований граф робить це в рамках звичайного виконання.
Є одна нюанс, яку легко пропустити. Пункт перевірки зберігає лише ті графи, яким він був переданий. Якщо один і той самий файл компілює другий граф без пункту перевірки, цей другий граф зовсім не має можливості зберігання.
Шини: розділення розмов
Значення thread_id зустрічається в усьому коді LangGraph, і йому потрібне точне пояснення. Шина представляє собою одну безперервну розмову чи завдання. Кожна шина має унікальний ID, і кожен пункт перевірки, створений для цієї розмови, групується під нею.
Чому кожна розмова потребує власної шини
Уявіть собі чат-бота підтримки, який одночасно обслуговує тисячі клієнтів. Один клієнт запитує про повернення грошей, а інший — про запізнену доставку. Це окремі розмови, які ведуться паралельно, і їх змішування, наприклад, повідомлення першому клієнту про посилку другого, було б серйозною помилкою.
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 використовує його для:
- пошуку наявних контрольних точок для цього потоку, якщо є історія даних для завантаження;
- створення нових контрольних точок під тим самим ID, поки триває виконання.
Якщо використати інший thread_id, буде створено нову, порожню розмову, ніби стан скасовано, хоча скомпільований граф та контрольна точка залишаються абсолютно однаковими об’єктами.
Як потоки відповідають реальним продуктам
- Інтерфейс чату: кожен відкритий чат фактично є окремим потоком, а зміна чату еквівалентна зміні 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
У прозовій формі послідовність виглядає так:
- Починається робота графіка. Ви викликаєте
graph.invoke(input, config)з певнимthread_id.
Не потрібно реалізовувати окремий режим відновлення. Достатньо лише повторно викликати граф у тій самій потоці, щоб LangGraph зміг визначити, де продовжувати.
Є одна деталь, щодо якої варто бути точним. Щоб продовжити роботу, яка була перервана або провалилась, ви викликаєте граф із параметром None як вхідними даними та тією самою конфігурацією, що дає ЛанГРафу інструкцію продовжити роботу з збереженого чекпоїнта, а не починати нову. Надання нових вхідних даних у існуючому потоці запускає нову роботу, яка будується на збереженому стані: ключі з редуктором, такі як список повідомлень, накопичуються, тоді як звичайні ключі перезаписуються новим значенням. Ви можете переглянути те, що було збережено, за допомогою graph.get_state(config) для останнього знімка та graph.get_state_history(config) для повної послідовності.
Саме цей механізм дозволяє агенту навмисно зупинитися, наприклад, щоб дочекатися рішення людини, та продовжити роботу через кілька годин чи днів на іншій машині, за умови, що ця машина може отримати доступ до того ж постійного сховища. Щоб побачити цей механізм на прикладі, перегляньте «Зупинка та продовження роботи агентів LangGraph за допомогою переривання та команди».
Найпростіший бекенд: InMemorySaver
LangGraph не прив’язує вас до однієї системи зберігання. Він підтримує кілька бекендів, тобто місць, де фізично зберігаються контрольні точки, і вони відрізняються переважно стійкістю та кількістю процесів, які можуть ними користуватися. Найпростішим є InMemorySaver.
Що це таке та як він зберігає контрольні точки
InMemorySaver — це інструмент для створення контрольних точок, який зберігає кожну з них у оперативній пам’яті поточного процесу Python у звичайному словнику в пам’яті, індексованому за ID потоку. Не існує жодних файлів чи баз даних — лише об’єкт 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 продовжить незавершене виконання.
Переваги
- Без налаштувань: не потрібні база даних чи зовнішні сервіси.
- Дуже швидкий, оскільки немає затримок диска чи мережі.
- Ідеально підходить для тестування окремих функцій.
- Зручний для навчання та експериментів у ноутбуках.
Обмеження
- Усе зникає, коли процес зупиняється. Це звичайна оперативна пам’ять, що саме й є проблемою, описаною на початку цього посібника.
- Її неможливо поділити між процесами чи серверами, оскільки кожен процес має власну пам’ять.
- Вона небезпечна для використання у продакшені, адже звичайне перезавантаження стирає всі дані.
Коли використовувати
Відповідні сфери застосування:
- локальна розробка та відладка;
- автоматизовані тести, як тестування окремих функцій, так і CI-пайплайни;
- швидкі прототипи та ноутбуки, де не має значення виживання після перезавантаження.
Усе, від чого залежать реальні користувачі, потребує контрольного механізму з підтримкою бази даних. У документації LangGraph чітко зазначено, що InMemorySaver призначений для дебаггінгу та тестування, і рекомендується використовувати надійну реалізацію, таку як PostgresSaver, у продакшені. Наступним логічним кроком є налаштування такої системи разом із іншими бекендами, такими як SQLite та Redis, а також користувацькими контрольними механізмами та процесами схвалення, створеними на основі interrupt() та Command; приклад роботи LangGraph на Postgres та Redis у продакшеновому середовищі наведено у Самостійне розгортання сервера агента LangGraph.
Основні висновки
- Функція збереження даних записує стан графа у надійне сховище, щоб процес продовжував працювати після збоїв, перезапусків та тривалих очікувань, замість того щоб починатися знову.
compile().thread_id ізолює окрему розмову чи завдання, тож один скомпільований граф може безпечно обслуговувати багатьох користувачів.None; новий вхідний даний у існуючу нитку запускає нову роботу на основі збереженого стану.InMemorySaver ідеально підходить для тестів та прототипів, але оскільки він знаходиться у пам’яті процесу, системи в промисловому використанні потребують чекпоїнтера, підтримуваного базою даних.