【KARAKURI LM 10本ノック】番外編: Chrome内蔵のローカルLLM (Gemini Nano)で「どこでもCopilot」を作ってみた

Yuki Yoshida
KARAKURI Techblog
Published in
16 min readAug 22, 2024
本記事中で紹介しているユーザスクリプト「どこでもCopilot」。ChromeビルトインのLLMで、任意のフォーム上で文章の続きをサジェスト。外部通信なし、無料で動作

こんにちは、カラクリR&Dの吉田です。今回は、10本ノック「番外編」ということで、Google Chrome の最新版に先日(2024年8月22日)組み込まれたローカルLLMの話をします。

本記事の概要

2024年8月22日に公開された Google Chrome の「バージョン128」にローカルLLM として Gemini Nano が組み込まれました。このLLMは、フロントエンド JavaScript から手軽に呼び出し可能で、ローカルLLMなので外部への通信は発生しません。しかも無料です。

本記事では、このLLMを体験する方法を記します。また、このLLMを応用して、任意のフォーム上で入力した文章の続きをリアルタイムで補完してくれる「どこでもCopilot」(冒頭の動画参照)をお手元で体験する方法も記します。

LLMのセットアップ手順

まず、Google Chrome に組み込まれたLLMを動かせる状態にするためのセットアップ手順を記します。なお、もし1次情報にあたりたい場合は、こちらの Google が出しているドキュメントをご参照ください。

  1. Chrome のバージョンを、バージョン128 以降にアップデートして、再起動します
  2. アドレスバーに chrome://flags と入力して設定画面を開き、以下の2項目を設定します
    Enables optimization guide on deviceの値を Enabled BypassPerfRequirement に設定します。なお、画面上部の入力欄で設定項目を名前で検索できますが、よく似た名前の設定項目 Enable optimization guide dogfood logging と間違えないようにご注意ください(1敗)
    Prompt API for Gemini Nanoの値を Enabled に設定します
  3. Chrome を再起動します
  4. Chrome のアドレスバーに chrome://components と入力して設定画面を開き、Optimization Guide On Device Model の「アップデートを確認」をクリックしてしばらく待ちます。LLMのモデルが裏でダウンロードされます。
    なお、この項目自体が表示されない場合は、以下の手順を踏んでからリトライすると良いそうです。
    ・ストレージの空き容量が22GB以上あることを確認してください
    ・開発者コンソールを開き、await window.ai.createTextSession(); を実行してください(エラーが表示されるはずですが、想定通りです)
    ・Chrome を再起動してください
  5. chrome://components の設定画面の中の Optimization Guide On Device Model の横に「バージョン:2024.x.x.x」のような表示が出れば、準備完了です
  6. Chrome の開発者コンソールを開き(Ctrl+Shift+j または Cmd+Option+j)、以下のコードを実行してください
const canCreate = await window.ai.canCreateTextSession();
if (canCreate === "no") {
console.log("Gemini Nano は利用できません");
} else if (canCreate === "after-download"){
console.log("Gemini Nano はモデルのダウンロード完了後に利用可能になります");
} else {
const session = await window.ai.createTextSession();
const result = await session.prompt("夏の暑い日には何を食べると良い?");
console.log(result);
}

数秒ほど経過したのちに、例えば以下のような結果がコンソールに表示されれば、LLMが動作しています!(推論結果は毎回揺らぎます)

 **以下は、夏の暑い日に食べるのに最適な食品の例です。**
- **冷たいスープ:**
夏の暑さを和らげるのに最適な方法です。キュウリ、トマト、レモンスライスなどを使った冷たいスープはいかがでしょうか。
- **サラダ:**
豊富な栄養素が詰まっており、体を冷やすのに役立ちます。シーザーサラダやグリーンサラダがおすすめです。
- **果物:**
果物は水分補給やエネルギー補給に最適です。マンゴー、スイカ、パイナップルなどの冷たい果物や、グアバやドラゴンフルーツなどのビタミン豊富な果物があります。
- **冷たい麺料理:**
ラーメンやパスタなどの冷たい麺料理は、体を冷やすのに最適です。
- **冷たいデザート:**
暑さを和らげるのに最適なデザートです。アイスクリーム、ゼリー、シャーベットなどがおすすめです。
- **冷たい飲み物:**
スポーツドリンクや冷たい飲み物は、水分補給に最適です。

