3度のLangGraph書き直し:本番環境のチェックポインティングから学んだこと
3週間にわたり、私たちのオンボーディング・エージェントは、保存すべきすべてのジョブを失い続けました。ログには何も表示されませんでした。エラーも警告もありません。チェックポインターは成功を報告していましたが、再開時にステートが消えていたのです。
真実を理解するまで、私は同じLangGraphパイプラインを3回書き直しました。フレームワークは、私が指示した通りに正確に動作していました。私が間違った指示を出していただけだったのです。
本番環境向けにステートフルなエージェントを構築する場合、デフォルト設定が助けてくれると思い込まないでください。何が壊れ、どのように修正したのかを以下に示します。
最初の失敗:サイレントなステート消失
私のエージェントは5ステップのオンボーディング・フローを処理していました。ユーザーが後で再開できるように、Postgresを使用して進捗を保存していました。しかし、再開するたびに必ずステップ1から始まってしまうのです。
原因はステートのスキーマにありました。LangGraphでは、各ノードがステートにマージされる更新内容を返します。マージ方法を指定しない場合、デフォルトでは上書き(overwrite)になります。
私はメッセージリストが追記(append)されるものだと思っていました。しかし実際には、新しいノードが追加されるたびに、履歴全体がたった一つのメッセージに置き換わっていました。チェックポインターは、間違ったデータを完璧に保存していたのです。
修正方法:明示的なリデューサー(reducer)を持つ Annotated フィールドを使用する。
• メッセージリストには add 演算子を使用する。
• 辞書(dict)にはカスタムのマージ関数を使用する。
• "step" のような単一の値に対してのみ、デフォルトの上書きを使用する。
2番目の失敗:デシリアライゼーションと並行性
2回目の書き直しでは、新たに2つの問題に直面しました:
- 破損した行:ステートにカスタムオブジェクトを保存していました。シリアライザーがそれらを処理できず、行自体は存在するものの、使用不可能なデータが作成されてしまいました。
- 重複キー:WhatsAppは、レスポンスが遅いとウェブフックをリトライします。2つのメッセージが同時に届いた場合、2つのグラフ実行が同じスレッドに書き込もうとし、データベースの衝突が発生しました。
修正方法:
• カスタムオブジェクトを排除する。プレーンな dict と標準的な LangChain の型のみを使用する。
• ウェブフックはグラフの外で処理する。キューとべき等性キー(idempotency key)を使用して、重複を排除する。
• データベースロックを追加する。1つのスレッドにつき、一度に1つの実行のみが行われるようにする。
3回目の書き直し:安定したパターン
最終バージョンでは、3つの原則に焦点を当てました:
• 小規模なグラフ: 巨大な1つのグラフを3つの小さなサブグラフに分割しました。これにより、バグの影響範囲を抑えることができました。 • 明示的なチェックポイント: すべてのノードの後にチェックポイントを作成するのをやめました。意味のある再開ポイントでのみチェックポイントを作成するようにした結果、データベースへの書き込みを60%削減できました。 • べき等(Idempotent)なノード: これは極めて重要です。すべてのノードは、2回実行されても同じ結果を返す必要があります。ノードがステート内でタスクが既に完了していることを検知した場合、すぐに処理を終了させるべきです。これにより、高コストなモデル呼び出しの二重課金を防ぐことができます。
あなたへの教訓:
- コードを書く前に、reducerのセマンティクスを読み込んでください。
- ステートにカスタムオブジェクトを保存しないでください。
- 並行性制御はグラフの外で行ってください。
- すべてのノードをべき等にしてください。
フレームワークが失敗したのではなく、私の想定が間違っていたのです。
オプションの学習コミュニティ: https://t.me/GyaanSetuAi