Web 应用中的 Edge AI 推理:适合什么,不适合什么
从一个分类需求说起
我在一个内容平台的项目里接到一个需求:对用户提交的 UGC 内容做实时情感分类,要求从提交到返回结果不超过 100ms。最初方案很直接——请求打到后端,调一个 BERT 模型,返回结果。测下来延迟在 200-400ms 之间,瓶颈在网络往返和模型排队。
团队有人提议把推理搬到边缘。听起来合理:边缘节点离用户近,少了几个 hop,延迟应该能降下来。我开始调研这个方案的可行性,结果发现「搬到边缘」这句话背后藏着一串需要逐个解决的问题——模型体积、运行时环境、冷启动、缓存策略、平台限制。
做完这轮调研,我形成了一个判断:Edge AI 不是把推理服务往边缘一放就完事,它更像是一个需要重新设计的系统。有些任务确实适合放在边缘,有些完全不适合,区别在于任务形态和资源约束是否匹配。
两种完全不同的 Edge
讨论 Edge AI 之前,有一个分类需要先搞清楚。「边缘」在 Web 应用的语境里至少指两种不同的运行环境,它们的约束和适用场景差异很大。
第一种是客户端浏览器。模型直接在用户设备上运行,通过 WebGPU 调用 GPU,或者用 WebAssembly 走 CPU。数据不出设备,隐私天然有保障。代价是模型体积直接影响加载时间,且不同设备、不同浏览器的性能差异巨大。
第二种是边缘函数(Edge Functions)。Cloudflare Workers、Vercel Edge Functions、Deno Deploy 这类平台。它们在全球有大量 PoP 节点,延迟低,但每个请求能用的 CPU 时间和内存都很紧。
| 维度 | 客户端浏览器 | 边缘函数(Workers) | 中心 API |
|---|---|---|---|
| 延迟来源 | 无网络往返 | 1 个 hop | 多个 hop + 排队 |
| 模型大小限制 | 设备内存(通常 < 500MB) | 函数内存 128MB-256MB | 几乎无限制 |
| 计算资源 | GPU(WebGPU)/ CPU(WASM) | vCPU 时间片(25-50ms) | 完整 GPU |
| 浏览器兼容 | 需要 WebGPU/WASM 支持 | 不依赖浏览器能力 | 不依赖 |
| 隐私 | 数据不出设备 | 数据经过第三方节点 | 数据经过后端 |
| 成本模型 | 用户设备承担 | 按请求/执行时间计费 | 按 GPU 时间计费 |
这三种环境不是互斥的。实际架构里,更常见的做法是混合部署——轻量任务走客户端或边缘,复杂任务留中心。
量化是边缘推理的基础功
在边缘跑模型,模型体积是第一个需要解决的问题。原始 FP32 的 BERT-base 约 440MB,放到边缘函数里连加载都困难。量化把模型权重从 FP32(32 位浮点)压缩到 INT8(8 位整数)甚至 INT4(4 位整数),模型体积缩小到 1/4 到 1/8。
WebLLM 的实测数据(arXiv 2412.15803)显示,4-bit 量化的 Llama-3.1-8B 在 Apple M3 Max 的浏览器里能跑到 41.1 tokens/s,同设备原生 MLC-LLM 跑 57.7 tokens/s,保留了约 71% 的吞吐量。Phi-3.5-mini(3.8B 参数)的情况更好:浏览器端 71.1 tokens/s,原生 89.3 tokens/s,保留约 80%。
nnJIT 框架(arXiv 2309.08978)走得更远,通过 JIT 自动生成适配浏览器的计算 kernel,在 Llama 2 7B 上跑到 12.16 tokens/s,比同环境基线快约 40%,整体推理加速达到 8.2 倍。
这些数据说明两件事:量化后的小模型(1-8B 参数)在浏览器端跑已经可行;但从浏览器到原生之间仍有 20-30% 的性能损失,不能忽视。
| 量化方式 | 精度 | 模型体积 | 精度损失 | 适用场景 |
|---|---|---|---|---|
| FP32(原始) | 32-bit 浮点 | 基准 | 无 | 后端服务,不考虑体积 |
| FP16 | 16-bit 浮点 | 约 1/2 | 极小 | GPU 推理,WebGPU FP16 混合精度 |
| INT8(动态量化) | 8-bit 整数 | 约 1/4 | 很小 | 边缘函数,分类 / 嵌入任务 |
| INT4(GPTQ/AWQ) | 4-bit 整数 | 约 1/8 | 可接受 | 浏览器端 LLM,边缘 7B 以下模型 |
Google 在 2024 I/O 上给出的经验是,2-8B 参数的量化模型是浏览器端推理的实用范围。再大的模型,当前设备的内存和算力都吃不消。
平台限制决定了能做什么
Vercel Edge Functions 和 Cloudflare Workers 是两种主流的 Web 边缘运行时,它们的 AI 推理能力差异明显。
| 指标 | Vercel Edge Functions | Cloudflare Workers | Cloudflare Workers AI |
|---|---|---|---|
| CPU 时间 | ~25ms / 请求 | ~50ms / 请求 | 平台托管 |
| 内存 | 最大 4GB(Serverless) | 128MB(Free)/ 更大(Paid) | 平台管理 |
| 模型支持 | 需自带运行时 | 需自带运行时 | 托管 Llama、Mistral 等 |
| 冷启动 | V8 isolate,较快 | V8 isolate,较快 | 无需管理 |
| 执行超时 | 10s(Hobby)/ 60s(Pro) | 30s(Free) | 更长,支持流式 |
| 延迟 | sub-50ms 级别 | sub-50ms 级别 | sub-50ms 级别 |
| 适合任务 | 路由、预处理、轻推理 | 路由、预处理、轻推理 | 完整 LLM 推理 |
核心矛盾在这里:边缘函数的 CPU 时间只有 25-50ms,而一次 LLM token 生成需要大量矩阵乘法。25ms 连一个 token 都可能生成不了。这意味着在边缘函数上跑 LLM 逐 token 生成基本不现实,只能做单次前向传播的轻量任务(分类、嵌入、短文本改写)。
Cloudflare 推出的 Workers AI 是另一条路。模型推理由平台托管,不在 Worker 的 CPU 时间里跑,避开了这个矛盾。代价是灵活度降低——只能用平台支持的模型,自定义模型需要走 Worker 的 CPU 预算。
案例一:边缘函数上的分类模型冷启动
这个需求是实时情感分类。我最初把 ONNX 格式的 BERT-base(约 440MB)直接加载到 Cloudflare Worker 里,结果第一次请求直接超时。
问题出在模型加载。边缘函数没有持久进程,每次冷启动都要重新加载模型。440MB 的文件从 KV 读到内存、解析 ONNX 图、分配推理上下文,整个过程远超 30 秒的执行超时。
换成 sentence-transformers/all-MiniLM-L6-v2(约 80MB,INT8 量化后约 23MB),模型体积问题解决了。但冷启动时的推理上下文初始化仍然需要 200-500ms,第一次请求的延迟远高于后续请求。
最终方案是两步优化:模型从 Worker 的启动阶段预加载,同时用一个后台定时器保持推理引擎的热状态。
// ❌ 每次请求都加载模型,冷启动必然超时
export default {
async fetch(request, env) {
const model = await loadModelFromKV('bert-base') // 440MB,每次冷启动重新读
const result = await session.run({ input_ids, attention_mask })
return Response.json(result)
}
}// ✅ 启动时预加载 + 后台保活,避免重复初始化
import { InferenceSession, Tensor } from 'onnxruntime-web'
// 模块加载阶段即创建 session,利用 isolate 复用
let sessionPromise = null
function getSession(env) {
if (!sessionPromise) {
// MiniLM INT8 量化后约 23MB,从 KV 读取一次
sessionPromise = env.MODEL_KV.get('minilm-int8', 'arrayBuffer')
.then(buf => InferenceSession.create(buf, {
executionProviders: ['wasm'],
// intra-op 线程数在边缘环境通常固定为 1
}))
}
return sessionPromise
}
// 后台定时器保持 isolate 热状态,减少冷启动概率
const keepAlive = setInterval(() => {
// 每 5 分钟发一个空操作,防止 V8 isolate 被回收
void 0
}, 300_000)
export default {
async fetch(request, env) {
const session = await getSession(env)
const inputIds = new Tensor('int64', tokenize(text), [1, seqLen])
const result = await session.run({ input_ids: inputIds })
return Response.json(classify(result))
}
}这个案例的核心经验:边缘函数上跑模型,模型体积决定了能不能加载,加载方式决定了冷启动延迟。两个问题都要在上线前解决。
案例二:浏览器端 WebGPU 推理的兼容性现实
另一个团队在用 WebGPU 做浏览器端文本摘要。桌面 Chrome 上测试一切正常,摘要生成只需几百毫秒。上线一周后,用户反馈涌入:Safari 用户完全无法使用,Firefox 用户报 WebGL 上下文丢失,移动端安卓用户的 Chrome 版本低于 113 直接不支持 WebGPU。
团队最初只做了简单的特性检测:
// ❌ 只检查 navigator.gpu,不够用
if ('gpu' in navigator) {
// 直接加载 WebGPU 模型
const adapter = await navigator.gpu.requestAdapter()
const session = await createSession(modelUrl, { backend: 'webgpu' })
}问题不只是「支不支持 WebGPU」。navigator.gpu 存在不代表能创建 adapter,能创建 adapter 不代表有足够内存加载模型,有足够内存不代表能在合理时间内完成推理。
// ✅ 多层降级:WebGPU → WASM → 中心 API
async function detectCapability() {
if (!('gpu' in navigator)) return 'api-fallback'
const adapter = await navigator.gpu?.requestAdapter()
if (!adapter) return 'api-fallback'
// 检查设备内存(粗略判断)
const memoryGB = navigator.deviceMemory || 4
if (memoryGB < 8) return 'wasm-fallback'
// 实际尝试分配 GPU 内存,验证可用性
try {
const device = await adapter.requestDevice()
// 用一个小型 tensor 验证 GPU 计算是否正常
const buffer = device.createBuffer({ size: 1024, usage: GPUBufferUsage.STORAGE })
buffer.destroy()
device.destroy()
return 'webgpu'
} catch {
return 'wasm-fallback'
}
}
async function summarize(text) {
const capability = await detectCapability()
switch (capability) {
case 'webgpu':
return runBrowserSummarize(text, { backend: 'webgpu' })
case 'wasm-fallback':
// WASM 后端速度慢,但功能一致
return runBrowserSummarize(text, { backend: 'wasm' })
case 'api-fallback':
default:
return fetch('/api/summarize', {
method: 'POST',
body: JSON.stringify({ text })
}).then(r => r.json())
}
}WebGPU 在不同环境下的实际表现:
| 环境 | WebGPU 状态 | 建议方案 |
|---|---|---|
| Chrome 113+(桌面) | 完整支持 | WebGPU 推理 |
| Edge 113+(桌面) | 完整支持 | WebGPU 推理 |
| Safari 18+(macOS 15+) | 实验性支持,部分 API 缺失 | WASM 降级或 API 降级 |
| Firefox(桌面) | 需手动开启 flag | WASM 降级 |
| Chrome Mobile 121+ | 支持,但内存受限 | INT4 量化 + 小模型 |
| iOS Safari | 不支持 WebGPU | API 降级 |
这个案例的教训:浏览器端推理不能只看「能不能跑」,还要考虑跑得好不好、回退路径顺不顺畅。
案例三:混合架构的一致性难题
案例二的延伸。浏览器端推理跑通之后,团队希望把更多任务搬到客户端——关键词提取、意图分类、短文本改写。这些任务在支持 WebGPU 的设备上延迟只有 30-50ms,比走边缘 API 的 200-300ms 快了一个量级。
但新的问题出现了:同一个任务,浏览器端和后端用的是同一个 ONNX 模型的不同版本(浏览器端是 INT4 量化版,后端是 FP16 版),输出结果不完全一致。用户在桌面浏览器看到关键词是 A、B、C,切换到 API 降级后看到 A、B、D。
// ❌ 浏览器端和后端用不同精度模型,输出不一致
// 浏览器端
const browserResult = await browserEngine.run(text, { quantization: 'int4' })
// 后端
const serverResult = await serverModel.run(text) // FP16
// browserResult !== serverResult,用户感知到结果跳变// ✅ 统一模型版本 + 一致性校验层
class HybridInference {
constructor() {
// 浏览器端用 INT8 量化,后端也用 INT8 部署
// 精度损失更小,两端输出更接近
this.browserEngine = new BrowserEngine({
modelUrl: '/models/classifier-int8.onnx',
quantization: 'int8',
// 固定随机种子以保证确定性输出
seed: 42
})
this.apiEndpoint = '/api/classify'
}
async classify(text) {
const browserResult = await this.browserEngine.run(text)
if (!this.browserEngine.isReliable()) {
// 浏览器端不可靠时(WebGPU 不可用、内存不足等),直接走 API
return this.callApi(text)
}
// 10% 的请求同时走 API,对比两端结果
if (Math.random() < 0.1) {
const apiResult = await this.callApi(text)
const consistent = this.compareResults(browserResult, apiResult)
// 记录一致性指标,不一致时触发告警
this.metrics.recordConsistency(consistent)
if (!consistent) {
// 不一致时以后端结果为准
return apiResult
}
}
return browserResult
}
compareResults(a, b) {
// 余弦相似度 > 0.95 视为一致
const similarity = cosineSimilarity(a.embeddings, b.embeddings)
return similarity > 0.95
}
async callApi(text) {
const res = await fetch(this.apiEndpoint, {
method: 'POST',
body: JSON.stringify({ text, model: 'classifier-int8' })
})
return res.json()
}
}混合架构的代价是维护成本翻倍——同一个模型要维护两个版本,要有一致性校验机制,灰度发布时两端要同步。
案例四:把多轮对话放到边缘函数的误判
最后一个案例是一个反面教材。团队想把一个多轮对话功能部署到 Cloudflare Workers,认为边缘节点离用户近、响应快。
这个功能用的是 Llama 2 13B,KV 缓存需要 2GB 以上的内存。Workers 的 CPU 时间只有 50ms,而生成一个 token 需要数百毫秒的矩阵计算。即使把 KV 缓存存到 D1 或 R2,跨存储的读写延迟也会把 token 生成时间拉到不可接受的水平。
边缘函数的定位是路由和预处理层,不是推理引擎。需要跑完整的 LLM 推理,应该用 Cloudflare Workers AI 这类专用服务——模型推理由平台托管,不受 Worker CPU 时间约束。
// ❌ 在 Worker 里直接跑 LLM 推理,CPU 时间远远不够
export default {
async fetch(request, env) {
const session = await InferenceSession.create('/models/llama-13b.onnx')
// 生成一个 token 就需要 200-500ms,远超 50ms CPU 限制
const output = await session.run({ input_ids })
return Response.json(output)
}
}// ✅ 用 Workers AI 托管推理,Worker 只做路由和流式传输
export default {
async fetch(request, env) {
const { messages } = await request.json()
// Workers AI 由平台管理 GPU,不受 Worker CPU 时间约束
const response = await env.AI.run('@cf/meta/llama-2-13b-chat-int8', {
messages,
stream: true
})
// 流式返回,用户体验接近实时
return new Response(response, {
headers: { 'content-type': 'text/event-stream' }
})
}
}| 方案 | 适用场景 | 主要限制 |
|---|---|---|
| Worker 内自跑推理 | 单次前向传播(分类、嵌入) | CPU 25-50ms,只能跑 < 100MB 量化模型 |
| Workers AI 托管推理 | LLM 推理、图像生成 | 只支持平台提供的模型,自定义受限 |
| Worker + 外部 API | 需要自定义模型 + 边缘路由 | 多一跳延迟,需要管理 API key |
| 浏览器端推理 | 隐私敏感、低延迟轻量任务 | 依赖设备性能和浏览器支持 |
决策路径
把上面的经验整理成一个判断流程:
上线前的评估清单
需求阶段
- 是否量化过延迟收益:有 P50/P95 基线数据,预期改善 > 30% 才值得引入边缘
- 是否确认隐私收益:数据合规要求是否强制数据不出设备或减少传输节点
- 是否评估了成本变化:边缘平台的请求计费 vs 中心化 GPU 成本,算清楚总账
- 是否考虑了维护复杂度:边缘架构增加运维成本,团队是否准备好
场景匹配阶段
- 任务是否适合边缘:单次前向传播(分类、嵌入、轻改写)适合,多轮生成不适合
- 模型体积是否可接受:量化后能否放进目标环境的内存限制
- 是否有可接受的降级方案:WebGPU 不可用、边缘超时时用户不会感知到断裂
技术验证阶段
- 是否测试过冷启动:用真实模型和真实数据测冷启动延迟,不要只看热路径
- 是否测过目标浏览器和设备的 WebGPU 兼容性:特别是 Safari 和移动端
- 是否验证过量化模型的精度损失:和 FP32 基线对比关键指标
- 是否评估了缓存复杂度:KV 缓存、模型分发、配置同步的一致性方案
- 是否在低端设备上测试过:内存 < 4GB 的设备是否还能正常工作
部署准备阶段
- 边缘平台的 CPU 时间和内存限制是否已确认并留有余量
- 回退链路是否已就绪:边缘不可用时自动切换到中心 API
- 推理延迟和错误率的监控是否已配置
- 模型分发和版本管理方案是否已确定:边缘节点的模型更新如何灰度
- A/B 测试方案是否就绪:边缘路径和中心路径的效果对比
参考资料
-
ONNX Runtime Web: Unleashes Generative AI in the Browser Using WebGPU — Microsoft Open Source Blog, Feb 2024. 链接
-
WebLLM: A High-Performance In-Browser LLM Inference Engine — arXiv 2412.15803, Dec 2024. 链接
-
I/O 2024 Web AI Wrap Up: New Models, Tools, and APIs — Google Developer Chrome Blog, May 2024. 链接
-
Empowering In-Browser Deep Learning Inference on Edge Devices (nnJIT) — arXiv 2309.08978, Jul 2024. 链接
-
Edge AI: Evaluation of Model Compression Techniques — arXiv 2409.02134, Sep 2024. 链接
-
Advances to Low-Bit Quantization Enable LLMs on Edge Devices — Microsoft Research, Feb 2025. 链接
-
Edge Inference Showdown: Cloudflare vs Vercel AI SDK — Suhas Bhairav, 2024. 链接
-
ONNX Runtime Web Tutorials — ONNX Runtime Official Docs. 链接