「どこでもCopilot」の体験方法

上記セットアップによって、ブラウザ内蔵のローカル LLM を JavaScript からすぐ呼び出せる状態にすることができました。

この LLM の活用例として、今回「どこでもCopilot」を作ってみました。冒頭の動画のように、任意のフォームに入力しかけている文章の続きを予測してサジェストし、入力を補助してくれるシステムです。「ユーザースクリプト」として開発しましたので、以下の手順を踏むことで、皆様のお手元の Chromeでも「どこでもCopilot」を体験することができます。

  1. Chromeにブラウザ拡張機能 Tampermonkey をインストールします
    ・「ユーザスクリプト」と呼ばれるカスタムのJavaScriptをWebページ上で動作させるための拡張機能です
  2. Tampermonkey に、以下のユーザスクリプトを登録し、有効化します
    ・もし、Tampermonkey の設定画面に「開発者モードを有効にしてください」のようなメッセージが表示された場合は、こちらの手順で「開発者モード」をオンにしてください。
// ==UserScript==
// @name どこでもCopilot
// @namespace https://about.karakuri.ai/
// @version 0.0.1
// @description 任意のフォーム上でGitHub Copilot風のテキスト補完
// @match *://*/*
// @grant none
// ==/UserScript==

(async function() {
'use strict';

let isProcessing = false;
let debounceTimer;

document.querySelectorAll('textarea').forEach(textarea => {
const wrapper = document.createElement('div');
wrapper.style.position = 'relative';
textarea.parentNode.insertBefore(wrapper, textarea);
wrapper.appendChild(textarea);

const overlay = document.createElement('div');
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.pointerEvents = 'none';
overlay.style.color = "rgba(0, 0, 0, 0)";
overlay.style.backgroundColor = 'transparent';
overlay.style.whiteSpace = 'pre-wrap';
overlay.style.wordWrap = 'break-word';
wrapper.appendChild(overlay);

function updateOverlay() {
overlay.style.font = window.getComputedStyle(textarea).font;
overlay.style.padding = window.getComputedStyle(textarea).padding;
overlay.style.width = `${textarea.offsetWidth}px`;
overlay.style.height = `${textarea.offsetHeight}px`;
}

updateOverlay();
window.addEventListener('resize', updateOverlay);

async function predictText() {
if (isProcessing) return;

const text = textarea.value;
const cursorPosition = textarea.selectionStart;

if (text.length >= 5) {
isProcessing = true;
try {
const session = await window.ai.createTextSession(); // v129 からは session が履歴をもつようになるため毎回初期化が必要
const input = text.replaceAll("\n", "").slice(-64).trim();
const prompt = `\`${input}\`

上の書き出しに続く文章を、50文字程度書いてみてください。`;
console.log(`prompt === ${prompt}`);

const rawPrediction = await session.prompt(prompt);
let prediction = rawPrediction.trim();
if (input.startsWith(prediction)) prediction = "";
else if (prediction.startsWith(input)) prediction = prediction.slice(input.length);
console.log(`prediction === ${prediction}`);
// TODO: より柔軟なアラインメント

const beforeCursor = text.slice(0, cursorPosition);
const afterCursor = text.slice(cursorPosition);
overlay.innerHTML = `${beforeCursor}<span style="color: lightgray;">${prediction}</span>${afterCursor}`;
} catch (error) {
console.error('予測エラー:', error);
} finally {
isProcessing = false;
}
} else {
overlay.textContent = text;
}
}

textarea.addEventListener('input', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(predictText, 300); // 300ミリ秒のデバウンス
});

textarea.addEventListener('scroll', () => {
overlay.scrollTop = textarea.scrollTop;
});

// Tabキーで予測テキストを確定
textarea.addEventListener('keydown', function(e) {
if (e.key === 'Tab' && overlay.textContent !== textarea.value) {
e.preventDefault();
this.value = overlay.textContent;
this.selectionStart = this.selectionEnd = this.value.length;
overlay.textContent = this.value;
}
});
});
})();

