コーディングエージェントを200ターン走らせたら、状態のスナップショットだけで5.3GB積み上がっていた。LangChainがこの数字を公開したとき、多くの人が「durable execution(耐障害実行)は素晴らしい」と褒めてきた裏で、誰もその保存コストを口にしていなかったことに気づかされた。DeltaChannelはそこに手を入れる仕組みで、同じ処理を129MBまで落とす。約41分の1だ。
なぜエージェントの状態はここまで太るのか
LangGraphのような実行ランタイムがエージェントを「途中で落ちても再開できる」ようにしているのは、各ステップごとに状態を丸ごとチェックポイントとして書き出しているからだ。会話履歴、生成したファイル、ツールの出力。これらは基本的に追記されていくだけの累積データで、消えることはめったにない。
問題は、ステップNのチェックポイントがステップ1からNまでの全部を含んでしまう点にある。ステップが進むほど1回の書き込みが重くなり、しかもそれを毎ステップ繰り返す。書き込み総量はステップ数の2乗、つまりO(N²)で膨らむ。LangChainのベンチマークでは、軽めのコーディング/検索ワークロードを500ターン回すとベースラインは4GBに達し、複数ファイルにまたがる機能実装を200ターン回すと5.3GBになった(LangChain公式ブログ)。
「落ちても再開できる」という安心を得るために、実は毎ステップで過去全部を書き直していた。エージェントを数時間動かす前提が当たり前になった今、この隠れ税がそのまま実行コストとレイテンシに乗ってくる。
差分だけ書いて、たまに全体を保存する
DeltaChannelの発想は、バージョン管理やデータベースのWAL(先行書き込みログ)を触ったことがあれば馴染みやすい。文書を1文字打つたびに全文を保存するのではなく、その回の追記分(デルタ)だけを記録する。そのままだと再構築のたびに全デルタを頭からたどる羽目になるので、snapshot_frequency=Kステップごとに一度だけ完全なスナップショットを書き、復元の起点を作る。
これで保存量の増え方が2乗から線形に変わる。ブログのグラフでは、両ワークロードとも保存量がsnapshot_frequencyにほぼ等しい水準へ漸近していく。5.3GBが129MBになったのはこのためだ。既存技術との差分を一言でいえば、「毎回フルスナップショット」を「デルタ+間欠スナップショット」に置き換えただけで、意味論は変えていない。
使い方
累積フィールドにDeltaChannelをリデューサ付きで指定する。ブログ掲載のコードはこうだ。
from typing_extensions import Annotated
from langgraph.channels.delta import DeltaChannel
def append(state: list[str], writes: list[list[str]]) -> list[str]:
return state + [item for batch in writes for item in batch]
class MyAgentState(TypedDict):
items: Annotated[list[str], DeltaChannel(reducer=append, snapshot_frequency=50)]
ここで見落としてはいけない制約がひとつある。リデューサは「バッチ不変(batching-invariant)」でなければならない。まとめて渡されても分割して渡されても、再構築結果が一致する必要がある、という条件だ。式で書くとこうなる。
reducer(reducer(state, [w1, w2]), [w3, w4]) == reducer(state, [w1, w2, w3, w4])
デルタを別々のタイミングで適用しても最終状態が変わらないことを保証するための約束事で、順序やバッチの切れ目に依存するリデューサを書くと復元結果が壊れる。単純な追記なら問題ないが、重複排除やソートを混ぜたくなったときにここで足をすくわれる。自前でチャネルを設計するなら、CRDTやログ再生系が必ずぶつかる「差分とスナップショットの整合性」問題に自分も入っていくことを意識しておいたほうがいい。
エージェントフレームワークのDeep Agents側では、v0.6からmessagesとfilesがデフォルトでDeltaChannelを使う。こちらはコード変更なしで恩恵を受けられ、snapshot_frequencyは50に設定されている。
「動くか」より「安く動かし続けられるか」
DeltaChannelと同期して、per-nodeタイムアウト(add_nodeへのtimeout=指定、TimeoutPolicy、NodeTimeoutError)も同じv1.2.0で入っている(2026年5月12日リリース)。片方は状態肥大、片方は暴走ノードの時間制御で、どちらも「短命なワークフロー」ではなく「何時間も回るエージェント」を前提にランタイムを作り直す動きの一部だ。エージェントの評価軸が、単発で動くかどうかから、長時間・低コストで走り続けられるかへ移っているのがよく分かる。
この機能を実務で見るときに個人的に注目しているのは、リリース後の修正履歴のほうだ。GitHubのリリースを追うと、空スレッドでのupdateState挙動(v1.2.5)、スナップショットのスーパーステップ上書き(v1.2.7)、JSONラウンドトリップ時のOverwrite保持(v1.2.7)、そして直近ではv1.2.9(2026年7月10日)まで、デルタとスナップショットの整合まわりの細かなバグ修正が続いている。差分ベースの状態管理は保存を軽くする一方で、復元の正しさを担保するのが難しいという古典的なトレードオフを、この修正の連なりがそのまま物語っている。
導入判断として言えば、Deep Agentsのようにデフォルト有効な経路に乗れるなら素直に乗ればいい。自前グラフで累積状態が数百ステップ育つ用途ならDeltaChannelを検討する価値は十分あるが、リデューサのバッチ不変性だけは設計時に必ず確認しておくこと。保存が41分の1になる代わりに、そこを間違えると再開したエージェントが壊れた状態から動き出す。