👨💻
WebLLMを使ってブラウザ完結かつローカルLLMでFunction callingを試してみる
前の記事
wllama(Wasm binding for llama.cpp)を使ってローカルLLMを使ったダッシュボードを作った
リポジトリ
実装したこと
-
マルチプロンプト Chat — System + User を結合し、ストリーム出力(
ask()
in Chat.tsx) -
OpenAI 互換 Function Call —
tools
配列 →tool_calls
JSON(weatherTool
切替トグル) -
DL 進捗バー —
initProgressCallback
で重みロード%を表示(エンジン初期化ブロック) -
クライアントキャッシュ削除 — IndexedDB & CacheStorage をワンクリックで削除(
clearCache()
) -
モード切替 UI — ラジオボタンで Chat / Function を選択(
Chat.tsx
冒頭 JSX)
内部構成
実行パス
Service‑Worker モードでは IndexedDB を CacheStorage に置き換え、fetch
ストリームで読み込みを最適化。
コード解剖(TypeScript + React)
主要部分のみ抜粋。全文はリポジトリ参照。
定数 & エンジン初期化
const MODEL_ID = "Hermes-2-Pro-Mistral-7B-q4f16_1-MLC";
const eng = await CreateMLCEngine(MODEL_ID, {
initProgressCallback: ({ progress }) => setProgress(progress),
});
await eng.reload(MODEL_ID); // chat API が保証される
Chat と Function‑Call の切替
const baseReq =
mode === "func"
? {
messages: [{ role: "user", content: userPrompt }],
tools: [weatherTool],
tool_choice: "auto",
}
: {
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
};
tools
使用時はモデルが内部 System プロンプトを挿入するため、独自 System プロンプトは送らない。
レスポンス処理
const { tool_calls, content } = res.choices[0].message;
setReply(tool_calls?.length ? JSON.stringify(tool_calls, null, 2) : content ?? "(空応答)");
JSON (tool_calls
) かテキストのいずれかを画面に表示。
キャッシュ削除
// IndexedDB
(await indexedDB.databases())
.filter(d => d.name?.startsWith("mlc_llm_db"))
.forEach(d => indexedDB.deleteDatabase(d.name!));
// CacheStorage
(await caches.keys())
.filter(k => k.startsWith("mlc-chat-cache"))
.forEach(k => caches.delete(k));
削除後に window.location.reload()
でエンジンを再初期化。
拡張アイデア
- ツール追加 — Schema 定義のみでフロント完結。
- モデル差し替え — Gemma・Phi‑3・Llama‑3 など MLC ビルド済み。
- 履歴永続化 — IndexedDB にメッセージも保存。
- PWA 化 — SW 版は済み、manifest とアイコンを足すだけ。
まとめ
- WASM + WebGPU + IndexedDB + Service‑Worker の純 Web 標準スタック。
- Function Callを一応試せる
感想
Function callingが使える7Bモデルをブラウザで動かしてみたが、多分結構調整が必要そう
かなり簡単に(ほぼ1ファイル)でセットアップできたので便利
Discussion