LangGraphでステートフルなチャットAIを実装した件

# はじめに
web、discord、twitter、youtubeなどで動作するチャットAI「シャノン」を作りました。
このチャットAI「シャノン」はtoolの使用が可能で、必要に応じて事前に作成しておいたbingSearchツールやwolframAlphaツールを使用することができます。
また、**内部状態として感情・行動計画を持ち、感情的な出力や現在のタスク進行状況によって行動計画を動的に修正することが可能**です。
今回は基幹部分のみ紹介していきます。
# 動作例
### Web上に構築したシャノン監視用UI
<img src="https://storage.googleapis.com/zenn-user-upload/1a1b70ab249b-20250302.png" width="***サイズ***">

- 左側のサイドバーでは、ログの検索、tool一覧の表示、定時タスクの表示・実行、各種bot（discord botなど）の状態表示・起動・停止が可能です。
- 中央のステータス画面では、シャノンの行動計画と感情をリアルタイムで確認できます。
- 右側のチャット欄では、シャノンとテキストと音声で会話できます。

### discord上での動作
検索ツール
<div align="top">
    <img src="https://storage.googleapis.com/zenn-user-upload/e2bb29cc0ba9-20240701.jpeg" width="220px">
    <img src="https://storage.googleapis.com/zenn-user-upload/b4582441eef5-20240701.jpeg" width="220px">
    <img src="https://storage.googleapis.com/zenn-user-upload/d5813a567e09-20240701.jpeg" width="220px">
</div>

画像認識・画像生成
<div align="top">
    <img src="https://storage.googleapis.com/zenn-user-upload/83ffe3c76414-20240701.jpeg" width="220px">
    <img src="https://storage.googleapis.com/zenn-user-upload/1b9526c12f2b-20240701.jpeg" width="220px">
    <img src="https://storage.googleapis.com/zenn-user-upload/1b2a67094fe3-20240701.jpeg" width="220px">
</div>

表作成＆表作成
<div align="top">
    <img src="https://storage.googleapis.com/zenn-user-upload/e230852ba347-20240701.jpeg" width="320px">
    <img src="https://storage.googleapis.com/zenn-user-upload/77ae2ce993d4-20240701.jpeg" width="320px">
</div>

新規スキル獲得関係
<div align="top">
    <img src="https://storage.googleapis.com/zenn-user-upload/9e8919fdd58d-20240701.jpeg" width="220px">
    <img src="https://storage.googleapis.com/zenn-user-upload/1c085abaa2d2-20240701.jpeg" width="220px">
    <img src="https://storage.googleapis.com/zenn-user-upload/b0efd774fd67-20240701.jpeg" width="220px">
</div>

### twitter上での動作
12星座占い・今日は何の日？・明日の天気予報
<div align="top">
    <img src="https://storage.googleapis.com/zenn-user-upload/45408f2995b9-20250303.png" width="220px">
    <img src="https://storage.googleapis.com/zenn-user-upload/4f9cbde5d090-20250303.png" width="220px">
    <img src="https://storage.googleapis.com/zenn-user-upload/de9d8ffab61e-20250303.png" width="220px">
</div>

- シャノンのアカウント
https://x.com/I_am_Sh4nnon

### Youtube上での動作
![](https://storage.googleapis.com/zenn-user-upload/ff28bf03848f-20250303.png)

- コメントの内容・ユーザー名、動画のタイトル・概要欄を元に生成

# 使用技術
### バックエンド
- LangGraph
- LangChain
- OpenAI API
- TypeScript
- Discord API
など
### フロントエンド
- React
- TypeScript
- chatscope
- chart.js
など

# 大まかな構成
![](https://storage.googleapis.com/zenn-user-upload/9949466bf8c5-20250303.jpg)
### LangGraph
- 状態としてタスクID、行動計画、感情パラメータなどを保持

行動計画の形式
```ts
export type TaskStatus = "pending" | "in_progress" | "completed" | "error";

export interface TaskTreeState {
  goal: string;
  strategy: string;
  status: TaskStatus;
  error?: string | null;
  subTasks?:
    | {
        subTaskGoal: string;
        subTaskStrategy: string;
        subTaskStatus: TaskStatus;
      }[]
    | null;
}
```

感情パラメータの形式
```ts
export interface EmotionType {
  emotion: string;
  parameters: {
    joy: number;
    trust: number;
    fear: number;
    surprise: number;
    sadness: number;
    disgust: number;
    anger: number;
    anticipation: number;
  };
}
```

- 開始ノード=>感情ノード=>プランニングノード=>ツール使用ノード=>感情ノードというようにループするように設計。プランニングノードで終了するかどうかを判定。
### 各Agentの連携
- 各AgentはeventBusをハブとして双方向にデータをやり取りできるように設計

eventBusのコード
```ts
export class EventBus {
  private listeners: Map<EventType, Array<(event: Event) => void>> = new Map();

  /**
   * イベントタイプに対応するコールバック関数を追加する
   * @param eventType イベントタイプ
   * @param callback コールバック関数
   */
  subscribe(
    eventType: EventType,
    callback: (event: Event) => void
  ): () => void {
    if (!this.listeners.has(eventType)) {
      this.listeners.set(eventType, []);
    }
    this.listeners.get(eventType)?.push(callback);

    // unsubscribe関数を返す
    return () => {
      const callbacks = this.listeners.get(eventType);
      if (callbacks) {
        this.listeners.set(
          eventType,
          callbacks.filter((cb) => cb !== callback)
        );
      }
    };
  }

  /**
   * イベントを送信する
   * @param event イベント
   */
  publish(event: Event) {
    this.listeners.get(event.type)?.forEach((callback) => {
      // targetMemoryZonesが指定されている場合、対象メモリゾーンのみに配信
      if (
        !event.targetMemoryZones ||
        event.targetMemoryZones.includes(event.memoryZone)
      ) {
        callback(event);
      }
    });
  }
}
```

- バックエンドとフロントエンドはWebSocketでリアルタイムに双方向通信できるように連携

# GitHub
https://github.com/R41R41/Shannon2