Chrome拡張機能でTransformers.jsを使う方法
作成している過程で、Manifest V3 のランタイム、モデルの読み込み、メッセージングに関して、いくつかの実用的な観察結果に遭遇しました。共有する価値があるので、以下にまとめます。
このガイドが対象としている人
本ガイドは、Manifest V3 の制約のもとで Transformers.js を使い、Chrome 拡張機能内でローカルの AI 機能を動かしたい開発者向けです。
読み終える頃には、このプロジェクトと同じアーキテクチャになっています。具体的には、モデルをホストするバックグラウンドのサービスワーカー、サイドパネルのチャット UI、ページ単位の操作のためのコンテンツスクリプトです。
私たちが作るもの
このガイドでは、Transformers.js Gemma 4 Browser Assistant の中核となるアーキテクチャを、公開されている拡張機能を参照し、オープンソースのコードベースを実装マップとして用いながら再現します。
- ライブ拡張機能: Chrome Web Store
- ソースコード: github.com/nico-martin/gemma4-browser-extension
- 完成形: バックグラウンドでホストされる Transformers.js エンジン、サイドパネルのチャット UI、ページの抽出とハイライトのためのコンテンツスクリプト。
1) Chrome 拡張機能のアーキテクチャ(MV3)
入る前に、簡単なスコープ注記です。ここでは React の UI レイヤーや Vite のビルド設定には深く踏み込みません。焦点は、各 Chrome ランタイムで何を動かすのか、そしてそれらの部品をどのようにオーケストレーションするのか、という高レベルの設計判断にあります。
Manifest V3 が初めての方は、まずこの短い概要を読んでください: Manifest V3 とは?.
1.1 実行コンテキストとエントリポイント
MV3 では、アーキテクチャは public/manifest.json から始まります。このプロジェクトでは、3 つのエントリポイントを定義しています。
background.service_worker = background.js。src/background/background.tsからビルドされます。side_panel.default_path = sidebar.html。src/sidebar/index.htmlからビルドされます。content_scripts[].js = content.js。matches: http(s)://*/*とrun_at: document_idleを伴い、src/content/content.tsからビルドされます。
バックグラウンドのサービスワーカーは、アクティブなタブに対してサイドパネルを開くために chrome.action.onClicked も処理します。把握しておくべき関連するエントリポイントとして、action.default_popup でポップアップを定義でき、素早い操作に適しています。このプロジェクトでは永続的なチャットのためにサイドパネルを使用していますが、オーケストレーションのパターンは同じです。
1.2 どこで何が動くか
重要な設計上の決定は、重いオーケストレーションをバックグラウンドに閉じ込め、UI/ページのロジックを薄く保つことです。
- バックグラウンド(
src/background/background.ts)はコントロールプレーンです。エージェントのライフサイクル、モデルの初期化、ツール実行、特徴抽出のような共有サービスを担います。 - サイドパネル(
src/sidebar/*)はインタラクション層です。チャットの入力/出力、ストリーミング更新、セットアップ用のコントロールを扱います。 - コンテンツスクリプト(
src/content/content.ts)はページブリッジです。DOM の抽出とハイライト操作を行います。
この分割の実務上の結果として、会話履歴もバックグラウンドに存在します(Agent.chatMessages)。UI は AGENT_GENERATE_TEXT のようなイベントを送信し、バックグラウンドはメッセージを追加して推論を実行し、その後 MESSAGES_UPDATE をサイドパネルへ発行します。
この分割により、モデルの二重ロードを回避し、UI の応答性を保ち、DOM アクセスに関する Chrome のセキュリティ境界を尊重できます。
1.3 メッセージングの契約
ランタイムが分離されると、メッセージングが基盤になります。このプロジェクトでは、すべてのメッセージが src/shared/types.ts 内の enum を通して型付けされています。
- サイドパネル -> バックグラウンド(
BackgroundTasks):CHECK_MODELS、INITIALIZE_MODELSAGENT_INITIALIZE、AGENT_GENERATE_TEXT、AGENT_GET_MESSAGES、AGENT_CLEAREXTRACT_FEATURES
- バックグラウンド -> サイドパネル(
BackgroundMessages):DOWNLOAD_PROGRESS、MESSAGES_UPDATE
- バックグラウンド -> コンテンツ(
ContentTasks):EXTRACT_PAGE_DATA、HIGHLIGHT_ELEMENTS、CLEAR_HIGHLIGHTS
オーケストレーションのルールはシンプルです。バックグラウンドが唯一のコーディネーターであり、サイドパネルとコンテンツスクリプトは、アクションを要求して結果を描画する専用のワーカーです。
典型的なリクエストの流れ:
- サイドパネルが
AGENT_GENERATE_TEXTを送信します。 - バックグラウンドは
Agent.chatMessagesに追加し、モデル/ツールの手順を実行します。 - バックグラウンドは
MESSAGES_UPDATEを発行します。 - サイドパネルは更新されたメッセージ一覧から再描画します。
2) Transformers.js統合の詳細
2.1 モデルと責務
src/shared/constants.ts では、この拡張機能は2つのモデルの役割を使用しています:
- TextGeneration / LLM:
onnx-community/gemma-4-E2B-it-ONNX(text-generation,q4f16) - VectorEmbeddings:
onnx-community/all-MiniLM-L6-v2-ONNX(feature-extraction,fp32)
この分担は意図的です。Gemma 4が推論/ツールの判断を担当し、一方MiniLMは、ask_website と find_history における意味的類似検索のためのベクトル埋め込みを生成します。
2.2 推論はどこで実行されるか
すべての推論はバックグラウンドで実行されます(src/background/background.ts):
pipeline("text-generation", ...)によるテキスト生成(新しいDynamicCacheクラスによって一貫したKVキャッシュが有効化されています)pipeline("feature-extraction", ...)による埋め込み(加えてベクトル正規化)
これにより、すべてのタブ/セッションに対して単一のモデルホストが提供され、重複したメモリ使用を避けつつ、サイドパネルUIの応答性も保たれます。モデルはバックグラウンドのサービスワーカーから読み込まれるため、成果物はサイトごとのオリジンではなく、拡張機能のオリジン(chrome-extension://<extension-id>)の下でキャッシュされます。これにより、拡張機能のインストール全体で共有される1つのキャッシュが得られます。
MV3ライフサイクルの注意:サービスワーカーはサスペンドされ、再起動されることがあります。そのため、モデル実行時の状態は復元可能だと扱い、必要に応じて再初期化するべきです。
2.3 ダウンロードとキャッシュのライフサイクル
モデルのライフサイクルは明示的です:
CHECK_MODELSは、すでにキャッシュされているものを調べ、残りのダウンロードサイズを見積もります。INITIALIZE_MODELSはモデルをダウンロード/初期化し、DOWNLOAD_PROGRESSをUIに送出します。- セットアップ後は、長寿命のインスタンスを再利用します:
src/background/agent/Agent.tsの生成パイプラインsrc/background/utils/FeatureExtractor.tsの埋め込みパイプライン
権限とプライバシーは、末尾にあるチェックボックスではなく、アーキテクチャの一部です。このプロジェクトでは、public/manifest.json にて sidePanel、storage、scripting、および tabs を要求し、さらに http(s)://*/* 用の host_permissions を指定します:
sidePanel:サイドパネルのUXを開いて制御するために必要です。storage:ツール/設定の状態をセッションをまたいで保持するために必要です。tabs+scripting:タブ対応のツールやページ単位のアクションに必要です。
返却形式: {"translated": "翻訳されたHTML"}host_permissionsonhttp(s)://*/*: 任意のWebサイトで動作するように設計されているため、コンテンツの抽出/ハイライトに必要です。
なぜこれを厳しめにするのか: 権限はユーザーの信頼と、Chrome Web Storeの審査リスクを左右します。機能が実際に必要とするものだけを要求し、推論が拡張機能の実行時にローカルで行われることを明確に述べてください。そうすることで、ユーザーは自分のデータがどこで処理されるのかを理解できます。
3) エージェントとツールの実行ループ
3.1 ツール呼び出しの基礎(この層が存在する理由)
実行ループの前に、モデルのツール呼び出しがどのように動くかを理解しておくと役立ちます(あらゆるエージェント型ワークフローの基盤です)。メッセージに加えてツールのスキーマ(name、description、parameters)を渡し、Transformers.jsが、それらの入力からモデルのチャットテンプレートを使って実際のプロンプトを組み立てます。チャットテンプレートはモデル固有のため、ツール呼び出しの正確な形式は、どのモデルを使うかによって異なります。Gemma-4スタイルのテンプレートでは、モデルがツールを呼び出すと判断したときに、特別なツール呼び出しトークンのブロックが出力されます。
import { pipeline } from "@huggingface/transformers";
const generator = await pipeline(
"text-generation",
"onnx-community/gemma-4-E2B-it-ONNX",
{
dtype: "q4f16",
device: "webgpu",
},
);
const messages = [{ role: "user", content: "What's the weather in Bern?" }];
const output = await generator(messages, {
max_new_tokens: 128,
do_sample: false,
tools: [
{
type: "function",
function: {
name: "getWeather",
description: "Get the weather in a location",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "The location to get the weather for",
},
},
required: ["location"],
},
},
},
],
});
生成時、モデルは例えば次のような出力を行えます:
<|tool_call>call:getWeather{location:<|"|>Bern<|"|>}<tool_call|>
これがまさに、このプロジェクトに正規化の層(webMcp)とパーサ(extractToolCalls)がある理由です。モデルの出力は、決定論的なツール実行に変換する必要があります。
3.2 このプロジェクトにおけるツール・インターフェース
src/background/agent/webMcp.tsx は、拡張機能のツールを、モデルが扱いやすい形に正規化します:
name,description,inputSchema,execute
例として、get_open_tabs、go_to_tab、open_url、close_tab、find_history、ask_website、highlight_website_element があります。
3.3 ループ設計(Agent.runAgent)
ここでの中核となる設計上の選択は、内部モデルのメッセージと、UIに面したチャットメッセージを分離することです:
- 内部モデルのトランスクリプト(
messages):generator(...)でメッセージとして使われる、system/user/tool/assistant の各ターン。 - UIトランスクリプト(
chatMessages):ユーザーに表示される内容。ストリーミングされたアシスタントのテキストに加え、ツール実行のメタデータ(tools)とパフォーマンス指標が含まれます。
実行フロー:
- ユーザー入力を
chatMessagesに追加し、アシスタントのプレースホルダーを作成して、トークンをストリーミングします。 - ストリーミング/最終のモデル出力を、
extractToolCalls.tsを使って{ message, toolCalls }に解析します。 - ユーザーに見せるアシスタントメッセージはプレーンテキストのまま保持し、ツール呼び出しはバックグラウンドで実行します。
- ツール結果をアシスタントのツールメタデータに追加し、次のプロンプトのターンとして結果をフィードバックします。
- ツール呼び出しが残らなくなるまで繰り返し、その後、アシスタントのコンテンツ+指標を確定します。
これにより、バックグラウンドで決定的なツールループを保ちつつ、ユーザーとのやり取りはクリーンに保てます。
4) データ境界と永続化
状態の配置は、MV3において非常に重要なもう一つのアーキテクチャ上の意思決定です。この実装では、状態をライフサイクルとアクセスパターンによって分割します:
- 会話状態:高速なターンごとのオーケストレーションのためのバックグラウンドメモリ(
Agent.chatMessages)。 - ツールの設定:
chrome.storage.local。設定をセッションをまたいで保持します。 - セマンティック履歴ベクトル:ローカルでの大規模な検索データ用の IndexedDB(
VectorHistoryDB)。 - 抽出済みページコンテンツ:アクティブなURLをキーにしたバックグラウンドキャッシュ(
WebsiteContentManager)。
第1.2節で説明したとおり、会話履歴をバックグラウンドに保持することで、UIの更新を通して一つの正規の状態を得られます。これにより、短命の状態はメモリに置き、永続的な設定は拡張機能ストレージに置き、重い検索データはローカルデータベースに置けます。
5) ビルドとパッケージングの注意
複雑なビルド設定は不要ですが、MV3では各実行時に予測可能な出力が必要です。
vite.config.tsにおけるマルチエントリのビルド:- manifest に合わせた出力名/パス(
sidebar.html、background.js、content.js)を確保する。 - コンテンツスクリプトは、実行時のチャンク読み込み問題を避けるために、自己完結した出力として保持する。
目標はシンプルです。Chrome の各エントリポイントごとに、public/manifest.json が期待する場所へ、1つの成果物を配置することです。
最終的な要点
このプロジェクト全体の鍵を握る建築上の選択は、「関心の分離」を明確にすることです。背景側がオーケストレーションとモデル実行を担い、UIは薄く保ち、コンテンツスクリプトがページアクセスを処理します。
このプロジェクトはサイドパネルを使用しますが、同じ方針は他の構成でも機能します:
- ポップアップ優先のアシスタント:素早いやり取りには
action.default_popupを使い、会話の状態とモデル実行は背景側が担います。 - サイドパネルのコパイロット:長時間実行される会話は永続的なパネルに保持し、背景側でツールのループ処理とキャッシュを処理します。
- タブごとのエージェント:各タブがそれぞれ独自の文脈を持つべき場合は、背景側で
tabIdごとに1つのエージェント状態を保持します。 - ハイブリッドUI(ポップアップ+サイドパネル+オプションページ):すべてのUIの入口は同じ背景側コーディネータと通信し、同じメッセージ契約を再利用します。
実務上のルールはシンプルです。状態がどこに置かれるかを決める(global、tabId、またはサイトスコープ)→その状態とモデル推論を背景側に保持する(基本的には背景サービスとして)→UI/コンテンツの実行環境は、焦点を絞ったクライアントとして振る舞わせる、ということです。



