如何在 Chrome 扩展中使用 Transformers.js
How to Use Transformers.js in a Chrome Extension
Hugging Face 发布教程,讲解如何在 Manifest V3 约束下用 Transformers.js 在 Chrome 扩展中运行本地 AI 功能,并以官方发布的 Gemma 4 浏览器助手扩展为参考实现。
我们最近发布了一个由 Gemma 4 E2B 驱动的 Transformers.js 演示浏览器扩展,帮助用户浏览网页。
在构建它的过程中,我们遇到了几个关于 Manifest V3 运行时、模型加载和消息传递的实用观察,值得分享。
适用对象
本指南面向希望在 Manifest V3 约束下使用 Transformers.js 在 Chrome 扩展中运行本地 AI 功能的开发者。
读完后,你将拥有与本项目相同的架构:托管模型的后台 service worker、侧边栏聊天 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 开始。本项目定义了三个入口点:
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构建。
后台 service worker 还处理 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 响应,并尊重 Chrome 围绕 DOM 访问的安全边界。
1.3 消息传递契约
一旦运行时分离,消息传递就成为骨干。在本项目中,所有消息都通过 src/shared/types.ts 中的枚举进行类型化。
- Side panel -> background (
BackgroundTasks):CHECK_MODELS,INITIALIZE_MODELSAGENT_INITIALIZE,AGENT_GENERATE_TEXT,AGENT_GET_MESSAGES,AGENT_CLEAREXTRACT_FEATURES
- Background -> side panel (
BackgroundMessages):DOWNLOAD_PROGRESS,MESSAGES_UPDATE
- Background -> content (
ContentTasks):EXTRACT_PAGE_DATA,HIGHLIGHT_ELEMENTS,CLEAR_HIGHLIGHTS
编排规则很简单:后台是唯一的协调者;侧边栏和内容脚本是专门的工作者,负责请求操作并渲染结果。
典型的请求流程:
- 侧边栏发送
AGENT_GENERATE_TEXT。 - 后台追加到
Agent.chatMessages并运行模型/工具步骤。 - 后台发出
MESSAGES_UPDATE。 - 侧边栏根据更新后的消息列表重新渲染。
2) Transformers.js 集成细节
2.1 模型与职责
在 src/shared/constants.ts 中,此扩展使用两种模型角色:
- 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 响应迅速。由于模型从后台 service worker 加载,产物缓存在扩展源(chrome-extension://<extension-id>)下,而不是按网站源分别缓存,从而为整个扩展安装提供一份共享缓存。
MV3 生命周期注意事项:service worker 可能被挂起并重启,因此模型运行时状态应视为可恢复,并在需要时重新初始化。
2.3 下载与缓存生命周期
模型生命周期是明确的:
CHECK_MODELS检查已缓存的内容并估算剩余下载大小。INITIALIZE_MODELS下载/初始化模型并向 UI 发出DOWNLOAD_PROGRESS。- Long-lived instances are reused after setup:
权限与隐私是架构的一部分,而不是最后才勾选的复选框。在本项目中,public/manifest.json 请求 sidePanel、storage、scripting 和 tabs,外加用于 http(s)://*/* 的 host_permissions:
sidePanel:打开和控制侧边栏 UX 所必需。storage:跨会话持久化工具/设置状态所必需。tabs+scripting:感知标签页的工具和页面级操作所必需。- 在
http(s)://*/*上的host_permissions:必需,因为内容提取/高亮设计为可在任意网站上工作。
为什么保持权限范围狭窄:权限决定用户信任和 Chrome 应用商店审核风险。只请求你的功能实际需要的内容,并明确说明推理在扩展运行时本地运行,以便用户了解其数据在何处处理。
3) 智能体与工具执行循环
3.1 工具调用基础(为什么存在这一层)
在执行循环之前,有必要了解模型工具调用如何工作(这是任何智能体工作流的基础)。你传入消息以及工具 schema(name、description 和 parameters),Transformers.js 会使用模型的聊天模板根据这些输入格式化实际提示词。由于聊天模板因模型而异,确切的工具调用格式取决于你使用的模型。使用 Gemma-4 风格的模板时,当模型决定调用某个工具,它会发出一个特殊的工具调用 token 块。
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,创建占位助手消息,并流式输出 token。 - 使用
extractToolCalls.ts解析流式/最终模型输出为{ message, toolCalls }。 - 保持用户可见的助手消息为纯文本,同时工具调用在后台执行。
- 将工具结果追加到助手工具元数据,并将结果作为下一轮提示反馈回去。
- 重复直到没有剩余工具调用,然后最终确定助手内容 + 指标。
这样既保持用户沟通简洁,又在后台保留确定性的工具循环。
4) 数据边界与持久化
状态放置是另一个在 MV3 中非常重要的架构决策。在此实现中,状态按生命周期和访问模式拆分:
- 对话状态:后台内存(
Agent.chatMessages),用于快速的逐轮编排。 - 工具偏好:
chrome.storage.local,使设置跨会话持久化。 - 语义历史向量:IndexedDB(
VectorHistoryDB),用于更大的本地检索数据。 - 提取的页面内容:后台缓存(
WebsiteContentManager),以活动 URL 为键。
如第 1.2 节所述,将对话历史保留在后台可在 UI 更新之间提供单一规范状态。这样可将短生命周期状态保留在内存中,将持久设置保留在扩展存储中,将大量检索数据保留在本地数据库中。
5) 构建与打包说明
你不需要复杂的构建设置,但 MV3 确实要求每个运行时都有可预测的输出。
- Multi-entry build in
vite.config.ts: - 确保输出名称/路径与 manifest 对齐(
sidebar.html、background.js、content.js)。 - 将内容脚本保持为自包含输出,以避免运行时 chunk 加载问题。
目标很简单:每个 Chrome 入口点一个产物,放在 public/manifest.json 期望的确切位置。
最终要点
解锁整个项目的架构选择是清晰的关注点分离:后台负责编排和模型执行,UI 界面保持轻量,内容脚本处理页面访问。
此项目使用侧边栏,但同样的方法也适用于其他设置:
- 弹窗优先助手:使用
action.default_popup进行快速交互,由后台负责对话状态和模型执行。 - 侧边栏副驾驶:在持久面板中保持长时间运行的对话,同时后台处理工具循环和缓存。
- 每标签页代理:当每个标签页应有自己的上下文时,在后台为每个
tabId保留一个代理状态。 - 混合 UI(弹窗 + 侧边栏 + 选项页):所有 UI 入口点都与同一个后台协调器通信,并复用相同的消息契约。
实用规则很简单:决定状态存放位置(global、tabId 或站点作用域),将该状态和模型推理保留在后台(基本上作为后台服务),并让 UI/内容运行时充当专注的客户端。
来源:Hugging Face Blog · huggingface.co