ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

浏览器扩展端侧AI推理架构:从Manifest V3到工程落地

浏览器扩展端侧AI推理架构:从Manifest V3到工程落地 1. 为什么要在浏览器扩展里做端侧 AI先算清楚这笔账1.1 需求拆解哪些功能天然适合放到扩展里先别急着写代码。我在动手之前会把候选功能列一遍页面翻译、长文摘要、表单智能填充、广告分类、图片 OCR、无障碍朗读、密码管理里的语义分析……它们的共同点是都要访问当前页面的上下文而且很多场景对延迟和隐私极其敏感。比如你打开一封英文邮件希望 500ms 内给出中文摘要走云端的话一个 RTT 就没了遇到弱网直接卡死再比如用户在私人文档里做关键词提取数据一旦上传产品的信任成本会非常高。把这些功能下沉到端侧扩展天然能拿到 DOM 结构还能在本地处理完直接操作页面整个链路比后端服务短得多。浏览器扩展环境下的端侧 AI 推理本质上是把原本放在服务器上的模型搬运到用户自己的设备里再把“读取页面信息—推理—回写页面”这条链路完整闭环。这个场景很特殊因为扩展不是普通网页它由多个独立执行环境拼成还要遵守 Manifest V3 的各种限制。也正因为特殊一旦架构没理清楚后面每个功能迭代都会撞墙。我见过太多项目卡在“模型下载好了但推理引擎不知道放哪跑”这种问题上所以这篇文章想从架构选型讲到工程落地把该避的坑都摊开说。1.2 端侧与云端的成本、隐私、可用性对比很多人会犹豫端侧 AI 到底比云端好在哪我一般会拉一张对比表直接把账算明白。对比维度云端方案扩展端侧方案推理延迟50~300ms 网络加服务端耗时10~100ms本机执行隐私数据离开设备基本不出浏览器离线能力依赖网络可用缓存模型离线跑模型更新服务端热更新需要扩展版本或授权拉取新模型单次成本按 token/调用次数计费只有电量和内存质量天花板可以用超大模型受设备内存限制只能小模型加量化这么一对比端侧至少在隐私、延迟和成本三项上有碾压性优势代价是模型规模被设备内存死死压住。我的经验是能塞进 4GB 内存设备的量化模型7B 以下在浏览器里用 WebGPU 跑质量已经足够应付摘要、分类、抽取这类任务真要追求顶级效果再考虑把部分任务发给云端做级联。架构设计时就要给这种“端云协同”留好口子不要一上来就赌死某一条路。1.3 架构师视角扩展不是小页面是分布式系统的一个节点很多人把浏览器扩展当成一个能开小窗的网页这是第一个误区。加载了扩展的浏览器实际上多了一组常驻的、彼此隔离的执行环境content script 住在页面上下文里service worker 有独立的生命周期offscreen document 又是另一个隐藏页面。它们之间没有全局变量没有共享内存只能靠 message 通信。这跟设计一台分布式交换机非常像——每个执行环境是一个端口消息要按协议转发节点会随时掉线。如果你用写单页应用的思路去写扩展后面一定会被“怎么拿不到变量”“状态怎么没了”折磨疯。所以我在设计系统时会借鉴分布式交换机系统架构里的分层思想内核只做消息路由业务逻辑按端口拆分状态统一收敛到可持久化的存储层。扩展的 service worker 就是那个控制平面页面上下文里的 content script 是接入端口模型推理引擎则是一个可被调度的计算节点。这样解耦之后哪怕某个端口被浏览器回收控制平面也能重建上下文整体可用性反而比单页面更稳。系统架构设计师看这个场景会很有共鸣因为它本质上是在一个受限环境里做高可用设计。2. 现代浏览器扩展体系下的架构核心模块划分与消息流2.1 Manifest V3 给架构划下的硬边界MV3 不是可选升级Chrome 已经从 2023 年起逐步禁用了 Manifest V2现在新项目基本都是 MV3。它给架构画了几条硬边界。第一后台脚本被 Service Worker 取代不能常驻一段时间没事件就会被杀掉第二默认 CSP 禁止远程代码执行“unsafe-eval”没了WebAssembly 需要用“wasm-unsafe-eval”单独放行第三API 面收窄按需声明权限跨源请求也要 host_permissions。第四content script 的注入时机和方式有明确限制动态注入代码受到强约束。这些边界在早期规划时就得想清楚否则后面改架构的成本非常高。我在最开始就把 manifest.json 当作架构文档来写权限、CSP、background 类型都明确而不是随手复制模板。比如我需要在 Service Worker 里做模型下载就提前声明 storage、unlimitedStorage 和对应的下载域。很多人在开发模式下没感觉一旦发布到商店进入严格审核才发现 manifest 权限写得一团糟。2.2 推理引擎放哪Service Worker、Offscreen Document 还是 Content Script这是架构里最容易被忽略的问题。先看三个执行环境的差异Service Worker没有 DOM不能直接操作页面但可以跑 fetch、缓存、IndexedDB适合做任务调度和模型仓库管理Content Script能访问页面 DOM但和页面共享进程长时间跑大规模计算会卡住网页而且它的 API 很受限Offscreen Document一个隐藏页面能使用 DOM、Audio、WebGPU适合承载真正吃算力的推理引擎。我的推荐做法是content script 只做 DOM 采集和结果回写service worker 负责消息路由、模型缓存、任务队列推理引擎放在 offscreen document 里。这样重计算被隔离到隐藏文档页面滚动不会被阻塞service worker 被杀后模型句柄虽然会丢但通过重新拉起 offscreen document 重建整个链路是可持续的。需要特别注意的是Service Worker 里能跑 WebAssembly 推理但如果要用 WebGPU最好放到 offscreen document否则上下文管理和生命周期维护会非常别扭。2.3 消息协议设计像设计交换机端口一样设计扩展内网跨上下文通信只有一种正规姿势——chrome.runtime.sendMessage 和 chrome.tabs.sendMessage但实际调用链非常容易变成意大利面条。我给项目定义了一个最小协议统一消息信封{ id: req_9957f, type: inference.request, payload: { task: summarize, text: ..., options: { maxTokens: 256 } } }所有异步响应都必须带同一个 id超时、错误也要用固定字段返回。service worker 相当于交换机根据 type 把消息转发给模型节点、模型仓库或者内容脚本。定义协议时我会写清楚谁发、谁收、超时多久、失败重试几次。这比在代码里到处 sendMessage 再回调要靠得住。之前我在做一个多扩展联动的项目时就把每个执行环境映射成交换机上的端口消息的路由、广播、单播都按统一规则执行。这套设计后来帮了大忙新增一个“批量摘要”能力时服务端只需要新增一个消息类型不用改任何端口逻辑。所以别嫌协议设计麻烦前期省掉的每一步后期都会以更难看的方式还回来。3. 端侧推理引擎的选型与落地细节3.1 引擎选型Transformers.js、ONNX Runtime Web、WebLLM 怎么选确定好架构后下一步就是选推理引擎。这里没有银弹只有匹配度。引擎底层模型格式优势注意点Transformers.jsWASM/WebGPUONNXHuggingFace 生态好API 友好包体积不小需要优化 tree-shakingONNX Runtime WebWASM/WebGPUONNX算子覆盖广可控性强原生绑定层集成成本高WebLLMWebGPUMLC/AWQ 量化能直接跑 7B 级 LLM依赖 WebGPUSafari 支持不友好我的选择逻辑是大多数场景先用 Transformers.js 快速验证因为它的 API 最贴近 PyTorch 习惯模型可以直接从 HuggingFace 转换。一旦发现算子覆盖或性能瓶颈再下沉到 ONNX Runtime Web 做精细控制。如果目标是跑 7B 级大语言模型做长对话或长摘要就直接考虑 WebLLM。选型不是越强越好要和你的目标设备集对齐。比如一个只跑文本分类的扩展用 WebLLM 就是杀鸡用牛刀包体积和启动时间都会让用户望而却步。3.2 模型获取、校验与缓存策略模型是整套系统里最重的资源。一个 7B 模型即使量化成 INT4也要 4GB 左右走网络拉回来非常占用流量更别提每次扩展更新都重新下载。我的做法是模型文件不跟扩展包一起打首次使用时通过带校验的下载流程拉取后续从 Cache Storage 读取manifest 里声明 storage、unlimitedStorage 权限给缓存足够空间下载时先取清单文件model.json / config.json校验 SHA-256再按分片下载避免一次大文件失败全部重来。这与很多浏览器扩展里下载管理器比如 FDM 浏览器扩展那类的思路很一致任务要有队列、断点、完整性和进度上报。把模型下载当成一组可恢复任务来管理而不是一次 fetch 完事。我在实际项目里还加了一个“模型预热”逻辑用户首次进入页面时只拉小模型等页面空闲了再后台下载大模型这样既不会抢首屏资源又能保证后续功能可用。3.3 量化参数是怎么定的从 FP32 到 INT8/INT4端侧推理绕不开量化。简单说就是用更少的比特数表示模型权重换来内存占用和速度上的提升代价是精度损失。FP32 的 7B 模型要 28GBINT8 是 7GBINT4 是 3.5GB。浏览器扩展的环境里普通笔记本 16GB、手机 8GB能留给推理的内存还得打折扣所以 INT4/INT8 几乎是必选项。实际操作中我会用一个三层策略。第一先用 INT8 验证正确性和 FP32 输出对比算相似度第二如果精度不够只对关键层之外的层做量化保留敏感层 FP16第三低端设备Arm 架构、国产麒麟系统这类自动降级到 INT4 加更短的上下文窗口。量化的另一个收益是速度INT8 在 WASM 上的速度比 FP32 明显快INT4 在 WebGPU 上还能配合硬件整数运算再提一截。不要凭感觉定拿你的真实任务跑一组基准数据再拍板。4. 工程实现规范从原型到可维护的代码4.1 目录结构与模块边界代码写多了你就会发现扩展项目的崩溃往往不是单点 bug而是模块边界失控。我推荐这样的分层目录extension/ manifest.json src/ content/ # 页面注入脚本DOM采集、UI注入 background/ # service worker消息路由、调度 offscreen/ # 推理宿主加载引擎、执行会话 model-store/ # 模型缓存、版本管理、校验 infer/ # 统一推理接口文本、图像、嵌入 messaging/ # 消息协议、信封、错误码 common/ # 工具函数、日志、能力检测 models/ # 内置小模型可选 dist/ # 构建产物边界规则要写清楚content 不 import 推理代码background 不直接操作 DOMmodel-store 不感知页面消息。这个边界一开始就要写进团队的工程规范里否则两个星期后就会有人把 ONNX 直接加载进 content script。别问我怎么知道的。从系统架构设计师的角度看模块边界就是系统边界。每个目录都是一个可独立替换的子系统这样测试也好写后面做性能优化也不会牵一发动全身。比如我想把推理引擎从 Transformers.js 换成 ONNX Runtime Web只需要改 offscreen 和 infer 两层content 和 messaging 完全不用动。4.2 代码规范与错误处理Promise、超时与重试扩展里消息通信最怕静默失败。Service Worker 被唤醒要时间offscreen document 冷启动要时间推理引擎加载要时间任何一个环节超时都应该暴露出来。我会把所有消息收发包一层 async/await加默认超时和有限重试async function sendWithTimeout(message, timeoutMs 8000) { const response await Promise.race([ chrome.runtime.sendMessage(message), new Promise((_, reject) setTimeout(() reject(new Error(timeout: ${message.id})), timeoutMs) ), ]); return response; }失败重试要带退避比如 200ms、500ms、1000ms三次后把错误上报。这里的关键是每条消息都要有 id否则你根本不知道超时的是哪一次请求。我在日志里会把整条消息链路打印出来包含发起方、目的地、用时这样排错时可以直接定位是哪个节点慢。错误码也要统一。我现在用的是一组三位数字100 系列给消息层200 系列给模型层300 系列给缓存层400 系列给设备能力不足。有了统一错误码用户截图反馈问题时我一眼就能判断问题出在哪个环节而不是从一堆“something went wrong”里猜。4.3 资源加载与生命周期管理Service Worker 活不过几十秒这是 MV3 的常见问题。模型句柄、推理上下文都放在内存里SW 一睡就全没了。我的方案是分层恢复元数据模型版本、任务状态落到 IndexedDB推理会话WASM 实例、WebGPU device放到 offscreen document尽量保持长效监听到 service worker 被重建时先查 IndexedDB再按需重建 offscreen document而不是全量重来。对应到 Linux 系统的 IOMMU 软件架构分析里讲的内存隔离思想端侧推理引擎的 WASM 线性内存也像 DMA 缓冲区一样需要显式分配、映射和释放。浏览器虽然不会真的蓝屏但内存泄漏是实打实的。推理完成后及时调用 session.release()、buffer.dispose()别等 GC。尤其是长驻的 offscreen document如果不主动释放几小时就能吃掉几百 MB 内存。5. 环境适配与性能调优实录5.1 检测设备算力从 uname 到 WebGPU 能力探测不同设备的差距比想象中大。桌面 x86 上 WebGPU 能流畅跑 7B 量化ARM 笔记本或国产 ARM 设备可能连 3B 都喘。我们在服务器上常用uname -m看系统架构在浏览器里就要用几个能力指标拼接navigator.hardwareConcurrencyCPU 线程数navigator.deviceMemory设备内存单位 GB部分浏览器支持navigator.gpu是否支持 WebGPU以及 adapter 名称WebAssembly 支持情况和 SIMD 能力。我会做成一个 capability 对象启动时检测一次后续按它决定模型档位和运行后端。这里要特别提一句像 U 盘安装麒麟系统 ARM 架构这样的国产设备浏览器环境往往不是标准 ChromiumWebGPU 支持可能缺失必须老老实实回退到 WASM并且把上下文窗口调小否则会直接 OOM。即便是同一台电脑Chrome 和 Safari 对 WebGPU 的支持程度也不同所以能力检测不能只在开发环境跑一次就完事。5.2 推理速度优化批处理、模型分片与 cache扩展端侧推理的延迟瓶颈往往不是算力而是调度。你从 content script 发 10 次 summarize 请求每次都重新加载模型和上下文就是灾难。我做了两件事请求合并100ms 窗口内的相似任务合并成一批模型一次 forward 处理多条请求队列参考下载管理类扩展比如 FDM 浏览器扩展的任务队列设计保证同一时刻只有一个大任务占用 GPU避免上下文切换。实际数据一个 3B 量化模型在 WebGPU 下单条 512 token 摘要大约 300ms合并成 4 条之后批处理总耗时不到 700ms吞吐提升近一倍。启用 model cache 后重复输入直接返回缓存结果延迟降到个位数毫秒。模型分片在加载阶段也很有用我习惯把 embedding 层和 decoder 层拆成两段先加载 embedding 做通用特征提取等用户真正触发生成式任务时再加载 decoder首屏速度能快很多。5.3 内存与电量移动端的隐形杀手在 ARM 设备上做端侧推理总会让我想起单片机比如 STM32 系统架构里的低功耗设计用中断代替轮询用休眠代替空转。浏览器扩展同样要省电不能让 WebGPU 一直占着 GPU。做法空闲超过 30 秒销毁推理上下文释放显存和内存推理触发的 content script 操作合并成一次 DOM 写入后台模型预加载只发生在用户明显可能要用的场景比如聚焦输入框时。移动端 Safari 在低电量模式下会主动降低后台页面优先级如果你不主动释放资源浏览器会替你释放到时候用户看到的就是白屏和卡死。所以“主动释放”不是优化是基本素养。我在 PC 上跑得好好的一到 MacBook 的 Safari 上就频繁崩溃排查到最后就是内存占用太高被系统杀了。6. 常见问题排查与避坑手册6.1 “未安装浏览器扩展程序”Codex 类工具的典型翻车现场最近很多 CLI 工具都会提示先安装对应的浏览器扩展程序比如部署 Codex 这类辅助编程工具时页面经常提示“codex 未安装浏览器扩展程序”。我第一次遇到时以为浏览器在骗我查了半天发现是扩展 ID 对不上。这里有三层原因扩展 ID 由 manifest 的 key 字段和签名决定开发模式和正式版 ID 不一样外部网页要和扩展通信必须在 manifest 里声明externally_connectable只列允许的扩展 ID浏览器静默禁用了扩展或者企业策略把扩展拦截了。排查顺序是先去 chrome://extensions 里确认扩展已启用再比对页面里的 ID 和 manifest 里声明的 ID最后检查externally_connectable的 matches 是否覆盖你的页面来源。这套排查不只适用 Codex任何网页-SDK 和扩展联动的场景都通用。其实这个问题本质上是消息协议在“跨进程边界”上的握手失败和我们在扩展内部通信里强调的 ID 关联是一致的。6.2 Service Worker 被休眠状态消失了现象很典型模型下载到一半进度突然归零推理任务发出后没有回包。原因都是 SW 被休眠。解决不是禁止休眠做不到而是把状态外置。所有任务进度、模型版本、配置都实时写入 IndexedDBSW 恢复后先恢复状态再继续跑。另一个技巧是给推理任务发一个定时 keepalive 消息但不能依赖因为 Chrome 还是会杀长任务。最好的姿势是把长任务设计成可中断、可恢复的而不是祈祷浏览器别杀进程。6.3 CSP 阻止 WASM / 远程模型加载打开 DevTools 你会看到类似 Refused to create a WebAssembly object because unsafe-eval is not an allowed source 的报错。这不是 bug。MV3 的默认 CSP 不放开 WASM需要你在 manifest 里写content_security_policy: { extension_pages: script-src self wasm-unsafe-eval; object-src self }如果你要远程拉模型还必须把模型服务域名加进host_permissions。但这会带来安全面和审核成本我建议模型文件走应用内置或首次授权下载而不是运行时随便跨域。每次发布到商店CSP 都是重点审查项写得太松会被拒写得太紧功能跑不起来。6.4 WebGPU 上下文丢失与回退GPU 上下文不会永远存在驱动更新、内存不足、设备休眠都会导致 device lost。一旦丢失所有计算立即失败。我会监听uncapturederror捕获后做两件事销毁当前 session启用 CPU 回退WASM 执行同样的模型给用户一个“已切换兼容模式”的提示。整套回退逻辑要在架构设计时就预留否则到时候只能装死。回退不是降级到不能用而是保证功能不断只是慢一些。用户不会原谅“闪退”但能理解“切换模式后变慢”。6.5 扩展间通信与竞态并发请求串号内容脚本往 service worker 发请求服务端响应是异步的如果两个请求都用同一个默认 channel很容易拿错结果。我用信封里的 id 做关联之后才彻底解决。另外同一时间多个 content script 发起加载模型请求会在 model-store 层触发重复下载。我给下载器加了 mutex同一个模型只有一个下载任务其他请求挂起等待。这个思路和下载管理器FDM 扩展里的任务锁一样核心就是防止共享资源的并发写坏。7. 留给后来者的实操体会做完这套系统我最大的体会是端侧 AI 推理在浏览器扩展里能不能落地瓶颈往往不是模型能力而是工程纪律。你要尊重 MV3 的边界、把消息协议当成接口规范、把设备差异当做一等需求再加上一层又一层回退。代码写完只是开始真正花时间的是模拟各种“浏览器抽风”现场去补洞。如果你现在正准备动手我的建议是先花一个下午把 manifest、消息链路、模型缓存、错误上报这四件事定好再写推理逻辑。先小步跑一个轻量模型把整条链路打通再慢慢换大模型。这套架构搭完之后后面加新功能几乎就是加消息类型而已非常顺。最后再分享一个小技巧给模型推理加一个诊断面板把每次推理耗时、内存、后端类型打到 console 或者一个隐藏 popup 里。我第一次看到自己的模型在设备上真实跑起来的数据时才发现之前很多猜测都是错的。带上数据再去优化幸福感会高很多。
返回列表