LangGraphを活用した推理アドベンチャーゲームエージェントの実装

# はじめに

LangGraphは、複雑なAIエージェントシステムを構築するための強力なフレームワークです。この記事では、LangGraphを活用して推理小説のストーリーを動的に生成しながら、登場人物との会話を通じて犯人を探す推理アドベンチャーゲームエージェントの実装について詳しく解説します。この実装例を通じて、LangGraphの特徴や活用方法について理解を深めていきましょう。

## LangGraphとは

LangGraphは、LangChainの上に構築されたフレームワークであり、エージェントやステートマシンの作成において、ループ型のワークフローを実現するためのグラフベースの構文を提供します。単なる一方向の処理だけでなく、条件分岐や複雑なフローの実装を可能にする点が特徴です。

従来のLangChainでは線形的なチェーン構造（A→B→C）が主流でしたが、LangGraphは条件分岐や複雑なフローの実装を容易にします。例えば「もしこの条件ならAへ、そうでなければBへ」といった分岐処理を簡潔に表現できるようになりました。

### LangGraphの主要コンポーネント

LangGraphは以下の主要なコンポーネントで構成されています：

1. **Graph**: 全体の処理構造を表す
2. **Node**: 個々の処理単位を表す要素
3. **Edge**: ノード間の接続を表し、遷移の条件やアクションを定義
4. **State**: ノード間で渡るデータ
5. **Router**: 条件付きで次のノードを決める関数
6. **Entry Point/Finish Point**: グラフの開始点と終了点
7. **Reducers**: Stateを状態更新するときに使うロジック

この構造により、複雑なAIエージェントシステムを視覚的かつ論理的に設計することができます。

## 推理アドベンチャーゲームエージェントの実装

今回実装した推理アドベンチャーゲームエージェントは、LangGraphの特徴を活かして、ストーリーの動的生成、NPCの作成、NPCとの会話を組み合わせた複合的なシステムを作成してみます。「オホーツクに消ゆ」や「探偵 神宮寺三郎シリーズ」のようなコマンド選択式のミステリーアドベンチャーのような仕組みと、進捗の管理までを実装します。

### システムの全体構造

まず、システム全体のグラフ構造を見てみましょう。`build_graph`関数でグラフを構築しています：

```python
def build_graph():
    """状態グラフを構築する関数"""
    # グラフの初期化
    graph = StateGraph(GraphState)
    # ノードの追加
    graph.add_node("generate_story", generate_story)
    graph.add_node("create_npc", create_npc)
    graph.add_node("talk_with_npc", talk_with_npc)
    # ルーターをノードとして追加
    graph.add_node("router", lambda x: x) # 単純な恒等関数
    # エントリーポイントを設定
    graph.set_entry_point("router")
    # エッジの追加
    graph.add_conditional_edges(
        "router",
        router,
        {
            "generate_story": "generate_story",
            "create_npc": "create_npc",
            "talk_with_npc": "talk_with_npc"
        }
    )
    graph.add_edge("generate_story", END)
    graph.add_edge("create_npc", END)
    graph.add_edge("talk_with_npc", END)
    # グラフのコンパイル
    g = graph.compile()
    return g
```

この実装では、`router`ノードをエントリーポイントとし、ユーザーの操作（`operation`）に応じて適切なノード（`generate_story`、`create_npc`、`talk_with_npc`）に遷移するように設計されています。このようにLangGraphでは、条件付きエッジを使用して複雑な処理フローを設計できます。

### 状態管理の実装

状態管理は`GraphState`クラスで行われています：

```python
class GraphState(TypedDict):
    # 生成されたストーリー
    story: str
    # NPCのリスト
    npcs: List[Dict[str, Any]]
    # NPCの会話履歴
    conversations: Dict[str, List[str]]
    # 現在選択されているNPC
    current_npc: Optional[str]
    # 操作の種類
    operation: Optional[str]
```

このクラスにより、ストーリー、NPCのリスト、会話履歴などの状態をノード間で共有・更新することができます[^4]。これはLangGraphの強力な状態管理機能を活用しており、複雑なアプリケーションでも一貫性のある状態を維持できる利点があります。

### NPCの作成と構造化

NPCの作成には、構造化された出力を得るためにPydanticモデルを活用しています：

```python
class NPC(BaseModel):
    name: str = Field(description="NPCの名前")
    age: int = Field(description="NPCの年齢")
    occupation: str = Field(description="NPCの職業")
    personality: str = Field(description="NPCの性格")
    background: str = Field(description="NPCの背景")
```

