T-CREATOR

WebLLM 使い方入門:チャット UI を 100 行で実装するハンズオン

WebLLM 使い方入門:チャット UI を 100 行で実装するハンズオン

ブラウザだけで動く AI チャットを、たった 100 行のコードで実装できるとしたら、試してみたくなりませんか?

WebLLM を使えば、サーバー不要でプライバシーを守りながら、本格的な AI チャット体験を提供できます。本記事では、初心者の方でも迷わず実装できるよう、段階的に解説していきます。実際に動くコードを書きながら、WebLLM の使い方を体験してみましょう。

背景

チャット UI が必要な理由

AI モデルを動かすだけでなく、ユーザーが自然に対話できるインターフェースが不可欠です。チャット UI は、AI との対話を直感的で親しみやすいものにしてくれます。

従来のサーバーベースの AI チャットでは、以下のような構成が一般的でした。

図の意図: 従来型のチャットシステムとWebLLMの違いを示します。

mermaidflowchart LR
  user["ユーザー"] -->|メッセージ送信| frontend["フロントエンド"]
  frontend -->|API リクエスト| backend["バックエンド<br/>サーバー"]
  backend -->|推論依頼| ai["AI モデル<br/>(GPU サーバー)"]
  ai -->|応答| backend
  backend -->|JSON| frontend
  frontend -->|表示| user

  style backend fill:#ffcccc
  style ai fill:#ffcccc

サーバー側での処理が必要で、インフラコストやプライバシーの懸念がありました。

WebLLM によるシンプル化

WebLLM を使えば、すべてブラウザ内で完結します。サーバー構築やバックエンド開発が不要になり、フロントエンドだけで完全な AI チャットを実装できるのです。

図の意図: WebLLMを使ったシンプルな構成を示します。

mermaidflowchart LR
  user["ユーザー"] -->|入力| chat["チャット UI<br/>(HTML/JS)"]
  chat -->|メッセージ| webllm["WebLLM<br/>エンジン"]
  webllm -->|WebGPU| gpu["ローカル GPU"]
  gpu -->|トークン| webllm
  webllm -->|応答| chat
  chat -->|表示| user

  style chat fill:#ccffcc
  style webllm fill:#ccffcc
  style gpu fill:#ccffcc

図で理解できる要点

  • バックエンドサーバーが不要
  • すべてブラウザ内で処理が完結
  • ローカル GPU を活用した高速な応答

課題

初心者が直面する実装の壁

WebLLM の公式ドキュメントは充実していますが、実際に動くチャット UI を作るには、いくつかのハードルがあります。

図の意図: 実装時に考慮すべきポイントを示します。

mermaidflowchart TD
  start["チャット UI 実装"] --> init["エンジンの<br/>初期化"]
  start --> ui["UI の構築"]
  start --> stream["ストリーミング<br/>応答"]
  start --> error["エラーハンドリング"]

  init --> init_solution["CreateMLCEngine<br/>の呼び出し"]
  ui --> ui_solution["メッセージ履歴<br/>の管理"]
  stream --> stream_solution["chunks.asyncGenerator<br/>の利用"]
  error --> error_solution["try-catch による<br/>エラー処理"]

  style start fill:#ffffcc
  style init_solution fill:#e1f5ff
  style ui_solution fill:#e1f5ff
  style stream_solution fill:#e1f5ff
  style error_solution fill:#e1f5ff

モデルの初期化タイミング

WebLLM エンジンの初期化には時間がかかります。ユーザーが初回メッセージを送信してから初期化すると、待ち時間が長くなってしまいます。

メッセージ履歴の管理

チャット形式では、過去のメッセージを記憶して文脈を保つ必要があります。どのようにメッセージ履歴を管理し、API に渡すかが重要です。

ストリーミング応答の実装

AI の応答をリアルタイムに表示するストリーミング機能は、ユーザー体験を大きく向上させます。しかし、非同期処理やイテレーターの扱いに慣れていないと、実装が難しく感じるでしょう。

解決策

100 行で実装するアプローチ

これらの課題を解決するため、以下のシンプルな設計で実装します。

#項目アプローチ
1HTML 構造最小限の要素(入力欄、送信ボタン、メッセージ表示)
2エンジン初期化ページロード時に非同期で初期化
3メッセージ履歴JavaScript の配列で管理
4ストリーミング応答for await ループで chunk を逐次表示
5エラーハンドリングtry-catch でエラーを捕捉し、ユーザーに通知

この設計により、コードをシンプルに保ちながら、実用的なチャット UI を実現できます。

コードの全体構成

実装は以下の 3 つのパートに分かれます。

  1. HTML 構造 - チャット UI の骨組み
  2. スタイリング - 見た目の調整
  3. JavaScript ロジック - WebLLM の制御とメッセージ処理

