Hugging Face 发布实用教程,展示如何在浏览器扩展中集成本地 AI 模型,适合前端开发者。
在构建这个项目的过程中,我们对 Manifest V3 运行时、模型加载和消息传递有了一些值得分享的实践观察。
本指南面向希望在 Manifest V3 的限制下,使用 Transformers.js 在 Chrome 扩展中运行本地 AI 功能的开发者。
阅读完本指南后,你将搭建出与该项目相同的架构:一个托管模型的后台 Service Worker、一个侧边栏聊天 UI,以及一个用于页面级操作的 content script。
本指南将参考已经发布的扩展,并以开源代码库作为实现蓝图,重建 Transformers.js Gemma 4 Browser Assistant 的核心架构。
在线扩展:Chrome Web Store
源代码:github.com/nico-martin/gemma4-browser-extension
最终成果:一个在后台托管的 Transformers.js 引擎、一个侧边栏聊天 UI,以及一个用于提取页面内容和高亮元素的 content script。
在深入之前,先简单说明一下范围:本文不会详细介绍 React UI 层或 Vite 构建配置,重点是高层架构决策——哪些代码在哪个 Chrome 运行时中执行,以及这些部分如何协同工作。
如果你还不了解 Manifest V3,可以先阅读这篇简短概览:什么是 Manifest V3?。
在 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 定义 popup,它非常适合执行快捷操作。这个项目使用侧边栏来承载持续性的聊天,但两者的编排模式是一样的。
关键的设计决策是:将繁重的编排工作放在后台,同时让 UI 和页面逻辑保持轻量。
后台(src/background/background.ts)是控制平面:负责 Agent 生命周期、模型初始化、工具执行,以及特征提取等共享服务。
侧边栏(src/sidebar/*)是交互层:负责聊天输入与输出、流式更新和初始化设置控件。
Content script(src/content/content.ts)是页面桥梁:负责提取 DOM 和执行高亮操作。
这种职责划分带来的一个实际结果是,对话历史也保存在后台(Agent.chatMessages)中:UI 发送 AGENT_GENERATE_TEXT 等事件,后台追加消息、运行推理,然后向侧边栏发出 MESSAGES_UPDATE。
这种拆分方式可以避免重复加载模型、保持 UI 响应流畅,同时遵循 Chrome 针对 DOM 访问设置的安全边界。
运行时彼此分离后,消息传递就成了整个架构的骨干。在这个项目中,所有消息都通过 src/shared/types.ts 中的枚举进行类型化。
侧边栏 -> 后台(BackgroundTasks):CHECK_MODELS、INITIALIZE_MODELS、AGENT_INITIALIZE、AGENT_GENERATE_TEXT、AGENT_GET_MESSAGES、AGENT_CLEAR、EXTRACT_FEATURES
CHECK_MODELS、INITIALIZE_MODELS
AGENT_INITIALIZE、AGENT_GENERATE_TEXT、AGENT_GET_MESSAGES、AGENT_CLEAR
后台 -> 侧边栏(BackgroundMessages):DOWNLOAD_PROGRESS、MESSAGES_UPDATE
DOWNLOAD_PROGRESS、MESSAGES_UPDATE
后台 -> content(ContentTasks):EXTRACT_PAGE_DATA、HIGHLIGHT_ELEMENTS、CLEAR_HIGHLIGHTS
EXTRACT_PAGE_DATA、HIGHLIGHT_ELEMENTS、CLEAR_HIGHLIGHTS
编排规则很简单:后台是唯一的协调者;侧边栏和 content script 是各司其职的专用工作单元,负责请求操作并渲染结果。
典型的请求流程如下:
侧边栏发送 AGENT_GENERATE_TEXT。
后台将消息追加到 Agent.chatMessages,然后执行模型和工具步骤。
后台发出 MESSAGES_UPDATE。
侧边栏根据更新后的消息列表重新渲染。
在 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 中的语义相似度搜索。
所有推理都在后台(src/background/background.ts)运行:
文本生成通过 pipeline("text-generation", ...) 完成,并使用我们新提供的 DynamicCache 类启用一致的 KV Caching。
向量嵌入通过 pipeline("feature-extraction", ...) 加向量归一化完成。
这样可以为所有标签页和会话提供单一的模型宿主,避免重复占用内存,并保持侧边栏 UI 的响应速度。由于模型从后台 Service Worker 加载,模型文件会缓存在扩展来源(chrome-extension://<extension-id>)下,而不是分别缓存在各个网站的来源中,因此整个扩展安装实例可以共享同一份缓存。
关于 MV3 生命周期需要注意:Service Worker 可能会被挂起并重新启动,因此模型的运行时状态应被视为可恢复状态,并在需要时重新初始化。
模型生命周期是显式管理的:
CHECK_MODELS 检查已经缓存的内容,并估算剩余下载大小。
INITIALIZE_MODELS 下载并初始化模型,同时向 UI 发出 DOWNLOAD_PROGRESS。
初始化完成后,会复用长期存活的实例:src/background/agent/Agent.ts 中的生成 pipeline,以及 src/background/utils/FeatureExtractor.ts 中的 embedding pipeline。
src/background/agent/Agent.ts 中的生成 pipeline
src/background/utils/FeatureExtractor.ts 中的 embedding pipeline
权限与隐私是架构的一部分,而不是项目收尾时才勾选的复选框。在这个项目中,public/manifest.json 申请了 sidePanel、storage、scripting 和 tabs 权限,以及针对 http(s)://*/* 的 host_permissions:
sidePanel:用于打开和控制侧边栏 UX。
storage:用于跨会话持久化工具和设置状态。
tabs + scripting:用于感知标签页的工具和页面级操作。
针对 http(s)://*/* 的 host_permissions:页面内容提取和高亮功能需要在任意网站上运行,因此必须申请该权限。
为什么要尽量缩小权限范围:权限会影响用户信任,也会影响 Chrome Web Store 的审核风险。只申请功能真正需要的权限,并明确说明推理是在扩展运行时中本地执行的,让用户清楚自己的数据在哪里被处理。
在介绍执行循环之前,有必要先了解模型工具调用的工作方式——这是所有 Agent 工作流的基础。你需要传入消息和工具 schema(名称、描述和参数),Transformers.js 会根据这些输入,使用模型的 chat template 格式化实际 prompt。由于 chat template 因模型而异,工具调用的确切格式取决于你使用的模型。使用 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)的原因:必须将模型输出转换成确定性的工具执行操作。
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。
这里的核心设计选择,是将模型内部消息与面向 UI 的聊天消息分开:
模型内部记录(messages):传给 generator(...) 的 system、user、tool 和 assistant 轮次。
UI 对话记录(chatMessages):用户实际看到的内容,包括流式输出的 assistant 文本、工具执行元数据(tools)和性能指标。
将用户输入添加到 chatMessages,创建一条占位的 assistant 消息,然后流式输出 token。
使用 extractToolCalls.ts 解析流式或最终模型输出,将其转换为 { message, toolCalls }。
面向用户的 assistant 消息保持为纯文本,而工具调用则在后台执行。
将工具结果附加到 assistant 的工具元数据中,并把结果作为下一轮 prompt 输入反馈给模型。
重复上述流程,直到不再有工具调用,然后确定最终的 assistant 内容和指标。
这样既能让用户看到的交流内容保持简洁,又能在后台保留一个确定性的工具执行循环。
状态应该放在哪里,是 MV3 中另一个非常重要的架构决策。在这个实现中,状态按照生命周期和访问模式进行拆分:
对话状态:保存在后台内存(Agent.chatMessages)中,以便快速完成逐轮编排。
工具偏好:保存在 chrome.storage.local 中,使设置能够跨会话持久化。
语义历史向量:保存在 IndexedDB(VectorHistoryDB)中,用于存储规模更大的本地检索数据。
提取出的页面内容:缓存在后台(WebsiteContentManager)中,以当前 URL 为键。
正如第 1.2 节所述,将对话历史保存在后台,可以在 UI 更新过程中维持唯一的标准状态。这种设计让短期状态保留在内存中、持久化设置保存在扩展存储中,而体量较大的检索数据则保存在本地数据库中。
你不需要复杂的构建配置,但 MV3 确实要求为每个运行时生成可预测的输出。
vite.config.ts 中的多入口构建:src/sidebar/index.html、src/background/background.ts、src/content/content.ts
src/sidebar/index.html
src/background/background.ts
src/content/content.ts
确保输出名称和路径与 manifest 保持一致(sidebar.html、background.js、content.js)。
让 content script 保持为独立、完整的输出,避免运行时加载 chunk 时出现问题。
目标很简单:为 Chrome 的每个入口点生成一个构建产物,并将其放在 public/manifest.json 所期望的确切位置。
支撑整个项目的关键架构选择,是清晰的关注点分离:后台负责编排和模型执行,UI 界面保持轻量,content script 负责页面访问。
这个项目使用侧边栏,但同样的方法也适用于其他方案:
以 popup 为主的助手:使用 action.default_popup 处理快速交互,由后台负责维护对话状态和执行模型。
侧边栏 Copilot:在持久化侧边栏中维持长时间对话,同时由后台处理工具循环和缓存。
按标签页划分的 Agent:如果每个标签页都需要拥有独立上下文,可以在后台按 tabId 分别保存 Agent 状态。
混合 UI(popup + 侧边栏 + 选项页面):所有 UI 入口点都与同一个后台协调器通信,并复用相同的消息协议。
实践规则很简单:确定状态的作用域和存放位置(全局、tabId 或网站级),将这些状态与模型推理保留在后台——本质上将其作为后台服务——然后让 UI 和 content 运行时充当职责明确的客户端。
本文提及的模型 3
我们博客中的更多文章
在 Transformers.js 中试验提议的 Cross-Origin Storage API
使用 Transformers.js 制作由 ML 驱动的 Web 游戏
· 注册或登录后发表评论
本文提及的模型 3