ウェブサイト検索

GenAIプラットフォームを使用したカスタムリアルタイムチャット体験の構築


紹介

あなたのニーズに完全にカスタマイズできるChatGPTのような体験を構築することを想像してみてください。今年のデプロイ会議でパブリックプレビューが開始されたDigitalOceanの新しいGenAIプラットフォームを使えば、まさにそれが可能です。

GenAIプラットフォームの特に魅力的な点は、その柔軟性です。多くのユースケースでは、全くコードを書く必要はなく、DigitalOceanコントロールパネルからJavaScriptのスニペットを取得して、アプリケーションにチャットボットを直接埋め込むことができます。しかし、開発者として私が本当に興奮するのは、APIを使用して自分が望むあらゆる種類の体験を構築できる能力です。

この記事では、ChatGPTの体験を模倣したカスタムチャットアプリケーションの構築について説明します。この例では、堅牢なチャットアプリケーションを構築するための3つの重要な要素を学びます。

  1. API認証 - GenAIプラットフォームへの安全な接続
  2. WebSocketを使用したリアルタイム通信 - 双方向通信の設定
  3. チャットサービスとメッセージ処理 - 会話の管理とレスポンスのストリーミング

最良の部分は、すべての要素を理解すると、自分の望むようにカスタマイズできることです。

GenAIプラットフォームの理解

DigitalOcean GenAIプラットフォームは、AIエージェントを迅速に構築およびスケーリングするためのオールインワンソリューションです。Anthropic、Meta、Mistral AIなどの組織からの最先端の生成AIモデルへのアクセスを提供し、AI開発を誰でも利用できるようにすることに重点を置いています。

これが特別な理由です:

  • 即時展開<&47;strong>: 数回のクリックでAIエージェントを構築し、展開します
  • ファウンデーションモデル: Anthropic、Meta、Mistral AIの主要なモデルにアクセスする
  • 柔軟な統合: ノーコードのチャットボット埋め込みまたは完全なAPIアクセスのいずれかを選択できます
  • 組み込みセキュリティ<&47;strong>: 有害なコンテンツをフィルタリングするためのカスタマイズ可能なガードレール
  • コスト効率的<&47;strong>: 自信を持ってスケーリングできる透明な価格設定
  • 高度な機能<&47;strong>:

    • 知識ベース統合のためのリトリーバル拡張生成(RAG)
    • カスタム機能のための関数ルーティング
    • エージェントのカスタマイズとガードレール

このプラットフォームはインフラ作業を担当し、AIの経験レベルに関係なくアプリケーションの構築に集中できるようにします。

OpenAI APIの互換性

GenAIプラットフォームの最も強力な側面の一つは、OpenAIとのAPI互換性です。これは、OpenAI互換のSDKやライブラリを使用してプラットフォームと対話できることを意味します。以前にOpenAIを使用したことがある場合、GenAIプラットフォームを使用するために必要なすべての経験をすでに持っています。もし始めたばかりであれば、OpenAIのために構築された広範なツールとライブラリのエコシステムを活用できます。

この互換性が開発者にとって何を意味するのかは次のとおりです。

  • お気に入りのOpenAI SDK(Python、Node.jsなど)を使用してください。
  • 既存のコードと例を活用する
  • 豊富なツールとライブラリのエコシステムにアクセスする
  • OpenAIに慣れていれば、学習曲線は最小限です。
  • 既存のアプリケーションのための簡単な移行パス

例えば、私たちのチャットアプリケーションでは、OpenAIのNode.js SDKを使用します。

const { OpenAI } = require('openai');

const client = new OpenAI({
    baseURL: agent_endpoint,
    apiKey: access_token,
});

const response = await client.chat.completions.create({
    model: "n/a", // Model is handled by the GenAI Platform
    messages: conversationHistory,
    stream: true,
});

この互換性は、開発と導入を劇的に簡素化します。新しいAPIを学ぶことや既存のコードを書き直す代わりに、ユーザーにとって重要な機能の構築に集中できます。

チャットアプリケーションの構築

技術的な詳細に入る前に、私たちがこれから探求するすべてがGitHubのサンプルアプリケーションで利用可能であることに注意する価値があります。リポジトリをクローンし、ローカルで実行して、これらのコンポーネントを実際に見ることができます。これにより、ついていきやすくなり、自分でコードを試すことができます。

このチャットアプリケーションが機能するための三つの重要な要素について掘り下げさせてください。

