(20) 実践!初回LangGraphプロジェクト:ジョークボットで学ぶエージェント型システムの基礎

(20) 実践!初回LangGraphプロジェクト:ジョークボットで学ぶエージェント型システムの基礎

こんにちは!前回のLangGraphの理論編に続いて、今回はいよいよ実践編です。ReadyTensorのエージェント型AI開発者認定プログラムの第5週レッスン2bをもとに、最初のLangGraphプロジェクトとして「ジョークボット」を構築していきます。

この記事では、LLMを使わずにLangGraphの基礎メカニクスを学ぶことで、エージェント型システムの動作原理をしっかりと理解できるようになります。

なぜジョークボットから始めるのか?

LangGraphの基礎に集中するアプローチ

ツール、エージェント、LLMに入る前に、 LangGraphの基礎 — グラフベースワークフローの構築と実行方法をマスターしましょう。

シンプル(で楽しく)保つために、異なるカテゴリからジョークを話してくれる ジョークボットを構築します。ここにはLLMはありません — そしてそれは意図的です。

学習目標の明確化

この例は有用なチャットボットの構築についてでは ありません。以下を助けることについてです:

  • LangGraphがどう動作するかを理解する
  • 状態あり、ノード駆動フローの構造化を練習する
  • 条件付きエッジ、ルーティング、状態更新で自信を得る

LangGraphは通常エージェントと言語モデルで使用されます。しかしここでは、 中核メカニクスに焦点を当てるためにそれらすべてをスキップします — 後で複雑さを追加する時、ボンネットの下で正確に何が起こっているかを知るために。

習得するスキル

以下を学びます:

  • 状態とリデューサー関数の定義
  • 再利用可能で、ロジック駆動ノードの作成
  • 条件付きルーティングでメニューループの構築
  • ステップ間で進化する情報の追跡

最後には、動作するLangGraphアプリ — そしてLangGraphシステムがどう動作するかの明確な精神モデルを持つことになります。

構築するもの:インタラクティブなジョーク話ボット

基本機能

あなたのプロジェクトは、LangGraphで構築されたシンプルで遊び心のあるジョーク話ボットです。 pyjokesライブラリを使用してジョークを取得します。

動作方法は以下の通りです:

  • シンプルなメニューとやりとりして、新しいジョークを聞く、ジョークカテゴリを切り替える、またはボットを終了する
  • ジョークは組み込みライブラリから来て、好きなだけ続けることができます
  • 裏では、LangGraphがロジック、状態遷移、ルーティングを管理します — すべて構造化された、検査可能なグラフ内で

追跡する情報

ボットは2つの重要なことを追跡します:

  • 選択したジョークのカテゴリ(例:neutral、chuck、またはall)
  • すでに聞いたジョーク(実行中のリストを見ることができるように)

グラフノードとして再利用可能な関数を構築し、選択に基づいて動的にルーティングし、LangGraphがステップ間で状態をどう進化させるかを見ます。

そして、あなたも笑うかもしれません 😄

ワークフロー設計:1つのグラフ、4つのノード

グラフ構造の理解

コードを書く前に、構築する グラフの構造を見てみましょう。

このフローはシンプルに見えるかもしれません — そしてそれがポイントです。興味深いほど十分なロジックでLangGraphメカニクスを教えることを意図しています。

4つの主要ノード

🟪 show_menu:メイン制御ノード

これは メイン制御ノードです。実行するたびに、ユーザーに3つのオプションを表示します:

  • 別のジョークを聞く
  • ジョークカテゴリを変更
  • アプリを終了

ユーザーの入力に基づいて、グラフは次のノードの1つにルーティングします。

🟩 fetch_joke:ジョーク取得ノード

このノードは選択されたカテゴリからジョークを取得して表示します。ジョークを表示した後、ユーザーが別の選択をできるように show_menuに戻ります。

🟦 update_category:カテゴリ更新ノード

これによりユーザーは新しいジョークカテゴリ(「chuck」や「neutral」など)を選択できます。更新後、メニューに戻ります。

⛔ exit_bot:終了ノード

ユーザーが終了を選択すると、このノードはお別れを言い、特別な <strong>end</strong>ノードにルーティングしてグラフを終了します。