また、改良版のNPC作成関数では、`StructuredOutputParser`を使用してより堅牢なNPC生成を実現しています：

```python
def create_structured_npc(state: GraphState) -&gt; GraphState:
    """構造化されたNPCを作成する関数"""
    from langchain_core.output_parsers import StructuredOutputParser
    from langchain_core.prompts import PromptTemplate
    # NPCのスキーマを定義
    npc_parser = StructuredOutputParser.from_pydantic(NPC)
    format_instructions = npc_parser.get_format_instructions()
    # ...プロンプト作成と実行のコード...
```

この実装により、LLMからの出力を確実に構造化された形式で取得することができます。また、この方法は「スキーマによる出力制約」と呼ばれるテクニックで、LangChainやLangGraphにおいて高品質な構造化データを得るための重要なパターンです。

### ストーリー生成の実装

ストーリー生成は、既存のストーリーを考慮して徐々に展開していくよう設計されています：

```python
def generate_story(state: GraphState) -&gt; GraphState:
    """ストーリーを生成する関数"""
    current_story = state.get("story", "")
    # システムプロンプトを作成
    system_prompt = """
    あなたは、推理小説のようなストーリーを作成するAIです。
    ある日、ペンションで起きた殺人事件が起き、吹雪で閉じ込められたオーナーと客によってストーリーが進んでいきます。
    すでに存在するストーリーに続けて、200文字程度の続きを作成してください。
    ストーリーは一貫性を持ち、徐々に展開していくようにしてください。
    """
    # ...LLM呼び出しと状態更新のコード...
```

ここでの工夫点は、ストーリーを一度に全て生成するのではなく、段階的に生成することで、ユーザーとの対話やNPCの会話に応じてストーリーを柔軟に展開させることができる点です[^2]。この手法はLangGraphの状態管理機能を活用した典型的な例で、複雑な対話型アプリケーションの開発に適しています。

### NPCとの会話の実装

NPCとの会話は、選択されたNPCがストーリーや他のNPCの会話を考慮して応答するよう設計されています：

```python
def talk_with_npc(state: GraphState) -&gt; GraphState:
    """指定されたNPCとストーリーについて会話する関数"""
    current_npc = state.get("current_npc")
    # ...NPCの情報取得...
    
    system_prompt = f"""
    あなたは「{current_npc}」というキャラクターとして振る舞います。
    説明: {npc_info["description"]}
    あなたはストーリーについて自分の視点から話します。
    すでに他のキャラクターが話した内容を考慮し、それに関連する話をしてください。
    プレイヤーに対して、起きた事件について、さまざまな角度から犯人に繋がるヒントを与え、プレイヤーが解決に導くようにします。
    あなたの発言は、他のキャラクターの発言につながる可能性があります。
    """
    
    # 他のNPCの会話履歴を取得して考慮
    other_conversations = []
    for name, convs in state.get("conversations", {}).items():
        if name != current_npc and convs:
            other_conversations.extend([f"{name}: {conv}" for conv in convs])
    
    # ...LLM呼び出しと状態更新のコード...
```

ここでの大きな工夫点は、他のNPCの会話履歴を考慮することで、NPCたちがお互いの発言を認識し、一貫性のある会話が行えるようにしている点です[^3][^4]。これはマルチエージェント連携の良い例であり、LangGraphを使用することで異なるエージェント間の情報共有と連携が容易になっています。

## 技術的な工夫点とLangGraphの活用

このプロジェクトでは、LangGraphの特性を活かした以下のような技術的な工夫を行っています：

### 1. エントリーポイントとタスクチェックポイントの活用

LangGraphの`set_entry_point`メソッドを使用して、システムのエントリーポイントを明示的に定義しています[^4]。これにより、システムの起動時に必ず実行される処理を指定することができます。また、各ノードがタスクのチェックポイントとなっており、処理の進行状況を明確に追跡できます。

### 2. 条件付きエッジによる動的なフロー制御

`add_conditional_edges`メソッドを使用して、ユーザーの操作に応じて適切なノードに遷移するように設定しています[^5]。これにより、ユーザーとシステムのインタラクションを柔軟に制御することができます。

```python
graph.add_conditional_edges(
    "router",
    router,
    {
        "generate_story": "generate_story",
        "create_npc": "create_npc",
        "talk_with_npc": "talk_with_npc"
    }
)
```


