LangGraph入門!LLMアプリ開発ライブラリのQuick StartをAmazon Bedrockでやってみる

本記事の目的

こんにちは、エンジニアのYokomachiです。

昨今LLM(大規模言語モデル)を活用したアプリケーション開発のアプローチとしてDifyやGumloop、Makeなどによるノーコード・ローコードによる開発もだいぶメインストリームになりつつある印象です。

弊社ギークフィードも先日Makeのオフィシャルパートナーとなり、社内活用や案件導入も進んできています。

が、今回はよりマニュアルにコードを書いてアプリケーション開発するために、LLMアプリケーション開発向けのライブラリ「LangGraph」を使ったマルチエージェントの開発に入門してみたいと思います。

LangGraphとは?

「LangGraph」は同じくLLMアプリ開発向けのライブラリ「LangChain」をベースに、ステートフルなマルチアクターアプリケーションを構築するために作られたライブラリです。

噛み砕くと、ユーザーの入力内容やワークフロー内の処理結果などを状態として保持し、そのデータに応じて動的に振る舞いを変えるアプリケーションを構築するためのライブラリ、ということになります。

Home | LangGraph

LangGraphの特徴を挙げてみます。

  • LangChainは一方向にタスクを処理する構造であるのに対し、LangGraphはサイクル(ループ)を含むフローをサポート
  • LangChainはシンプルなワークフロー向け、LangGraphはより複雑で複数のエージェントが連携するワークフロー向け
  • LangGraphはワークフローに人間が介入し、エージェントが計画した次のアクションを承認または中断することが可能

また、LangGraphはその名の通りエージェントのワークフローをグラフで定義します。

このグラフは、Nodes(点)とEdges(線)で構成され、各NodeやEdgeで使用される情報はStateで管理されます。

State: アプリケーションの現在のスナップショットを表す共有データ構造。 Pythonの型は何でもかまいませんが、通常はTypedDictかPydantic BaseModelです。

Nodes: エージェントのロジックをエンコードするPython関数。 現在のStateを入力として受け取り、何らかの計算や副作用を実行し、更新されたStateを返します。

Edges: 現在のStateに基づいて、次に実行するNodeを決定するPython関数。 条件分岐であったり、固定遷移であったりします。

LangGraph Glossary | LangGraphより引用

グラフのイメージは以下のようになります。

 

 

導入

チュートリアルの内容について

今回はLangGraphの公式サイトで公開されているQuick Startの内容を、上から順にやっていきたいと思います。

Quick Start | LangGraph

このQuick Startは全部で7パートからなり、最初は単純なチャットボットを構築し、パートを経るごとにLangGraphの特徴的な機能を少しずつ追加していく形のチュートリアルとなっています。

最終的にはTool use、Checkpoint、Human-in-the-loop、Time Travelをサポートするチャットボットが出来上がります。

また、ユーザーの入力制御周りに関してはQuick Startの内容からカスタマイズしています。

各セクションでコードの内容の説明はしていきますが、全体のコードはGithubで公開していますのでこちらもご覧ください。

https://github.com/n-yokomachi/langgraph_tutorial_with_bedrock

構成図

構成図は以下の通りとなります。

 

LLMアプリケーションとなるPythonスクリプトはCloud9上で開発を行います。

Quick StartのサンプルコードではAnthropicのAPIを利用していますが、今回はAmazon BedrockのAnthropic Claude 3.5 Sonnetを利用できるように若干コードをカスタマイズします。

なお、BedrockのAPIはモデル間でパラメータが統一されているConverse APIを使用します。

動作環境

OS:Amazon Linux release 2023.5.20240916

ライブラリ:requirements.txtを参照 (EC2上で全ライブラリについて書き出しているので不要なものも含まれます)

 

LLM:Amazon Bedrock Anthropic Claude 3.5 Sonnet (事前にAWSコンソール上でLLMを有効化しておく必要があります。)

Part 1: Build a Basic Chatbot

このセクションではまず基本的なチャットボットの構築を行います。

ユーザーが入力したメッセージに直接LLMが回答して終了するだけのものです。

