JSONに答えはある。でも、数字だ
1本目で、サーバーは集計JSON(top_offenders など)を返すようになりました。
答えは、そこにあります。ですが、それは数字の羅列です。
{ "subnet": "192.0.2.0/24", "req": 21, "rate404": 0.62 } ——
これを「192.0.2.0/24 からの分散スキャンが疑われます」という人間の言葉にしたい。
その数字→言語の翻訳を、サーバーではなくブラウザの WebLLM にやらせます。
192.0.2.x などはドキュメント用予約レンジで、実在のアクセス元ではありません。
推論を、クライアントへ完全委譲
sasagawa.tokyo の RAG は、VPS で llama.cpp を回していました。あちらは対話なので、同期・低レイテンシが要る。 でもログ分析は非同期でよい。今すぐミリ秒で返す必要はありません。だから、推論をまるごとクライアントに逃がせます。
これが効くのは、まさに2GB VPS が非力だからです。サーバーは1本目の集計SQL相当だけ。 重い生成は、それを見ているユーザーのGPUが担う。「非力だからこそ、クライアントに逃がす」——制約が設計を決めます。
JSONを、LLMが読める文脈に
集計JSONをそのまま貼って「これ何?」では、質の高い出力は出ません。役割と出力形式を与えます。
const messages = [ { role: "system", content: "あなたはセキュリティアナリスト。渡された集計を、" + "日本語で簡潔に、リスクの高い順に説明せよ。" + "憶測は避け、数値の根拠を添えること。" }, { role: "user", content: "次のアクセスログ集計から、最も怪しいアクセス元と理由を述べよ。\n" + JSON.stringify(aggregate, null, 2) }, ];
同じデータでも、文脈設計で出力の質が変わります。これは TF.js 記事の normalize や、
1本目のJSONスキーマ設計と同じ——後段が扱いやすい入力に整える思想です。
@mlc-ai/web-llm を、単一HTMLで
WebLLM は ESM CDN から import できます。単一HTMLで完結。まず navigator.gpu で
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本目の主題です。
トークンを、逐次流す
生成はストリーミングで受け取り、届いたトークンを画面に流します。 これは RAG の consumer.py のストリーミングや、 TF.js 記事のトークン演出の、実物版です。
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 で、分析らしく落ち着いた出力にします。
WebLLMが動かない環境
WebGPU非対応ブラウザ、CDNブロック、メモリ不足、DL失敗——動かない理由はいくつもあります。 そのとき、集計JSONからテンプレートで決定的に文章を組みます。LLMなしで、要点は正確に。
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 ? "存在しないパスへの探索が疑われます。" : ""); }
小型モデルは、数値を正しく読めないことがある
このデモを Pixel 10 Pro(Adreno)で実際に動かして、はっきりしたことがあります。 分析タスクでは、モデルの大きさがそのまま正確さに効く。同じ集計JSONを渡しても、結果はモデルで大きく違いました。
0.5B — 数値がでたらめ
文章にはなるが、数値の意味を取り違える。ip_count:6 を「3件中で6回、平均1.5件」のように、
集計値を無意味な計算に変えてしまう。実用外です。
1B — 流暢だが、誤読する
日本語は滑らかだが、ドメイン知識が弱い。「/24」をIPレンジの広さと誤解し、
「rate404:0.62」を「404回返している」と読み違える。もっともらしいだけに、かえって危うい。
1.5B — 実用の下限
ここでようやく、req・rate404・ip_count を概ね正しく解釈し、
「192.0.2.0/24 が最も危険」という正しい結論に達します(分散の高低の評価に軽微な誤りは残る)。
分析用途の最小十分は、この 1.5B 級でした。
そして皮肉なことに、最も正確で簡潔だったのは擬似生成でした。決定的なテンプレートは、数値を絶対に間違えません。 つまりこの規模のタスクでは、擬似生成が土台の主役、LLMは表現を豊かにする彩り。 §5で「LLMは付加価値」と書いたことが、実機で裏付けられた形です。
集計JSONが、日本語で話す
1本目の集計JSONを、WebLLM に解釈させます。実WebLLM(WebGPU対応環境ではモデルをDLして本物が動く)と、 擬似生成を、モードで切り替えられます。WebGPU対応でも擬似を選べるので、待ちたくない時の比較にどうぞ。
推論はすべてブラウザ側。VPS の CPU / RAM は動きません。 実WebLLMなら文脈を汲んだ自然な説明を、擬似生成なら決定的だが正確な要点を返します。 どちらのモードでも、サーバーは1本目の集計JSONを返しただけです。
推論は、クライアントに置ける
集計JSON → WebLLM → 自然言語レポート。推論はクライアント、サーバー負荷ゼロ。 WebGPU必須・初回DL・端末差という代償を、非対応環境への擬似フォールバックで受け止める。
この記事のまとめ
- ログ分析は非同期でよい → 推論をまるごとクライアントの WebLLM に委譲、サーバー無傷。
- プロンプトは role と出力形式を与える。JSON丸投げにしない。
@mlc-ai/web-llmを ESM CDN から import、navigator.gpuで WebGPU 検出。- ストリーミングで逐次生成。低 temperature で分析らしく。
- 非対応環境は決定的な擬似生成へ。LLMは付加価値、無くても機能する。
- 実機知見:小型(0.5B〜1B)は数値を誤読しがち。分析の実用下限は 1.5B、最も正確なのは擬似生成。
代償は、実際どのくらいか。Pixel 10 Pro(Adreno)と iPad mini(Apple GPU)で、初回DL・モデルサイズ・生成速度を実測。 「日次サマリ生成」は何秒で終わり、どのモデルが分析タスクに最小十分か。実機ベンチで、実用性を判定します。