🤖

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

2025/03/03に公開

はじめに

web、discord、twitter、youtubeなどで動作するチャットAI「シャノン」を作りました。
このチャットAI「シャノン」はtoolの使用が可能で、必要に応じて事前に作成しておいたbingSearchツールやwolframAlphaツールを使用することができます。
また、内部状態として感情・行動計画を持ち、感情的な出力や現在のタスク進行状況によって行動計画を動的に修正することが可能です。
今回は基幹部分のみ紹介していきます。

動作例

Web上に構築したシャノン監視用UI


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

discord上での動作

検索



画像認識


画像生成

表作成

画像認識&表作成

新規スキル獲得



twitter上での動作

12星座占い

今日は何の日?

明日の天気予報

Youtube上での動作

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

使用技術

バックエンド

  • LangGraph
  • LangChain
  • OpenAI API
  • TypeScript
  • Discord API
    など

フロントエンド

  • React
  • TypeScript
  • chatscope
  • chart.js
    など

大まかな構成

LangGraph

  • 状態としてタスクID、行動計画、感情パラメータなどを保持
行動計画の形式
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;
}
感情パラメータの形式
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のコード
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

Discussion