システムの流れ

システム全体は、ユーザーが明示的に終了を選択するまでループします。この設定により、以下を練習する素晴らしい小さなサンドボックスが得られます:

  • ループ動作
  • 条件付きルーティング
  • 動的状態更新

LangGraphによる実装:5つのステップ

統一された構築プロセス

前のレッスンで、任意のLangGraphシステムを構築する5つのステップを学びました:

  1. 状態を定義
  2. ノード関数を書く
  3. グラフビルダーを作成
  4. ノードとエッジを追加
  5. グラフをコンパイルして実行

ここでも同じ構造に従います — しかし今回は実践的にやります。

ステップ1:状態を定義

ボットが記憶すべきことを宣言することから始めます。

class Joke(BaseModel):
    text: str
    category: str

class JokeState(BaseModel):
    jokes: Annotated[List[Joke], add] = []
    jokes_choice: Literal["n", "c", "q"] = "n"  # 次のジョーク、カテゴリ変更、または終了
    category: str = "neutral"
    language: str = "en"
    quit: bool = False

重要なポイント

  • Annotatedaddと一緒に使用して、新しいジョークをリストに自動的に追加
  • jokesのリデューサーは新しいジョークを既存のリストに追加し、これまでに話されたことを追跡
  • 他のフィールドでは、現在の値を単純に上書きするデフォルトリデューサーを使用

ステップ2:ノード関数を書く

各ノードは状態を受け取り、更新を返す単なる関数です。

メニュー表示ノード

def show_menu(state: JokeState) -> dict:
    user_input = input("[n] Next  [c] Category  [q] Quit\n> ").strip().lower()
    return {"jokes_choice": user_input}

ジョーク取得ノード

def fetch_joke(state: JokeState) -> dict:
    joke_text = get_joke(language=state.language, category=state.category)
    new_joke = Joke(text=joke_text, category=state.category)
    return {"jokes": [new_joke]}

カテゴリ更新ノード

def update_category(state: JokeState) -> dict:
    categories = ["neutral", "chuck", "all"]
    selection = int(input("Select category [0=neutral, 1=chuck, 2=all]: ").strip())
    return {"category": categories[selection]}

終了ノード

def exit_bot(state: JokeState) -> dict:
    return {"quit": True}

ルーター関数

次に行くノードを決定する ルーター関数も定義します:

def route_choice(state: JokeState) -> str:
    if state.jokes_choice == "n":
        return "fetch_joke"
    elif state.jokes_choice == "c":
        return "update_category"
    elif state.jokes_choice == "q":
        return "exit_bot"
    return "exit_bot"

重要な設計原則:状態管理の考え方

宣言的アプローチ

ノード関数のどれも状態を直接変更していないことに気づいたかもしれません。代わりに、各ノードはフィールド更新の辞書を返すだけです。

これは設計によるものです:

  • 状態をその場で変更しません
  • {field_name: new_value}を返すだけです
  • LangGraphがグラフ内で次の状態の作成を処理します

利点

この宣言的アプローチにより、ロジックがきれいで、予測可能で、デバッグしやすくなります — 特にグラフが複雑になるにつれて。

だから: 変更したものだけを返し、残りはLangGraphに処理させてください。

ステップ3&4:グラフを作成してノード+エッジを追加

ビルダー関数を使用してグラフを作成します。

def build_joke_graph() -> CompiledStateGraph:
    workflow = StateGraph(JokeState)

    1. ノード追加
    workflow.add_node("show_menu", show_menu)
    workflow.add_node("fetch_joke", fetch_joke)
    workflow.add_node("update_category", update_category)
    workflow.add_node("exit_bot", exit_bot)

    1. エントリーポイント設定
    workflow.set_entry_point("show_menu")

    1. 条件付きエッジ追加
    workflow.add_conditional_edges(
        "show_menu",
        route_choice,
        {
            "fetch_joke": "fetch_joke",
            "update_category": "update_category",
            "exit_bot": "exit_bot",
        }
    )

    1. 固定エッジ追加
    workflow.add_edge("fetch_joke", "show_menu")
    workflow.add_edge("update_category", "show_menu")
    workflow.add_edge("exit_bot", END)

    return workflow.compile()

