目标读者 :隐私优先、数据不出机、想在前端跑本地 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) 跑起来,覆盖:
- 流式聊天(含 Qwen3
enable_thinking) - 本地文档摘要
- Agent 风格工具调用(
tools/tool_choice) - 与云端 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 不卡顿、跨页复用
要点只有三条:
- 运行时是浏览器,不是 Node:算力来自用户本机 GPU(经 WebGPU)。
- 模型是 MLC 预编译产物 :
model_id形如Qwen3-8B-q4f16_1-MLC,权重从 Hugging Face / CDN 拉取后本地缓存。 - 调用面是 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 行)。它做三件事:
- 加载
Qwen3-8B-q4f16_1-MLC - 流式聊天 + 本地摘要
- 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)
- 工具尽量本地:读当前页 DOM、查 IndexedDB、调扩展 storage------才符合「不出机」。
- Schema 写短:小模型对又长又绕的 JSON Schema 更容易胡编参数。
- 失败要兜底 :
tool_calls为空或参数非法时,直接规则分支,别死循环。 - 安全 :本地 Agent ≠ 无害;恶意页面配 XSS 仍可能诱导模型调用危险扩展 API------工具白名单必做。
七、踩坑清单(建议贴进团队 Wiki)
| # | 现象 | 处理 |
|---|---|---|
| 1 | 点了按钮像死机 | 必接 initProgressCallback;大模型首次下载以分钟计 |
| 2 | OOM / 黑屏式失败 | 降级到 Qwen3-4B / 1.7B;try/catch 阶梯加载 |
| 3 | 热更新后第二次加载挂掉 | 组件卸载调用 engine.unload() |
| 4 | Safari 用户投诉 | 启动检测 navigator.gpu,给云端或提示换浏览器 |
| 5 | 多轮越聊越笨 | 剥 <think>;控制历史轮数;摘要压缩 |
| 6 | UI 卡顿 | CreateWebWorkerMLCEngine |
| 7 | 公司代理拦 HF | 自建镜像 / 改 appConfig.model_list 的 model 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、进度条、模型阶梯降级,就能把这些代价收进工程可控范围。
下一步你可以:
- 把示例改成 React + Worker,做成内部「本地摘要」插件
- 用
appConfig指到公司镜像,去掉公网 HF 依赖 - 对标业务做 A/B:同 prompt 下 4B vs 8B vs 云端小模型的质量/延迟