API認証

APIを使用する際には、安全な認証が不可欠です。GenAIプラットフォームは、アクセスキーを使用してリクエストを認証し、毎回機密情報を送信することなくAPIとやり取りするためのシンプルで安全な方法を提供します。

あなたは次のことをする必要があります:

  1. DigitalOceanコントロールパネルでアクセスキーを作成します
  2. アプリケーション内で安全に保存してください
  3. APIリクエストを認証するためにそれを使用してください

これを私たちのチャットアプリケーションに実装する方法は次のとおりです。

const { OpenAI } = require('openai');

class TokenService {
    constructor() {
        this.AGENT_ENDPOINT = process.env.AGENT_ENDPOINT + "/api/v1/";
        this.AGENT_KEY = process.env.AGENT_KEY;
        
        if (!this.AGENT_ENDPOINT || !this.AGENT_KEY) {
            throw new Error('Missing required configuration');
        }
    }

    getClient() {
        return new OpenAI({
            baseURL: this.AGENT_ENDPOINT,
            apiKey: this.AGENT_KEY,
        });
    }
}

この合理化されたアプローチ:

  • 認証には単一のアクセスキーを使用します
  • 最小限のセットアップとメンテナンスが必要です
  • セキュリティのベストプラクティスに従う
  • OpenAI SDKとシームレスに連携します

GenAIプラットフォームにAPIコールを行う際、クライアントを使用してエージェントと対話することができます。

const client = tokenService.getClient();
const response = await client.chat.completions.create({
    model: "n/a", // Model is handled by the GenAI Platform
    messages: conversationHistory,
    stream: true,
});

WebSocketを使用したリアルタイム通信

WebSocketは、単一のTCP接続を介してフルデュプレックス通信チャネルを提供する通信プロトコルです。従来のHTTPリクエストとは異なり、WebSocketはクライアントとサーバー間で持続的な接続を維持し、次のことを可能にします。

  • リアルタイムの双方向通信
  • 低遅延(接続後に新しいハンドシェイクが不要)
  • データの効率的なストリーミング
  • より良い資源の活用

このチャットアプリには、WebSocketが理想的です。なぜなら、私たちが次のことを可能にするからです。

  1. 生成されると同時にAIの応答をリアルタイムでストリーミングする
  2. 各チャットセッションの接続状態を維持する
  3. 再接続シナリオを優雅に処理する
  4. 接続状況について即時のフィードバックを提供する

サーバーサイド実装

ここでは、Node.js用の人気で軽量なWebSocketクライアントおよびサーバー実装であるws<&47;code>パッケージを使用しています。

WebSocketサーバーを設定する方法は次のとおりです。

const { WebSocketServer } = require('ws');
const WS_PING_INTERVAL = 30000; // 30 seconds

// Create WebSocket server with no direct HTTP server attachment
const wss = new WebSocketServer({ noServer: true });

// Handle WebSocket connections
wss.on('connection', async (ws, req) => {
    // Extract chat session ID from URL parameters
    const chatId = new URL(req.url, 'ws://localhost').searchParams.get('chatId');
    if (!chatId) {
        ws.close(1008, 'Chat ID is required');
        return;
    }

    try {
        // Store connection in chat service
        chatService.addConnection(chatId, ws);
        
        // Implement connection health checks
        const pingInterval = setInterval(() => {
            if (ws.readyState === ws.OPEN) ws.ping();
        }, WS_PING_INTERVAL);
        
        // Clean up on connection close
        ws.on('close', () => {
            clearInterval(pingInterval);
            chatService.removeConnection(chatId);
        });
    } catch (error) {
        ws.close(1011, 'Failed to initialize connection');
    }
});

// Set up WebSocket upgrade handling
server.on('upgrade', (request, socket, head) => {
    wss.handleUpgrade(request, socket, head, (ws) => {
        wss.emit('connection', ws, request);
    });
});

サーバー実装の重要な側面:

  • noServer: true<&47;code>を使用してWebSocketのアップグレードを手動で処理し、接続プロセスに対するより多くの制御を提供し、接続を受け入れる前にセッションデータを検証できるようにします。
  • 接続の健康監視のためにping/pongを実装します
  • チャットセッションごとの接続を管理します
  • 接続終了時のクリーンアップを処理します

接続管理