### 3. 状態（State）の効果的な管理

TypedDictを使用して型安全な状態管理を実現しています[^6]。これにより、ノード間でのデータの受け渡しが明確になり、バグの発生リスクを低減させています。

### 4. NPCの構造化出力による質の向上

Pydanticモデルと`StructuredOutputParser`を組み合わせることで、LLMからの出力を確実に構造化された形式で取得し、高品質なNPC生成を実現しています[^2][^5]。


![スクリーンショット 2025-05-09 7.27.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43766/1206298b-bdbe-429d-9de2-521ace491b57.png)



## 実行させてみたら。。

1. 山田健一（やまだ けんいち）
- **年齢**: 45歳
- **職業**: ペンションオーナー
- **性格**: 温厚で親切な人物だが、状況に対して非常に敏感で、ストレスに弱い。責任感が強く、客を守ろうとするが、内心では恐怖を感じている。冷静に対応しようと努力するが、時折焦りが見え隠れする。

2. 佐藤翔太（さとう しょうた）
- **年齢**: 22歳
- **職業**: 大学生（心理学専攻）
- **性格**: 明るく社交的だが、内心は少し自信がない。友人関係を大切にし、特に松本とは親しい間柄。表情が豊かで、感情が顔に出やすい。松本の死に対して強いショックを受けているが、同時に疑われることに不安を抱いている。

3. 田中美香（たなか みか）
- **年齢**: 38歳
- **職業**: 会社員（営業職）
- **性格**: 頭の回転が早く、合理的な考えをする。母親としての一面もあり、他人の面倒をよく見るが、内心では自分のことを優先したい思いも持っている。冷静な判断ができる反面、感情的になることもある。松本の死に直面し、恐怖と不安を抱えている。

4. 鈴木達也（すずき たつや）
- **年齢**: 68歳
- **職業**: 退職した元教師
- **性格**: 穏やかで落ち着いているが、過去の経験から疑念を持ちやすい。人間関係に敏感で、周囲の雰囲気を察知するのが得意。松本の死に対して非常に冷静だが、内心では「いったい誰が？」と疑いを抱いている。若者たちに対しては優しく接するが、時折厳しい言葉を投げかけることも。

吹雪の中、ペンションの中は緊張感に包まれていた。一人の客が叫び声を上げた。「誰かが殺された！」その声が響き渡った。オーナーの佐藤は、慌てて部屋を飛び出した。佐藤は、血の跡を追いながら、他の客たちを呼び寄せた。客たちは恐怖で震えながら、佐藤の後を追った。廊下の奥、部屋の扉が開いていた。

「こんな状況で、一体何が起きたのかしら…。血の跡、誰かが傷ついているのかもしれない。私はメモを取る準備をしておくわ。周囲の人たちの行動も注視しないと。みんな、冷静になりましょう。何か手がかりが見つかるかもしれません。」

「皆さん、落ち着いてください。まず、状況を整理しましょう。」私はペンションの雰囲気が緊迫しているのを感じ、周囲を見渡して声をかけた。「血の跡があるということは、何か手がかりが残っているかもしれません。誰が最後に被害者を見たのか、思い出せる人はいませんか？」

「この状況、誰かが計画的に仕組んだ可能性があります。私の取材ノートには、ここにいる全員の情報が記録されています。もしかしたら、誰かの過去が関わっているのかもしれません。冷静に行動して、手がかりを探りましょう。」

## まとめ

本記事では、LangGraphを活用した推理アドベンチャーゲームエージェントの実装について詳しく解説しました。LangGraphの特徴である状態管理、条件付きエッジ、ノード間の情報共有などを活用することで、複雑なマルチエージェントシステムを構築できることが分かりました。

推理小説というストーリーテリングと、NPCとの対話を組み合わせたこのプロジェクトは、LangGraphの可能性を示す興味深い例と言えます。今回紹介した技術やアプローチは、教育、カスタマーサポート、ビジネスプロセス自動化など、様々な分野に応用できる可能性を秘めています。

LangGraphは比較的新しいフレームワークですが、複雑なAIエージェントシステムの構築に強力なツールとなっており、今後さらに多くの創造的な応用が期待されます。

# 全ての実装

https://github.com/oggata/multiplayer-survival


![スクリーンショット 2025-04-25 17.58.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43766/244c738d-b08f-4287-b39a-80acd4afd8a2.png)