115 行把 Qwen3-8B 跑进浏览器:WebLLM 本地推理实战

目标读者 :隐私优先、数据不出机、想在前端跑本地 LLM 的开发者

预计阅读 :18~25 分钟

主线 :用 @mlc-ai/web-llm + WebGPU,在浏览器加载 Qwen3-8B-q4f16_1-MLC,完成聊天 / 摘要 / Agent 工具调用,并对比云端 API 的延迟与成本

关键词 :WebLLM、MLC、WebGPU、Qwen3-8B、本地推理、隐私计算、Function Calling

官方参考mlc-ai/web-llm · WebLLM Docs · MLC Models


开篇:密钥、账单、日志------云端 API 的三座大山

多数产品集成大模型的路径是:

text 复制代码
前端 → 你的后端 → 云端 LLM API → 返回 tokens

这条链路顺滑,但有三个绕不开的痛:

痛点 现实
密钥与合规 Key 泄露、审计日志、跨境传输、行业数据出境限制
按量账单 摘要/客服/内网文档一开量,月费直线上去
延迟不可控 跨洋 RTT + 排队;离线/内网直接不可用

如果你的场景是:医疗病历摘要、法务合同草稿、企业内部知识问答、浏览器插件读本地页------「数据必须出机」这件事本身就可能否决方案。

WebLLM@mlc-ai/web-llm)给出另一条路:模型权重进浏览器缓存,推理走 WebGPU ,OpenAI 兼容的 chat.completions API------密钥零、服务端零、数据不出标签页

