langgraph-persistence について
langgraph-persistenceは、PythonとTypeScriptのLangGraphにおけるチェックポイントとメモリ設定のガイドです。スレッド単位の状態、スレッド横断ストア、履歴操作、サブグラフの永続化設定を扱います。
主な機能
- 01スーパー・ステップごとのグラフ状態保存
- 02スレッドIDによる会話の分離
- 03スレッド横断メモリの保存
- 04チェックポイント履歴の確認・再実行・分岐
- 05サブグラフのチェックポイント設定
活用シーン
- 01呼び出しをまたいだグラフ状態の保存
- 02スレッドごとの会話状態の分離
- 03会話間でのユーザー設定の共有
- 04過去のグラフ状態の再実行や調整
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ langchain-ai/ langchain-skills --skill langgraph-persistenceインストール後、Claude Code で/langgraph-persistenceを入力して読み込まれたか確認します。
詳細
LangGraph の永続化レイヤーは、グラフの状態をチェックポイントに保存することで、堅牢な実行を可能にします。
- チェックポインター:各スーパーステップでグラフの状態を保存/読み込みします
- スレッド ID:個別のチェックポイントシーケンス(会話)を識別します
- ストア:スレッドをまたいでユーザーの設定や事実を記憶します
2 種類のメモリ:
- 短期(チェックポインター):スレッド単位の会話履歴
- 長期(ストア):スレッドをまたいだユーザーの設定や事実
| チェックポインター | 用途 | 本番環境対応 |
|---|---|---|
InMemorySaver | テスト、開発 | いいえ |
SqliteSaver | ローカル開発 | 部分的 |
PostgresSaver | 本番環境 | はい |
チェックポインターのセットアップ
メモリ内チェックポイント機能とスレッドベースの状態永続化を使って、基本的なグラフをセットアップします。from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict, Annotated
import operator
class State(TypedDict):
messages: Annotated[list, operator.add]
def add_message(state: State) -> dict:
return {"messages": ["Bot response"]}
checkpointer = InMemorySaver()
graph = (
StateGraph(State)
.add_node("respond", add_message)
.add_edge(START, "respond")
.add_edge("respond", END)
.compile(checkpointer=checkpointer) # Pass at compile time
)
# ALWAYS provide thread_id
config = {"configurable": {"thread_id": "conversation-1"}}
result1 = graph.invoke({"messages": ["Hello"]}, config)
print(len(result1["messages"])) # 2
result2 = graph.invoke({"messages": ["How are you?"]}, config)
print(len(result2["messages"])) # 4 (previous + new)
メモリ内チェックポイント機能とスレッドベースの状態永続化を使って、基本的なグラフをセットアップします。
import { MemorySaver, StateGraph, StateSchema, MessagesValue, START, END } from "@langchain/langgraph";
import { HumanMessage } from "@langchain/core/messages";
const State = new StateSchema({ messages: MessagesValue });
const addMessage = async (state: typeof State.State) => {
return { messages: [{ role: "assistant", content: "Bot response" }] };
};
const checkpointer = new MemorySaver();
const graph = new StateGraph(State)
.addNode("respond", addMessage)
.addEdge(START, "respond")
.addEdge("respond", END)
.compile({ checkpointer });
// ALWAYS provide thread_id
const config = { configurable: { thread_id: "conversation-1" } };
const result1 = await graph.invoke({ messages: [new HumanMessage("Hello")] }, config);
console.log(result1.messages.length); // 2
const result2 = await graph.invoke({ messages: [new HumanMessage("How are you?")] }, config);
console.log(result2.messages.length); // 4 (previous + new)
本番環境へのデプロイ向けに、PostgreSQL ベースのチェックポイント機能を設定します。
import os
from langgraph.checkpoint.postgres import PostgresSaver
# Run once during deployment (not at application startup):
# PostgresSaver.from_conn_string(os.environ["DATABASE_URL"]).setup()
with PostgresSaver.from_conn_string(os.environ["DATABASE_URL"]) as checkpointer:
graph = builder.compile(checkpointer=checkpointer)
本番環境へのデプロイ向けに、PostgreSQL ベースのチェックポイント機能を設定します。
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
// Run once during deployment (not at application startup):
// await PostgresSaver.fromConnString(process.env.DATABASE_URL!).setup();
const checkpointer = PostgresSaver.fromConnString(process.env.DATABASE_URL!);
const graph = builder.compile({ checkpointer });
スレッド管理
異なるスレッド ID 間で状態が分離されることを示します。# Different threads maintain separate state
alice_config = {"configurable": {"thread_id": "user-alice"}}
bob_config = {"configurable": {"thread_id": "user-bob"}}
graph.invoke({"messages": ["Hi from Alice"]}, alice_config)
graph.invoke({"messages": ["Hi from Bob"]}, bob_config)
# Alice's state is isolated from Bob's
異なるスレッド ID 間で状態が分離されることを示します。
// Different threads maintain separate state
const aliceConfig = { configurable: { thread_id: "user-alice" } };
const bobConfig = { configurable: { thread_id: "user-bob" } };
await graph.invoke({ messages: [new HumanMessage("Hi from Alice")] }, aliceConfig);
await graph.invoke({ messages: [new HumanMessage("Hi from Bob")] }, bobConfig);
// Alice's state is isolated from Bob's
状態履歴とタイムトラベル
タイムトラベル:チェックポイントの履歴を確認し、過去の状態から再実行または分岐します。config = {"configurable": {"thread_id": "session-1"}}
result = graph.invoke({"messages": ["start"]}, config)
# Browse checkpoint history
states = list(graph.get_state_history(config))
# Replay from a past checkpoint
past = states[-2]
result = graph.invoke(None, past.config) # None = resume from checkpoint
# Or fork: update state at a past checkpoint, then resume
fork_config = graph.update_state(past.config, {"messages": ["edited"]})
result = graph.invoke(None, fork_config)
タイムトラベル:チェックポイントの履歴を確認し、過去の状態から再実行または分岐します。
const config = { configurable: { thread_id: "session-1" } };
const result = await graph.invoke({ messages: ["start"] }, config);
// Browse checkpoint history (async iterable, collect to array)
const states: Awaited<ReturnType<typeof graph.getState>>[] = [];
for await (const state of graph.getStateHistory(config)) {
states.push(state);
}
// Replay from a past checkpoint
const past = states[states.length - 2];
const replayed = await graph.invoke(null, past.config); // null = resume from checkpoint
// Or fork: update state at a past checkpoint, then resume
const forkConfig = await graph.updateState(past.config, { messages: ["edited"] });
const forked = await graph.invoke(null, forkConfig);
実行を再開する前に、グラフの状態を手動で更新します。
config = {"configurable": {"thread_id": "session-1"}}
# Modify state before resuming
graph.update_state(config, {"data": "manually_updated"})
# Resume with updated state
result = graph.invoke(None, config)
実行を再開する前に、グラフの状態を手動で更新します。
const config = { configurable: { thread_id: "session-1" } };
// Modify state before resuming
await graph.updateState(config, { data: "manually_updated" });
// Resume with updated state
const result = await graph.invoke(null, config);
サブグラフのチェックポインター適用範囲
サブグラフをコンパイルする際、checkpointer パラメーターで永続化の動作を制御します。これは、割り込みを使用するサブグラフ、複数ターンのメモリが必要なサブグラフ、または並列実行するサブグラフで重要です。
| 機能 | checkpointer=False | None(デフォルト) | True |
|---|---|---|---|
| 割り込み(HITL) | いいえ | はい | はい |
| 複数ターンのメモリ | いいえ | いいえ | はい |
| 複数回の呼び出し(異なるサブグラフ) | はい | はい | 警告(名前空間の競合が発生する可能性があります) |
| 複数回の呼び出し(同じサブグラフ) | はい | はい | いいえ |
| 状態の確認 | いいえ | 警告(現在の呼び出しのみ) | はい |
各モードの使い分け
checkpointer=False— サブグラフで割り込みや永続化が不要な場合に使用します。最もシンプルな選択肢で、チェックポイントのオーバーヘッドはありません。None(デフォルト/checkpointerを省略) — サブグラフでinterrupt()が必要ですが、複数ターンのメモリは不要な場合に使用します。呼び出しごとに状態はリセットされますが、一時停止と再開が可能です。呼び出しごとに一意の名前空間が割り当てられるため、並列実行にも対応します。checkpointer=True— サブグラフで呼び出しをまたいで状態を記憶する必要がある場合(複数ターンの会話)に使用します。各呼び出しは前回の続きから実行されます。
警告:ステートフルなサブグラフ(checkpointer=True)では、1 つのノード内で同じサブグラフインスタンスを複数回呼び出すことはできません。呼び出しは同じチェックポイントの名前空間に書き込むため、競合します。
# No interrupts needed — opt out of checkpointing
subgraph = subgraph_builder.compile(checkpointer=False)
# Need interrupts but not cross-invocation persistence (default)
subgraph = subgraph_builder.compile()
# Need cross-invocation persistence (stateful)
subgraph = subgraph_builder.compile(checkpointer=True)
サブグラフに適したチェックポインターモードを選択します。
// No interrupts needed — opt out of checkpointing
const subgraph = subgraphBuilder.compile({ checkpointer: false });
// Need interrupts but not cross-invocation persistence (default)
const subgraph = subgraphBuilder.compile();
// Need cross-invocation persistence (stateful)
const subgraph = subgraphBuilder.compile({ checkpointer: true });
### 並列サブグラフの名前空間分離
複数の異なるステートフルなサブグラフを並列実行する場合は、安定した名前空間分離のため、それぞれを固有のノード名を持つ StateGraph でラップします。
from langgraph.graph import MessagesState, StateGraph
def create_sub_agent(model, *, name, **kwargs):
"""Wrap an agent with a unique node name for namespace isolation."""
agent = create_agent(model=model, name=name, **kwargs)
return (
StateGraph(MessagesState)
.add_node(name, agent) # unique name -> stable namespace
.add_edge("__start__", name)
.compile()
)
fruit_agent = create_sub_agent(
"gpt-4.1-mini", name="fruit_agent",
tools=[fruit_info], prompt="...", checkpointer=True,
)
veggie_agent = create_sub_agent(
"gpt-4.1-mini", name="veggie_agent",
tools=[veggie_info], prompt="...", checkpointer=True,
)
import { StateGraph, StateSchema, MessagesValue, START } from "@langchain/langgraph";
function createSubAgent(model: string, { name, ...kwargs }: { name: string; [key: string]: any }) {
const agent = createAgent({ model, name, ...kwargs });
return new StateGraph(new StateSchema({ messages: MessagesValue }))
.addNode(name, agent) // unique name -> stable namespace
.addEdge(START, name)
.compile();
}
const fruitAgent = createSubAgent("gpt-4.1-mini", {
name: "fruit_agent", tools: [fruitInfo], prompt: "...", checkpointer: true,
});
const veggieAgent = createSubAgent("gpt-4.1-mini", {
name: "veggie_agent", tools: [veggieInfo], prompt: "...", checkpointer: true,
});
注:ノードとして追加されたサブグラフ(add_node 経由)には、名前ベースの名前空間が自動的に割り当てられるため、このラッパーは不要です。
長期メモリ(Store)
Store を使用してスレッドをまたいでメモリを保持し、会話間でユーザーの好みを共有します。from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
# Save user preference (available across ALL threads)
store.put(("alice", "preferences"), "language", {"preference": "short responses"})
# Node with store — access via runtime
from langgraph.runtime import Runtime
def respond(state, runtime: Runtime):
prefs = runtime.store.get((state["user_id"], "preferences"), "language")
return {"response": f"Using preference: {prefs.value}"}
# Compile with BOTH checkpointer and store
graph = builder.compile(checkpointer=checkpointer, store=store)
# Both threads access same long-term memory
graph.invoke({"user_id": "alice"}, {"configurable": {"thread_id": "thread-1"}})
graph.invoke({"user_id": "alice"}, {"configurable": {"thread_id": "thread-2"}}) # Same preferences!
Store を使用してスレッドをまたいでメモリを保持し、会話間でユーザーの好みを共有します。
import { MemoryStore } from "@langchain/langgraph";
const store = new MemoryStore();
// Save user preference (available across ALL threads)
await store.put(["alice", "preferences"], "language", { preference: "short responses" });
// Node with store — access via runtime
const respond = async (state: typeof State.State, runtime: any) => {
const item = await runtime.store?.get(["alice", "preferences"], "language");
return { response: `Using preference: ${item?.value?.preference}` };
};
// Compile with BOTH checkpointer and store
const graph = builder.compile({ checkpointer, store });
// Both threads access same long-term memory
await graph.invoke({ userId: "alice" }, { configurable: { thread_id: "thread-1" } });
await graph.invoke({ userId: "alice" }, { configurable: { thread_id: "thread-2" } }); // Same preferences!
基本的な Store 操作:put、get、search、delete。
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
store.put(("user-123", "facts"), "location", {"city": "San Francisco"}) # Put
item = store.get(("user-123", "facts"), "location") # Get
results = store.search(("user-123", "facts"), filter={"city": "San Francisco"}) # Search
store.delete(("user-123", "facts"), "location") # Delete
修正方法
状態を永続化できるよう、config に常に thread_id を指定します。# WRONG: No thread_id - state NOT persisted!
graph.invoke({"messages": ["Hello"]})
graph.invoke({"messages": ["What did I say?"]}) # Doesn't remember!
# CORRECT: Always provide thread_id
config = {"configurable": {"thread_id": "session-1"}}
graph.invoke({"messages": ["Hello"]}, config)
graph.invoke({"messages": ["What did I say?"]}, config) # Remembers!
状態を永続化できるよう、config に常に thread_id を指定します。
// WRONG: No thread_id - state NOT persisted!
await graph.invoke({ messages: [new HumanMessage("Hello")] });
await graph.invoke({ messages: [new HumanMessage("What did I say?")] }); // Doesn't remember!
// CORRECT: Always provide thread_id
const config = { configurable: { thread_id: "session-1" } };
await graph.invoke({ messages: [new HumanMessage("Hello")] }, config);
await graph.invoke({ messages: [new HumanMessage("What did I say?")] }, config); // Remembers!
本番環境での永続化には、InMemorySaver の代わりに PostgresSaver を使用します。
# WRONG: Data lost on process restart
checkpointer = InMemorySaver() # In-memory only!
# CORRECT: Use persistent storage for production
from langgraph.checkpoint.postgres import PostgresSaver
with PostgresSaver.from_conn_string("postgresql://...") as checkpointer:
checkpointer.setup() # only needed on first use to create tables
graph = builder.compile(checkpointer=checkpointer)
本番環境での永続化には、MemorySaver の代わりに PostgresSaver を使用します。
// WRONG: Data lost on process restart
const checkpointer = new MemorySaver(); // In-memory only!
// CORRECT: Use persistent storage for production
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
const checkpointer = PostgresSaver.fromConnString("postgresql://...");
await checkpointer.setup(); // only needed on first use to create tables
Overwrite を使用して、reducer を経由せずに状態値を置き換えます。
from langgraph.types import Overwrite
# State with reducer: items: Annotated[list, operator.add]
# Current state: {"items": ["A", "B"]}
# update_state PASSES THROUGH reducers
graph.update_state(config, {"items": ["C"]}) # Result: ["A", "B", "C"] - Appended!
# To REPLACE instead, use Overwrite
graph.update_state(config, {"items": Overwrite(["C"])}) # Result: ["C"] - Replaced
Overwrite を使用して、reducer を経由せずに状態値を置き換えます。
import { Overwrite } from "@langchain/langgraph";
// State with reducer: items uses concat reducer
// Current state: { items: ["A", "B"] }
// updateState PASSES THROUGH reducers
await graph.updateState(config, { items: ["C"] }); // Result: ["A", "B", "C"] - Appended!
// To REPLACE instead, use Overwrite
await graph.updateState(config, { items: new Overwrite(["C"]) }); // Result: ["C"] - Replaced
グラフノードでは Runtime オブジェクト経由で Store にアクセスします。
# WRONG: Store not available in node
def my_node(state):
store.put(...) # NameError! store not defined
# CORRECT: Access store via runtime
from langgraph.runtime import Runtime
def my_node(state, runtime: Runtime):
runtime.store.put(...) # Correct store instance
グラフノードでは runtime パラメーター経由で Store にアクセスします。
// WRONG: Store not available in node
const myNode = async (state) => {
store.put(...); // ReferenceError!
};
// CORRECT: Access store via runtime
const myNode = async (state, runtime) => {
await runtime.store?.put(...); // Correct store instance
};
### 避けるべきこと
- 本番環境で
InMemorySaverを使用する — 再起動時にデータが失われます。PostgresSaverを使用してください thread_idを忘れる — 指定しないと状態は永続化されませんupdate_stateが reducer を迂回すると想定する — reducer を経由します。置き換えるにはOverwriteを使用してください- 同じステートフルなサブグラフ(
checkpointer=True)を1つのノード内で並列実行する — 名前空間の競合が発生します - ノード内で Store に直接アクセスする —
Runtimeパラメーター経由でruntime.storeを使用してください
コメント
Markdown 対応