それぞれを段階的に見ていきましょう。

具体例

HTML の準備

まず、チャット UI の骨組みとなる HTML を作成します。

html<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebLLM チャット</title>
  <style>
    /* スタイルは次のセクションで追加 */
  </style>
</head>
<body>
  <div id="app">
    <!-- チャット画面のコンテナ -->
  </div>
</body>
</html>

このシンプルな構造から始めます。app という ID の div がチャット UI 全体を包みます。

UI コンポーネントの配置

次に、メッセージ表示エリアと入力欄を追加します。

html<div id="app">
  <div id="chat-container">
    <div id="messages">
      <!-- メッセージがここに表示される -->
    </div>
    <div id="input-area">
      <input
        type="text"
        id="user-input"
        placeholder="メッセージを入力..."
      />
      <button id="send-btn">送信</button>
    </div>
  </div>
</div>

メッセージ履歴を表示する messages エリアと、ユーザー入力用の input-area を配置しました。

CSS スタイリング

見やすいチャット UI にするため、基本的なスタイルを追加します。

cssbody {
  margin: 0;
  padding: 0;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background-color: #f5f5f5;
}

#app {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  padding: 20px;
}

ページ全体を中央に配置し、清潔感のある背景色にしています。

チャットコンテナのスタイル

チャット画面の枠組みにスタイルを適用します。

css#chat-container {
  width: 100%;
  max-width: 600px;
  height: 80vh;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: column;
}

角丸で影をつけることで、モダンなデザインになります。

メッセージ表示エリアのスタイル

メッセージが表示される部分にスクロール機能を追加します。

css#messages {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.message {
  padding: 12px 16px;
  border-radius: 8px;
  max-width: 80%;
  word-wrap: break-word;
}

.user-message {
  background-color: #007bff;
  color: white;
  align-self: flex-end;
}

.ai-message {
  background-color: #e9ecef;
  color: #333;
  align-self: flex-start;
}

ユーザーメッセージは右寄せの青、AI メッセージは左寄せのグレーで区別しています。

入力エリアのスタイル

入力欄と送信ボタンのスタイルを整えます。

css#input-area {
  display: flex;
  padding: 16px;
  border-top: 1px solid #e0e0e0;
  gap: 8px;
}

#user-input {
  flex: 1;
  padding: 12px;
  border: 1px solid #ddd;
  border-radius: 6px;
  font-size: 14px;
}

#send-btn {
  padding: 12px 24px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
}

#send-btn:hover {
  background-color: #0056b3;
}

#send-btn:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

ボタンにホバー効果と無効状態のスタイルを追加し、ユーザビリティを向上させています。

WebLLM ライブラリの読み込み

JavaScript で WebLLM を使用するため、CDN から読み込みます。

html<script type="module">
  import { CreateMLCEngine } from "https://esm.run/@mlc-ai/web-llm";

  // この後に実装コードを記述
</script>

type="module" を指定することで、ES モジュールとして WebLLM をインポートできます。

グローバル変数の定義

エンジンとメッセージ履歴を管理する変数を用意します。

javascriptlet engine = null;
let messages = [];
const messagesContainer = document.getElementById("messages");
const userInput = document.getElementById("user-input");
const sendBtn = document.getElementById("send-btn");

これらの変数で、WebLLM エンジンとチャット状態を保持します。

エンジンの初期化処理

ページ読み込み時に WebLLM エンジンを初期化します。

javascriptasync function initEngine() {
  try {
    // 初期化中のメッセージを表示
    addMessage("ai", "AI を初期化しています...");

    // WebLLM エンジンを作成
    engine = await CreateMLCEngine("Llama-3.1-8B-Instruct-q4f32_1");

    // 完了メッセージを表示
    addMessage("ai", "準備完了!メッセージを送信してください。");
    sendBtn.disabled = false;
  } catch (error) {
    addMessage("ai", `エラー: ${error.message}`);
  }
}

初期化には数秒かかるため、ユーザーに状態を知らせるメッセージを表示しています。

メッセージ表示関数

チャット画面にメッセージを追加する関数です。

javascriptfunction addMessage(role, content) {
  const messageDiv = document.createElement("div");
  messageDiv.className = `message ${role}-message`;
  messageDiv.textContent = content;
  messagesContainer.appendChild(messageDiv);

  // 最新メッセージまでスクロール
  messagesContainer.scrollTop = messagesContainer.scrollHeight;
}

新しいメッセージが追加されたら、自動的にスクロールして表示します。

メッセージ送信処理

ユーザーがメッセージを送信したときの処理を実装します。