WebSocket接続を適切に管理することは、信頼性の高いチャットアプリケーションにとって重要です。アクティブな接続を追跡し、切断を円滑に処理し、もはや必要でないリソースをクリーンアップする必要があります。以下は、チャットサービスでアクティブな接続を維持する方法です:

class ChatService {
    constructor() {
        this.activeConnections = new Map();
        this.connectionTimeouts = new Map();
        this.CLEANUP_TIMEOUT = 5 * 60 * 1000; // 5 minutes
    }

    addConnection(id, ws) {
        // Remove any existing connection
        if (this.activeConnections.has(id)) {
            this.removeConnection(id);
        }

        // Store new connection
        this.activeConnections.set(id, ws);

        // Set up cleanup timeout
        ws.on('close', () => {
            this.connectionTimeouts.set(id, setTimeout(() => {
                this.conversations.delete(id);
                this.connectionTimeouts.delete(id);
            }, this.CLEANUP_TIMEOUT));
        });
    }

    removeConnection(id) {
        const connection = this.activeConnections.get(id);
        if (connection?.readyState === 1) {
            connection.send(JSON.stringify({ content: 'Connection closed' }));
        }
        this.activeConnections.delete(id);
    }
}

ここで何が起こっているのかを分解してみましょう:

  1. 接続ストレージ<&47;strong>:

    • アクティブなWebSocket接続をセッションIDでキー付けして保存するためにMap<&47;code>を使用します。
    • 別のMap<&47;code>は、切断されたセッションのクリーンアップタイムアウトを追跡します
    • 5分間のCLEANUP_TIMEOUT<&47;code>は、ユーザーがコンテキストを失うことなく再接続する時間を提供します。
  2. 接続を追加する<&47;strong>:

    • 新しい接続を追加する前に、そのセッションの既存の接続をクリーンアップします。
    • これによりリソースのリークが防止され、セッションごとに1つのアクティブな接続が確保されます。
    • 各接続はユニークなチャットセッションIDに関連付けられています。
  3. クリーンアップ処理<&47;strong>:

    • 接続が閉じると、クリーンアップタイマーが開始されます。
    • ユーザーが5分以内に再接続しない場合、会話履歴を削除します。
    • これは、ユーザーを再接続するためのコンテキストを保持することと、リソースを解放することのバランスを取ります。
  4. 接続解除<&47;strong>:

    • 接続がまだアクティブな場合、接続を閉じる前にクライアントに通知します。
    • すべての接続リソースは、メモリリークを防ぐために適切にクリーンアップされています。

この接続の慎重な管理は、同時に多くのユーザーがいる場合でも、私たちのチャットアプリケーションが安定して効率的であることを確保するのに役立ちます。

クライアントサイド実装

クライアント側のWebSocket実装は、リアルタイム通信のいくつかの重要な側面を処理する必要があります。

  • 接続を確立し維持すること
  • 切断を優雅に処理する
  • 受信メッセージの処理
  • ユーザーに視覚的フィードバックを提供する

これらの機能を実装する方法は次のとおりです。

const MAX_RECONNECT_ATTEMPTS = 5;
const RECONNECT_DELAY = 1000; // Start with 1 second
let reconnectAttempts = 0;
let ws = null;

function initializeWebSocket() {
    if (ws) {
        ws.close();
        ws = null;
    }

    // Determine WebSocket protocol based on page protocol
    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
    ws = new WebSocket(`${protocol}//${window.location.host}?chatId=${window.chatId}`);
    
    // Update UI connection status
    updateConnectionStatus('connecting', 'Connecting...');
    
    // Handle incoming messages
    ws.onmessage = (event) => {
        const data = JSON.parse(event.data);
        if (data.content === 'Connected to chat stream') {
            // Initial connection successful
            updateConnectionStatus('connected', 'Connected');
            reconnectAttempts = 0;
        } else if (data.content) {
            // Process chat message
            addMessage(data.content, false);
        } else if (data.error) {
            // Handle error messages
            console.error('WebSocket Error:', data.error);
            addMessage(`Error: ${data.error}`, false);
        }
    };

    // Implement reconnection with exponential backoff
    ws.onclose = (event) => {
        updateConnectionStatus('disconnected', 'Connection lost');
        
        if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
            // Calculate delay with exponential backoff
            const delay = Math.min(RECONNECT_DELAY * Math.pow(2, reconnectAttempts), 30000);
            updateConnectionStatus('reconnecting', 
                `Reconnecting in ${delay/1000} seconds...`);
            setTimeout(initializeWebSocket, delay);
            reconnectAttempts++;
        } else {
            // Max retries reached
            updateConnectionStatus('disconnected', 
                'Connection failed. Please refresh the page.');
        }
    };

    // Handle connection errors
    ws.onerror = (error) => {
        console.error('WebSocket error:', error);
        updateConnectionStatus('error', 'Connection error');
    };
}