グラフは以下のとおりです。

 

コードの概要は以下の通りです。

  • StateクラスでNodeなどで利用するオブジェクトと、その更新方法をreducer関数として定義
  • chatbotという関数をgraph_builder.add_node()でNodeとして追加
  • chatbotがinvokeするLLMはChatBedrockConverseでConverse APIを実行するように変更
  • graph_builder.set_entry_point(), graph_builder.set_finish_point()でグラフの始点、終点をEdgeとして定義
  • 最後のWhile文はこのグラフをチャットボットとして利用するためのインタフェース
  • ソースコード

 

実行結果は以下のようになります。

「langgraphとは何ですか?」という質問にLLMが自身の知識範囲で回答しています。

Part 2: Enhancing the Chatbot with Tools

このセクションではチャットボットにWeb検索ツールのNodeを追加します。

グラフは以下の通りです。

 

Part1からの変更点は以下の通りです。

  • Web検索を行うAPIとしてTavily Search APIを使うように設定し、add_node()でNodeに追加
  • bind_tools()でLLMが使えるToolの紐づけ(tool use)を定義
  • 事前定義された関数tools_condition()を条件付きエッジとして定義(tools_condition()はLLM実行後のメッセージが「tool_calls」だった場合、toolsノードを呼び出すように定義されている)
  • ソースコード

 

実行結果は以下のようになります。

「langgraphとは何ですか?」と質問に対し、今度はWeb検索を実行してその結果を要約していることがわかります。

Part 3: Adding Memory to the Chatbot

このセクションではチャットボットにメモリを追加し、会話のコンテキストを保存できるようにします。

LangGraphではこのメモリをCheckpointと呼称し、会話履歴の保存だけではなく、グラフ全体の状態を保存し、グラフの中断や再開、エラーからの回復や、後述のhuman-in-the-loopやTime travel機能に活用できるようです。

まずは単純な会話保存としてのメモリを実装します。

グラフはPart 2から変更ありません。

 

Part2からの変更点は以下の通りです。

  • CheckpointerとしてMemorySaverをインポート
  • MemorySaverをCheckpointerとして引数に渡してグラフをコンパイル
  • チャットボットと会話する際にはスレッドIDを指定するように変更
  • ソースコード

 

実行結果は以下のようになります。

最初にインプットした私の名前を記憶していることがわかります。

Part 4: Human-in-the-loop

このセクションではグラフのワークフローに人間が介入できるようにします。

interrupt_before機能を使用して、フローの中断を行い、人間が再開を指示できるようにします。

グラフは以下のようになります。

 

Part3からの変更点は以下の通りです。

  • グラフのコンパイルの第二引数にinterrupt_beforeでtoolsを指定。これによりtoolsの使用前に中断が入るようになる
  • 入力制御のロジックに、何も入力しなかったときはNoneをgraph.stream()の第一引数に渡すように処理追加。stream()にNoneが渡されたとき、中断部分からの再開指示となる。
  • ソースコード

 

実行結果は以下のようになります。

LLMの1回目の回答でいきなりTool useをせず、検索エンジンを使う提案が行われます。

その後、無入力でNoneを渡すとそれをGOサインとしてTool useを実行し、検索結果の要約を行っていることがわかります。

Part 5: Manually Updating the State

このセクションではHuman-in-the-loopの一環として、グラフの状態を手動で変更する方法を実装します。

先ほどのようにLLMがTool useを提案した際に、ユーザー自身が手動で状態を変更し、LLMがすでに適切な回答をしたと判断させます。

具体的なシナリオとしては、まずユーザーが「LangGraphとは何か?」と質問をすると、LLMは「Webで検索してみましょう」と提案します。この状態に対して「『LangGraphとは~です』という回答をLLMがすでに回答した」という情報を手動で追加します。これによりLLMは、Tool useの必要がないと判断し会話を終了します。

グラフはPart5から変更ありません。

 

また、ソースコードに関してもグラフ自体のソースコードには変更ありません。

入力制御の部分で、中断が入った際に「update」を入力すると、グラフの状態を変更する関数を呼び出すようにコードを追加しています。

  • ソースコード

 