上記ユーザスクリプトの登録後、フォーム(テキストエリア)の設置されている任意のサイト(例:<textarea> — フォーム部品:テキストエリア — とほほのWWW入門)を開いてください。フォーム(テキストエリア)にテキストを入力していくと、本記事冒頭の動画のように「テキストの続き」が薄い文字でサジェストされます。サジェストが表示されているときに Tab キーを押すと、サジェスト内容を「採用」できます。

「どこでもCopilot」のFAQ

プロンプトについて

  • 今回は非常にシンプルなプロンプトにしています。再掲すると以下の通りです:
const prompt = `\`${input}\`

上の書き出しに続く文章を、50文字程度書いてみてください。`;
  • 時に、LLMからの返答の書き出しに input の内容がそのまま含まれる場合があります。そのような場合、input の部分を取り除いてから予測結果とするロジックを入れています。ただし、返答の書き出しが input のテキストから少しでもずれてしまった場合、上記ロジックが作動せず、その結果、例えば「ログイン方法は」という入力に続けて薄文字で「ログイン方法は以下の通りです」とサジェストされるようなことが起こってしまいます。この問題については、ロジック側でもう少し柔軟なアラインメントをとるようにすることで、多少は緩和できる可能性があります。
  • ちなみに、公式ドキュメントによれば、所定のフォーマットで few-shot examples を記述することが推奨とのことなので、それに沿って以下のようなプロンプトを最初は試していました。
const prompt = `
A text snippet: 平素より大変お世話になって
Its possible continuation is: おります。<ctrl23>

A text snippet: ご多用のところ恐れ入りますが、何卒
Its possible continuation is: 宜しくお願いいたします。<ctrl23>

A text snippet: 添付ファイルにてお
Its possible continuation is: 送りいたします。<ctrl23>

A text snippet: 送料についてのご質問で
Its possible continuation is: しょうか。<ctrl23>

A text snippet: ${input}
Its possible continuation is:`;
  • ところが、こちらのプロンプトでは、モデルが高い頻度で空文字列を返答してきて、ほとんど使い物になりませんでした。そこで、方針転換して現在のシンプルなプロンプトを使用することにしました。
  • 今後、LLMの精度が上がれば、上記のようなfew-shotによる性能向上も効果を発揮してくるのではないかと期待されます。
  • そもそも、ブラウザに内蔵されている Gemini Nano はインストラクションチューニングされているように見えますが、インストラクションチューニング前の(文章の続きを予測するだけの)モデルがもし利用可能になれば、それに続きを予測させる方が体験が良くなるかもしれません。

サジェストの精度の問題

  • 現状のブラウザビルトインLLMの日本語の性能的に、やむを得ないという印象です。現状、ブラウザに搭載されている Gemini Nano は 3.25B の 4-bit 量子化モデルであり、特に日本語に特化はしていません。(プロンプトを適切に書き換えた上で)英語の補完をさせると、もう少し性能が出るかもしれません。

思うこと

ローカルLLMの利点は、第一回の記事でも記した通り「セキュリティ・無料・無制限」と考えていますが、Gemini Nano を体験してみて、個人的に「無料」の威力が想像以上に大きいと感じました。

「無料」ということは、文字通り LLM を「湯水」のごとく叩けるということです。

「無料」であることによって、LLMをアプリケーションやウェブサービスに組み込むことが従来よりも格段にやりやすくなるはずです。例えば、毎日10万人が利用するウェブサービスのロジックにLLMを入れたいとなった場合に、1人が1日あたりLLMを1回呼び出すとすると、毎日のべ10万回LLMが呼び出されることになります。GPT-4等を使用する場合は、もちろん10万回分のAPI呼び出しコストが発生します。一方、ローカルLLMを使用する場合は、API呼び出しコストは「ゼロ」です。利用者10万人の端末の計算資源をフルに活用することで、10万回のLLM推論のコストをカットできるというわけです。

将来的には、LLMがあらゆるアプリケーションやウェブサービスの中に組み込まれ、ユビキタスな(遍在的な)存在になっていくだろうと予想しています。

おわりに

今後も、KARAKURI LM(に限らずローカルLLM全般)に関する情報を、KARAKURI LM 10本ノックという形で本テックブログにて共有してまいります。

最後に、皆様にお願いがあります。執筆の励みになりますので、Clap(拍手アイコン)のタップをお願いします!

--

--