主要な特徴を分解してみましょう:

  1. 接続管理<&47;strong>:

    • 安全な接続(wss:<&47;code>)と非安全な接続(ws:<&47;code>)の両方を処理します。
    • 新しい接続を作成する前に既存の接続を閉じます
    • 接続URLにチャットセッションIDを含めます
    • UIの接続状態を維持します
  2. メッセージ処理<&47;strong>:

    • 受信したJSONメッセージを解析します
    • 接続状態、チャット内容、エラーなど、異なるメッセージタイプを処理します。
    • 新しいメッセージでUIを更新します
    • デバッグのためのログエラー
  3. スマート再接続<&47;strong>:

    • サーバーの洪水を防ぐために指数バックオフを実装します
    • 1秒の遅延から始まり、各試行ごとに倍増します
    • キャップの最大遅延は30秒です。
    • 再接続試行の合計を5回に制限します
    • 再接続の試み中に明確なフィードバックを提供します
  4. エラーハンドリング<&47;strong>:

    • WebSocketエラーをキャッチしてログに記録します
    • 接続状態を反映するようにUIを更新します
    • ユーザーフレンドリーなエラーメッセージを提供します
    • 接続が永久に失敗した場合のアクションを提案します

クライアント実装の特徴:

  • 安全/非安全接続のためのプロトコル検出
  • 視覚的接続状態の更新
  • 指数バックオフによる自動再接続
  • 無限ループを防ぐための最大再試行回数
  • エラーハンドリングとユーザーフィードバック

接続状態管理

リアルタイムアプリケーションでユーザーの信頼を築くことは重要であり、これを達成するための簡単な方法の一つは接続状態を表示することです。私たちはすでにWebSocket実装で接続状況を追跡しているので、これをユーザーに簡単に提示できます。

function updateConnectionStatus(status, message) {
    const statusDot = document.querySelector('.status-dot');
    const statusText = document.querySelector('.status-text');
    
    // Update visual indicators
    statusDot.className = `status-dot ${status}`;
    statusText.textContent = message;
    
    // Disable/enable chat input based on connection status
    const chatInput = document.getElementById('message-input');
    const sendButton = document.querySelector('button[type="submit"]');
    const isConnected = status === 'connected';
    
    chatInput.disabled = !isConnected;
    sendButton.disabled = !isConnected;
}

このシンプルな追加により、ユーザーは接続状態について即座にフィードバックを受け取り、接続が失われると自動的に入力が無効になります。これはアプリケーションをより洗練され、信頼性のあるものに感じさせる小さな工夫です。

チャットサービスとメッセージ処理

GenAIプラットフォームでチャットインターフェースを構築する際の興味深い課題の一つは、ストリーミングレスポンスの処理です。AIモデルはテキストをトークンごとに生成するため、いくつかの興味深い疑問が生じます。

  1. これらの小さな部分をどのように滑らかな読書体験に変えることができるでしょうか?
  2. 意味のある対話のために、会話の文脈をどのように維持できますか?
  3. ストリーミング中に発生する可能性のあるエラーをどのように管理できますか?
  4. レスポンシブなユーザーインターフェースを維持するための最良の方法は何ですか?

これらの課題に対処するために、私たちが取ることができるアプローチがあります。ここでは、トークンを意味のある塊に集めてからクライアントに送信するバッファリング戦略を実装し、文脈に応じた応答のために会話履歴を維持します。

メッセージ処理アプローチ

各トークンが到着するたびに送信するのではなく、コンテンツをバッファリングして自然な区切りポイントで送信することができます。こちらが実装です:

async processStream(stream, ws, sessionId) {
    const conversationHistory = this.conversations.get(sessionId);
    let currentChunk = [];
    let fullResponse = [];
    
    for await (const chunk of stream) {
        if (chunk.choices[0]?.delta?.content) {
            const content = chunk.choices[0].delta.content;
            currentChunk.push(content);
            
            // Send chunk at natural breaks (punctuation or word count)
            if (this.shouldSendChunk(content, currentChunk)) {
                const message = currentChunk.join('');
                fullResponse.push(message);
                ws.send(JSON.stringify({ content: message }));
                currentChunk = [];
            }
        }
    }
    
    // Store the complete response in conversation history
    if (fullResponse.length > 0) {
        conversationHistory.push({
            role: 'assistant',
            content: fullResponse.join('')
        });
    }
}