javascriptasync function sendMessage() {
  const userMessage = userInput.value.trim();
  if (!userMessage || !engine) return;

  // ユーザーメッセージを表示
  addMessage("user", userMessage);
  messages.push({ role: "user", content: userMessage });

  // 入力欄をクリア&無効化
  userInput.value = "";
  sendBtn.disabled = true;

  // AI の応答を取得
  await getAIResponse();

  // 送信ボタンを再度有効化
  sendBtn.disabled = false;
}

ユーザーの入力を履歴に追加し、AI からの応答を待ちます。

ストリーミング応答の実装

AI の応答をリアルタイムで表示する処理です。

javascriptasync function getAIResponse() {
  try {
    // AI メッセージ用の空の div を作成
    const aiMessageDiv = document.createElement("div");
    aiMessageDiv.className = "message ai-message";
    messagesContainer.appendChild(aiMessageDiv);

    let fullResponse = "";

    // ストリーミングで応答を取得
    const chunks = await engine.chat.completions.create({
      messages: messages,
      stream: true,
    });

    // chunk を逐次処理
    for await (const chunk of chunks) {
      const content = chunk.choices[0]?.delta?.content || "";
      fullResponse += content;
      aiMessageDiv.textContent = fullResponse;

      // スクロール位置を更新
      messagesContainer.scrollTop = messagesContainer.scrollHeight;
    }

    // 履歴に追加
    messages.push({ role: "assistant", content: fullResponse });
  } catch (error) {
    addMessage("ai", `エラー: ${error.message}`);
  }
}

for await ループを使い、トークンが生成されるたびに画面を更新しています。これにより、ChatGPT のようなタイピングエフェクトが実現できます。

イベントリスナーの設定

送信ボタンと Enter キーにイベントを紐付けます。

javascriptsendBtn.addEventListener("click", sendMessage);

userInput.addEventListener("keypress", (e) => {
  if (e.key === "Enter") {
    sendMessage();
  }
});

Enter キーでもメッセージを送信できるようにすることで、使い勝手が向上します。

ページ読み込み時の初期化

最後に、ページが読み込まれたらエンジンを初期化します。

javascript// ページ読み込み時に実行
initEngine();

これで、ユーザーがページにアクセスした瞬間から、自動的に WebLLM エンジンが起動し始めます。

完成したコードの全体像

ここまでのコードを 1 つにまとめると、約 100 行で動作するチャット UI が完成します。

