news 2026/10/7 5:22:08

Manifest V3 下浏览器扩展端侧 AI 推理架构设计与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Manifest V3 下浏览器扩展端侧 AI 推理架构设计与工程实践

1. 端侧 AI 推理与浏览器扩展的碰撞点在哪

浏览器扩展这个赛道,过去十年基本被两类东西占据:一类是广告拦截、密码管理这种轻量工具,另一类是爬虫辅助、页面注入这种灰产边缘的脚本。但最近一年我注意到一个明显的变化——越来越多的开发者开始把端侧 AI 推理往扩展里塞。这个趋势背后有几个很实在的驱动力。

第一是隐私合规的压力。以前做文本摘要、图片识别、语义搜索这类功能,最省事的做法是把数据传到云端 API,但这两年不管是企业内训场景还是个人用户,对“数据不出本机”的要求越来越硬。你做一个会议纪要总结的扩展,如果每次都要把会议内容传到远端,很多公司 IT 部门直接就不让装了。

第二是WebGPU 和 WASM SIMD 的成熟。以前在浏览器里跑模型基本是玩具级别,现在 WebGPU 在主流浏览器上的覆盖率已经相当可观,配合 ONNX Runtime Web 或者 Transformers.js 这类运行时,在扩展的 background service worker 或者 offscreen document 里跑一个量化后的小模型,延迟已经能压到可接受的范围。

第三是Manifest V3 的架构倒逼。MV3 把 background page 换成了 service worker,生命周期变得很短,这对需要持续加载模型权重的 AI 推理来说是个麻烦事。但反过来想,这种限制也逼着开发者去思考更合理的架构——模型该放哪、推理该在哪触发、状态怎么保持,这些问题在 MV2 时代很多人是糊弄过去的。

这篇文章我想聊的就是这套东西:在 Manifest V3 的约束下,怎么把端侧 AI 推理系统合理地搭起来。不是那种“跑个 demo 就完事”的教程,而是从架构分层、模型部署、通信机制到工程踩坑的完整梳理。适合已经写过扩展、想往 AI 方向走的开发者,也适合做端侧推理但对浏览器环境不熟的人。

2. 整体架构设计:为什么不能把模型直接塞进 service worker

2.1 MV3 生命周期对推理系统的致命影响

先把这个最核心的矛盾讲清楚。Manifest V3 的 background 是service worker,它的生命周期由浏览器控制——没有事件的时候会被挂起,通常 30 秒到 5 分钟不等。这意味着什么?如果你把模型权重加载在 service worker 的全局变量里,用户切个标签页、发个呆,worker 被回收,下次事件来了重新启动,模型得重新加载。

一个量化后的 BERT 小模型大概 20-40MB,从 IndexedDB 或者 Cache Storage 读出来再初始化 ONNX Runtime 的 session,冷启动轻松超过 1 秒。用户每次点扩展图标都要等一秒多,这个体验是没法接受的。

所以架构设计的第一个决策就是:推理执行环境不能依赖 service worker 的常驻状态。我试过几种方案,下面这张表是我实际对比下来的结果。

方案模型加载位置冷启动延迟内存占用适用场景
Service Worker 内直接推理SW 全局变量800ms-2s低极轻量模型(<5MB)
Offscreen Document 常驻Offscreen 页面首次 1-2s,后续 <50ms中高中等模型,需要频繁推理
独立推理页面 + 消息通信扩展页面首次 1-2s中需要 UI 交互的推理
WASM 在 content script页面上下文每次注入高不推荐,污染宿主页面

2.2 分层架构:把职责切干净

我最终采用的架构是四层分离,这个划分方式参考了传统端侧推理系统的思路,但针对浏览器环境做了调整。

第一层是模型管理层。这一层不负责推理,只负责模型的获取、缓存、版本管理和完整性校验。模型文件放在扩展包内还是运行时下载,这是个需要权衡的问题。打包进扩展的话,CRX 体积会暴涨,Chrome Web Store 对包体积有隐性限制(超过 100MB 审核会变慢),而且模型更新要重新发版。运行时下载的话,需要处理网络失败、缓存失效、版本迁移这些问题。我的做法是:小模型(<10MB)打包,大模型运行时下载并缓存到 Cache Storage。