本文用约 115 行 TypeScript ,把 Qwen3-8B(q4f16) 跑起来,覆盖:

  1. 流式聊天(含 Qwen3 enable_thinking
  2. 本地文档摘要
  3. Agent 风格工具调用(tools / tool_choice
  4. 与云端 API 的延迟 / 成本对照表

一、架构一眼看懂:浏览器里的「迷你推理机」

Data stays in-tab · WebGPU accelerated 你的 Web App Vite / React / 插件 @mlc-ai/web-llm CreateMLCEngine WebGPU GPU 着色器算力 Qwen3-8B q4f16_1-MLC 缓存:IndexedDB / Cache API(首次下载权重,之后秒开) API 形态:engine.chat.completions.create ------ 与 OpenAI SDK 几乎同构 可选:CreateWebWorkerMLCEngine / ServiceWorker ------ UI 不卡顿、跨页复用

要点只有三条:

  1. 运行时是浏览器,不是 Node:算力来自用户本机 GPU(经 WebGPU)。
  2. 模型是 MLC 预编译产物model_id 形如 Qwen3-8B-q4f16_1-MLC,权重从 Hugging Face / CDN 拉取后本地缓存。
  3. 调用面是 OpenAI 兼容 :你会写的 messages / stream / tools,这里几乎原样可用。

二、为什么是 WebLLM + Qwen3-8B?

2.1 和「把模型塞进浏览器」的其他路线比

方案 加速 模型规模 API 成熟度 适合
WebLLM (MLC) WebGPU 到 7B/8B 量级 OpenAI 兼容,文档全 生产级前端本地推理
Transformers.js WASM/WebGPU 偏小模型 / 编码器 HF 生态强 分类、嵌入、轻量生成
ONNX Runtime Web WASM/WebGPU 自定义导出 偏底层 已有 ONNX 管线
云端 API 服务端 GPU 任意大 最成熟 效果优先、可出网

隐私优先 + 要「真能对话的 8B」时,WebLLM 是目前最接近「产品可用」的浏览器方案

2.2 Qwen3 系列在 WebLLM 里的档位

WebLLM 已内置 Qwen3 多档(含 thinking 开关),常用 ID:

model_id 大致显存门槛 体感
Qwen3-0.6B-q4f16_1-MLC 演示 / 弱设备兜底
Qwen3-1.7B-q4f16_1-MLC 轻量助手
Qwen3-4B-q4f16_1-MLC ~4.5GB+ 性价比推荐
Qwen3-8B-q4f16_1-MLC ~8GB+ 质量更接近「能干活」

本文主推 8B:摘要与工具调用质量明显好于 0.6B/1.7B;若读者机器吃力,文末给出一键降级策略。
云端 API 用户文本 → 你的服务器 → 厂商 有 Key / 有账单 / 有出境风险 延迟 = 网络 + 排队 + 推理 WebLLM 本地 用户文本 → 本机 WebGPU 无 Key / 边际成本≈0 / 数据不出机 延迟 = 本机吞吐(首次冷启动另计)


三、开工前:环境与硬件门槛

3.1 浏览器

  • 推荐 :Chrome / Edge 113+(WebGPU 稳定)
  • Safari / Firefox:多数场景仍应 feature-detect 后降级(提示换浏览器或走云端)
  • 检测一行就够:
ts 复制代码
if (!navigator.gpu) {
  throw new Error("当前浏览器不支持 WebGPU,请使用 Chrome/Edge 113+");
}

3.2 显存与磁盘

  • Qwen3-8B q4f16 :建议独立显存 / 统一内存 ≥ 8GB 可用 ;可在 chrome://gpu 看 GPU 信息
  • 首次下载 :权重体积按 GB 计,务必接 initProgressCallback,否则用户以为页面卡死
  • 之后:走浏览器缓存,二次打开接近「秒进引擎」

3.3 工程脚手架

bash 复制代码
npm create vite@latest webllm-qwen3 -- --template vanilla-ts
cd webllm-qwen3
npm i @mlc-ai/web-llm
npm run dev

生产构建注意:模型 wasm / 权重走 CDN,前端包本身不大;不要把整模打进你的静态资源仓库。


四、115 行实战:聊天 · 摘要 · Agent 工具调用

下面是一份可直接粘进 src/main.ts 的完整示例(行数含注释与类型,约 115 行)。它做三件事:

  1. 加载 Qwen3-8B-q4f16_1-MLC
  2. 流式聊天 + 本地摘要
  3. OpenAI 风格 tools 触发一次「查天气」工具,再把结果喂回模型
ts 复制代码
import {
  CreateMLCEngine,
  type MLCEngineInterface,
  type ChatCompletionMessageParam,
  type ChatCompletionTool,
} from "@mlc-ai/web-llm";

const MODEL = "Qwen3-8B-q4f16_1-MLC";
const out = document.querySelector<HTMLPreElement>("#out")!;

async function createEngine(): Promise<MLCEngineInterface> {
  if (!("gpu" in navigator)) throw new Error("需要 WebGPU(Chrome/Edge 113+)");
  return CreateMLCEngine(MODEL, {
    initProgressCallback: (p) => {
      out.textContent = `加载中:${p.text}\n`;
    },
  });
}

async function streamChat(
  engine: MLCEngineInterface,
  messages: ChatCompletionMessageParam[],
) {
  // enable_thinking:false 更快;要深度推理可改 true / 或缺省
  const stream = await engine.chat.completions.create({
    messages,
    stream: true,
    stream_options: { include_usage: true },
    extra_body: { enable_thinking: false },
  });
  let text = "";
  for await (const chunk of stream) {
    text += chunk.choices[0]?.delta?.content ?? "";
    out.textContent = text;
    if (chunk.usage) console.log("usage", chunk.usage);
  }
  return text;
}

/** 本地摘要:原文不出浏览器 */
async function summarize(engine: MLCEngineInterface, doc: string) {
  return streamChat(engine, [
    {
      role: "system",
      content: "你是本地隐私助手。只输出三条中文要点,勿编造。",
    },
    { role: "user", content: `请摘要:\n\n${doc.slice(0, 6000)}` },
  ]);
}

const tools: ChatCompletionTool[] = [
  {
    type: "function",
    function: {
      name: "get_weather",
      description: "查询城市当前天气",
      parameters: {
        type: "object",
        properties: { city: { type: "string" } },
        required: ["city"],
      },
    },
  },
];

async function get_weather(city: string) {
  // 真实项目可换成 IndexedDB / 本地 API
  return JSON.stringify({ city, temp_c: 26, cond: "晴", source: "local-mock" });
}

async function agentTurn(engine: MLCEngineInterface, user: string) {
  const messages: ChatCompletionMessageParam[] = [
    { role: "system", content: "你可以调用工具;用中文简洁回答。" },
    { role: "user", content: user },
  ];
  const first = await engine.chat.completions.create({
    messages,
    tools,
    tool_choice: "auto",
    extra_body: { enable_thinking: false },
  });
  const msg = first.choices[0].message;
  if (!msg.tool_calls?.length) {
    out.textContent = msg.content ?? "";
    return msg.content ?? "";
  }
  messages.push(msg);
  for (const call of msg.tool_calls) {
    const args = JSON.parse(call.function.arguments || "{}");
    const result =
      call.function.name === "get_weather" ? await get_weather(args.city) : "{}";
    messages.push({
      role: "tool",
      tool_call_id: call.id,
      content: result,
    } as ChatCompletionMessageParam);
  }
  return streamChat(engine, messages);
}

async function main() {
  const engine = await createEngine();
  await streamChat(engine, [
    { role: "user", content: "用一句话介绍 WebLLM,面向前端工程师。" },
  ]);
  await summarize(
    engine,
    "本周迭代:完成登录 SSO、修复发票导出崩溃、WebLLM POC 合入主干。",
  );
  await agentTurn(engine, "北京今天天气怎么样?");
  await engine.unload(); // 释放显存(热更新场景尤其重要)
}

main().catch((e) => {
  out.textContent = String(e);
});

对应 HTML 只需一个出口:

html 复制代码
<!doctype html>
<html lang="zh-CN">
  <head><meta charset="UTF-8" /><title>WebLLM Qwen3-8B</title></head>
  <body>
    <pre id="out">准备加载...</pre>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

One engine · three tasks ① 流式聊天 stream: true enable_thinking 可关 usage 在末包 ② 本地摘要 原文截断进 prompt 不经后端、不出机 适合插件/内网页 ③ 工具调用 tools + tool_choice 本地函数 / 本地库 再二次补全

4.1 Qwen3 的 thinking 开关(别踩坑)

官方实践(见 web-llm examples/qwen3):

ts 复制代码
extra_body: { enable_thinking: false }  // 更快、更像普通聊天
// 或缺省 / true ------ 会产出 <think>...</think> 推理块

多轮对话时,历史里的 assistant 消息建议剥掉 think 块 再塞回 messages,否则上下文被推理草稿污染。

4.2 生产级:Worker 化,别堵死主线程

ts 复制代码
import { CreateWebWorkerMLCEngine } from "@mlc-ai/web-llm";

const engine = await CreateWebWorkerMLCEngine(
  new Worker(new URL("./worker.ts", import.meta.url), { type: "module" }),
  MODEL,
  { initProgressCallback: (p) => console.log(p.text) },
);

worker.ts 里按官方示例挂上 WebLLM worker 入口即可。UI 线程只收 token,滚动与输入不会被矩阵乘「冻住」。


五、延迟与成本:本地 vs 云端,怎么算才诚实

数字会因机型、量化、输出长度波动;下面给的是 工程决策用的量级感,不是实验室标称峰值。
Rough order-of-magnitude · 决策用而非营销用 云端 API TTFT ~0.4--2.0s(含 RTT) WebLLM 首包 热启动常 <1s(本机) 云端 1k tokens 按厂商价 × 调用次数 WebLLM 1k tokens ≈ ¥0(电费可忽略) 隐藏成本:首次下载流量、用户 GPU、Safari 不兼容带来的支持成本

5.1 延迟

阶段 云端 API WebLLM(本机 8B q4)
冷启动 几乎无(服务已热) 首次下载 + 编译/加载,可达数分钟
热启动后首 token 受 RTT/排队影响 主要看本机 GPU;中高端机常可对话
离线 / 内网 直接失败 仍可用(权重已缓存)
长上下文 大模型更强 受浏览器显存与 context_window 限制

结论 :比「谁更快」更关键的是------你能不能接受冷启动,以及用户 GPU 是否够格。内网文档助手、浏览器插件、二次打开的高频场景,本地往往更稳。

5.2 成本(粗算)

假设某功能每天 1 万次摘要,每次约 1.5k input + 0.3k output tokens:

云端(示意价) WebLLM
Token 费 按厂商单价 × 日调用,月费可观 ≈ 0
带宽 / 存储 API 流量小 首次权重下载由用户承担
运维 Key 轮换、限流、审计 兼容性矩阵、降级策略
合规 数据处理协议、出境评估 原文不出机(仍要注意前端 XSS 等)

隐私场景下,「省钱」往往是副产品;真正买到的是合规空间与离线能力

5.3 吞吐现实:别拿云端 70B 的幻觉来要求浏览器 8B

本地 8B 适合:

  • 结构化摘要、改写、分类、表单填充
  • 带少量工具的「浅 Agent」
  • 敏感文本的「先本地后可选上传」

不适合:

  • 强多步推理竞赛题、超长仓库级 coding
  • 要求与旗舰云端模型「同句感」的客服话术

正确产品形态经常是 混合:默认 WebLLM,复杂任务再征得用户同意后走云端。


六、Agent 工具调用:浏览器里的「手」

WebLLM 的 function calling 仍标为 初步支持 (官方 examples 目录有 function-calling),但 tools / tool_choice 字段已经能跑通主路径。实践建议:
User 意图 Qwen3-8B 本地 Tool tool 结果写回 messages → 二次补全 工具只应访问用户已授权的本机能力(页面 DOM / IndexedDB / 扩展 API)

  1. 工具尽量本地:读当前页 DOM、查 IndexedDB、调扩展 storage------才符合「不出机」。
  2. Schema 写短:小模型对又长又绕的 JSON Schema 更容易胡编参数。
  3. 失败要兜底tool_calls 为空或参数非法时,直接规则分支,别死循环。
  4. 安全 :本地 Agent ≠ 无害;恶意页面配 XSS 仍可能诱导模型调用危险扩展 API------工具白名单必做。

七、踩坑清单(建议贴进团队 Wiki)

# 现象 处理
1 点了按钮像死机 必接 initProgressCallback;大模型首次下载以分钟计
2 OOM / 黑屏式失败 降级到 Qwen3-4B / 1.7Btry/catch 阶梯加载
3 热更新后第二次加载挂掉 组件卸载调用 engine.unload()
4 Safari 用户投诉 启动检测 navigator.gpu,给云端或提示换浏览器
5 多轮越聊越笨 <think>;控制历史轮数;摘要压缩
6 UI 卡顿 CreateWebWorkerMLCEngine
7 公司代理拦 HF 自建镜像 / 改 appConfig.model_listmodel URL
8 以为「完全离线」 首次仍需拉权重;真正离线要 PWA + 预缓存策略

阶梯降级示例:

ts 复制代码
const CANDIDATES = [
  "Qwen3-8B-q4f16_1-MLC",
  "Qwen3-4B-q4f16_1-MLC",
  "Qwen3-1.7B-q4f16_1-MLC",
];

async function createWithFallback() {
  let lastErr: unknown;
  for (const id of CANDIDATES) {
    try {
      return await CreateMLCEngine(id, {
        initProgressCallback: (p) => console.log(id, p.text),
      });
    } catch (e) {
      lastErr = e;
      console.warn("fallback from", id, e);
    }
  }
  throw lastErr;
}

八、什么时候该上 WebLLM,什么时候老实调云端?

选 WebLLM 选云端 API
数据分级高、不能出境/出机 效果必须顶格、可用最强模型
高频短任务、边际成本敏感 低频但极难任务
离线 / 弱网 / 内网 用户设备参差、无法保证 WebGPU
浏览器插件、本地页助手 需要统一运维与审计日志中心

更务实的默认:本地优先 + 云端可选升级。产品文案写清楚「默认不上云」,比事后解释日志更省事。


九、一句话收束

115 行不是炫技,是把「隐私优先」变成可运行的默认路径。

WebLLM 把 Qwen3-8B 放进 WebGPU:聊天、摘要、浅 Agent 都能在标签页内完成;你换来的是零密钥、近零 token 账单,以及「数据不出机」的合规叙事。代价是显存门槛、冷启动与浏览器兼容性------用 Worker、进度条、模型阶梯降级,就能把这些代价收进工程可控范围。

下一步你可以:

  1. 把示例改成 React + Worker,做成内部「本地摘要」插件
  2. appConfig 指到公司镜像,去掉公网 HF 依赖
  3. 对标业务做 A/B:同 prompt 下 4B vs 8B vs 云端小模型的质量/延迟

参考链接

相关推荐
AI技术新视界1 小时前
失控的认知外包:大语言模型如何像病毒般入侵人类思维与社会生态
人工智能·llm·认知科学
后端小肥肠1 小时前
还在找PPT 生成工具?我集成了 GitHub 高星 Skill,自动匹配最优方案
人工智能·aigc·agent
二川bro1 小时前
幻觉≠提示词注入!拆解GPT‑6 Astra三层防御架构的致命短板
人工智能
雪庭1 小时前
pmb 面向 AI 编码智能体的本地记忆系统
人工智能
姜穆澜1 小时前
机器学习实战指南:从算法原理到工程落地
人工智能·算法·机器学习
styshoo1 小时前
[NVSentinel] gpu-health-monitor模块之dcgm_watcher
人工智能
Cosolar1 小时前
从 ChatGPT 到 Astra:四年走完的路,AGI 真的来了吗?
人工智能·aigc·openai
ting94520001 小时前
Dograh 深度技术解析:开源自托管语音智能体平台架构、流水线与工程实践
人工智能·架构
思考着亮1 小时前
13.GraphRAG
人工智能