
1. 先把问题说清楚为什么非要端侧推理还是跑在浏览器扩展里我做了几年浏览器扩展也折腾过一阵子端侧模型部署最近一年明显感觉到一个趋势越来越多的开发者想把 AI 能力塞进浏览器扩展里而不是塞进服务器。原因很直接——用户对隐私越来越敏感对延迟的容忍度越来越低云端的幻觉、不可控的合规风险、按次调用的成本都逼着大家把推理从后端往前端迁移。而浏览器扩展恰好是个很特别的宿主环境它既有独立于网页内容的权限又能调用本地模型还比一个单独的网页应用多了一层生命周期管理。换句话说扩展成了端侧 AI 推理一个比较理想的边角料载体。这个标题很长但核心其实就三件事架构怎么设计、工程上怎么实现、规范该怎么定。很多团队做到一半就会发现在一个内存受限、CPU/GPU 共享、还要考虑浏览器兼容性的环境里跑模型跟平时写 Node.js 服务完全不是一个思路。本文是我整理出来的实践总结不吹概念只讲我在真实项目里踩过的坑和沉淀下来的做法。适合准备做浏览器扩展 AI 功能、或者已经在做端侧推理方案选型的朋友参考。我会从架构拆解开始一路讲到工程实现细节和问题排查尽量落地到能抄作业的程度。2. 整体设计与架构思路扩展不是 Web 网页别拿 Web Worker 想当然2.1 浏览器扩展环境的特殊性决定了架构必须分层做浏览器扩展端侧 AI最容易犯的第一个错误就是把扩展当成普通网页来设计。普通网页里一个 JavaScript 文件、一个 Web Worker、一个 fetch 请求自给自足但扩展的环境是多进程、多线程、多上下文混合的。你至少会碰到几种不同类型的执行环境background service worker、content script、popup 页面、options 页面可能还有 offscreen document。这些环境之间不是想通信就能通信的需要通过消息 API 来传递数据。更麻烦的是浏览器对每个扩展的 service worker 有生命周期——它不是常驻的空闲几秒就可能被杀掉等下一次事件触发时再唤醒。这对端侧 AI 这种需要加载模型、保持推理状态的任务来说是个特别关键的约束。所以架构的第一步是划分推理运行时和扩展 UI/逻辑层。我最后采用的方案是把所有重量级的 AI 推理放在一个独立的 context 里比如 offscreen document 或者一个长期打开的扩展页面并用一个专门的进程承载。background 只做消息路由和生命周期管理UI 层只管发请求和收结果。这样做的原因很简单如果模型加载和推理逻辑直接塞在 service worker 里一旦 worker 被挂起所有模型状态都没了冷启动时要重新加载一次模型用户等十秒体验直接崩了。2.2 模块划分背后的取舍可卸载性、可复用性和权限隔离模块划分不能只按功能还得考虑可卸载性和权限隔离。浏览器扩展有一个很有利的特性扩展包可以配置可选权限用户不认可就不授予。如果 AI 推理功能是一个独立模块那用户完全可以选择只装AI 助手或只装普通工具这在工程上表现为 manifest 里定义 optional permissions在代码里按需求动态引入推理模块。我用的模块结构大致是extension-root/ background/ # 消息路由、生命周期、模块注册 inference/ # 模型加载、推理执行、缓存管理 ui/ # popup、options、侧边栏 common/ # 数据类型定义、协议、工具函数 assets/models/ # 本地模型文件或索引这里有个容易犯的错把模型加载逻辑和业务逻辑耦合在一起。比如content script 想提取页面文本然后直接调模型做摘要看起来方便但一旦 content script 被浏览器隔离环境限死或者跨域资源访问受限整套逻辑就废了。我的做法是所有页面分析的结果先通过消息传到 background再统一丢给推理模块处理。推理模块只负责输入一个结构化对象、输出一个结构化对象完全不知道上游是谁。这个边界在最初设计时觉得很啰嗦但工作后才发现它救了大命——因为浏览器扩展不同版本之间的消息协议经常变化如果每条业务线都单独对接推理模块每次升级都要改几十处。2.3 为什么没选云端推理成本、延迟和隐私三笔账我知道很多团队最终还是会选云端因为他们觉得端侧精度不够。这里不讨论模型能力只算账。延迟方面即使是一张 200ms 能完成的 OCR 推理如果放在云上加上网络 RTT、排队、解包通常要 1 到 1.5 秒而同样的任务在本地跑首帧出结果通常 300ms 以内。成本方面假设一个扩展有 1 万日活每人每天触发 50 次推理云端单次成本 0.01 元一天就是 5000 元一年 180 万。这个数字对一个中小团队来说不是小数目。隐私方面用户的网页内容是高度敏感的数据一旦传到服务器就需要处理合规、声明、审计端侧处理从源头就规避了这个问题。当然我并不是完全否定云端。混合架构是合理的本地推理优先置信度低于阈值或遇到用户主动要求联网增强时再调用云上大模型。关键是这个降级逻辑要明确写进规范里并且用户必须知情。标题里说的架构与工程实现规范很大一部分就是在管这件事哪些任务必须端侧哪些可以网上哪些要用户授权必须有明确的矩阵。3. 核心细节解析与实操要点模型、格式和加载策略是命门3.1 模型选型和量化的硬指标内存不是无限大的浏览器扩展环境可支配的内存非常有限。一个普通扩展如果一下加载一个 7B 的 fp16 模型直接能把浏览器整个 tab 拖垮。我在实践中总结出一个公式扩展可用的推理内存 ≈ 浏览器剩余内存 × 0.3再减去扩展自身 DOM 和 UI 开销。也就是说如果你在 16GB 内存的设备上浏览器占用了 8GB那扩展能较安全使用的可能只有 2GB 左右这种情况下要跑超过 2B 参数的模型基本上只能靠极低比特量化。选模型我一般遵循几个原则参数量小优先任务能用 0.5B 就不上 1.5B能用 3B 就不上 7B。在端侧参数量不是精度是灾难。量化格式首选 int8 / int4ONNX Runtime Web 和 WebNN 生态对量化支持比较成熟优先用 Q8 甚至 Q4 模型。我在实际测试中一个 7B 模型的 Q4 版内存占用约为 3.5GBQ8 约为 7GB后者在绝大多数浏览器环境里已经没法工作了。任务专用模型优先于通用模型比如做表单自动填写一个 0.5B 的 NER 模型效果比 7B 的通用对话模型更稳定速度还快 10 倍。3.2 模型加载的冷与热缓存和预加载策略浏览器扩展没有传统意义上进程常驻的概念所以模型的加载策略必须区分冷启动和热启动。冷启动指浏览器刚启动、扩展 service worker 被重建模型文件全部需要从磁盘读取并初始化。这一步通常最慢耗时从数百毫秒到数秒不等取决于模型大小和磁盘 IO。我做的优化有二把模型文件放到扩展包的 assets 目录并用browser.storage.local保存模型元数据版本、哈希、量化类型避免每次启动都校验文件完整性。使用offscreen document里的requestAnimationFrame或定时器保活让推理模块尽可能不被浏览器回收。实测 Chrome 中只要 offscreen 页面持续有音频播放或视频解码等高优先级活动进程就不容易被挂起如果只是空转有时还是会被回收所以还要加一层唤醒重载逻辑。热加载则指在模型已经驻留内存后多次推理之间的状态保持。热加载的优化主要是避免重复创建推理会话。ONNX Runtime Web 的InferenceSession创建成本很高我一般会维护一个 session 池按模型版本缓存只有模型文件变化时才重建。3.3 输入输出端的工程化处理归一化、tokenizer 与分片很多教程提到端侧模型就只讲加载与推理却忽略了输入输出的工程化。这里面的坑非常多。首先是文本输入的归一化网页上提取的文本可能有大量换行、零宽字符、HTML 实体直接灌进 tokenizer 结果会很差。我封装了一个文本清洗管线去除不可见字符、统一换行符、截断上下文窗口并且计算 token 数来控制分片。分片策略尤其重要。一个长网页动辄上万个字符模型的上下文窗口往往不够。我的做法是滑动窗口分片 重叠摘要。举个例子像 FAQ 页面的问题回答列表我会把一个长文本按 1000 token 左右切成块相邻块重叠 20%每块独立跑推理最后再用一个轻量级聚合模型合并结果。这样既避免信息截断又不会让单次推理输入过长导致内存溢出。说到内存溢出这是浏览器端推理最大的隐形杀手。我常常看到一些团队在 PC 上测试没事一到低端笔记本上就崩溃。原因是模型激活内存和输入长度成正比长文本会瞬间把内存打满。所以必须在输入管线里设置硬上限比如If input token 2000直接走粗加工方案如只取首尾各 500 token。这个限制要写进实现规范不能只当 bug 得补丁。4. 实操过程与核心环节实现从 manifest 配置到推理线程的真实部署流水线4.1 第一步Manifest V3 下的权限与生命周期配置现代浏览器扩展必须用 Manifest V3MV3。MV3 对后台脚本的限制比 V2 严格得多service worker 不能像以前那样无限制运行。我最终的 manifest 关键片段如下{ manifest_version: 3, name: Local AI Assistant, permissions: [storage, offscreen, activeTab], optional_permissions: [scripting, downloads], background: { service_worker: background.js, type: module }, action: { default_popup: popup.html }, content_scripts: [ { matches: [all_urls], js: [content.js], run_at: document_idle } ] }注意offscreen权限不是默认有的Chrome 需要显式声明。Safari 和 Firefox 对 offscreen document 的支持不同Firefox 更习惯用 native messaging 或后台页面需要做平台适配。4.2 第二步推理线程的构建与消息协议我最终把推理模块跑在offscreen.html里的一个 Web Worker 中。为什么不是直接在 offscreen document 的 JS 线程跑因为模型推理是 CPU/GPU 密集任务如果把主线程卡住用户拖拽窗口都成问题。构建方式如下// offscreen.js 中创建 worker const worker new Worker(inference-worker.js); worker.onmessage (event) { const msg event.data; if (msg.type INFERENCE_RESULT) { chrome.runtime.sendMessage({ type: AI_RESULT, taskId: msg.taskId, data: msg.data }); } }; // background.js 中负责路由 chrome.runtime.onMessage.addListener((msg, sender, sendResponse) { if (msg.type RUN_INFERENCE) { chrome.runtime.sendMessage({ type: FORWARD_TO_INFERENCE, payload: msg.payload }, (res) { sendResponse(res); }); return true; // 异步响应 } });这里有一个非常容易掉进去的坑MV3 的 service worker 可能在收到消息的同时正在被回收如果你在 background 里直接访问 offscreen worker可能出现Extension context invalidated的报错。解决办法是在消息进来时先keep alive比如用chrome.runtime.connect建立长连接再向推理模块转发。我在实际开发中甚至专门写了一个心跳检测器每隔 20 秒从 offscreen 页向 background 发一个空信号保证上下文不失效。虽然不优雅但非常实用。4.3 第三步ONNX Runtime 与 WebGPU 的选型和数据格式转换端侧推理框架目前我用得比较多的是 ONNX Runtime Webort-web。它的优点是对 WebGPU 支持完善可以调用 GPU 加速。另一个选择是 Transformers.js基于 ONNX Runtime封装了许多预训练模型的转换逻辑对前端开发者友好。但对于生产级扩展我更推荐直接用 ort-web 自己写推理管线原因是可以更精细地控制 session 参数和缓存。具体实现的关键步骤是数据格式转换。模型期望的输入往往是浮点张量而 JavaScript 里从 TypedArray 转成ort.Tensor非常简单import * as ort from onnxruntime-web; async function runInference(inputTokens) { const feeds {}; const inputTensor new ort.Tensor(int64, BigInt64Array.from(inputTokens), [1, inputTokens.length]); feeds[input_ids] inputTensor; feeds[attention_mask] new ort.Tensor(int64, BigInt64Array.from(mask), [1, inputTokens.length]); const session await getSession(); // 从缓存取 session const results await session.run(feeds); return results[output]; }这里要提醒的是BigInt64Array转换成ort.Tensor时在部分浏览器的旧版本上有兼容性问题。我会用Number数组先转成普通Float32Array再让 ort 内部处理。看似多一步却能避免线上 5% 用户的崩溃。4.4 第四步内存与并发控制的工程规范推理并发控制必须做成硬限制。我的规范是同一时间只允许一个推理任务在 worker 内运行新任务到来时如果当前有任务则进入队列队列长度超过 3 就丢弃最新任务并提示The model is busy每个任务执行前记录内存水位完成任务后主动调用gc()在 worker 里可用但不要依赖并清空大对象引用。为什么队列长度只设 3因为端侧推理不像服务端可以无限制排队。用户连续点按钮时模型在 5 秒内只能处理两三个任务如果一直排队浏览器内存会持续增长最后整个 tab 白屏。这个取舍在多数情况下是合理的——我们做的是交互式工具不是高并发 API。内存水位监控我做了个简单方案通过performance.memory.usedJSHeapSize采样在推理前和推理后各取一次如果差值超过可配置阈值则下一次推理前清空 session 缓存并重载。这个方法不精确但作为防护足够用了。5. 常见问题与排查技巧实录我在扩展端侧推理里踩过的坑5.1 问题一Service Worker 频繁被终止导致推理中断现象用户使用扩展过程中AI 回答突然中断过一会儿又恢复正常。排查后确认是 service worker 生命周期问题。我之前已经用 offscreen 保活但还是有概率被回收。后来发现chrome.offscreen有获得 前提offscreen document 必须在扩展安装时或运行时创建而且需要用户交互或事件触发。我把 offscreen 创建时机从后台启动改为用户第一次点击 action 时创建同时每次推理后延迟 30 秒销毁极大降低了回收概率。修复思路提炼成两条规范所有 AI 功能必须经用户手势触发后再初始化推理推理过程中如果出现了Extension context invalidated错误捕获后自动重建上下文并重新尝试一次。5.2 问题二GPU 加速可用但实际不稳定在使用 WebGPU 时我遇到了非常诡异的第二次推理结果全是 NaN的问题。查了一圈发现是部分显卡驱动下 WebGPU 的 float16 支持有问题。ort-web 允许设置执行设备和精度ort.env.webgpu.precision fp32; // 强制用 fp32改成 fp32 后问题消失但速度下降约 25%。我权衡后默认用 fp16但加一个设备检测首次推理结果如果有 NaN自动切换为 fp32 并重试。这个降级机制在规范里也叫推理一致性回退是个很实用的兜底策略。5.3 问题三Content Script 拿到的页面文本包含大量噪音这是端侧 AI 的一个经典问题。网页上的正文提取如果直接document.body.innerText会混入导航、cookie 弹窗、广告文本。这些噪音会让摘要模型生成完全无关的内容。我试过 Readability.js、Trafilatura 等方案最终认为在扩展里不要只依赖一种提取器。我的规范是多源提取 分类器打分分别用 Readability 提取正文、用 meta description、用标题和 URL 特征最后用一个 0.3B 的小分类器判断哪段文本最像正文。这个流程看起来重但实际运行在小模型上只需几十毫秒比任何正则都强。5.4 问题四模型文件从扩展包加载失败或跨浏览器不兼容把模型打包进扩展会导致扩展包体积过大我常用 CDN 动态加载模型文件但浏览器扩展对跨域请求控制严格。在 MV3 中跨域请求需要在host_permissions里明确声明域名而且不能动态添加。我的解决办法是先让用户在一个授权页面确认信任某个模型源然后把这个源写入chrome.storagebackground 再发起请求。注意Safari 对跨域模型加载限制更多我一般会把模型放在扩展包或本地 HTTP 服务中避免用动态远程加载。5.5 问题五低端设备上推理速度达不到可用阈值如果模型在目标设备上单次推理超过 3 秒这种功能基本没人用。我做过一个性能基线表作为规范供团队参考设备类型推荐模型参数量化层目标延迟中高端桌面1.5B-3BINT8300ms普通笔记本0.5B-1.5BINT8800ms低端笔记本/平板小于0.5BINT41500ms手机浏览网页小于0.2BINT42500ms这个表的意思不是低端设备就该慢而是提醒开发者在功能设计时就要预留降级路径。比如摘要功能可以做成两种按钮快速摘要走小模型时延低但精度一般和深度摘要需要更高配置的模型但用户确知会慢。合理的设计就是在用户点按钮前动态检测硬件能力决定显示哪种能力。6. 工程实现规范的落地清单从代码评审到发布检查6.1 必须写进规范的五条硬性要求端侧 AI 功能不能只靠开发者自觉我在团队里定的规范分五层。第一层资源边界所有推理任务必须经过统一的资源管理模块禁止在业务代码里直接创建InferenceSession。这样便于统计总内存和切换模型。第二层数据隐私任何文本、图像、音频数据在未经用户授权的情况下不得离开本地。日志记录只允许记录任务的哈希值和耗时禁止记录内容本身。如果有云端降级功能必须每次动态弹窗获取单独授权。第三层生命周期推理模块必须有冷启动、热启动、回收三个状态机任何状态下都能优雅处理新任务。如果收到任务时模型未加载先响应LOADING状态并显示进度同时禁止再次重复触发加载。第四层兼容性代码中不能使用只在单一浏览器的专有 API。对每个浏览器Chrome、Edge、Firefox、Safari都写一份适配层。WebGPU 不可用时回退到 WebAssembly。第五层异常恢复推理出错时不能直接弹报错框应先尝试会话重建并重试一次如果再失败再提示用户模型损坏并支持重新下载。6.2 发布前的验证清单与性能回归测试每次发版前我会跑一个流程化的测试脚本重点验证这几项冷启动时模型加载是否在目标设备上不超过 2 秒如果超时则优化模型文件或改预加载策略。连续触发 10 次推理后页面是否出现内存持续增长用 Performance API 记录曲线异常则定位是 session 泄漏还是任务队列堆积。切换 tab 或关闭浏览器后重新打开时能否快速恢复到之前状态。在至少 5 种浏览器环境Chrome 稳定版、Chrome Beta、Firefox、Safari、Edge下跑一遍推理回归用例。验证离线场景断网后如果模型已缓存扩展的 AI 功能还能正常使用。这个清单的初衷是避免我电脑上能用就行的心态。浏览器扩展的宿主千变万化只要有一类环境批量报错用户流失就比功能新增还快。6.3 维护与扩展模型热更新和 A/B 测试的方案模型不是一成不变的。我会在后台维护一个模型仓库用版本清单文件进行热更新升级。具体做法是扩展每天首次启动时请求一个轻量 JSON 清单检查是否有新版本模型若存在且用户同意则后台下载到本地存储下次推理时切换。因为有哈希校验模型文件损坏或篡改都能识别。A/B 测试则要更谨慎不要直接对用户部署模型而是在本地根据storage里的实验分组参数决定加载哪个模型这样不会造成不可逆影响。听起来这些规则多到麻烦但经历过一次模型更新引入中文乱码、又花了三周回滚的事件后我就意识到规范的价值不是约束而是保命。7. 最后说几句我自己的实操心得做了几个月的浏览器扩展端侧推理最大的体会是这个方向离产品可用比想象中要近但离稳定可靠还差得远。最大的瓶颈不是模型能力而是工程韧性。浏览器本身对扩展的限制非常不友好动不动就回收后台、限制权限、锁死跨域这些都需要在架构层面提前化解而不是等到线上出问题再去打补丁。另一个体会是端侧推理里的所谓大模型其实是被约束过的小模型我们更应该关注怎么让任务和模型匹配而不是追求参数量。如果你能把一个 1B 的量化模型在浏览器扩展里稳定跑出 200ms 以下的延迟满足用户 80% 的诉求那你已经比大多数塞一个云端大模型接口的AI 扩展更实用了。最后再分享一个小技巧在调试 service worker 和 offscreen 生命周期时打开chrome://serviceworker-internals/和 DevTools 里的 Application 面板可以实时看到扩展后台被停止和唤起的记录。很多灵异事件其实都是 30 秒自动回收导致的把这些日志记录下来排查速度快得多。浏览器扩展端侧推理这条路还很长但我相信架构和规范做扎实了后面会越走越顺。