17. Persistensi State dan Checkpointing
Di Bab 15 kita membangun ulang loop agen sebagai StateGraph, dan di Bab 16 kita menggunakan komponen prebuilt serta routing multi-cabang untuk membuat sesuatu yang lebih rumit. Setiap graph yang kita tulis sejauh ini punya satu keterbatasan yang sama: graph tidak menyimpan state-nya.
Graph menyimpan dan mengelola state hanya selama satu panggilan invoke(), tidak lebih dari itu. Ketika kamu memanggilnya, LangGraph membuat state baru yang segar, menjalankan node-node-nya, menggabungkan setiap nilai kembalian ke dalam state sesuai aturan reducer, lalu menyerahkan state akhir kembali ke pemanggil. Setelah state itu diserahkan, graph tidak lagi mengingatnya. Panggilan invoke() berikutnya dimulai dari state yang benar-benar baru tanpa koneksi apa pun ke panggilan sebelumnya.
Dua masalah muncul dari sini. Pertama, messages juga merupakan bagian dari state, jadi agen tidak bisa mengingat apa pun yang kamu katakan sebelumnya. Kedua, jika sebuah run gagal di tengah jalan, semua yang sudah dikerjakan sampai titik itu lenyap. Misalnya node ketiga melempar exception: hasil yang diproduksi dua node pertama ikut lenyap bersamanya, dan kamu harus mulai lagi dari awal. Di 15.1 kita menyebutkan "pemulihan dari interupsi" sebagai salah satu alasan untuk beralih ke LangGraph — inilah masalah yang kita maksud.
LangGraph menangani ini di level framework. Pasang sebuah checkpointer ke sebuah graph, dan LangGraph akan otomatis menyimpan snapshot state di setiap langkah eksekusi. Snapshot yang tersimpan itu bertahan melampaui panggilan invoke(), sehingga panggilan berikutnya bisa melanjutkan dari titik terakhir. Properti ini—state yang bertahan melampaui satu run—disebut persistensi(persistence).
Sebenarnya kamu sudah pernah menggunakan checkpointer. Di Bab 11, ketika kita memberi agen RAG percakapan memori multi-giliran, kita meneruskan create_agent(..., checkpointer=InMemorySaver()) dan sebuah thread_id. Saat itu, yang perlu kamu ketahui hanyalah bahwa checkpointer menyimpan riwayat percakapan per thread_id; kita tidak pernah menjelaskan caranya. Dan di Bab 16, ketika kita memperkenalkan parameter checkpointer, kita bilang "kita akan membahas cara kerjanya di Bab 17." Inilah bab tersebut.
Bab ini terdiri dari tiga bagian. Di 17.1 kita memasang checkpointer ke sebuah graph dan melakukan percakapan multi-giliran dengan thread_id. Di 17.2 kita membuka checkpoint yang tersimpan untuk melihat apa yang diketahui agen pada momen tertentu—checkpoint adalah alat utamamu untuk melacak mengapa sebuah agen berperilaku tidak semestinya. Di 17.3 kita mengambil sebuah graph yang gagal di tengah run dan melanjutkannya dari titik berhentinya, bukan dari awal.
17.1) Membawa State Antar Panggilan
17.1.1) Sebuah Graph yang Lupa
Kita membuka bab ini dengan mengatakan bahwa sebuah graph tidak menyimpan state-nya. Mari kita buktikan dalam kode.
Graph di bawah ini memiliki bentuk yang sama dengan graph say_hello dari 15.2. Satu-satunya perbedaan adalah node-nya mengembalikan respons LLM alih-alih string tetap.
from langgraph.graph import StateGraph, MessagesState, START, END
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-5-mini")
def llm_call(state: MessagesState):
response = model.invoke(state["messages"])
return {"messages": [response]}
builder = StateGraph(MessagesState)
builder.add_node(llm_call)
builder.add_edge(START, "llm_call")
builder.add_edge("llm_call", END)
graph = builder.compile() # tanpa checkpointerSekarang mari kita lakukan percakapan dua giliran. Kita beri tahu model nama kita di panggilan pertama, lalu tanyakan siapa nama kita di panggilan kedua.
# Panggilan pertama — kita memberikan nama kita
graph.invoke({"messages": [{"role": "user", "content": "Hi, my name is Bob."}]})
# Panggilan kedua — kita menanyakannya kembali
result = graph.invoke({"messages": [{"role": "user", "content": "What's my name?"}]})
print(result["messages"][-1].content)Output:
Maaf, tapi saya tidak tahu nama Anda. Bisakah Anda memberi tahu saya nama Anda?Satu-satunya pesan yang diterima model di panggilan kedua adalah "What's my name?". State dari panggilan pertama sudah tidak ada lagi di graph, sehingga pesan-pesan sebelumnya—yang membawa nama—tidak pernah sampai ke model.
Tentu saja kita bisa memperbaiki ini sendiri. Simpan messages yang dikembalikan panggilan pertama dan teruskan bersama panggilan kedua. Itulah persis cara kita mengelola riwayat percakapan di Bab 8. Tapi kemudian kita harus menulis kode sendiri untuk menyimpan dan mengambil riwayat untuk setiap percakapan dan setiap pengguna. Itulah pekerjaan yang diambil alih oleh checkpointer.
17.1.2) Checkpoint dan Checkpointer
Sebuah checkpointer adalah objek yang tugasnya menyimpan state. Kamu membuat sebuah instance—InMemorySaver(), misalnya—dan meneruskannya ke builder.compile(checkpointer=...) untuk memasangnya ke graph-mu.
Setelah checkpointer terpasang, graph menyalin seluruh state dan menyimpannya seiring eksekusi berjalan. Setiap salinan yang tersimpan itu disebut checkpoint. Anggap saja seperti sebuah foto: seluruh state pada momen itu, terjaga persis seperti keadaannya.
Fitur autosave dalam video game adalah gambaran mental yang tepat. Game diam-diam merekam progresmu setiap kali kamu melewati titik penting, sehingga kamu bisa keluar dan kembali lagi nanti, atau mati tanpa harus mengulang dari awal. Checkpointer melakukan hal ini persis untuk sebuah graph.
Jadi kapan "titik penting" itu? LangGraph membagi eksekusi sebuah graph menjadi beberapa tahap, dan setiap tahap disebut super-step. Sebuah checkpoint disimpan setiap kali sebuah super-step selesai.
Alasan disebut super-step dan bukan sekadar step adalah karena satu tahap bisa menjalankan beberapa node sekaligus. Dalam graph seperti yang ada di 17.1.1, di mana node-node membentuk garis lurus, menjalankan satu node adalah satu super-step. Tapi dalam graph di mana beberapa node berjalan paralel, semua node itu bersama-sama membentuk satu super-step tunggal.
Jadi bahkan satu panggilan invoke() pun meninggalkan beberapa checkpoint. Kita akan mengeluarkannya dan melihat persis apa yang dikandung masing-masing di 17.2.
InMemorySaver yang kita gunakan sebagai contoh adalah checkpointer paling sederhana yang ada. Sesuai namanya, ia menyimpan checkpoint di memori proses (RAM). Tidak ada yang perlu diinstal dan tidak ada yang perlu dikonfigurasi, yang membuatnya cocok untuk belajar dan pengembangan lokal. Konsekuensinya adalah setiap checkpoint yang tersimpan lenyap ketika proses di-restart. Kita akan melihat alternatif untuk production di 17.1.5.
17.1.3) Menambahkan Checkpointer
Memasang checkpointer hanya membutuhkan dua hal.
- Buat sebuah instance checkpointer dan teruskan ke
compile(). - Teruskan sebuah
configyang berisithread_idsetiap kali kamu memanggilinvoke().
Kita akan segera membahas mengapa yang kedua ini diperlukan. Untuk sekarang, cukup ketahui bahwa itu memberi tahu checkpointer percakapan tersimpan mana yang ingin kamu lanjutkan.
Mari kita terapkan keduanya ke graph dari 17.1.1.
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-5-mini")
def llm_call(state: MessagesState):
response = model.invoke(state["messages"])
return {"messages": [response]}
builder = StateGraph(MessagesState)
builder.add_node(llm_call)
builder.add_edge(START, "llm_call")
builder.add_edge("llm_call", END)
# 1. Buat sebuah checkpointer dan teruskan ke compile()
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
# 2. Teruskan sebuah config yang membawa thread_id ke invoke()
config = {"configurable": {"thread_id": "1"}}
graph.invoke(
{"messages": [{"role": "user", "content": "Hi, my name is Bob."}]},
config,
)
result = graph.invoke(
{"messages": [{"role": "user", "content": "What's my name?"}]},
config,
)
print(result["messages"][-1].content)Output:
Nama Anda Bob.Dua panggilan yang sama seperti di 17.1.1, dan hasil yang berbeda. Kali ini namanya menempel.
Berikut alasannya. Sebuah graph dengan checkpointer terpasang memuat state tersimpan sebelum menjalankan node llm_call. State itu sudah berisi pertukaran pertama. Pesan baru yang kita teruskan kemudian digabungkan ke dalamnya. Seperti yang kita lihat di 15.2.2, field messages membawa reducer add_messages, sehingga pesan baru ditambahkan ke daftar yang sudah ada. Model akhirnya menerima tiga pesan: sapaan, balasan pertamanya sendiri, dan pertanyaan baru.
Kita hanya mengirim pesan baru, dan LangGraph memuat percakapan sebelumnya dari checkpoint terakhir. Riwayat percakapan yang kita kelola secara manual di Bab 8 kini dikelola oleh LangGraph.
17.1.4) thread_id: Identifier yang Memisahkan Percakapan
Sebuah thread_id adalah identifier yang membedakan satu percakapan dari percakapan lainnya. Kamu yang memilih nilainya. Kita menggunakan "1" di 17.1.3, tapi string apa pun bisa dipakai. Panggil graph dengan thread_id yang sama dan kamu melanjutkan percakapan itu; panggil dengan yang berbeda dan kamu memulai percakapan terpisah.
Mari kita buktikan. Kita akan membuat Alice dan Bob melakukan percakapan berbeda melalui graph yang sama.
def send(thread_id: str, text: str) -> str:
config = {"configurable": {"thread_id": thread_id}}
result = graph.invoke(
{"messages": [{"role": "user", "content": text}]},
config,
)
return result["messages"][-1].content
# Percakapan Alice
send("alice", "My favorite color is teal.")
# Percakapan Bob — thread_id yang berbeda
send("bob", "My favorite color is orange.")
# Tanyakan lagi ke masing-masing
print("Alice:", send("alice", "What's my favorite color?"))
print("Bob: ", send("bob", "What's my favorite color?"))Output:
Alice: Warna favorit Anda teal.
Bob: Warna favorit Anda oranye.Kedua percakapan melewati objek graph yang sama dan checkpointer yang sama, namun keduanya tidak pernah tercampur. thread_id adalah primary key yang digunakan checkpointer untuk menyimpan dan mencari state. Key berbeda, penyimpanan benar-benar terpisah.
Jadi apa yang terjadi jika kamu meninggalkan nilai itu?
graph.invoke({"messages": [{"role": "user", "content": "Hello"}]})Output:
ValueError: Checkpointer requires one or more of the following 'configurable' keys: thread_id, checkpoint_ns, checkpoint_idGraph sama sekali tidak berjalan. Setelah checkpointer terpasang, thread_id bukan opsional—ia wajib ada.
Mengingat apa yang baru saja kita lihat, itu masuk akal. Sebelum menjalankan sebuah node, checkpointer harus memuat state tersimpan — dan thread_id adalah yang memberi tahu state percakapan mana yang harus dimuat.
Inilah bentuk dasar sebuah layanan chatbot: satu graph, satu checkpointer, dan satu thread_id per pengguna atau per ruang chat.
17.1.5) Keterbatasan InMemorySaver, dan Alternatif untuk Production
Kita sudah mengatakan sebelumnya bahwa InMemorySaver menyimpan checkpoint di memori. Dua keterbatasan menyertai pilihan itu.
Restart proses dan semuanya hilang. Deploy ulang layanan atau hidupkan kembali server, dan setiap percakapan yang terkumpul sejauh ini lenyap.
Proses yang terpisah tidak bisa membagikannya. Layanan nyata menyebarkan permintaan masuk ke beberapa proses. Setiap proses memiliki memorinya sendiri, sehingga percakapan yang disimpan oleh proses A tidak terlihat oleh proses B. Seorang pengguna bisa mengirim thread_id yang sama setiap kali dan tetap menyaksikan percakapan berantakan, tergantung proses mana yang kebetulan menerima permintaan.
Itulah mengapa production menggunakan checkpointer yang menyimpan checkpoint di database.
SqliteSaver/AsyncSqliteSaver(langgraph-checkpoint-sqlite) — menyimpan semuanya dalam satu file. Cocok untuk layanan kecil yang berjalan di satu server, atau untuk prototipe lokal.PostgresSaver/AsyncPostgresSaver(langgraph-checkpoint-postgres) — menyimpan checkpoint di server database. Tambahkan lebih banyak server dan setiap proses tetap melihat checkpoint yang sama. Inilah checkpointer yang direkomendasikan dokumentasi LangGraph untuk production.
Semuanya mengimplementasikan interface yang sama dengan InMemorySaver. Kode graph-mu, node-node-mu, dan cara kamu menggunakan thread_id tetap persis seperti apa adanya. Satu-satunya yang berubah adalah cara kamu membuat checkpointer.
create_agent, yang kamu temui di Bab 16, menggunakan checkpointer dengan cara yang sama. Teruskan sebuah checkpointer ke parametercheckpointer-nya, dan teruskan sebuahconfigyang membawathread_idkeinvoke(). Inilah yang membuat agen RAG percakapan di Bab 11 mengingat giliran-giliran sebelumnya.
Kita akan terus menggunakan InMemorySaver untuk sisa bab ini. Di mana pun checkpoint kebetulan tersimpan, cara kamu bekerja dengannya tetap sama.
17.2) Memeriksa State dan Debugging
Di 17.1.2 kita mengatakan checkpointer menyimpan state di setiap super-step. Mari kita keluarkan checkpoint-checkpoint itu dan lihat.
Membuka checkpoint yang tersimpan bukan sekadar rasa ingin tahu belaka. Agen berperilaku tidak semestinya. Mereka memanggil tool dalam loop yang tidak pernah berakhir, mereka kehilangan jejak giliran-giliran sebelumnya, mereka mengambil cabang yang tidak pernah kamu duga. Untuk mencari tahu mengapa, kamu perlu mengetahui seperti apa state pada momen itu—dan checkpoint punya jawabannya.
LangGraph memberimu dua metode.
graph.get_state(config)— mengembalikan checkpoint paling baru untuk percakapan itu.graph.get_state_history(config)— mengembalikan setiap checkpoint untuk percakapan itu, dari yang terbaru dulu.
Keduanya membutuhkan thread_id di config, karena mereka harus tahu checkpoint siapa yang kamu tanyakan.
Checkpoint yang dikembalikan kedua metode ini direpresentasikan sebagai objek StateSnapshot.
17.2.1) StateSnapshot: Apa yang Ada di Dalam Sebuah Checkpoint
Mari kita lihat apa yang sebenarnya dikandung sebuah checkpoint. Kita tidak akan menggunakan LLM untuk contoh ini. LLM mengembalikan sesuatu yang berbeda setiap kali, yang tidak membantu ketika kita ingin menelusuri setiap field satu per satu. Sebagai gantinya kita akan membangun graph kecil yang mengembalikan nilai-nilai tetap.
State akan memiliki dua jenis field. foo tidak punya reducer, jadi ia ditimpa; bar punya reducer, jadi ia terakumulasi. Susunannya sama dengan reducer add_messages yang kita pasang ke messages di 15.2.2—di sini kita menggunakan operator.add milik Python sebagai reducer untuk menggabungkan list.
from operator import add
from typing_extensions import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
class State(TypedDict):
foo: str # tanpa reducer → ditimpa
bar: Annotated[list[str], add] # reducer add → terakumulasi
def node_a(state: State):
return {"foo": "a", "bar": ["a"]}
def node_b(state: State):
return {"foo": "b", "bar": ["b"]}
builder = StateGraph(State)
builder.add_node(node_a)
builder.add_node(node_b)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_b", END)
graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"foo": "", "bar": []}, config)
snapshot = graph.get_state(config)
print(snapshot)Output:
StateSnapshot(
values={'foo': 'b', 'bar': ['a', 'b']},
next=(),
config={'configurable': {'thread_id': '1', 'checkpoint_ns': '',
'checkpoint_id': '1f17da03-9654-65d8-8002-9e59231bb481'}},
metadata={'source': 'loop', 'step': 2, 'parents': {}},
created_at='2026-07-12T03:17:33.637368+00:00',
parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '',
'checkpoint_id': '1f17da03-9653-6ca0-8001-7c24c8ec66f2'}},
tasks=(),
interrupts=()
)Delapan field. Mari kita bahas satu per satu.
values— state seperti keadaannya pada checkpoint ini.baradalah['a', 'b']karena reduceraddmengakumulasi apa yang dikembalikan kedua node;fooadalah'b'karena ia tidak punya reducer, jadi penulisan terakhir menang. Inilah field yang menjawab "seperti apa state pada momen itu?"next— sebuah tuple berisi nama-nama node yang akan dijalankan setelah checkpoint ini. Tuple kosong()berarti tidak ada yang tersisa untuk dijalankan, yaitu graph sudah selesai.('node_b',)berartinode_bmasih menanti di depan.config— alamat dari checkpoint ini.thread_idmengidentifikasi percakapannya, dancheckpoint_idmengidentifikasi momen mana di dalamnya. LangGraph menetapkancheckpoint_idsecara otomatis setiap kali menyimpan sebuah checkpoint.metadata— pencatatan tentang run.sourcememberi tahu dari mana checkpoint berasal:"input"berarti ia dibangun dari input yang kamu berikan keinvoke(), dan"loop"berarti ia diproduksi saat graph sedang berjalan.stepadalah nomor super-step.created_at— kapan checkpoint disimpan. Berguna ketika kamu menyelaraskannya dengan log-mu.parent_config—configdari checkpoint tepat sebelum yang ini. Ikuti dan kamu bisa berjalan mundur menelusuri run. NilainyaNoneuntuk checkpoint yang paling pertama.tasks— catatan eksekusi untuk node-node yang tercantum dinext. Pada saat sebuah checkpoint disimpan, node-node itu belum dijalankan; setelah mereka berjalan, hasilnya dilampirkan ke checkpoint ini. Sebuah node yang berhasil meninggalkan nilai kembaliannya diresult, dan sebuah node yang gagal meninggalkan exception-nya dierror.interrupts— di mana graph berhenti sejenak untuk menyerahkan kendali kembali ke seseorang. LangGraph bisa berhenti di tengah run dan menunggu seseorang menyetujui sebuah langkah atau menyuplai sebuah nilai, dan field ini mencatat jeda-jeda tersebut.
Kamu membaca field-field ini sebagai atribut. metadata adalah sebuah dictionary, jadi kamu mengeluarkan nilai darinya dengan sebuah key.
snapshot = graph.get_state(config)
print(snapshot.values) # {'foo': 'b', 'bar': ['a', 'b']}
print(snapshot.next) # ()
print(snapshot.metadata["step"]) # 2Dari semua ini, yang paling sering kamu gunakan saat debugging adalah next. Jika next tidak kosong, graph tidak sampai ke akhir—ia berhenti di suatu tempat di tengah. Dan ketika kita melanjutkan graph yang gagal di 17.3, field inilah tempat kita memulai.
17.2.2) Menelusuri Riwayat Checkpoint
get_state() hanya menunjukkan checkpoint paling baru. Tapi debugging sering berarti menanyakan "bagaimana kita bisa sampai di sini?"—dan untuk itu kamu butuh keseluruhan lintasan run. get_state_history() memberikannya kepadamu.
for snap in graph.get_state_history(config):
print(f"step={snap.metadata['step']:>2} "
f"next={str(snap.next):<16} values={snap.values}")Output:
step= 2 next=() values={'foo': 'b', 'bar': ['a', 'b']}
step= 1 next=('node_b',) values={'foo': 'a', 'bar': ['a']}
step= 0 next=('node_a',) values={'foo': '', 'bar': []}
step=-1 next=('__start__',) values={'bar': []}Checkpoint terbaru muncul lebih dulu, jadi kerjakan dari bawah ke atas untuk mengikuti run secara berurutan.
- step -1 — tepat setelah
invoke()menerima input. Perhatikan bahwa{"foo": "", "bar": []}yang kita teruskan tidak muncul divalues. Memasukkan input ke dalam state itu sendiri adalah sebuah tahap, dan tahap itu belum berjalan.__start__dinextadalah node internal yang melakukannya.barmuncul sebagai[], tapi itu bukan nilai yang kita teruskan. Sebuah field dengan reducer dimulai dengan nilai kosong agar penulisan bisa terakumulasi ke dalamnya.footidak punya reducer, jadi ia tidak punya nilai awal sama sekali — itulah mengapa ia tidak muncul di sini. - step 0 —
__start__sudah berjalan dan input kini ada di dalam state.foo=''danbar=[]adalah nilai yang kita teruskan.node_aberikutnya. - step 1 — hasil dari menjalankan
node_a.fookini'a'danbaradalah['a'], dengannode_bberikutnya. - step 2 — hasil dari menjalankan
node_b.nextkosong, jadi graph sudah selesai.
Sebuah next yang tidak kosong di step 0 atau step 1 tidak berarti graph berhenti di sana. Sebuah checkpoint yang diambil saat graph masih berjalan tentu punya sebuah node yang menanti berikutnya. Ketika 17.2.1 mengatakan "sebuah next yang tidak kosong berarti graph berhenti," itu sedang berbicara tentang checkpoint pada titik di mana sesuatu berjalan salah. Di tengah riwayat, next hanya menunjukkan kepadamu jalur mana yang diambil graph.
17.3) Melanjutkan dari Titik Kegagalan
Karena state disimpan di akhir setiap super-step, sebuah kegagalan di tengah run tidak membawa pergi pekerjaan yang sudah selesai—pekerjaan itu masih ada di dalam checkpoint. Tidak ada alasan untuk mengulang dari awal. Kamu melanjutkan dari titik berhentinya.
17.3.1) Melanjutkan dengan invoke(None, config)
Melanjutkan itu sederhana: teruskan None di tempat input.
graph.invoke(None, config)Itu berarti "tidak ada input baru; lanjutkan dari state yang tersimpan." config tetap butuh sebuah thread_id, tentu saja, karena LangGraph harus tahu percakapan mana yang harus dilanjutkan.
Mari kita simulasikan sebuah kegagalan dan lanjutkan dari sana. Kita akan membangun graph dua node yang node keduanya gagal hanya pada run pertamanya. Ia harus berhasil pada percobaan ulang, atau kita tidak akan pernah melihat proses melanjutkan benar-benar bekerja.
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
class State(TypedDict):
step_1_done: bool
step_2_done: bool
first_try = True # flag untuk membuat run pertama gagal
def step_1(state: State):
print("step_1 running (expensive work)")
return {"step_1_done": True}
def step_2(state: State):
global first_try
if first_try:
first_try = False
print("step_2 failed (API timeout)")
raise RuntimeError("External API timed out")
print("step_2 running")
return {"step_2_done": True}
builder = StateGraph(State)
builder.add_node(step_1)
builder.add_node(step_2)
builder.add_edge(START, "step_1")
builder.add_edge("step_1", "step_2")
builder.add_edge("step_2", END)
graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "job-42"}}
try:
graph.invoke({"step_1_done": False, "step_2_done": False}, config)
except RuntimeError as e:
print("Failed:", e)Output:
step_1 running (expensive work)
step_2 failed (API timeout)
Failed: External API timed outstep_1 berhasil dan step_2 melempar error. Mari kita cari tahu di mana graph berhenti, menggunakan get_state() yang kita pelajari di 17.2.
snapshot = graph.get_state(config)
print("next =", snapshot.next)
print("values =", snapshot.values)Output:
next = ('step_2',)
values = {'step_1_done': True, 'step_2_done': False}next adalah ('step_2',), yang memberi tahu kita bahwa graph berhenti di tengah menjalankan step_2. Dan di values, step_1_done bernilai True—hasil dari step_1 masih ada di dalam checkpoint.
Sekarang kita lanjutkan dengan None.
result = graph.invoke(None, config)
print("Final =", result)Output:
step_2 running
Final = {'step_1_done': True, 'step_2_done': True}step_1 running (expensive work) tidak pernah tercetak. step_1 tidak berjalan untuk kedua kalinya. LangGraph memuat state yang tersimpan dan melanjutkan di step_2. Kita tidak membayar untuk langkah pertama yang mahal itu dua kali.
17.3.2) Hal yang Perlu Diwaspadai Saat Melanjutkan
Ketika kamu melanjutkan, node yang gagal berjalan lagi. Jika node itu memanggil LLM atau mengakses API eksternal, panggilan-panggilan itu terjadi lagi juga—dan mereka mungkin kembali dengan sesuatu yang berbeda.
Di situlah jebakannya. Jika step_2 mengirim email lalu gagal, melanjutkan berarti mengirim email kedua. LangGraph hanya menjamin bahwa ia tidak akan menjalankan ulang node-node yang berhasil.
Jadi setiap node yang mungkin berjalan lagi harus bersifat idempoten(idempotent): melakukan hal yang sama dua kali seharusnya menempatkanmu di posisi yang sama. Periksa apakah email sudah terkirim sebelum mengirimnya; beri unique key pada tabel database sehingga insert duplikat tidak bisa terjadi.