モバイル&ワイヤレスブロードバンドでインターネットへ

gwaw.jp
 
MongoDB × WebLLM ②/ブラウザ推論

ブラウザが、推論を引き受ける
WebLLM でログを言語化

集計JSONに答えはある。でも、それは数字だ。「192.0.2.0/24 / req:21 / rate404:0.62」を、人間が読める言葉にしたい。 その翻訳を、サーバーではなくブラウザの WebLLM にやらせます。2GB VPS に推論負荷ゼロで、日本語の防御レポートを生成する—— 実WebLLM のロードから、非対応環境の擬似フォールバックまで。

WebLLM Qwen2.5-1.5B WebGPU streaming fallback

[ § 0 ] 導入

JSONに答えはある。でも、数字だ

1本目で、サーバーは集計JSON(top_offenders など)を返すようになりました。 答えは、そこにあります。ですが、それは数字の羅列です。

{ "subnet": "192.0.2.0/24", "req": 21, "rate404": 0.62 } —— これを「192.0.2.0/24 からの分散スキャンが疑われます」という人間の言葉にしたい。 その数字→言語の翻訳を、サーバーではなくブラウザの WebLLM にやらせます。

IP はすべてマスク済み。 例に出す 192.0.2.x などはドキュメント用予約レンジで、実在のアクセス元ではありません。
[ § 1 ] なぜ WebLLM か

推論を、クライアントへ完全委譲

sasagawa.tokyo の RAG は、VPS で llama.cpp を回していました。あちらは対話なので、同期・低レイテンシが要る。 でもログ分析は非同期でよい。今すぐミリ秒で返す必要はありません。だから、推論をまるごとクライアントに逃がせます。

これが効くのは、まさに2GB VPS が非力だからです。サーバーは1本目の集計SQL相当だけ。 重い生成は、それを見ているユーザーのGPUが担う。「非力だからこそ、クライアントに逃がす」——制約が設計を決めます。

代償は、正直に。 WebLLM には WebGPU 必須初回モデルDL(数百MB)端末差という代償があります。 この記事はその代償を承知で組み、非対応環境には擬似フォールバックを用意します(§5)。代償の実測は3本目で。
[ § 2 ] プロンプト設計

JSONを、LLMが読める文脈に

集計JSONをそのまま貼って「これ何?」では、質の高い出力は出ません。役割と出力形式を与えます

prompt.js — システム+ユーザー
const messages = [
  { role: "system", content:
    "あなたはセキュリティアナリスト。渡された集計を、" +
    "日本語で簡潔に、リスクの高い順に説明せよ。" +
    "憶測は避け、数値の根拠を添えること。" },
  { role: "user", content:
    "次のアクセスログ集計から、最も怪しいアクセス元と理由を述べよ。\n" +
    JSON.stringify(aggregate, null, 2) },
];

同じデータでも、文脈設計で出力の質が変わります。これは TF.js 記事の normalize や、 1本目のJSONスキーマ設計と同じ——後段が扱いやすい入力に整える思想です。

[ § 3 ] エンジンをロード

@mlc-ai/web-llm を、単一HTMLで

WebLLM は ESM CDN から import できます。単一HTMLで完結。まず navigator.gpu で WebGPU の有無を確認し、無ければ即フォールバックへ。

engine.js — ロードと WebGPU 検出
import { CreateMLCEngine } from
  "https://esm.run/@mlc-ai/web-llm";

if (!navigator.gpu) fallbackToPseudo();   // WebGPU 非対応 → 擬似へ

const engine = await CreateMLCEngine(
  "Qwen2.5-1.5B-Instruct-q4f16_1-MLC",       // 既定。小型ほどDLは軽いが指示追従は弱い
  { initProgressCallback: (p) => showProgress(p.progress) }  // DL進捗
);

モデルは差し替え可能です(デモでは 0.5B / 1B / 1.5B を切り替えられます)。既定は Qwen2.5-1.5B。ここは実機で確かめて分かったことですが、小型(0.5B〜1B)では数値の解釈を誤りがちで、 分析タスクでは指示追従と数値理解が安定する 1.5B 級が実用の下限でした(詳細は §6)。 初回はDLしますが、2回目以降はブラウザキャッシュ(IndexedDB)から高速に起動します。 「どのモデルが最小十分か」の実測は、3本目の主題です。

[ § 4 ] 生成する

トークンを、逐次流す

生成はストリーミングで受け取り、届いたトークンを画面に流します。 これは RAG の consumer.py のストリーミングや、 TF.js 記事のトークン演出の、実物版です。

generate.js — streaming
const stream = await engine.chat.completions.create({
  messages, stream: true, temperature: 0.3,
});
for await (const chunk of stream) {
  const t = chunk.choices[0].delta.content ?? "";
  appendToken(t);           // 1トークンずつ画面へ
}

DL中・生成中・完了の状態を出し、長い沈黙を作らない——RabbitMQ 記事の ping と同じ配慮です。 低い temperature で、分析らしく落ち着いた出力にします。