html<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebLLM チャット</title>
  <style>
    body { margin: 0; padding: 0; font-family: 'Segoe UI', sans-serif; background-color: #f5f5f5; }
    #app { display: flex; justify-content: center; align-items: center; height: 100vh; padding: 20px; }
    #chat-container { width: 100%; max-width: 600px; height: 80vh; background: white; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; }
    #messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
    .message { padding: 12px 16px; border-radius: 8px; max-width: 80%; word-wrap: break-word; }
    .user-message { background-color: #007bff; color: white; align-self: flex-end; }
    .ai-message { background-color: #e9ecef; color: #333; align-self: flex-start; }
    #input-area { display: flex; padding: 16px; border-top: 1px solid #e0e0e0; gap: 8px; }
    #user-input { flex: 1; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
    #send-btn { padding: 12px 24px; background-color: #007bff; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; }
    #send-btn:hover { background-color: #0056b3; }
    #send-btn:disabled { background-color: #ccc; cursor: not-allowed; }
  </style>
</head>
<body>
  <div id="app">
    <div id="chat-container">
      <div id="messages"></div>
      <div id="input-area">
        <input type="text" id="user-input" placeholder="メッセージを入力..." />
        <button id="send-btn" disabled>送信</button>
      </div>
    </div>
  </div>

  <script type="module">
    import { CreateMLCEngine } from "https://esm.run/@mlc-ai/web-llm";

    let engine = null;
    let messages = [];
    const messagesContainer = document.getElementById("messages");
    const userInput = document.getElementById("user-input");
    const sendBtn = document.getElementById("send-btn");

    async function initEngine() {
      try {
        addMessage("ai", "AI を初期化しています...");
        engine = await CreateMLCEngine("Llama-3.1-8B-Instruct-q4f32_1");
        addMessage("ai", "準備完了!メッセージを送信してください。");
        sendBtn.disabled = false;
      } catch (error) {
        addMessage("ai", `エラー: ${error.message}`);
      }
    }

    function addMessage(role, content) {
      const messageDiv = document.createElement("div");
      messageDiv.className = `message ${role}-message`;
      messageDiv.textContent = content;
      messagesContainer.appendChild(messageDiv);
      messagesContainer.scrollTop = messagesContainer.scrollHeight;
    }

    async function sendMessage() {
      const userMessage = userInput.value.trim();
      if (!userMessage || !engine) return;

      addMessage("user", userMessage);
      messages.push({ role: "user", content: userMessage });
      userInput.value = "";
      sendBtn.disabled = true;

      await getAIResponse();
      sendBtn.disabled = false;
    }

    async function getAIResponse() {
      try {
        const aiMessageDiv = document.createElement("div");
        aiMessageDiv.className = "message ai-message";
        messagesContainer.appendChild(aiMessageDiv);

        let fullResponse = "";
        const chunks = await engine.chat.completions.create({
          messages: messages,
          stream: true,
        });

        for await (const chunk of chunks) {
          const content = chunk.choices[0]?.delta?.content || "";
          fullResponse += content;
          aiMessageDiv.textContent = fullResponse;
          messagesContainer.scrollTop = messagesContainer.scrollHeight;
        }

        messages.push({ role: "assistant", content: fullResponse });
      } catch (error) {
        addMessage("ai", `エラー: ${error.message}`);
      }
    }

    sendBtn.addEventListener("click", sendMessage);
    userInput.addEventListener("keypress", (e) => {
      if (e.key === "Enter") sendMessage();
    });

    initEngine();
  </script>
</body>
</html>

このファイルを保存してブラウザで開くだけで、完全に動作する AI チャットが起動します。

動作確認のポイント

実装したチャット UI を動かす際の確認事項です。

#確認項目内容
1ブラウザ対応Chrome、Edge、Safari の最新版を使用
2WebGPU の有効化chrome://flags で WebGPU が有効になっているか確認
3初期化の待機モデルダウンロードには初回数分かかる(2 回目以降は高速)
4メモリ使用量開発者ツールの Performance タブで確認可能
5エラーメッセージコンソールでエラーログを確認

初回のモデルダウンロードは時間がかかりますが、IndexedDB にキャッシュされるため、2 回目以降は数秒で起動します。

カスタマイズのヒント

基本的なチャット UI ができたら、以下のような拡張が可能です。

モデルの切り替え

異なるモデルを試すには、CreateMLCEngine の引数を変更します。

javascript// より小さいモデルに変更(高速だが精度は下がる)
engine = await CreateMLCEngine("Llama-3.2-3B-Instruct-q4f16_1");

// より大きいモデルに変更(精度は上がるがメモリ消費が増える)
engine = await CreateMLCEngine("Llama-3.1-70B-Instruct-q4f16_1");

使用可能なモデルの一覧は、WebLLM の公式ドキュメントで確認できます。

ローディングインジケーター

AI が応答を生成している間、ローディング表示を追加できます。

javascriptasync function getAIResponse() {
  // ローディング表示を追加
  const loadingDiv = document.createElement("div");
  loadingDiv.className = "message ai-message";
  loadingDiv.textContent = "考え中...";
  messagesContainer.appendChild(loadingDiv);

  try {
    // 既存のコード
    // ...

    // ローディング表示を削除
    messagesContainer.removeChild(loadingDiv);
  } catch (error) {
    messagesContainer.removeChild(loadingDiv);
    addMessage("ai", `エラー: ${error.message}`);
  }
}

ユーザーに処理中であることを明示することで、待ち時間のストレスを軽減できます。

メッセージのクリア機能

会話履歴をリセットするボタンを追加することもできます。

javascriptfunction clearMessages() {
  messages = [];
  messagesContainer.innerHTML = "";
  addMessage("ai", "会話をリセットしました。");
}

// HTML にボタンを追加
// <button onclick="clearMessages()">会話をクリア</button>

長い会話が続いた場合、履歴をクリアすることでメモリ使用量を抑えられます。

まとめ

本記事では、WebLLM を使ってブラウザ上で動作する AI チャット UI を 100 行で実装する方法を解説しました。

わずか 100 行のコードで、サーバー不要のプライバシー保護された AI チャットが実現できることに驚かれたのではないでしょうか。この実装をベースに、デザインのカスタマイズや機能追加を行うことで、より高度なアプリケーションに発展させることができます。

実装のポイントをまとめます。

#ポイント説明
1シンプルな構成HTML、CSS、JavaScript だけで完結
2CDN からの読み込みnpm インストール不要で即座に試せる
3ストリーミング応答for await で chunk を逐次表示し、UX を向上
4エラーハンドリングtry-catch でエラーを捕捉し、ユーザーに適切に通知
5メッセージ履歴の管理配列で履歴を保持し、文脈を維持
6初期化の非同期処理ページロード時に自動で WebLLM エンジンを起動

WebLLM は、Web 技術だけで本格的な AI アプリケーションを構築できる画期的なライブラリです。この記事で実装したチャット UI をベースに、ぜひご自身のプロジェクトに応用してみてください。

モデルの選択、UI のカスタマイズ、追加機能の実装など、可能性は無限大です。ローカルで動作する AI の力を、あなたのアプリケーションに取り入れてみましょう。

関連リンク