主な特徴を見てみましょう:

  1. スマートチャンクing<&47;strong>:

    • トークンを意味のある塊に集める
    • 自然な区切り(句読点や単語数)で更新を送信します。
    • ユーザーにとってよりスムーズな読書体験を提供します
  2. 会話履歴<&47;strong>:

    • 各チャットセッションのコンテキストを記憶に保持します
    • セッション中にユーザーのメッセージとAIの応答の両方を保存します
    • 接続中により一貫性のある文脈に沿ったインタラクションを可能にします。
    • 各セッションには独自の履歴があり、5分間の非活動後にクリアされます。
  3. エラーハンドリング<&47;strong>:

    • ストリーミング中の接続状態を監視します
    • 優雅に切断を処理します
    • ユーザーに明確なエラーメッセージを提供します
    • リソースのリークを防ぐ
  4. 資源管理<&47;strong>:

    • ストリーミング応答を効率的に処理します
    • 接続が閉じるとリソースをクリーンアップします
    • 各セッションのために別々のコンテキストを維持します
    • ユーザーエクスペリエンスとメモリ使用量のバランスを取る

この実装は、堅牢なチャット体験を作成します。

  • ユーザーにとって反応が良く自然に感じられます
  • 会話の文脈を効果的に維持する
  • エラーを優雅に処理します
  • 複数のユーザーとともにスケールします

もちろん、これは問題にアプローチする一つの方法に過ぎません。異なるチャンク戦略、代替のエラーハンドリングアプローチ、または会話の状態を管理する他の方法など、特定のニーズにより適した他の戦略を見つけることができるかもしれません。

アプリケーションを実行中

チャットアプリケーションの構築について読むのは一つのことですが、自分で手を動かして試してみるのはまったく別の体験です。始めるのは簡単で、いくつかのシンプルなステップでできます。

  1. リポジトリをクローンし、依存関係をインストールします:

    git clone https://github.com/wadewegner/do-genai-customchatbot.git
    cd do-genai-customchatbot
    npm install
    
  2. エージェントを作成して構成します:

    • GenAIプラットフォームのクイックスタートガイドに従って、エージェントを作成してください。
    • これらの指示に従って、エージェントのエンドポイントを公開してください。
    • 次のステップのために、エージェントのID、キー、およびエンドポイントURLをコピーしてください。
  3. .envに環境変数を設定してください:

    API_BASE=https://cluster-api.do-ai.run/v1
    AGENT_ID=your-agent-id
    AGENT_KEY=your-agent-key
    AGENT_ENDPOINT=your-agent-endpoint
    SESSION_SECRET=your-session-secret
    
  4. サーバーを起動します:

    npm start
    

今、ブラウザを開いてhttp:&47;&47;localhost:3000<&47;code>にアクセスし、チャットを始めることができます!これはアプリケーションを試し、自分のニーズに合わせてカスタマイズするチャンスです。チャットインターフェースを変更したり、メッセージ処理を調整したり、新しい機能を追加したりしてみてください。GenAIプラットフォームの柔軟性で可能性は無限大です。

あなたの作品を世界と共有する準備ができたら、DigitalOceanのApp Platformはアプリケーションを公開して展開し、実行するのに最適な場所です。

チャットボットを超えて

この例はチャットインターフェースを示していますが、GenAIプラットフォームの機能はこのユースケースをはるかに超えています。あなたは次のことができます:

  • RAGパイプラインを使用してカスタム知識ベースアシスタントを構築する
  • カスタムモデルを使用してコンテンツ生成ツールを作成する
  • コード分析と生成システムを実装する
  • 言語翻訳サービスを開発する
  • 既存のアプリケーションにAI機能を追加する

GenAIプラットフォームでの構築に最も興奮するのは、AI開発の創造的な側面に集中できることです。ここで構築したチャットアプリケーションはほんの始まりに過ぎません。これは、あなたが基盤として構築し、実験し、完全に自分のものにできるものです。このウォークスルーがあなた自身のプロジェクトのアイデアを刺激することを願っています。さあ、これらのパターンを使って素晴らしいものを創造するのはあなたの番です!