[ § 5 ] フォールバック

WebLLMが動かない環境

WebGPU非対応ブラウザ、CDNブロック、メモリ不足、DL失敗——動かない理由はいくつもあります。 そのとき、集計JSONからテンプレートで決定的に文章を組みます。LLMなしで、要点は正確に。

pseudo.js — 擬似生成(決定的)
function pseudoReport(agg) {
  const top = agg.top_offenders[0];
  return `最も怪しいのは ${top.subnet} です。` +
    `直近で ${top.req} 件、404率 ${(top.rate404*100)|0}%、` +
    `${top.ip_count} 個のIPに分散しています。` +
    (top.rate404 > 0.5
      ? "存在しないパスへの探索が疑われます。" : "");
}
LLMは付加価値、無くても機能する。 擬似生成も同じトークン演出で流し、「擬似生成」と明示します。これは TF.js の3段フォールバック、 iseeit ログ基盤の「LLMは付加価値」と同じ割り切り。本物が動けば豊かに、動かなくても要点は返す
[ § 6 ] 実機で分かった限界

小型モデルは、数値を正しく読めないことがある

このデモを Pixel 10 Pro(Adreno)で実際に動かして、はっきりしたことがあります。 分析タスクでは、モデルの大きさがそのまま正確さに効く。同じ集計JSONを渡しても、結果はモデルで大きく違いました。

0.5B — 数値がでたらめ

文章にはなるが、数値の意味を取り違える。ip_count:6 を「3件中で6回、平均1.5件」のように、 集計値を無意味な計算に変えてしまう。実用外です。

1B — 流暢だが、誤読する

日本語は滑らかだが、ドメイン知識が弱い。「/24」をIPレンジの広さと誤解し、 「rate404:0.62」を「404回返している」と読み違える。もっともらしいだけに、かえって危うい。

1.5B — 実用の下限

ここでようやく、reqrate404ip_count を概ね正しく解釈し、 「192.0.2.0/24 が最も危険」という正しい結論に達します(分散の高低の評価に軽微な誤りは残る)。 分析用途の最小十分は、この 1.5B 級でした。

初回DLは、フォアグラウンドで。 実機で分かったもう一つの落とし穴。1.5Bの初回DL(数百MB)の途中でタブをバックグラウンドへ回すと、 ロードが中断してフォールバックします。DLが終わるまで画面を離れない——この実機ならではの地雷は、3本目で詳しく扱います。

そして皮肉なことに、最も正確で簡潔だったのは擬似生成でした。決定的なテンプレートは、数値を絶対に間違えません。 つまりこの規模のタスクでは、擬似生成が土台の主役、LLMは表現を豊かにする彩り。 §5で「LLMは付加価値」と書いたことが、実機で裏付けられた形です。

[ § 7 ] 体感デモ

集計JSONが、日本語で話す

1本目の集計JSONを、WebLLM に解釈させます。実WebLLM(WebGPU対応環境ではモデルをDLして本物が動く)と、 擬似生成を、モードで切り替えられます。WebGPU対応でも擬似を選べるので、待ちたくない時の比較にどうぞ。

Interactive · Aggregate → natural language
集計JSON → WebLLM 解釈
実WebLLMは初回に数百MBのモデルをDLします(要WebGPU・以降はキャッシュ)。非対応環境は自動で擬似生成にフォールバックします。
モデル(実WebLLM時)
入力:サーバーから届いた集計JSON(1本目の出力)
環境を確認中…
「解釈させる」を押すと、集計JSONを自然言語のレポートに変換します。
VPS 負荷
CPU
~1%
RAM
無風
推論はブラウザ側 → サーバー無傷

推論はすべてブラウザ側。VPS の CPU / RAM は動きません。 実WebLLMなら文脈を汲んだ自然な説明を、擬似生成なら決定的だが正確な要点を返します。 どちらのモードでも、サーバーは1本目の集計JSONを返しただけです。

[ § 8 ] まとめ

推論は、クライアントに置ける

集計JSON → WebLLM → 自然言語レポート。推論はクライアント、サーバー負荷ゼロ。 WebGPU必須・初回DL・端末差という代償を、非対応環境への擬似フォールバックで受け止める。

この記事のまとめ

  • ログ分析は非同期でよい → 推論をまるごとクライアントの WebLLM に委譲、サーバー無傷。
  • プロンプトは role と出力形式を与える。JSON丸投げにしない。
  • @mlc-ai/web-llm を ESM CDN から import、navigator.gpu で WebGPU 検出。
  • ストリーミングで逐次生成。低 temperature で分析らしく。
  • 非対応環境は決定的な擬似生成へ。LLMは付加価値、無くても機能する。
  • 実機知見:小型(0.5B〜1B)は数値を誤読しがち。分析の実用下限は 1.5B、最も正確なのは擬似生成。

『ブラウザが、推論を引き受ける — WebLLM でログを言語化』を公開しました。