第二层是推理执行层。这一层是真正跑模型的地方,我选择用offscreen document来承载。Offscreen document 是 MV3 引入的一个特殊页面,它没有 UI,但生命周期比 service worker 长得多,只要你不主动关闭,它可以一直活着。这就解决了模型常驻的问题。创建方式是在 service worker 里调用chrome.offscreen.createDocument,指定理由为WORKERS或者BLOBS。

第三层是通信协调层。service worker 作为消息中枢,负责在 content script、popup、offscreen document 之间转发消息。这里有个坑:offscreen document 和 service worker 之间的通信用的也是chrome.runtime.sendMessage,但消息的 target 需要明确指定,否则会广播到所有上下文。

第四层是 UI 交互层。popup、side panel、content script 注入的浮层都属于这一层。它们不直接碰模型,只发请求、收结果。

2.3 为什么选 Offscreen Document 而不是别的

有人会问,为什么不用 SharedWorker 或者直接开一个隐藏的扩展页面?SharedWorker 在扩展环境里的支持一直不太稳定,而且它和 service worker 的通信要走 MessageChannel,调试起来很痛苦。隐藏扩展页面(chrome-extension://xxx/hidden.html)倒是能用,但它会出现在浏览器的标签页管理里,用户可能误关,而且每个窗口都会开一个实例,内存浪费。

Offscreen document 的好处是:全局唯一、无 UI、生命周期可控、支持完整的 DOM 和 Web API。这意味着你可以在里面用 WebGPU、WebAssembly、甚至 Web Worker 嵌套。我实测下来,在 offscreen document 里跑一个 30MB 的量化模型,常驻内存大概 150-200MB,对于现代设备来说是可以接受的。

注意:offscreen document 同时只能存在一个,创建前要先调chrome.offscreen.hasDocument()检查,否则会报错。而且它不支持chrome.tabs等部分 API,别把不该放的逻辑塞进去。

3. 模型部署与推理引擎的工程细节

3.1 模型格式选择:ONNX 还是别的

浏览器里跑推理,模型格式的选择直接决定了你能用哪些运行时。目前主流的路子有这么几条:

  • ONNX + ONNX Runtime Web:生态最成熟,支持 WebGPU 和 WASM 后端,量化工具链完整。缺点是 ORT 的 wasm 文件本身就有几 MB,首次加载有开销。
  • TensorFlow.js:适合 TF 生态的模型,但 WebGPU 后端还在实验阶段,性能不如 ORT 稳定。
  • Transformers.js:底层其实也是 ONNX Runtime,但封装了 Hugging Face 的模型加载流程,适合 NLP 任务快速上手。
  • 自定义 WASM:用 Rust 或 C++ 编译自己的推理内核,性能最好但开发成本极高。

我的建议是:除非你有极强的性能定制需求,否则直接用 ONNX Runtime Web。它的 WebGPU EP 在 Chrome 113+ 上已经比较稳定,WASM SIMD 后端作为兜底也能跑。

模型导出这块,PyTorch 转 ONNX 用torch.onnx.export,注意 opset 版本别太低,建议 17 以上,否则一些 attention 相关的算子可能不支持。导出后一定要用onnxruntime的 Python 版跑一遍验证输出一致性,我踩过好几次导出后数值对不上的坑,最后发现是某个算子在不同 opset 下行为有差异。

3.2 量化:端侧推理的必修课

浏览器环境内存和算力都有限,不做量化基本没法用。量化的核心思路是把 FP32 的权重压缩成 INT8 甚至 INT4,模型体积能降到原来的 1/4 到 1/8,推理速度也能提升 2-3 倍。

ONNX Runtime 提供了几种量化方式:

  • 动态量化:权重离线量化,激活值运行时量化。最简单,quantize_dynamic一行搞定,适合 LSTM、BERT 这类模型。
  • 静态量化:需要校准数据集,精度损失更小,但流程复杂。
  • QAT(量化感知训练):在训练阶段就模拟量化,精度最好,但需要重新训练。

对于浏览器扩展场景,我一般用动态量化就够了。实测一个 110M 参数的 BERT,FP32 是 440MB,INT8 动态量化后 110MB,再配合模型剪枝能压到 60MB 左右。当然扩展里不会用这么大的模型,一般用 6 层的小模型或者蒸馏版本。

量化后的精度损失要实测。我做过一个文本分类任务,INT8 量化后准确率从 92.3% 掉到 91.1%,这个损失在大多数场景下可以接受。但如果你的任务是实体识别这种对边界敏感的任务,建议做静态量化或者保留 FP16。

3.3 模型缓存策略:Cache Storage 的正确用法

运行时下载的模型要缓存,不然每次冷启动都重新下载,用户流量和等待时间都受不了。浏览器扩展里可用的缓存方案有 IndexedDB、Cache Storage 和 OPFS(Origin Private File System)。

Cache Storage 是最合适的,因为它本来就是为存储 Response 对象设计的,模型文件作为 fetch 的响应存进去,读取时直接cache.match拿到 ArrayBuffer,非常自然。IndexedDB 存二进制也可以,但 API 更繁琐,而且大文件读写性能不如 Cache Storage。

具体做法是:给每个模型版本建一个 cache name,比如model-cache-v1.2.0,模型文件用固定的 URL 路径作为 key。更新模型时创建新的 cache,旧的在确认新版本可用后删除。这里要注意Cache Storage 的配额,Chrome 对扩展的存储配额大概是可用磁盘空间的 60%,但单个 origin 有上限,模型文件别超过几百 MB。

// 模型缓存的核心逻辑 async function getModelBuffer(modelUrl, version) { const cacheName = `model-cache-${version}`; const cache = await caches.open(cacheName); let response = await cache.match(modelUrl); if (!response) { response = await fetch(modelUrl); if (!response.ok) throw new Error(`模型下载失败: ${response.status}`); // 克隆一份存入缓存,原响应返回给调用方 await cache.put(modelUrl, response.clone()); } return await response.arrayBuffer(); }

实操心得:模型下载一定要做分片和断点续传。我遇到过一次用户网络不稳定,200MB 的模型下了三次都失败,最后加了 Range 请求分片下载才解决。另外下载过程中要给用户进度反馈,不然用户以为扩展卡死了。

4. 通信机制与状态管理的实战方案

4.1 Service Worker 与 Offscreen 的消息通道

前面说了 service worker 是消息中枢,但这里有个细节很多人会踩坑:service worker 被挂起后,之前建立的 MessagePort 会失效。所以不能用长连接的方式,每次通信都要重新建立通道。

标准的做法是 service worker 收到请求后,先确保 offscreen document 存在,然后通过chrome.runtime.sendMessage发消息,offscreen 那边监听chrome.runtime.onMessage处理。但这里有个问题:sendMessage是广播的,popup、content script 都会收到,需要在消息里加target字段做过滤。

// service worker 侧:转发推理请求 async function ensureOffscreen() { const exists = await chrome.offscreen.hasDocument(); if (!exists) { await chrome.offscreen.createDocument({ url: 'offscreen.html', reasons: ['WORKERS'], justification: '运行端侧AI推理' }); } } async function runInference(payload) { await ensureOffscreen(); return chrome.runtime.sendMessage({ target: 'offscreen', type: 'INFERENCE', data: payload }); } // offscreen 侧:处理推理请求 chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.target !== 'offscreen') return false; if (msg.type === 'INFERENCE') { handleInference(msg.data) .then(result => sendResponse({ ok: true, result })) .catch(err => sendResponse({ ok: false, error: err.message })); return true; // 保持通道开放以支持异步响应 } });

注意return true这行,这是异步sendResponse的关键,忘了写的话消息通道会立即关闭,调用方收到 undefined。

4.2 推理任务的队列与并发控制

端侧推理是计算密集型任务,同时跑多个推理会把 CPU/GPU 打满,导致浏览器卡顿。我见过一个扩展没做并发控制,用户快速点击几次按钮,直接触发了 5 个推理任务并行,页面直接卡死。

解决方案是在 offscreen document 里维护一个任务队列,串行执行推理。队列的实现很简单,用一个 Promise 链或者数组加标志位就行。但要注意任务超时和取消,用户可能等不及想取消,这时候要能中断推理。ONNX Runtime 的 session run 本身不支持中断,但可以在队列层面做逻辑取消——任务还没开始执行就标记为取消,执行中的任务只能等它跑完。

class InferenceQueue { constructor() { this.queue = []; this.running = false; } async enqueue(task) { return new Promise((resolve, reject) => { this.queue.push({ task, resolve, reject, cancelled: false }); this.process(); }); } async process() { if (this.running || this.queue.length === 0) return; this.running = true; const item = this.queue.shift(); if (item.cancelled) { item.reject(new Error('任务已取消')); this.running = false; this.process(); return; } try { const result = await item.task(); item.resolve(result); } catch (err) { item.reject(err); } finally { this.running = false; this.process(); } } }

4.3 状态同步:模型加载状态怎么让 UI 知道

模型加载是个耗时操作,UI 层需要知道当前状态——是未加载、加载中、还是就绪。这个状态不能存在 service worker 的全局变量里,因为会被回收。我的做法是把状态存在chrome.storage.session里,这是 MV3 专门为会话级状态设计的存储,service worker 重启后数据还在,浏览器关闭才清空。

状态对象大概长这样:

{ modelStatus: 'loading', // idle | loading | ready | error modelVersion: '1.2.0', loadProgress: 0.65, lastError: null }

UI 层通过chrome.storage.session.onChanged监听变化,实时更新界面。这里有个细节:storage.session默认对 content script 不可见,需要在manifest.json里设置"access_level": "TRUSTED_AND_UNTRUSTED_CONTEXTS",否则 content script 读不到状态。

踩坑记录:storage.session有 10MB 的配额限制,别把模型权重或者大数组往里塞,只存状态元数据。我一开始把推理中间结果也存进去了,结果大文件直接写入失败,排查了半天。

5. 性能优化与常见问题排查

5.1 WebGPU 后端的启用与降级

WebGPU 是端侧推理性能的关键,但它的可用性不是 100%。Chrome 113+ 默认开启,但有些企业策略会禁用,Firefox 和 Safari 的支持情况也不一样。所以必须做能力检测和降级。

检测逻辑是:先看navigator.gpu是否存在,存在的话尝试requestAdapter(),拿到 adapter 再创建 device。任何一步失败就降级到 WASM 后端。ONNX Runtime Web 支持在创建 session 时指定 execution provider 列表,它会自动选择可用的。

async function createSession(modelBuffer) { const providers = []; if (navigator.gpu) { try { const adapter = await navigator.gpu.requestAdapter(); if (adapter) providers.push('webgpu'); } catch (e) { console.warn('WebGPU 不可用,降级到 WASM'); } } providers.push('wasm'); return ort.InferenceSession.create(modelBuffer, { executionProviders: providers, graphOptimizationLevel: 'all' }); }

实测数据:同一个模型,WebGPU 后端推理耗时 45ms,WASM SIMD 后端 180ms,差距大概 4 倍。对于实时性要求高的场景(比如输入即推理),WebGPU 是必须的;对于后台批处理任务,WASM 也能接受。

5.2 内存泄漏的排查与规避

端侧推理最容易出的问题就是内存泄漏。浏览器扩展的内存不像原生应用那么好管理,泄漏积累到一定程度,标签页直接崩溃。

常见的泄漏点有这么几个:

  • Tensor 对象没释放:ONNX Runtime 的 Tensor 底层是 WASM 内存,虽然 JS 有 GC,但 WASM 堆的释放有时滞后。大量推理后要手动调tensor.dispose()。
  • 事件监听器没移除:offscreen document 里如果给 DOM 加了监听器,页面销毁时要清理。
  • 闭包持有大对象:推理结果如果被闭包引用,GC 回收不掉。建议推理完成后把中间变量置 null。

排查工具就用 Chrome DevTools 的 Memory 面板,对 offscreen document 做 heap snapshot,对比推理前后的对象数量。我一般会跑 100 次推理,看内存是否稳定在某个水位,如果持续上涨就是有泄漏。

5.3 常见问题速查表

下面这张表是我在实际开发和用户反馈中整理出来的高频问题,基本覆盖了 80% 的故障场景。

问题现象可能原因排查方法解决方案
推理请求无响应offscreen 未创建或已销毁检查hasDocument()每次请求前确保 offscreen 存在
首次推理特别慢模型冷加载看加载日志时间戳提前预热,扩展启动时预加载
推理结果乱码输入张量形状不对打印 tensor dims核对模型输入签名
内存持续增长Tensor 未释放Heap snapshot 对比手动 dispose,置空引用
WebGPU 初始化失败浏览器策略禁用检查navigator.gpu降级到 WASM 后端
消息发送失败service worker 已挂起看 SW 控制台重试机制 + 状态检查
模型下载中断网络不稳定看 fetch 错误码分片下载 + 断点续传
扩展包体积过大模型打包进 CRX看构建产物改为运行时下载

5.4 冷启动优化的几个实用技巧

冷启动是端侧推理体验的命门。用户点开扩展,等 2 秒才出结果,这个体验基本就废了。我总结了几个有效的优化手段。

第一是预加载。在扩展安装或者浏览器启动时,就触发模型加载。chrome.runtime.onInstalled事件里可以启动 offscreen document 并开始加载模型,等用户真正用的时候模型已经就绪。这个做法会占用一些内存,但对于高频使用的扩展是值得的。

第二是模型分片加载。如果模型很大,可以先加载一部分(比如 embedding 层),让用户能开始输入,后面的层在后台继续加载。这个需要模型本身支持分阶段执行,实现起来复杂一些,但对大模型场景很有效。

第三是结果缓存。相同的输入没必要重复推理,用 LRU 缓存存最近 N 条推理结果。对于文本分类、意图识别这类任务,用户重复输入的概率不低,缓存命中能直接省掉推理时间。

第四是降低精度换速度。如果 WebGPU 可用,用 FP16 而不是 FP32,速度能提升 30% 左右,精度损失很小。ONNX Runtime 支持在 session 创建时指定精度。

6. 工程化落地的一些经验之谈

6.1 构建流程:模型和代码要分开管理

扩展的构建流程里,模型文件不应该和代码走同一套打包逻辑。我的做法是:代码用 Vite 或者 Webpack 打包,模型文件单独放在public/models/目录,构建时只做拷贝不做处理。模型版本用单独的 JSON 文件管理,包含版本号、文件列表、哈希值。

{ "version": "1.2.0", "models": [ { "name": "text-classifier", "path": "models/classifier-int8.onnx", "size": 25165824, "sha256": "a1b2c3..." } ] }

哈希值用于完整性校验,下载后比对,防止文件损坏或者被篡改。这个在安全敏感场景下很重要。

6.2 调试技巧:怎么在 offscreen 里打断点

Offscreen document 没有 UI,不能像普通页面那样右键检查。调试方法是:在chrome://extensions里找到你的扩展,点击 “service worker” 链接打开 SW 的 DevTools,然后在 Console 里执行chrome.offscreen相关命令,或者直接在 SW 的 Sources 面板里找到 offscreen.html 对应的上下文。

更简单的办法是在manifest.json里临时给 offscreen 页面加一个可见的入口,比如在 popup 里放一个按钮,点击后chrome.tabs.create打开 offscreen.html。这样就能用常规的 DevTools 调试了。调试完记得把这个入口去掉。

6.3 版本兼容:不同浏览器内核的差异

Chrome 和 Edge 都是 Chromium 内核,扩展 API 基本一致,但 Edge 在某些 API 的实现上有细微差别。比如chrome.offscreen在 Edge 的早期版本里支持不完整,需要做特性检测。Firefox 的扩展体系是另一套(WebExtensions),MV3 的支持还在推进中,offscreen document 在 Firefox 上根本没有对应实现。

所以如果你的扩展要跨浏览器,必须做能力检测和降级方案。Firefox 上可以降级到用 background page(MV2)或者隐藏的扩展页面来跑推理。这个工作量不小,但如果目标用户覆盖 Firefox,就得做。

6.4 安全考量:模型和数据的边界

端侧推理的一个核心卖点是隐私,但前提是你真的做到了数据不出本机。有几个点要注意:

  • 模型文件本身可能包含敏感信息。有些模型在训练时可能记住了训练数据,虽然概率很低,但在安全敏感场景下要考虑。
  • 推理中间结果不要外传。有些开发者为了“优化”,把推理的中间特征传到远端做后处理,这就破坏了端侧的隐私承诺。
  • 扩展的权限要最小化。只申请必要的权限,host_permissions别写<all_urls>,按需申请。

经验:如果你的扩展要上架商店,审核时会对权限和网络请求做严格检查。端侧推理的扩展如果还带着一堆远端请求,很容易被拒。把网络请求限制在模型下载这一个用途上,审核会顺利很多。

6.5 用户反馈里最常被问到的几个问题

做了一段时间后,用户反馈里高频出现的问题其实就那么几个。一个是“为什么第一次用这么慢”,这个前面说了,预加载能解决大部分。另一个是“能不能支持更大的模型”,这个受限于浏览器内存和扩展包体积,只能引导用户理解端侧的边界。还有一个是“为什么有时候结果不准”,这个往往是量化精度损失导致的,需要在模型选择和量化策略上做权衡。

我个人的体会是,端侧 AI 推理在浏览器扩展里落地,技术难点不在模型本身,而在工程约束的平衡。你要在包体积、内存占用、推理延迟、精度损失这几个维度里找平衡点,没有银弹,只有针对具体场景的取舍。一个文本摘要的扩展和一个图片识别的扩展,最优架构可能完全不同。多测、多调、多听用户反馈,比一开始就追求完美架构更实际。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 5:22:08

基于Simulink的11电平MMC并网控制模型搭建与仿真调试

在Simulink里把11电平三相MMC逆变器并网控制模型完整跑通&#xff0c;我前前后后折腾了一个多月。第一版模型搭得很快&#xff0c;波形出来也“像那么回事”&#xff0c;但一查相电压波形&#xff0c;根本不是11级阶梯&#xff0c;子模块电容电压乱跳&#xff0c;桥臂电流里二倍…

作者头像 李华
网站建设 2026/10/7 5:21:40

从逻辑门到完整4位ALU:手把手搭建CPU算数核心

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 5:21:38

4核16G轻量云主机价格全解析:京东云选购避坑指南

前两天有个做独立开发的哥们问我一句话&#xff1a;“京东云的轻量云主机&#xff0c;4核16G&#xff0c;搞活动到底多少钱&#xff1f;”他说这话时&#xff0c;手里开着三家的活动页来回切换&#xff0c;已经比了两个小时。这个问题听着简单&#xff0c;可真要回答&#xff0…

作者头像 李华
网站建设 2026/10/7 5:21:37

基于线性决策规则的分布鲁棒优化机组组合Matlab实现

搞电力调度的人最怕的就是风电出力预测不准带来的连锁反应。你按照预报值把明天96个时段的机组开机计划排好&#xff0c;后半夜风突然小了&#xff0c;火电还顶着最小技术出力&#xff0c;系统频率往下掉&#xff0c;备用被吃掉一大块&#xff1b;后半夜风又突然大了&#xff0…

作者头像 李华
网站建设 2026/10/7 5:20:48

基于CD4013的硬件自锁开关设计:D触发器实现按键翻转与消抖

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 5:20:43

游戏场景地编必修课:白模搭建的核心价值与实操流程

1. 白模到底在解决什么问题&#xff1a;为什么我不建议直接开铺做了几年场景地编&#xff0c;我见过太多新人&#xff08;包括曾经的我自己&#xff09;拿到一张概念图&#xff0c;第一个念头就是打开资产库开铺——树先种一圈&#xff0c;石头撒一把&#xff0c;房子拖进来再说…

作者头像 李华