実行結果は以下のようになります。

「LangGraphはステートフルでマルチアクターなLLMアプリケーションを構築するためのライブラリです。」というのはこちらが手動で追加したLLMの回答です。これによりTool useをせずにワークフローが完了しています。

なお、上記はStateにメッセージを追加する方法となります。

LLMがすでに回答したメッセージを直接上書きするにはメッセージのIDを指定する必要があるようです。

Part 6: Customizing State

このセクションではグラフが利用するStateに新しいフィールドを追加して、より複雑なフローを定義できるようにします。

Part4, 5ではTool useの前には必ず中断するようにグラフを定義していましたが、人間に介入させるかどうかの選択肢をグラフに持たせるようにします。

シナリオとしては、このLLMアプリを使うユーザーのほかにアプリ運営側に「専門家」がいるとします。ユーザーがLLMアプリでは回答できない専門的な質問を行った場合に、「専門家」に対してユーザーの質問をエスカレーションするTool use機能をこのセクションで追加します。

グラフは以下のようになります。

 

Part5からのコードの変更点は以下の通りです。

  • Stateにask_humanフラグを追加
  • RequestAssistanceクラスを「人間の専門家にユーザーの要求をエスカレーションするTool」として追加し、Web検索のToolsと同様にLLMにバインド
  • 「専門家」の介入後に動作するhuman_nodeノードを追加。直前に「専門家」が介入した場合、ask_humanフラグを解除する。「専門家」が応答をしなかった場合も、「No response ~」というメッセージを返して、ask_humanフラグを解除しフローを続行する。
  • ソースコード

 

実行結果は以下のようになります

  1. まずユーザー入力として「AIエージェントの構築に専門家のサポートが必要です。サポートを依頼できますか?」と質問しました。これは専門家の介入が必要な質問のため、RequestAssistanceがTool useされています。
  2. 専門家からの入力を再現するためにupdateを入力します。
  3. 専門家からの入力をもとにLLMが回答を生成しています。
  4. 続いてユーザー入力として「LangGraphの一般的な特徴について教えてください」と質問しました。この場合、LLMはWeb検索のTool use選択し、その検索結果で回答を生成しています。

というように、LLMがユーザーの入力に応じて使用するToolを選択していることがわかります。

Part 7: Time Travel

最後のセクションです。

このセクションではTime travel機能を使い、グラフの状態を特定の状況に巻き戻してみます。

これにより任意の個所からワークフローの分岐をやり直したり、別の結果を誘導させることができるようになります。

Part6のグラフから内容自体は変わりませんが、ユーザーからの入力のバリエーションを増やしてみます。

  • replayを入力すると、graph.get_state_history()でこれまでの会話履歴を取得して表示します。
  • to_replay {checkpoint_id}を入力すると、会話履歴の特定のチェックポイントにさかのぼります。
  • ソースコード

 

実行結果は以下のようになります

  1. まずは私の年齢をインプットします。
  2. 続けて明日の天気を聞きます。
  3. 続けて私の名前をインプットします。
  4. ここでreplayを入力し、会話履歴を表示させます。
  5. 序盤のメッセージ数1のときのcheckpoint_idをto_replayで渡してグラフの状態を巻き戻します。
  6. 私の名前を聞いてみましたが、名前をインプットしたのはこのチェックポイントより後のため、参照できずに答えられない状態になっていることが確認できました。

おわりに

というわけで長くなりましたが、LangGraphのQuick Start全7パートをやってみました。

まだ理解が追いつかない部分も多くありますが、概観や特徴をつかむのにはよい練習になりました。

また実際に手元でグラフのワークフローが動くのは単純に面白いですね。

今後もLLM関連のライブラリには知見を深めていきたいですし、ノーコード・ローコードアプリとの組み合わせだとか、より複雑なグラフの構築なども試して、自信を持ってLLMアプリ開発できますと言えるくらいにスキルを身に着けていきたいところです。

参考

この記事が気に入ったら
いいね ! しよう

Twitter で

【採用情報】一緒に働く仲間を募集しています

採用情報
ページトップへ