構造の説明

この関数は以下を行います:

  • StateGraphをインスタンス化し、状態タイプを指定
  • ノードを登録し、各機能を名前に関連付け
  • エントリーポイントを設定し、グラフの開始点を定義
  • ルーティングロジックを定義し、条件付きおよび固定エッジを追加
  • グラフをコンパイルし、実行のために準備

ステップ5:グラフを実行

すべてが配線されたら、グラフを実行するメイン関数を持つことができます:

def main():
    graph = build_joke_graph()
    final_state = graph.invoke(JokeState(), config={"recursion_limit": 100})

それだけです — 動作する最初の完全なLangGraphです。

実際の動作:ジョークボットの体験

ユーザーインターフェース

スクリプトを実行すると、シンプルですが機能的なインタラクティブメニューが得られます。以下ができます:

  • 好きなだけジョークをループ
  • セッション中にカテゴリを切り替え
  • 完了時にきれいに終了
⚠️ はい、UIは極めて… ミニマリストです。LangGraphを学ぶためにここにいるのであって、デザイン賞を獲得するためではありません 😅

サンプルセッション

典型的なセッションは以下のような感じです:

🎉==========================================================🎉
    LangGraphジョークボットへようこそ!
    この例はLLMなしでエージェント型状態フローを実演します
============================================================

🚀==========================================================🚀
    ジョークボットセッションを開始中...
============================================================
🎭 メニュー | カテゴリ:NEUTRAL | ジョーク:0
--------------------------------------------------
オプションを選んでください:
[n] 🎭 次のジョーク  [c] 📂 カテゴリ変更  [q] 🚪 終了
ユーザー入力:n

😂 3人のデータベース管理者がNoSQLバーに入った。しばらくして彼らは出て行った。テーブルが見つからなかったからだ。

============================================================
🎭 メニュー | カテゴリ:NEUTRAL | ジョーク:1
--------------------------------------------------
オプションを選んでください:
[n] 🎭 次のジョーク  [c] 📂 カテゴリ変更  [q] 🚪 終了
ユーザー入力:q

🚪==========================================================🚪
    さようなら!
============================================================

注目すべきポイント

動作中の完全なループを見ることができます:

  • 入力 → 状態更新 → 再ルーティング → 繰り返し
  • グラフが遷移を処理する
  • 状態(カテゴリやジョーク数など)が自然に進化する
  • ユーザーは常に一貫したメニュー体験に戻る

シンプルで、予測可能で、拡張しやすいです — これはエージェント型ワークフロー構築の初期段階で正確に求めるものです。

拡張演習:ボットをカスタマイズしよう

スキルアップのための課題

最初のLangGraphシステムを構築したので、その新しい筋肉を伸ばす時です。ジョークボットにいくつかの追加機能で拡張してみてください。

🗣️ 演習1:多言語対応

pyjokesは複数の言語(endeesなど)をサポートします。ボットを多言語にしましょう!

実装内容:

  • JokeStatelanguageという新しいフィールドを追加
  • ユーザーが言語を選択できる新しいノード(例:update_language)を追加
  • 新しいメニューオプション([l] 🌐 言語変更など)を追加してルーティングロジックを更新

🔄 演習2:リセット機能

ユーザーにジョーク履歴をリセットする方法を与えましょう。

実装内容:

  • 新しいメニューオプション[r] 🔁 ジョーク履歴リセットを追加
  • 過去のジョークのリストをクリアする新しいノード(例:reset_jokes)を作成
  • 新しいオプションをサポートするようにルーティングロジックを更新

✨ ボーナス演習:LLM駆動ジョーク

次のレッスンでは、 pyjokesを実際のLLM呼び出しに置き換えますが、今それを自分で試したい場合:

実装内容:

  • fetch_jokeノードロジックをLLM呼び出し(OpenAI、Anthropic、Mistralなど)と交換
  • 選択されたカテゴリの単一ジョークを返すプロンプトを設計
  • categoryを使用してテーマを制御:
    • "programmer" → 一般的な開発ジョーク
    • "chuck" → チャック・ノリスジョーク
    • "dad" → 古典的なお父さんジョーク

コメント

タイトルとURLをコピーしました